diff --git a/website/src/components/GalleryWorkbench/hooks/useRouteSync.ts b/website/src/components/GalleryWorkbench/hooks/useRouteSync.ts index e52b07af..9b55c7ba 100644 --- a/website/src/components/GalleryWorkbench/hooks/useRouteSync.ts +++ b/website/src/components/GalleryWorkbench/hooks/useRouteSync.ts @@ -42,6 +42,10 @@ interface SerializedGallerySceneOptions { shadow?: boolean; self?: boolean; reach?: number; + sp?: boolean; + sd?: number; + sst?: SceneOptionsState["shadowStyle"]; + sfa?: boolean; ground?: boolean; gc?: string; fl?: boolean; @@ -101,6 +105,10 @@ const COMPACT_KEY_BY_OPTION: Record = { shadow: "S", self: "Z", reach: "E", + sp: "D", + sd: "F", + sst: "H", + sfa: "I", ground: "g", gc: "G", fl: "L", @@ -134,6 +142,7 @@ const BOOLEAN_OPTIONS = new Set([ "ap", "c", "i", "ar", "axes", "sel", "hov", "helper", "solid", "fill", "outline", "shadow", "ground", "fl", "fm", "fj", "fc", "fiy", + "self", "sp", "sfa", ]); function getRoutePresetValue(): string { @@ -274,6 +283,10 @@ function sceneOptionsPayload( addBoolean(out, "shadow", options.castShadow, defaults.castShadow); addBoolean(out, "self", options.selfShadow, defaults.selfShadow); addNumber(out, "reach", options.shadowMaxExtend, defaults.shadowMaxExtend); + addBoolean(out, "sp", options.shadowParametric, defaults.shadowParametric); + addNumber(out, "sd", options.shadowDefinition, defaults.shadowDefinition); + addString(out, "sst", options.shadowStyle, defaults.shadowStyle); + addBoolean(out, "sfa", options.shadowFollowAnimation, defaults.shadowFollowAnimation); addBoolean(out, "ground", options.showGround, defaults.showGround); addString(out, "gc", options.groundColor, defaults.groundColor); addBoolean(out, "fl", options.fpvLook, defaults.fpvLook); @@ -377,6 +390,9 @@ function encodeCompactValue(key: SerializedGallerySceneOptionKey, value: Seriali if (key === "drag" && (value === "orbit" || value === "pan" || value === "fpv")) { return encodeEnum(value, { orbit: "o", pan: "p", fpv: "f" }); } + if (key === "sst" && (value === "vector" || value === "pixel")) { + return encodeEnum(value, { vector: "v", pixel: "p" }); + } return typeof value === "string" ? value : undefined; } @@ -444,6 +460,10 @@ function isDragMode(value: unknown): value is SceneOptionsState["dragMode"] { return value === "orbit" || value === "pan" || value === "fpv"; } +function isShadowStyle(value: unknown): value is SceneOptionsState["shadowStyle"] { + return value === "vector" || value === "pixel"; +} + function isVec3(value: unknown): value is SceneTarget { return Array.isArray(value) && value.length === 3 && @@ -493,6 +513,10 @@ function sceneOptionsFromPayload(o: SerializedGallerySceneOptions): Partial { + if (fpvSettleTimerRef.current) window.clearTimeout(fpvSettleTimerRef.current); + fpvSettleTimerRef.current = window.setTimeout(() => { + const st = scene.camera.state; + const eye = fpv.getOrigin(); + onCameraChangeRef.current?.({ + rotX: st.rotX ?? 90, + rotY: st.rotY ?? 0, + zoom: (st.zoom ?? 1) / LEGACY_ZOOM_COMPAT, + target: [eye[0], eye[1], eye[2]], + }); + }, 900); + }); return fpv; } const factory = options.dragMode === "pan" ? createPolyMapControls : createPolyOrbitControls; @@ -1061,11 +1086,13 @@ export function VanillaScene({ return controls; }; if (controlsRef.current) controlsRef.current.destroy(); + if (fpvSettleTimerRef.current) { window.clearTimeout(fpvSettleTimerRef.current); fpvSettleTimerRef.current = 0; } controlsRef.current = buildControls(); return () => { // Effect re-runs when deps change — destroy only on full unmount, // which is signaled by the scene Effect 1 cleanup destroying scene. // Until then, the next effect run will reuse + update controlsRef. + if (fpvSettleTimerRef.current) { window.clearTimeout(fpvSettleTimerRef.current); fpvSettleTimerRef.current = 0; } }; }, [ options.renderer,