Support scenic-only scenes and blackout fades
This commit is contained in:
parent
679044d0b3
commit
4c6982bf68
@ -235,9 +235,9 @@ const getAssetSecondaryLabel = (submission: Submission | undefined) =>
|
|||||||
|
|
||||||
const getDefaultAssetIds = (payload: RepositoryState) => {
|
const getDefaultAssetIds = (payload: RepositoryState) => {
|
||||||
const approvedIds = new Set(getApprovedAssets(payload).map((asset) => asset.id));
|
const approvedIds = new Set(getApprovedAssets(payload).map((asset) => asset.id));
|
||||||
const favorites = payload.collections.find((collection) => collection.kind === "favorites");
|
const curated = payload.collections.find((collection) => collection.id === "collection-curated-library");
|
||||||
const favoriteIds = favorites?.assetIds.filter((assetId) => approvedIds.has(assetId)) ?? [];
|
const curatedIds = curated?.assetIds.filter((assetId) => approvedIds.has(assetId)) ?? [];
|
||||||
return (favoriteIds.length > 0 ? favoriteIds : Array.from(approvedIds)).slice(0, 12);
|
return (curatedIds.length > 0 ? curatedIds : Array.from(approvedIds)).slice(0, 12);
|
||||||
};
|
};
|
||||||
|
|
||||||
const findSceneById = (payload: RepositoryState, sceneId: string) =>
|
const findSceneById = (payload: RepositoryState, sceneId: string) =>
|
||||||
@ -263,8 +263,7 @@ const buildParamsForScene = (
|
|||||||
|
|
||||||
const buildPresentationFromCue = (
|
const buildPresentationFromCue = (
|
||||||
payload: RepositoryState,
|
payload: RepositoryState,
|
||||||
cue: Cue | undefined,
|
cue: Cue | undefined
|
||||||
fallbackAssetIds: string[]
|
|
||||||
): SurfacePresentation | null => {
|
): SurfacePresentation | null => {
|
||||||
if (!cue) {
|
if (!cue) {
|
||||||
return null;
|
return null;
|
||||||
@ -281,7 +280,7 @@ const buildPresentationFromCue = (
|
|||||||
: findCollectionAssets(payload, cue.collectionId);
|
: findCollectionAssets(payload, cue.collectionId);
|
||||||
|
|
||||||
const assetMap = new Map(payload.photoAssets.map((asset) => [asset.id, asset] as const));
|
const assetMap = new Map(payload.photoAssets.map((asset) => [asset.id, asset] as const));
|
||||||
const assets = (cueAssetIds.length > 0 ? cueAssetIds : fallbackAssetIds)
|
const assets = cueAssetIds
|
||||||
.map((assetId) => assetMap.get(assetId))
|
.map((assetId) => assetMap.get(assetId))
|
||||||
.filter((asset): asset is PhotoAsset => Boolean(asset));
|
.filter((asset): asset is PhotoAsset => Boolean(asset));
|
||||||
|
|
||||||
@ -322,13 +321,12 @@ const getSuggestedAssetsForScene = (payload: RepositoryState, sceneId: string, f
|
|||||||
const approved = getApprovedAssets(payload);
|
const approved = getApprovedAssets(payload);
|
||||||
const curatedCollection = payload.collections.find((collection) => collection.id === "collection-curated-library");
|
const curatedCollection = payload.collections.find((collection) => collection.id === "collection-curated-library");
|
||||||
const curatedIds = new Set(curatedCollection?.assetIds ?? []);
|
const curatedIds = new Set(curatedCollection?.assetIds ?? []);
|
||||||
const favorites = new Set(payload.collections.find((collection) => collection.kind === "favorites")?.assetIds ?? []);
|
|
||||||
const recommendedLimit = Math.min(scene?.inputRules.maxAssets ?? 8, 12);
|
const recommendedLimit = Math.min(scene?.inputRules.maxAssets ?? 8, 12);
|
||||||
const prioritized = approved
|
const prioritized = approved
|
||||||
.slice()
|
.slice()
|
||||||
.sort((left, right) => {
|
.sort((left, right) => {
|
||||||
const leftScore = (curatedIds.has(left.id) ? 3 : 0) + (favorites.has(left.id) ? 2 : 0);
|
const leftScore = curatedIds.has(left.id) ? 3 : 0;
|
||||||
const rightScore = (curatedIds.has(right.id) ? 3 : 0) + (favorites.has(right.id) ? 2 : 0);
|
const rightScore = curatedIds.has(right.id) ? 3 : 0;
|
||||||
return rightScore - leftScore;
|
return rightScore - leftScore;
|
||||||
})
|
})
|
||||||
.map((asset) => asset.id)
|
.map((asset) => asset.id)
|
||||||
@ -414,10 +412,10 @@ const createInitialLiveState = (payload: RepositoryState) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
cueState: armedState,
|
cueState: armedState,
|
||||||
programPresentation: buildPresentationFromCue(payload, programCue, defaultAssetIds),
|
programPresentation: buildPresentationFromCue(payload, programCue),
|
||||||
programTransition: programCue?.transitionIn ?? null,
|
programTransition: programCue?.transitionIn ?? null,
|
||||||
selectedSceneId: previewScene?.id ?? payload.scenes[0]?.id ?? "",
|
selectedSceneId: previewScene?.id ?? payload.scenes[0]?.id ?? "",
|
||||||
selectedAssetIds: previewCueAssetIds.length > 0
|
selectedAssetIds: previewCue
|
||||||
? previewCueAssetIds
|
? previewCueAssetIds
|
||||||
: previewScene
|
: previewScene
|
||||||
? getSuggestedAssetsForScene(payload, previewScene.id, defaultAssetIds)
|
? getSuggestedAssetsForScene(payload, previewScene.id, defaultAssetIds)
|
||||||
@ -630,10 +628,6 @@ export const App = () => {
|
|||||||
const canMoveSelectedCueUp = selectedCueIndex > 0 && !cueMoveInFlight;
|
const canMoveSelectedCueUp = selectedCueIndex > 0 && !cueMoveInFlight;
|
||||||
const canMoveSelectedCueDown = selectedCueIndex >= 0 && selectedCueIndex < cueStack.length - 1 && !cueMoveInFlight;
|
const canMoveSelectedCueDown = selectedCueIndex >= 0 && selectedCueIndex < cueStack.length - 1 && !cueMoveInFlight;
|
||||||
const canSaveCue = Boolean(selectedScene && previewParams && !cueMutationInFlight && (!cueDraft.id || cueDraftDirty));
|
const canSaveCue = Boolean(selectedScene && previewParams && !cueMutationInFlight && (!cueDraft.id || cueDraftDirty));
|
||||||
const favoriteCollection: Collection | undefined = useMemo(
|
|
||||||
() => state?.collections.find((collection) => collection.kind === "favorites"),
|
|
||||||
[state?.collections]
|
|
||||||
);
|
|
||||||
const curatedCollection: Collection | undefined = useMemo(
|
const curatedCollection: Collection | undefined = useMemo(
|
||||||
() => state?.collections.find((collection) => collection.id === "collection-curated-library"),
|
() => state?.collections.find((collection) => collection.id === "collection-curated-library"),
|
||||||
[state?.collections]
|
[state?.collections]
|
||||||
@ -776,7 +770,7 @@ export const App = () => {
|
|||||||
[previewPresentation]
|
[previewPresentation]
|
||||||
);
|
);
|
||||||
const programPresentation = programOutputState?.presentation ?? null;
|
const programPresentation = programOutputState?.presentation ?? null;
|
||||||
const programActivationKey = `${programOutputState?.outputRevision ?? 0}:${createPresentationStructureHash(programPresentation)}`;
|
const programActivationKey = createPresentationStructureHash(programPresentation);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatchAdmin({
|
dispatchAdmin({
|
||||||
@ -793,13 +787,14 @@ export const App = () => {
|
|||||||
|
|
||||||
const nextPreset = matchPresetForScene(scene, availablePresets);
|
const nextPreset = matchPresetForScene(scene, availablePresets);
|
||||||
const preservedAssetIds = filterAvailableAssetIds(state, selectedAssetIds);
|
const preservedAssetIds = filterAvailableAssetIds(state, selectedAssetIds);
|
||||||
|
const shouldPreserveEmptySelection = selectedAssetIds.length === 0;
|
||||||
dispatchAdmin({
|
dispatchAdmin({
|
||||||
type: "previewSceneSelected",
|
type: "previewSceneSelected",
|
||||||
scene,
|
scene,
|
||||||
presetId: nextPreset?.id ?? availablePresets[0]?.id ?? effectPresetLibrary[0]?.id ?? "",
|
presetId: nextPreset?.id ?? availablePresets[0]?.id ?? effectPresetLibrary[0]?.id ?? "",
|
||||||
params: buildParamsForScene(scene, undefined, nextPreset),
|
params: buildParamsForScene(scene, undefined, nextPreset),
|
||||||
assetIds:
|
assetIds:
|
||||||
preservedAssetIds.length > 0
|
preservedAssetIds.length > 0 || shouldPreserveEmptySelection
|
||||||
? preservedAssetIds
|
? preservedAssetIds
|
||||||
: getSuggestedAssetsForScene(state, scene.id, getDefaultAssetIds(state))
|
: getSuggestedAssetsForScene(state, scene.id, getDefaultAssetIds(state))
|
||||||
});
|
});
|
||||||
@ -827,7 +822,7 @@ export const App = () => {
|
|||||||
scene,
|
scene,
|
||||||
presetId: matchedPreset?.id ?? availablePresets[0]?.id ?? effectPresetLibrary[0]?.id ?? "",
|
presetId: matchedPreset?.id ?? availablePresets[0]?.id ?? effectPresetLibrary[0]?.id ?? "",
|
||||||
params: buildParamsForScene(scene, cue.parameterOverrides, matchedPreset),
|
params: buildParamsForScene(scene, cue.parameterOverrides, matchedPreset),
|
||||||
assetIds: cueAssetIds.length > 0 ? cueAssetIds : getSuggestedAssetsForScene(state, scene.id, getDefaultAssetIds(state)),
|
assetIds: cueAssetIds,
|
||||||
armPreview: options.armPreview
|
armPreview: options.armPreview
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -851,10 +846,7 @@ export const App = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const preset = matchPresetForScene(scene, availablePresets, draft.effectPresetId);
|
const preset = matchPresetForScene(scene, availablePresets, draft.effectPresetId);
|
||||||
const nextAssetIds =
|
const nextAssetIds = draft.assetIds ? filterAvailableAssetIds(state, draft.assetIds) : [];
|
||||||
draft.assetIds && draft.assetIds.length > 0
|
|
||||||
? filterAvailableAssetIds(state, draft.assetIds)
|
|
||||||
: getSuggestedAssetsForScene(state, scene.id, getDefaultAssetIds(state));
|
|
||||||
|
|
||||||
dispatchAdmin({
|
dispatchAdmin({
|
||||||
type: "generatedCueDraftLoaded",
|
type: "generatedCueDraftLoaded",
|
||||||
@ -1001,8 +993,16 @@ export const App = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const setBlackout = (blackout: boolean) => {
|
const setBlackout = (blackout: boolean) => {
|
||||||
|
if (cueState.blackout === blackout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const programCue = programOutputState?.presentation?.cue ?? null;
|
||||||
|
const blackoutTransition = blackout
|
||||||
|
? programCue?.transitionOut ?? defaultCueTransition
|
||||||
|
: programCue?.transitionIn ?? defaultCueTransition;
|
||||||
dispatchAdmin({ type: "blackoutSet", blackout });
|
dispatchAdmin({ type: "blackoutSet", blackout });
|
||||||
publishProgramOutput(programOutputState?.presentation ?? null, blackout, programOutputState?.transition ?? null);
|
publishProgramOutput(programOutputState?.presentation ?? null, blackout, blackoutTransition);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -1377,17 +1377,22 @@ export const App = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLoadFavorites = () => {
|
const handleLoadCuratedAssets = () => {
|
||||||
if (!state) {
|
if (!state) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextAssetIds = getDefaultAssetIds(state);
|
const approvedIds = new Set(approvedAssets.map((asset) => asset.id));
|
||||||
|
const curatedAssetIds = (curatedCollection?.assetIds.filter((assetId) => approvedIds.has(assetId)) ?? []).slice(0, 12);
|
||||||
|
const nextAssetIds = curatedAssetIds.length > 0 ? curatedAssetIds : getDefaultAssetIds(state);
|
||||||
dispatchAdmin({
|
dispatchAdmin({
|
||||||
type: "selectedAssetsReplaced",
|
type: "selectedAssetsReplaced",
|
||||||
assetIds: nextAssetIds,
|
assetIds: nextAssetIds,
|
||||||
metadataAssetId: nextAssetIds[0] ?? null,
|
metadataAssetId: nextAssetIds[0] ?? null,
|
||||||
status: "Favorites bank loaded into preview."
|
status:
|
||||||
|
curatedAssetIds.length > 0
|
||||||
|
? "Curated library loaded into preview."
|
||||||
|
: "No curated images available. Loaded default approved media instead."
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1749,8 +1754,8 @@ export const App = () => {
|
|||||||
<button onClick={handleLoadSuggestedAssets} disabled={!selectedScene}>
|
<button onClick={handleLoadSuggestedAssets} disabled={!selectedScene}>
|
||||||
Suggested media
|
Suggested media
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handleLoadFavorites} disabled={!state}>
|
<button onClick={handleLoadCuratedAssets} disabled={!state}>
|
||||||
Favorites
|
Curated
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1786,8 +1791,8 @@ export const App = () => {
|
|||||||
<button onClick={handleLoadSuggestedAssets} disabled={!selectedScene}>
|
<button onClick={handleLoadSuggestedAssets} disabled={!selectedScene}>
|
||||||
Suggested
|
Suggested
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handleLoadFavorites} disabled={!state}>
|
<button onClick={handleLoadCuratedAssets} disabled={!state}>
|
||||||
Favorites
|
Curated
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handleClearSelectedAssets} disabled={selectedAssetIds.length === 0}>
|
<button onClick={handleClearSelectedAssets} disabled={selectedAssetIds.length === 0}>
|
||||||
Clear
|
Clear
|
||||||
@ -2316,7 +2321,7 @@ export const App = () => {
|
|||||||
presentation={programPresentation}
|
presentation={programPresentation}
|
||||||
blackout={cueState.blackout}
|
blackout={cueState.blackout}
|
||||||
transition={programOutputState?.transition ?? null}
|
transition={programOutputState?.transition ?? null}
|
||||||
activationKey={`${programActivationKey}:${cueState.blackout ? "blackout" : "live"}`}
|
activationKey={programActivationKey}
|
||||||
qualityProfile="program"
|
qualityProfile="program"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -2532,7 +2537,7 @@ export const App = () => {
|
|||||||
<div className="build-media-workarea">
|
<div className="build-media-workarea">
|
||||||
<div className="build-media-browser">
|
<div className="build-media-browser">
|
||||||
<p className="bank-summary bank-summary--visible">
|
<p className="bank-summary bank-summary--visible">
|
||||||
Favorites: <strong>{favoriteCollection?.assetIds.length ?? 0}</strong> / Curated library: <strong>{curatedCollection?.assetIds.length ?? 0}</strong>
|
Curated library: <strong>{curatedCollection?.assetIds.length ?? 0}</strong> / Approved bank: <strong>{filteredApprovedAssets.length}</strong>
|
||||||
</p>
|
</p>
|
||||||
{renderApprovedBank("build")}
|
{renderApprovedBank("build")}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import type { CueTransition } from "@goodgrief/shared-types";
|
import type { CueTransition } from "@goodgrief/shared-types";
|
||||||
import { SceneViewport } from "../features/live/SceneViewport";
|
import { SceneViewport } from "../features/live/SceneViewport";
|
||||||
import { readProgramOutputState, subscribeProgramOutput, type ProgramOutputState } from "../features/live/output-sync";
|
import {
|
||||||
|
createPresentationStructureHash,
|
||||||
|
readProgramOutputState,
|
||||||
|
subscribeProgramOutput,
|
||||||
|
type ProgramOutputState
|
||||||
|
} from "../features/live/output-sync";
|
||||||
import "./output.css";
|
import "./output.css";
|
||||||
|
|
||||||
const enterFullscreen = async () => {
|
const enterFullscreen = async () => {
|
||||||
@ -126,7 +131,7 @@ export const ProgramOutputApp = () => {
|
|||||||
presentation={outputState?.presentation ?? null}
|
presentation={outputState?.presentation ?? null}
|
||||||
blackout={outputState?.blackout ?? false}
|
blackout={outputState?.blackout ?? false}
|
||||||
transition={transition}
|
transition={transition}
|
||||||
activationKey={`${outputState?.presentationHash ?? "program-empty"}:${outputState?.outputRevision ?? 0}:${outputState?.blackout ? "blackout" : "live"}`}
|
activationKey={createPresentationStructureHash(outputState?.presentation ?? null)}
|
||||||
/>
|
/>
|
||||||
<div className={`output-overlay ${overlayVisible ? "output-overlay--visible" : ""}`}>
|
<div className={`output-overlay ${overlayVisible ? "output-overlay--visible" : ""}`}>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -65,7 +65,7 @@ export const SceneViewport = ({
|
|||||||
surface.registerMany(defaultScenePlugins);
|
surface.registerMany(defaultScenePlugins);
|
||||||
surface.setQualityProfile(qualityProfileRef.current);
|
surface.setQualityProfile(qualityProfileRef.current);
|
||||||
surface.setBusy(busyRef.current);
|
surface.setBusy(busyRef.current);
|
||||||
surface.setBlackout(blackoutRef.current);
|
surface.setBlackout(blackoutRef.current, null, true);
|
||||||
surfaceRef.current = surface;
|
surfaceRef.current = surface;
|
||||||
|
|
||||||
const resize = () => {
|
const resize = () => {
|
||||||
@ -92,8 +92,8 @@ export const SceneViewport = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
surfaceRef.current?.setBlackout(blackout);
|
surfaceRef.current?.setBlackout(blackout, transition ?? defaultTransition);
|
||||||
}, [blackout]);
|
}, [blackout, transition]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
surfaceRef.current?.setQualityProfile(qualityProfile);
|
surfaceRef.current?.setQualityProfile(qualityProfile);
|
||||||
|
|||||||
@ -909,30 +909,6 @@ const createTextStrip = (
|
|||||||
return { mesh, texture };
|
return { mesh, texture };
|
||||||
};
|
};
|
||||||
|
|
||||||
const fallbackLoadedAssets = (definition: SceneDefinition, count: number): LoadedPhotoAsset[] => {
|
|
||||||
const palette = ["#98a8c0", "#d8b28f", "#b5c8d8", "#bea5d6"];
|
|
||||||
return Array.from({ length: count }, (_, index) => ({
|
|
||||||
asset: {
|
|
||||||
id: `${definition.sceneKey}-fallback-${index}`,
|
|
||||||
submissionId: definition.id,
|
|
||||||
originalKey: "",
|
|
||||||
mimeType: "placeholder",
|
|
||||||
processingStatus: "ready",
|
|
||||||
moderationStatus: "approved",
|
|
||||||
createdAt: new Date(0).toISOString(),
|
|
||||||
width: 1200,
|
|
||||||
height: 900
|
|
||||||
},
|
|
||||||
texture: null,
|
|
||||||
aspect: 4 / 3,
|
|
||||||
dominantColor: palette[index % palette.length] ?? "#8ea0b4",
|
|
||||||
sourceUrl: null
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const withAssets = (input: SceneActivationInput, minCount: number) =>
|
|
||||||
input.loadedAssets.length > 0 ? input.loadedAssets : fallbackLoadedAssets(input.definition, minCount);
|
|
||||||
|
|
||||||
const combineInstances = (...instances: Array<SceneInstance | null | undefined>): SceneInstance => {
|
const combineInstances = (...instances: Array<SceneInstance | null | undefined>): SceneInstance => {
|
||||||
const active = instances.filter((instance): instance is SceneInstance => Boolean(instance));
|
const active = instances.filter((instance): instance is SceneInstance => Boolean(instance));
|
||||||
if (active.length === 1) {
|
if (active.length === 1) {
|
||||||
@ -1034,7 +1010,7 @@ const buildTextOverlay = (input: SceneActivationInput): SceneInstance | null =>
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const palette = paletteFromAssets(withAssets(input, 1), input.params.scenicTreatment);
|
const palette = paletteFromAssets(input.loadedAssets, input.params.scenicTreatment);
|
||||||
const root = new THREE.Group();
|
const root = new THREE.Group();
|
||||||
const textures: THREE.Texture[] = [];
|
const textures: THREE.Texture[] = [];
|
||||||
const animated: Array<{
|
const animated: Array<{
|
||||||
@ -1179,7 +1155,7 @@ const buildWitnessFloat = (input: SceneActivationInput): SceneInstance => {
|
|||||||
const { composition, photoTreatment } = input.params;
|
const { composition, photoTreatment } = input.params;
|
||||||
const mode = input.modeKey ?? "near_witness";
|
const mode = input.modeKey ?? "near_witness";
|
||||||
const count = clamp(1 + Math.round(composition.supportCount), 1, 3);
|
const count = clamp(1 + Math.round(composition.supportCount), 1, 3);
|
||||||
const assets = withAssets(input, count).slice(0, count);
|
const assets = input.loadedAssets.slice(0, count);
|
||||||
const palette = paletteFromAssets(assets, input.params.scenicTreatment);
|
const palette = paletteFromAssets(assets, input.params.scenicTreatment);
|
||||||
const backdrop = buildBackdropSystem(input, palette);
|
const backdrop = buildBackdropSystem(input, palette);
|
||||||
const root = new THREE.Group();
|
const root = new THREE.Group();
|
||||||
@ -1240,7 +1216,7 @@ const buildPortalFrame = (input: SceneActivationInput): SceneInstance => {
|
|||||||
const { composition, photoTreatment } = input.params;
|
const { composition, photoTreatment } = input.params;
|
||||||
const mode = input.modeKey ?? "soft_gate";
|
const mode = input.modeKey ?? "soft_gate";
|
||||||
const count = clamp(1 + Math.round(composition.supportCount), 1, 2);
|
const count = clamp(1 + Math.round(composition.supportCount), 1, 2);
|
||||||
const assets = withAssets(input, count).slice(0, count);
|
const assets = input.loadedAssets.slice(0, count);
|
||||||
const palette = paletteFromAssets(assets, input.params.scenicTreatment);
|
const palette = paletteFromAssets(assets, input.params.scenicTreatment);
|
||||||
const backdrop = buildBackdropSystem(input, palette);
|
const backdrop = buildBackdropSystem(input, palette);
|
||||||
const root = new THREE.Group();
|
const root = new THREE.Group();
|
||||||
@ -1313,7 +1289,7 @@ const buildOrbitGallery = (input: SceneActivationInput): SceneInstance => {
|
|||||||
const { composition, photoTreatment } = input.params;
|
const { composition, photoTreatment } = input.params;
|
||||||
const mode = input.modeKey ?? "halo_arc";
|
const mode = input.modeKey ?? "halo_arc";
|
||||||
const count = clamp(1 + Math.round(composition.supportCount), 1, 3);
|
const count = clamp(1 + Math.round(composition.supportCount), 1, 3);
|
||||||
const assets = withAssets(input, count).slice(0, count);
|
const assets = input.loadedAssets.slice(0, count);
|
||||||
const palette = paletteFromAssets(assets, input.params.scenicTreatment);
|
const palette = paletteFromAssets(assets, input.params.scenicTreatment);
|
||||||
const backdrop = buildBackdropSystem(input, palette);
|
const backdrop = buildBackdropSystem(input, palette);
|
||||||
const root = new THREE.Group();
|
const root = new THREE.Group();
|
||||||
@ -1374,7 +1350,7 @@ const buildSuspensionField = (input: SceneActivationInput): SceneInstance => {
|
|||||||
const { composition, photoTreatment } = input.params;
|
const { composition, photoTreatment } = input.params;
|
||||||
const mode = input.modeKey ?? "hover_shelf";
|
const mode = input.modeKey ?? "hover_shelf";
|
||||||
const count = clamp(1 + Math.round(composition.supportCount), 2, 4);
|
const count = clamp(1 + Math.round(composition.supportCount), 2, 4);
|
||||||
const assets = withAssets(input, count).slice(0, count);
|
const assets = input.loadedAssets.slice(0, count);
|
||||||
const palette = paletteFromAssets(assets, input.params.scenicTreatment);
|
const palette = paletteFromAssets(assets, input.params.scenicTreatment);
|
||||||
const backdrop = buildBackdropSystem(input, palette);
|
const backdrop = buildBackdropSystem(input, palette);
|
||||||
const root = new THREE.Group();
|
const root = new THREE.Group();
|
||||||
@ -1440,7 +1416,7 @@ const buildChorusArray = (input: SceneActivationInput): SceneInstance => {
|
|||||||
const { composition, photoTreatment } = input.params;
|
const { composition, photoTreatment } = input.params;
|
||||||
const mode = input.modeKey ?? "grid_choir";
|
const mode = input.modeKey ?? "grid_choir";
|
||||||
const count = clamp(1 + Math.round(composition.supportCount), 3, 4);
|
const count = clamp(1 + Math.round(composition.supportCount), 3, 4);
|
||||||
const assets = withAssets(input, count).slice(0, count);
|
const assets = input.loadedAssets.slice(0, count);
|
||||||
const palette = paletteFromAssets(assets, input.params.scenicTreatment);
|
const palette = paletteFromAssets(assets, input.params.scenicTreatment);
|
||||||
const backdrop = buildBackdropSystem(input, palette);
|
const backdrop = buildBackdropSystem(input, palette);
|
||||||
const root = new THREE.Group();
|
const root = new THREE.Group();
|
||||||
@ -1501,7 +1477,7 @@ const buildEqualCollage = (input: SceneActivationInput): SceneInstance => {
|
|||||||
const { composition, photoTreatment } = input.params;
|
const { composition, photoTreatment } = input.params;
|
||||||
const mode = input.modeKey ?? "quadrant";
|
const mode = input.modeKey ?? "quadrant";
|
||||||
const count = clamp(1 + Math.round(composition.supportCount), 2, 4);
|
const count = clamp(1 + Math.round(composition.supportCount), 2, 4);
|
||||||
const assets = withAssets(input, count).slice(0, count);
|
const assets = input.loadedAssets.slice(0, count);
|
||||||
const palette = paletteFromAssets(assets, input.params.scenicTreatment);
|
const palette = paletteFromAssets(assets, input.params.scenicTreatment);
|
||||||
const backdrop = buildBackdropSystem(input, palette);
|
const backdrop = buildBackdropSystem(input, palette);
|
||||||
const root = new THREE.Group();
|
const root = new THREE.Group();
|
||||||
@ -1560,7 +1536,7 @@ const buildArrivalRelay = (input: SceneActivationInput): SceneInstance => {
|
|||||||
const { composition, photoTreatment } = input.params;
|
const { composition, photoTreatment } = input.params;
|
||||||
const mode = input.modeKey ?? "edge_queue";
|
const mode = input.modeKey ?? "edge_queue";
|
||||||
const count = clamp(1 + Math.round(composition.supportCount), 1, 4);
|
const count = clamp(1 + Math.round(composition.supportCount), 1, 4);
|
||||||
const assets = withAssets(input, count).slice(0, count);
|
const assets = input.loadedAssets.slice(0, count);
|
||||||
const palette = paletteFromAssets(assets, input.params.scenicTreatment);
|
const palette = paletteFromAssets(assets, input.params.scenicTreatment);
|
||||||
const backdrop = buildBackdropSystem(input, palette);
|
const backdrop = buildBackdropSystem(input, palette);
|
||||||
const root = new THREE.Group();
|
const root = new THREE.Group();
|
||||||
@ -1759,6 +1735,14 @@ interface TransitionRuntime {
|
|||||||
startedAtMs: number;
|
startedAtMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BlackoutRuntime {
|
||||||
|
fromLevel: number;
|
||||||
|
toLevel: number;
|
||||||
|
style: CueTransition["style"];
|
||||||
|
durationMs: number;
|
||||||
|
startedAtMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
const transitionStyleToValue = (style: CueTransition["style"]) => {
|
const transitionStyleToValue = (style: CueTransition["style"]) => {
|
||||||
switch (style) {
|
switch (style) {
|
||||||
case "dissolve":
|
case "dissolve":
|
||||||
@ -1879,6 +1863,18 @@ export class RenderSurface {
|
|||||||
});
|
});
|
||||||
private readonly compositeFromQuad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this.compositeFromMaterial);
|
private readonly compositeFromQuad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this.compositeFromMaterial);
|
||||||
private readonly compositeToQuad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this.compositeToMaterial);
|
private readonly compositeToQuad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this.compositeToMaterial);
|
||||||
|
private readonly blackoutQuad = new THREE.Mesh(
|
||||||
|
new THREE.PlaneGeometry(2, 2),
|
||||||
|
new THREE.MeshBasicMaterial({
|
||||||
|
color: "#000000",
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0,
|
||||||
|
depthWrite: false,
|
||||||
|
depthTest: false,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
toneMapped: false
|
||||||
|
})
|
||||||
|
);
|
||||||
private readonly shutterBars = Array.from({ length: 8 }, () =>
|
private readonly shutterBars = Array.from({ length: 8 }, () =>
|
||||||
new THREE.Mesh(
|
new THREE.Mesh(
|
||||||
new THREE.PlaneGeometry(2.4, 0.18),
|
new THREE.PlaneGeometry(2.4, 0.18),
|
||||||
@ -1922,8 +1918,9 @@ export class RenderSurface {
|
|||||||
};
|
};
|
||||||
private currentRuntime: SceneRuntime | null = null;
|
private currentRuntime: SceneRuntime | null = null;
|
||||||
private transitionRuntime: TransitionRuntime | null = null;
|
private transitionRuntime: TransitionRuntime | null = null;
|
||||||
|
private blackoutRuntime: BlackoutRuntime | null = null;
|
||||||
private lastFrameMs = 0;
|
private lastFrameMs = 0;
|
||||||
private blackout = false;
|
private blackoutLevel = 0;
|
||||||
private activationToken = 0;
|
private activationToken = 0;
|
||||||
private activePresentationKey: string | undefined;
|
private activePresentationKey: string | undefined;
|
||||||
private qualityProfile: SurfaceQualityProfile = "program";
|
private qualityProfile: SurfaceQualityProfile = "program";
|
||||||
@ -1950,8 +1947,9 @@ export class RenderSurface {
|
|||||||
|
|
||||||
this.compositeFromQuad.position.z = 0;
|
this.compositeFromQuad.position.z = 0;
|
||||||
this.compositeToQuad.position.z = 0.01;
|
this.compositeToQuad.position.z = 0.01;
|
||||||
|
this.blackoutQuad.position.z = 0.019;
|
||||||
this.veilOverlay.position.z = 0.02;
|
this.veilOverlay.position.z = 0.02;
|
||||||
this.compositeScene.add(this.compositeFromQuad, this.compositeToQuad, this.veilOverlay);
|
this.compositeScene.add(this.compositeFromQuad, this.compositeToQuad, this.blackoutQuad, this.veilOverlay);
|
||||||
this.shutterBars.forEach((bar, index) => {
|
this.shutterBars.forEach((bar, index) => {
|
||||||
bar.position.set(0, 0.86 - index * 0.24, 0.03 + index * 0.001);
|
bar.position.set(0, 0.86 - index * 0.24, 0.03 + index * 0.001);
|
||||||
this.compositeScene.add(bar);
|
this.compositeScene.add(bar);
|
||||||
@ -1968,13 +1966,6 @@ export class RenderSurface {
|
|||||||
const deltaMs = this.lastFrameMs === 0 ? 16.6 : timestamp - this.lastFrameMs;
|
const deltaMs = this.lastFrameMs === 0 ? 16.6 : timestamp - this.lastFrameMs;
|
||||||
this.lastFrameMs = timestamp;
|
this.lastFrameMs = timestamp;
|
||||||
|
|
||||||
if (this.blackout) {
|
|
||||||
this.renderer.setClearColor("#000000", 1);
|
|
||||||
this.renderer.setRenderTarget(null);
|
|
||||||
this.renderer.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.renderer.setClearColor("#040508", 1);
|
this.renderer.setClearColor("#040508", 1);
|
||||||
const context = {
|
const context = {
|
||||||
elapsedMs: timestamp,
|
elapsedMs: timestamp,
|
||||||
@ -1989,6 +1980,7 @@ export class RenderSurface {
|
|||||||
this.renderRuntimeToTarget(this.transitionRuntime.from, this.fromTarget);
|
this.renderRuntimeToTarget(this.transitionRuntime.from, this.fromTarget);
|
||||||
this.renderRuntimeToTarget(this.transitionRuntime.to, this.toTarget);
|
this.renderRuntimeToTarget(this.transitionRuntime.to, this.toTarget);
|
||||||
this.renderCompositeTransition(progress, this.transitionRuntime.style);
|
this.renderCompositeTransition(progress, this.transitionRuntime.style);
|
||||||
|
this.renderBlackoutOverlay(timestamp);
|
||||||
if (progress >= 1) {
|
if (progress >= 1) {
|
||||||
this.finishTransition();
|
this.finishTransition();
|
||||||
}
|
}
|
||||||
@ -1998,12 +1990,14 @@ export class RenderSurface {
|
|||||||
if (!this.currentRuntime) {
|
if (!this.currentRuntime) {
|
||||||
this.renderer.setRenderTarget(null);
|
this.renderer.setRenderTarget(null);
|
||||||
this.renderer.clear();
|
this.renderer.clear();
|
||||||
|
this.renderBlackoutOverlay(timestamp);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSceneRuntime(this.currentRuntime, context);
|
updateSceneRuntime(this.currentRuntime, context);
|
||||||
this.renderRuntimeToTarget(this.currentRuntime, this.toTarget);
|
this.renderRuntimeToTarget(this.currentRuntime, this.toTarget);
|
||||||
this.renderTargetToScreen(this.toTarget);
|
this.renderTargetToScreen(this.toTarget);
|
||||||
|
this.renderBlackoutOverlay(timestamp);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2061,8 +2055,33 @@ export class RenderSurface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setBlackout(blackout: boolean) {
|
setBlackout(blackout: boolean, transition?: CueTransition | null, immediate = false) {
|
||||||
this.blackout = blackout;
|
const nextLevel = blackout ? 1 : 0;
|
||||||
|
const now = this.lastFrameMs || (typeof performance !== "undefined" ? performance.now() : 0);
|
||||||
|
|
||||||
|
if (this.blackoutRuntime) {
|
||||||
|
const progress = clamp((now - this.blackoutRuntime.startedAtMs) / this.blackoutRuntime.durationMs, 0, 1);
|
||||||
|
const eased = THREE.MathUtils.smoothstep(progress, 0, 1);
|
||||||
|
this.blackoutLevel = THREE.MathUtils.lerp(this.blackoutRuntime.fromLevel, this.blackoutRuntime.toLevel, eased);
|
||||||
|
this.blackoutRuntime = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.abs(nextLevel - this.blackoutLevel) < 0.001) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (immediate || !transition || transition.style === "cut" || transition.durationMs <= 0) {
|
||||||
|
this.blackoutLevel = nextLevel;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.blackoutRuntime = {
|
||||||
|
fromLevel: this.blackoutLevel,
|
||||||
|
toLevel: nextLevel,
|
||||||
|
style: transition.style,
|
||||||
|
durationMs: transition.durationMs,
|
||||||
|
startedAtMs: now
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePresentation(presentation: SurfacePresentation | null, activationKey?: string) {
|
updatePresentation(presentation: SurfacePresentation | null, activationKey?: string) {
|
||||||
@ -2131,6 +2150,7 @@ export class RenderSurface {
|
|||||||
this.toTarget.dispose();
|
this.toTarget.dispose();
|
||||||
this.compositeFromMaterial.dispose();
|
this.compositeFromMaterial.dispose();
|
||||||
this.compositeToMaterial.dispose();
|
this.compositeToMaterial.dispose();
|
||||||
|
(this.blackoutQuad.material as THREE.Material).dispose();
|
||||||
(this.veilOverlay.material as THREE.Material).dispose();
|
(this.veilOverlay.material as THREE.Material).dispose();
|
||||||
this.shutterBars.forEach((bar) => (bar.material as THREE.Material).dispose());
|
this.shutterBars.forEach((bar) => (bar.material as THREE.Material).dispose());
|
||||||
this.renderer.dispose();
|
this.renderer.dispose();
|
||||||
@ -2220,6 +2240,7 @@ export class RenderSurface {
|
|||||||
this.compositeToQuad.scale.set(1, 1, 1);
|
this.compositeToQuad.scale.set(1, 1, 1);
|
||||||
this.compositeFromQuad.rotation.z = 0;
|
this.compositeFromQuad.rotation.z = 0;
|
||||||
this.compositeToQuad.rotation.z = 0;
|
this.compositeToQuad.rotation.z = 0;
|
||||||
|
(this.blackoutQuad.material as THREE.MeshBasicMaterial).opacity = 0;
|
||||||
|
|
||||||
const veilMaterial = this.veilOverlay.material as THREE.MeshBasicMaterial;
|
const veilMaterial = this.veilOverlay.material as THREE.MeshBasicMaterial;
|
||||||
veilMaterial.opacity = 0;
|
veilMaterial.opacity = 0;
|
||||||
@ -2266,6 +2287,7 @@ export class RenderSurface {
|
|||||||
this.compositeToQuad.scale.set(1, 1, 1);
|
this.compositeToQuad.scale.set(1, 1, 1);
|
||||||
this.compositeFromQuad.rotation.z = 0;
|
this.compositeFromQuad.rotation.z = 0;
|
||||||
this.compositeToQuad.rotation.z = 0;
|
this.compositeToQuad.rotation.z = 0;
|
||||||
|
(this.blackoutQuad.material as THREE.MeshBasicMaterial).opacity = 0;
|
||||||
(this.veilOverlay.material as THREE.MeshBasicMaterial).opacity = 0;
|
(this.veilOverlay.material as THREE.MeshBasicMaterial).opacity = 0;
|
||||||
this.shutterBars.forEach((bar, index) => {
|
this.shutterBars.forEach((bar, index) => {
|
||||||
const material = bar.material as THREE.MeshBasicMaterial;
|
const material = bar.material as THREE.MeshBasicMaterial;
|
||||||
@ -2277,6 +2299,77 @@ export class RenderSurface {
|
|||||||
this.renderer.render(this.compositeScene, this.compositeCamera);
|
this.renderer.render(this.compositeScene, this.compositeCamera);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderBlackoutOverlay(timestamp: number) {
|
||||||
|
let style: CueTransition["style"] = "dissolve";
|
||||||
|
let progress = this.blackoutLevel > 0 ? 1 : 0;
|
||||||
|
let toBlack = this.blackoutLevel >= 1;
|
||||||
|
|
||||||
|
if (this.blackoutRuntime) {
|
||||||
|
const rawProgress = clamp((timestamp - this.blackoutRuntime.startedAtMs) / this.blackoutRuntime.durationMs, 0, 1);
|
||||||
|
const eased = THREE.MathUtils.smoothstep(rawProgress, 0, 1);
|
||||||
|
this.blackoutLevel = THREE.MathUtils.lerp(this.blackoutRuntime.fromLevel, this.blackoutRuntime.toLevel, eased);
|
||||||
|
style = this.blackoutRuntime.style;
|
||||||
|
progress = eased;
|
||||||
|
toBlack = this.blackoutRuntime.toLevel > this.blackoutRuntime.fromLevel;
|
||||||
|
if (rawProgress >= 1) {
|
||||||
|
this.blackoutLevel = this.blackoutRuntime.toLevel;
|
||||||
|
this.blackoutRuntime = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.blackoutLevel <= 0.001) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.compositeFromMaterial.opacity = 0;
|
||||||
|
this.compositeToMaterial.opacity = 0;
|
||||||
|
this.compositeFromQuad.position.set(0, 0, 0);
|
||||||
|
this.compositeToQuad.position.set(0, 0, 0.01);
|
||||||
|
this.compositeFromQuad.scale.set(1, 1, 1);
|
||||||
|
this.compositeToQuad.scale.set(1, 1, 1);
|
||||||
|
this.compositeFromQuad.rotation.z = 0;
|
||||||
|
this.compositeToQuad.rotation.z = 0;
|
||||||
|
|
||||||
|
const blackoutMaterial = this.blackoutQuad.material as THREE.MeshBasicMaterial;
|
||||||
|
blackoutMaterial.opacity = clamp(this.blackoutLevel, 0, 1);
|
||||||
|
this.blackoutQuad.position.set(0, 0, 0.019);
|
||||||
|
this.blackoutQuad.scale.set(1, 1, 1);
|
||||||
|
this.blackoutQuad.rotation.z = 0;
|
||||||
|
|
||||||
|
const veilMaterial = this.veilOverlay.material as THREE.MeshBasicMaterial;
|
||||||
|
veilMaterial.opacity = 0;
|
||||||
|
|
||||||
|
this.shutterBars.forEach((bar, index) => {
|
||||||
|
const material = bar.material as THREE.MeshBasicMaterial;
|
||||||
|
material.opacity = 0;
|
||||||
|
bar.position.set(0, 0.86 - index * 0.24, 0.03 + index * 0.001);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (style === "mist_reveal" && this.blackoutRuntime) {
|
||||||
|
veilMaterial.opacity = (0.02 + this.blackoutLevel * 0.08) * Math.sin(progress * Math.PI);
|
||||||
|
blackoutMaterial.opacity = clamp(this.blackoutLevel * 1.02, 0, 1);
|
||||||
|
} else if (style === "depth_drift" && this.blackoutRuntime) {
|
||||||
|
const offset = (toBlack ? 1 - progress : progress - 1) * 0.06;
|
||||||
|
this.blackoutQuad.position.x = offset;
|
||||||
|
this.blackoutQuad.scale.set(1.03, 1, 1);
|
||||||
|
veilMaterial.opacity = Math.sin(progress * Math.PI) * 0.05;
|
||||||
|
} else if (style === "shutter_reveal" && this.blackoutRuntime) {
|
||||||
|
blackoutMaterial.opacity = clamp(Math.max(this.blackoutLevel, toBlack ? progress * 0.82 : this.blackoutLevel), 0, 1);
|
||||||
|
this.shutterBars.forEach((bar, index) => {
|
||||||
|
const material = bar.material as THREE.MeshBasicMaterial;
|
||||||
|
const bandProgress = THREE.MathUtils.clamp((progress - index * 0.05) / 0.45, 0, 1);
|
||||||
|
material.opacity = (1 - bandProgress) * 0.3;
|
||||||
|
bar.position.x = toBlack ? -1.2 + bandProgress * 2.4 : 1.2 - bandProgress * 2.4;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousAutoClear = this.renderer.autoClear;
|
||||||
|
this.renderer.autoClear = false;
|
||||||
|
this.renderer.setRenderTarget(null);
|
||||||
|
this.renderer.render(this.compositeScene, this.compositeCamera);
|
||||||
|
this.renderer.autoClear = previousAutoClear;
|
||||||
|
}
|
||||||
|
|
||||||
private finishTransition() {
|
private finishTransition() {
|
||||||
if (!this.transitionRuntime) {
|
if (!this.transitionRuntime) {
|
||||||
return;
|
return;
|
||||||
@ -2303,6 +2396,7 @@ export class RenderSurface {
|
|||||||
disposeSceneRuntime(this.transitionRuntime.to);
|
disposeSceneRuntime(this.transitionRuntime.to);
|
||||||
this.transitionRuntime = null;
|
this.transitionRuntime = null;
|
||||||
}
|
}
|
||||||
|
this.blackoutRuntime = null;
|
||||||
this.activePresentationKey = undefined;
|
this.activePresentationKey = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -28,16 +28,6 @@ export const defaultCollections: Collection[] = [
|
|||||||
assetIds: [],
|
assetIds: [],
|
||||||
tagIds: ["tag-archive", "tag-portrait"]
|
tagIds: ["tag-archive", "tag-portrait"]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: "collection-favorites",
|
|
||||||
name: "Favorites",
|
|
||||||
kind: "favorites",
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
description: "Operator-trusted images for flexible live use.",
|
|
||||||
locked: false,
|
|
||||||
assetIds: [],
|
|
||||||
tagIds: ["tag-portrait", "tag-quiet"]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "collection-choir-swell",
|
id: "collection-choir-swell",
|
||||||
name: "Choir Swell",
|
name: "Choir Swell",
|
||||||
|
|||||||
@ -119,7 +119,7 @@ export const defaultSceneDefinitions: SceneDefinition[] = [
|
|||||||
complexity: "medium",
|
complexity: "medium",
|
||||||
performanceRisk: "low",
|
performanceRisk: "low",
|
||||||
inputRules: {
|
inputRules: {
|
||||||
minAssets: 1,
|
minAssets: 0,
|
||||||
maxAssets: 3,
|
maxAssets: 3,
|
||||||
recommendedTags: ["portrait", "hands", "still life", "quiet"]
|
recommendedTags: ["portrait", "hands", "still life", "quiet"]
|
||||||
},
|
},
|
||||||
@ -153,7 +153,7 @@ export const defaultSceneDefinitions: SceneDefinition[] = [
|
|||||||
complexity: "medium",
|
complexity: "medium",
|
||||||
performanceRisk: "low",
|
performanceRisk: "low",
|
||||||
inputRules: {
|
inputRules: {
|
||||||
minAssets: 1,
|
minAssets: 0,
|
||||||
maxAssets: 2,
|
maxAssets: 2,
|
||||||
recommendedTags: ["portrait", "window", "still life"]
|
recommendedTags: ["portrait", "window", "still life"]
|
||||||
},
|
},
|
||||||
@ -187,7 +187,7 @@ export const defaultSceneDefinitions: SceneDefinition[] = [
|
|||||||
complexity: "medium",
|
complexity: "medium",
|
||||||
performanceRisk: "medium",
|
performanceRisk: "medium",
|
||||||
inputRules: {
|
inputRules: {
|
||||||
minAssets: 1,
|
minAssets: 0,
|
||||||
maxAssets: 3,
|
maxAssets: 3,
|
||||||
recommendedTags: ["portrait", "flowers", "still life", "archive"]
|
recommendedTags: ["portrait", "flowers", "still life", "archive"]
|
||||||
},
|
},
|
||||||
@ -221,7 +221,7 @@ export const defaultSceneDefinitions: SceneDefinition[] = [
|
|||||||
complexity: "medium",
|
complexity: "medium",
|
||||||
performanceRisk: "low",
|
performanceRisk: "low",
|
||||||
inputRules: {
|
inputRules: {
|
||||||
minAssets: 2,
|
minAssets: 0,
|
||||||
maxAssets: 4,
|
maxAssets: 4,
|
||||||
recommendedTags: ["portrait", "family", "still life", "room"]
|
recommendedTags: ["portrait", "family", "still life", "room"]
|
||||||
},
|
},
|
||||||
@ -255,7 +255,7 @@ export const defaultSceneDefinitions: SceneDefinition[] = [
|
|||||||
complexity: "medium",
|
complexity: "medium",
|
||||||
performanceRisk: "low",
|
performanceRisk: "low",
|
||||||
inputRules: {
|
inputRules: {
|
||||||
minAssets: 3,
|
minAssets: 0,
|
||||||
maxAssets: 4,
|
maxAssets: 4,
|
||||||
recommendedTags: ["family", "portrait", "group", "archive"]
|
recommendedTags: ["family", "portrait", "group", "archive"]
|
||||||
},
|
},
|
||||||
@ -289,7 +289,7 @@ export const defaultSceneDefinitions: SceneDefinition[] = [
|
|||||||
complexity: "medium",
|
complexity: "medium",
|
||||||
performanceRisk: "medium",
|
performanceRisk: "medium",
|
||||||
inputRules: {
|
inputRules: {
|
||||||
minAssets: 2,
|
minAssets: 0,
|
||||||
maxAssets: 4,
|
maxAssets: 4,
|
||||||
recommendedTags: ["portrait", "still life", "room", "window"]
|
recommendedTags: ["portrait", "still life", "room", "window"]
|
||||||
},
|
},
|
||||||
@ -323,7 +323,7 @@ export const defaultSceneDefinitions: SceneDefinition[] = [
|
|||||||
complexity: "low",
|
complexity: "low",
|
||||||
performanceRisk: "low",
|
performanceRisk: "low",
|
||||||
inputRules: {
|
inputRules: {
|
||||||
minAssets: 1,
|
minAssets: 0,
|
||||||
maxAssets: 4,
|
maxAssets: 4,
|
||||||
recommendedTags: ["live", "portrait", "recent"]
|
recommendedTags: ["live", "portrait", "recent"]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -198,7 +198,8 @@ const ensureSafeCue = (cues: Cue[]) => {
|
|||||||
|
|
||||||
const mergeCollections = (state: RepositoryState, importedAssetIds: string[]) => {
|
const mergeCollections = (state: RepositoryState, importedAssetIds: string[]) => {
|
||||||
const defaultCollectionIds = new Set(defaultCollections.map((collection) => collection.id));
|
const defaultCollectionIds = new Set(defaultCollections.map((collection) => collection.id));
|
||||||
const existingCollectionMap = new Map(state.collections.map((collection) => [collection.id, collection] as const));
|
const existingCollections = state.collections.filter((collection) => collection.kind !== "favorites");
|
||||||
|
const existingCollectionMap = new Map(existingCollections.map((collection) => [collection.id, collection] as const));
|
||||||
|
|
||||||
const mergedDefaults = defaultCollections.map((collection) => {
|
const mergedDefaults = defaultCollections.map((collection) => {
|
||||||
const existing = existingCollectionMap.get(collection.id);
|
const existing = existingCollectionMap.get(collection.id);
|
||||||
@ -218,7 +219,7 @@ const mergeCollections = (state: RepositoryState, importedAssetIds: string[]) =>
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const customCollections = state.collections
|
const customCollections = existingCollections
|
||||||
.filter((collection) => !defaultCollectionIds.has(collection.id))
|
.filter((collection) => !defaultCollectionIds.has(collection.id))
|
||||||
.map((collection) => ({
|
.map((collection) => ({
|
||||||
...collection,
|
...collection,
|
||||||
@ -296,7 +297,7 @@ const buildGeneratedCueDraft = (state: RepositoryState, payload: CueGeneratePayl
|
|||||||
const usablePool =
|
const usablePool =
|
||||||
assetPool.length >= scene.inputRules.minAssets ? assetPool : approvedAssets;
|
assetPool.length >= scene.inputRules.minAssets ? assetPool : approvedAssets;
|
||||||
const maxAssets = Math.min(scene.inputRules.maxAssets ?? usablePool.length, usablePool.length);
|
const maxAssets = Math.min(scene.inputRules.maxAssets ?? usablePool.length, usablePool.length);
|
||||||
const minAssets = Math.min(scene.inputRules.minAssets, maxAssets);
|
const minAssets = Math.min(Math.max(scene.inputRules.minAssets, usablePool.length > 0 ? 1 : 0), maxAssets);
|
||||||
const assetCount = Math.max(minAssets, Math.round(random(minAssets, maxAssets + 0.49)));
|
const assetCount = Math.max(minAssets, Math.round(random(minAssets, maxAssets + 0.49)));
|
||||||
const selectedAssets = shuffle(usablePool).slice(0, assetCount);
|
const selectedAssets = shuffle(usablePool).slice(0, assetCount);
|
||||||
|
|
||||||
@ -543,10 +544,6 @@ export class StateStore {
|
|||||||
submission.status = "approved_all";
|
submission.status = "approved_all";
|
||||||
asset.moderationStatus = "approved";
|
asset.moderationStatus = "approved";
|
||||||
asset.approvedAt = new Date().toISOString();
|
asset.approvedAt = new Date().toISOString();
|
||||||
const favorites = state.collections.find((collection) => collection.kind === "favorites");
|
|
||||||
if (favorites && !favorites.assetIds.includes(assetId)) {
|
|
||||||
favorites.assetIds.unshift(assetId);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
submission.status = "pending_moderation";
|
submission.status = "pending_moderation";
|
||||||
}
|
}
|
||||||
@ -619,13 +616,6 @@ export class StateStore {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
if (payload.decision === "approved") {
|
|
||||||
const favorites = state.collections.find((collection) => collection.kind === "favorites");
|
|
||||||
if (favorites && !favorites.assetIds.includes(assetId)) {
|
|
||||||
favorites.assetIds.unshift(assetId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.collectionIds?.length) {
|
if (payload.collectionIds?.length) {
|
||||||
const collections = state.collections.filter((collection) => payload.collectionIds?.includes(collection.id));
|
const collections = state.collections.filter((collection) => payload.collectionIds?.includes(collection.id));
|
||||||
for (const collection of collections) {
|
for (const collection of collections) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user