Files
DECNET/decnet_web/src/components/MazeNET/useMazeLayoutStore.ts
anti f2b3393669 chore: relicense to AGPL-3.0-or-later and add SPDX headers
Replaces LICENSE (GPLv3 -> AGPLv3) and prepends
`SPDX-License-Identifier: AGPL-3.0-or-later` to every source file
across decnet/, decnet_web/, tests/, scripts/, and tools/.

Rationale: closes the GPLv3 ASP loophole so any party operating a
modified DECNET as a network service must offer their modified
source. Personal copyright (Samuel Paschuan) + inbound=outbound
contributions make a future unilateral relicense infeasible.

- LICENSE: full AGPL-3.0 text (gnu.org/licenses/agpl-3.0.txt)
- COPYRIGHT: project copyright notice
- tools/add_spdx_headers.py: idempotent header injector
  (shebang- and PEP 263-aware)

Touches 1565 source files (.py, .ts, .tsx, .js, .jsx, .css, .sh).
No behavior change; comments only.
2026-05-22 21:04:16 -04:00

113 lines
3.5 KiB
TypeScript

// SPDX-License-Identifier: AGPL-3.0-or-later
import { useCallback, useEffect, useRef } from 'react';
import type { Net, MazeNode } from './types';
/** Per-topology canvas layout persisted to localStorage. Keyed by
* topology id so two topologies don't share positions. Stored keys
* for missing LAN/decky ids are pruned on save (self-heal). */
interface NetLayout { x: number; y: number; w: number; h: number }
interface NodeLayout { x: number; y: number }
export interface LayoutSnapshot {
nets: Record<string, NetLayout>;
nodes: Record<string, NodeLayout>;
}
const EMPTY: LayoutSnapshot = { nets: {}, nodes: {} };
const SAVE_DEBOUNCE_MS = 300;
function storageKey(topologyId: string): string {
return `mazenet.layout.${topologyId}`;
}
export function loadLayout(topologyId: string | null): LayoutSnapshot {
if (!topologyId) return EMPTY;
try {
const raw = window.localStorage.getItem(storageKey(topologyId));
if (!raw) return EMPTY;
const parsed = JSON.parse(raw) as Partial<LayoutSnapshot>;
return {
nets: parsed.nets ?? {},
nodes: parsed.nodes ?? {},
};
} catch {
return EMPTY;
}
}
function saveLayout(topologyId: string, snap: LayoutSnapshot): void {
try {
window.localStorage.setItem(storageKey(topologyId), JSON.stringify(snap));
} catch {
/* quota exhausted or private mode — layout reverts to grid. */
}
}
/** Apply stored positions on top of grid-laid-out entities. Entities
* without a stored entry keep their grid position. */
export function applyLayout(
nets: Net[],
nodes: MazeNode[],
layout: LayoutSnapshot,
): { nets: Net[]; nodes: MazeNode[] } {
const adjustedNets = nets.map((n) => {
const saved = layout.nets[n.id];
return saved ? { ...n, x: saved.x, y: saved.y, w: saved.w, h: saved.h } : n;
});
const adjustedNodes = nodes.map((n) => {
const saved = layout.nodes[n.id];
return saved ? { ...n, x: saved.x, y: saved.y } : n;
});
return { nets: adjustedNets, nodes: adjustedNodes };
}
/** Debounced writer — every nets/nodes change is captured and flushed
* to localStorage after a short idle window. Also prunes entries for
* LANs / deckies that no longer exist in the current topology. */
export function useLayoutPersistor(
topologyId: string | null,
nets: Net[],
nodes: MazeNode[],
): void {
const timerRef = useRef<number | null>(null);
useEffect(() => {
if (!topologyId) return;
if (timerRef.current !== null) window.clearTimeout(timerRef.current);
timerRef.current = window.setTimeout(() => {
const snap: LayoutSnapshot = { nets: {}, nodes: {} };
for (const n of nets) {
if (n.kind === 'internet') continue;
snap.nets[n.id] = { x: n.x, y: n.y, w: n.w, h: n.h };
}
for (const n of nodes) {
snap.nodes[n.id] = { x: n.x, y: n.y };
}
saveLayout(topologyId, snap);
timerRef.current = null;
}, SAVE_DEBOUNCE_MS);
return () => {
if (timerRef.current !== null) {
window.clearTimeout(timerRef.current);
timerRef.current = null;
}
};
}, [topologyId, nets, nodes]);
}
/** Clear the stored layout for a topology — call after delete so stale
* entries don't linger forever. */
export function clearLayout(topologyId: string): void {
try {
window.localStorage.removeItem(storageKey(topologyId));
} catch {
/* ignore */
}
}
/** Hook form for consumers that prefer a stable callback. */
export function useClearLayout(): (topologyId: string) => void {
return useCallback((id: string) => clearLayout(id), []);
}