Skip to content

Commit 92bbaeb

Browse files
committed
fix(website): persist FPV camera position to the scene URL on movement settle
1 parent a087231 commit 92bbaeb

1 file changed

Lines changed: 33 additions & 6 deletions

File tree

website/src/components/VanillaScene/VanillaScene.tsx

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,9 @@ export function VanillaScene({
391391
onBuildRef.current = onBuild;
392392
const onCameraChangeRef = useRef(onCameraChange);
393393
onCameraChangeRef.current = onCameraChange;
394+
// Debounce handle for syncing FPV position to options/URL once movement
395+
// settles (FPV emits per-frame; we only want the resting spot).
396+
const fpvSettleTimerRef = useRef(0);
394397
const onSelectionChangeRef = useRef(onSelectionChange);
395398
onSelectionChangeRef.current = onSelectionChange;
396399
const onHoverChangeRef = useRef(onHoverChange);
@@ -952,14 +955,20 @@ export function VanillaScene({
952955
const scene = sceneRef.current;
953956
const camera = cameraRef.current;
954957
if (!scene || !camera) return;
958+
// FPV is authoritative over the live camera. We DO sync FPV's position back
959+
// to options (debounced, so the scene URL captures where you walked), but
960+
// re-applying that camera here would fight FPV's own per-frame writes — its
961+
// target is a derived look-ahead point, not the stored eye. Skip; the
962+
// initial pose still restores via scene creation (Effect 1) on load.
963+
if (options.dragMode === "fpv") return;
955964
camera.update({
956965
rotX: options.rotX,
957966
rotY: options.rotY,
958967
zoom: options.zoom * LEGACY_ZOOM_COMPAT,
959968
target: options.target as Vec3,
960969
});
961970
scene.applyCamera();
962-
}, [options.rotX, options.rotY, options.zoom, options.target]);
971+
}, [options.rotX, options.rotY, options.zoom, options.target, options.dragMode]);
963972

964973
// Effect 2b — lighting + shadow updates. Runs only when the light, shadow,
965974
// textureLighting, or ground color actually change (sliders, not camera).
@@ -1039,11 +1048,27 @@ export function VanillaScene({
10391048
lookSensitivity: options.fpvLookSensitivity,
10401049
invertY: options.fpvInvertY,
10411050
});
1042-
// FPV is authoritative over the camera while engaged — don't echo
1043-
// its per-frame writes back into React state; that round-trip fights
1044-
// the rAF tick and causes visible jitter on mouselook and walk.
1045-
// The React side picks up the final camera state when the user
1046-
// exits FPV mode (next controls rebuild reads scene.getOptions()).
1051+
// FPV is authoritative over the camera while engaged — don't echo its
1052+
// per-frame writes back into React state; that round-trip fights the
1053+
// rAF tick and jitters mouselook/walk. But once movement SETTLES
1054+
// (~900ms idle) we sync the resting pose to options so the scene URL
1055+
// captures where you walked. We store the EYE position (getOrigin) as
1056+
// `target`: on reload FPV re-seeds its origin from camera.target, so
1057+
// the eye lands back at that spot. The camera-apply effect skips fpv
1058+
// mode, so this write never fights the live controls.
1059+
fpv.addEventListener("change", () => {
1060+
if (fpvSettleTimerRef.current) window.clearTimeout(fpvSettleTimerRef.current);
1061+
fpvSettleTimerRef.current = window.setTimeout(() => {
1062+
const st = scene.camera.state;
1063+
const eye = fpv.getOrigin();
1064+
onCameraChangeRef.current?.({
1065+
rotX: st.rotX ?? 90,
1066+
rotY: st.rotY ?? 0,
1067+
zoom: (st.zoom ?? 1) / LEGACY_ZOOM_COMPAT,
1068+
target: [eye[0], eye[1], eye[2]],
1069+
});
1070+
}, 900);
1071+
});
10471072
return fpv;
10481073
}
10491074
const factory = options.dragMode === "pan" ? createPolyMapControls : createPolyOrbitControls;
@@ -1061,11 +1086,13 @@ export function VanillaScene({
10611086
return controls;
10621087
};
10631088
if (controlsRef.current) controlsRef.current.destroy();
1089+
if (fpvSettleTimerRef.current) { window.clearTimeout(fpvSettleTimerRef.current); fpvSettleTimerRef.current = 0; }
10641090
controlsRef.current = buildControls();
10651091
return () => {
10661092
// Effect re-runs when deps change — destroy only on full unmount,
10671093
// which is signaled by the scene Effect 1 cleanup destroying scene.
10681094
// Until then, the next effect run will reuse + update controlsRef.
1095+
if (fpvSettleTimerRef.current) { window.clearTimeout(fpvSettleTimerRef.current); fpvSettleTimerRef.current = 0; }
10691096
};
10701097
}, [
10711098
options.renderer,

0 commit comments

Comments
 (0)