diff --git a/frontend/src/components/PacketVisualizer3D.tsx b/frontend/src/components/PacketVisualizer3D.tsx index 6022231..c22c1c5 100644 --- a/frontend/src/components/PacketVisualizer3D.tsx +++ b/frontend/src/components/PacketVisualizer3D.tsx @@ -25,6 +25,7 @@ import { type ContactAdvertPathSummary, } from '../types'; import { getRawPacketObservationKey } from '../utils/rawPacketIdentity'; +import { getVisualizerSettings, saveVisualizerSettings } from '../utils/visualizerSettings'; import { Checkbox } from './ui/checkbox'; import { type NodeType, @@ -34,7 +35,6 @@ import { COLORS, PARTICLE_COLOR_MAP, PARTICLE_SPEED, - DEFAULT_OBSERVATION_WINDOW_SEC, PACKET_LEGEND_ITEMS, parsePacket, getPacketLabel, @@ -1007,19 +1007,55 @@ export function PacketVisualizer3D({ const mouseRef = useRef(new THREE.Vector2()); // Options - const [showAmbiguousPaths, setShowAmbiguousPaths] = useState(true); - const [showAmbiguousNodes, setShowAmbiguousNodes] = useState(false); - const [useAdvertPathHints, setUseAdvertPathHints] = useState(true); - const [splitAmbiguousByTraffic, setSplitAmbiguousByTraffic] = useState(true); - const [chargeStrength, setChargeStrength] = useState(-200); - const [observationWindowSec, setObservationWindowSec] = useState(DEFAULT_OBSERVATION_WINDOW_SEC); - const [letEmDrift, setLetEmDrift] = useState(true); - const [particleSpeedMultiplier, setParticleSpeedMultiplier] = useState(2); - const [showControls, setShowControls] = useState(true); - const [autoOrbit, setAutoOrbit] = useState(false); - const [pruneStaleNodes, setPruneStaleNodes] = useState(false); + const [savedSettings] = useState(getVisualizerSettings); + const [showAmbiguousPaths, setShowAmbiguousPaths] = useState(savedSettings.showAmbiguousPaths); + const [showAmbiguousNodes, setShowAmbiguousNodes] = useState(savedSettings.showAmbiguousNodes); + const [useAdvertPathHints, setUseAdvertPathHints] = useState(savedSettings.useAdvertPathHints); + const [splitAmbiguousByTraffic, setSplitAmbiguousByTraffic] = useState( + savedSettings.splitAmbiguousByTraffic + ); + const [chargeStrength, setChargeStrength] = useState(savedSettings.chargeStrength); + const [observationWindowSec, setObservationWindowSec] = useState( + savedSettings.observationWindowSec + ); + const [letEmDrift, setLetEmDrift] = useState(savedSettings.letEmDrift); + const [particleSpeedMultiplier, setParticleSpeedMultiplier] = useState( + savedSettings.particleSpeedMultiplier + ); + const [showControls, setShowControls] = useState(savedSettings.showControls); + const [autoOrbit, setAutoOrbit] = useState(savedSettings.autoOrbit); + const [pruneStaleNodes, setPruneStaleNodes] = useState(savedSettings.pruneStaleNodes); const [repeaterAdvertPaths, setRepeaterAdvertPaths] = useState([]); + // Persist visualizer controls to localStorage on change + useEffect(() => { + saveVisualizerSettings({ + showAmbiguousPaths, + showAmbiguousNodes, + useAdvertPathHints, + splitAmbiguousByTraffic, + chargeStrength, + observationWindowSec, + letEmDrift, + particleSpeedMultiplier, + pruneStaleNodes, + autoOrbit, + showControls, + }); + }, [ + showAmbiguousPaths, + showAmbiguousNodes, + useAdvertPathHints, + splitAmbiguousByTraffic, + chargeStrength, + observationWindowSec, + letEmDrift, + particleSpeedMultiplier, + pruneStaleNodes, + autoOrbit, + showControls, + ]); + useEffect(() => { let cancelled = false; diff --git a/frontend/src/utils/visualizerSettings.ts b/frontend/src/utils/visualizerSettings.ts new file mode 100644 index 0000000..82b17ab --- /dev/null +++ b/frontend/src/utils/visualizerSettings.ts @@ -0,0 +1,89 @@ +const VISUALIZER_SETTINGS_KEY = 'remoteterm-visualizer-settings'; + +export interface VisualizerSettings { + showAmbiguousPaths: boolean; + showAmbiguousNodes: boolean; + useAdvertPathHints: boolean; + splitAmbiguousByTraffic: boolean; + chargeStrength: number; + observationWindowSec: number; + letEmDrift: boolean; + particleSpeedMultiplier: number; + pruneStaleNodes: boolean; + autoOrbit: boolean; + showControls: boolean; +} + +export const VISUALIZER_DEFAULTS: VisualizerSettings = { + showAmbiguousPaths: true, + showAmbiguousNodes: false, + useAdvertPathHints: true, + splitAmbiguousByTraffic: true, + chargeStrength: -200, + observationWindowSec: 15, + letEmDrift: true, + particleSpeedMultiplier: 2, + pruneStaleNodes: false, + autoOrbit: false, + showControls: true, +}; + +export function getVisualizerSettings(): VisualizerSettings { + try { + const raw = localStorage.getItem(VISUALIZER_SETTINGS_KEY); + if (!raw) return { ...VISUALIZER_DEFAULTS }; + const parsed = JSON.parse(raw) as Partial; + return { + showAmbiguousPaths: + typeof parsed.showAmbiguousPaths === 'boolean' + ? parsed.showAmbiguousPaths + : VISUALIZER_DEFAULTS.showAmbiguousPaths, + showAmbiguousNodes: + typeof parsed.showAmbiguousNodes === 'boolean' + ? parsed.showAmbiguousNodes + : VISUALIZER_DEFAULTS.showAmbiguousNodes, + useAdvertPathHints: + typeof parsed.useAdvertPathHints === 'boolean' + ? parsed.useAdvertPathHints + : VISUALIZER_DEFAULTS.useAdvertPathHints, + splitAmbiguousByTraffic: + typeof parsed.splitAmbiguousByTraffic === 'boolean' + ? parsed.splitAmbiguousByTraffic + : VISUALIZER_DEFAULTS.splitAmbiguousByTraffic, + chargeStrength: + typeof parsed.chargeStrength === 'number' + ? parsed.chargeStrength + : VISUALIZER_DEFAULTS.chargeStrength, + observationWindowSec: + typeof parsed.observationWindowSec === 'number' + ? parsed.observationWindowSec + : VISUALIZER_DEFAULTS.observationWindowSec, + letEmDrift: + typeof parsed.letEmDrift === 'boolean' ? parsed.letEmDrift : VISUALIZER_DEFAULTS.letEmDrift, + particleSpeedMultiplier: + typeof parsed.particleSpeedMultiplier === 'number' + ? parsed.particleSpeedMultiplier + : VISUALIZER_DEFAULTS.particleSpeedMultiplier, + pruneStaleNodes: + typeof parsed.pruneStaleNodes === 'boolean' + ? parsed.pruneStaleNodes + : VISUALIZER_DEFAULTS.pruneStaleNodes, + autoOrbit: + typeof parsed.autoOrbit === 'boolean' ? parsed.autoOrbit : VISUALIZER_DEFAULTS.autoOrbit, + showControls: + typeof parsed.showControls === 'boolean' + ? parsed.showControls + : VISUALIZER_DEFAULTS.showControls, + }; + } catch { + return { ...VISUALIZER_DEFAULTS }; + } +} + +export function saveVisualizerSettings(settings: VisualizerSettings): void { + try { + localStorage.setItem(VISUALIZER_SETTINGS_KEY, JSON.stringify(settings)); + } catch { + // localStorage may be unavailable + } +}