()
| 103 | * - Handles browser back/forward navigation |
| 104 | */ |
| 105 | export function useUrlState() { |
| 106 | const { controls } = useThree(); |
| 107 | const reconstruction = useReconstructionStore((s) => s.reconstruction); |
| 108 | const flyToState = useCameraStore((s) => s.flyToState); |
| 109 | |
| 110 | // Track whether we've applied initial state from URL |
| 111 | const hasAppliedInitialState = useRef(false); |
| 112 | const updateTimeoutRef = useRef<number | null>(null); |
| 113 | const lastEncodedState = useRef<string>(''); |
| 114 | |
| 115 | /** |
| 116 | * Update URL hash with current camera state (debounced) |
| 117 | */ |
| 118 | const updateUrlHash = useCallback(() => { |
| 119 | if (updateTimeoutRef.current !== null) { |
| 120 | window.clearTimeout(updateTimeoutRef.current); |
| 121 | } |
| 122 | |
| 123 | updateTimeoutRef.current = window.setTimeout(() => { |
| 124 | const viewState = getControlsViewState(controls); |
| 125 | if (!viewState) return; |
| 126 | |
| 127 | const encoded = encodeCameraState(viewState); |
| 128 | const hashUpdate = getNextCameraHashUpdate({ |
| 129 | href: window.location.href, |
| 130 | encodedState: encoded, |
| 131 | lastEncodedState: lastEncodedState.current, |
| 132 | }); |
| 133 | if (!hashUpdate) return; |
| 134 | |
| 135 | lastEncodedState.current = hashUpdate.encodedState; |
| 136 | window.history.replaceState(null, '', hashUpdate.url); |
| 137 | }, URL_UPDATE_DEBOUNCE_MS); |
| 138 | }, [controls]); |
| 139 | |
| 140 | /** |
| 141 | * Restore camera state from URL hash |
| 142 | */ |
| 143 | const restoreFromHash = useCallback(async () => { |
| 144 | const hash = window.location.hash; |
| 145 | if (!hash) return false; |
| 146 | |
| 147 | const state = await decodeCameraStateFromHash(hash); |
| 148 | if (!state) return false; |
| 149 | |
| 150 | flyToState(state); |
| 151 | lastEncodedState.current = encodeCameraState(state); |
| 152 | return true; |
| 153 | }, [flyToState]); |
| 154 | |
| 155 | // Apply initial state from URL when reconstruction loads |
| 156 | useEffect(() => { |
| 157 | if (!reconstruction || hasAppliedInitialState.current) return; |
| 158 | |
| 159 | restoreFromHash().then((applied) => { |
| 160 | hasAppliedInitialState.current = true; |
| 161 | if (applied) { |
| 162 | appLogger.info('[URL State] Restored camera state from URL hash'); |
nothing calls this directly
no test coverage detected