Skip to content
File

Blob: worker/src/client/exhibit/diagram/wires.tsx

typescript101 lines
1import { useEffect, useState } from 'react';
2import type { RefObject } from 'react';
3import { anchors } from './diagram-model.ts';
4import type { Branch, Selection } from './diagram-model.ts';
5import { layoutWires } from './wire-layout.ts';
6import type { BranchPoints, Point, Wire, WireLayout } from './wire-layout.ts';
7 
8function measureLayout(root: HTMLDivElement, desktop: boolean): WireLayout {
9 const bounds = root.getBoundingClientRect();
10 // The overlay starts inside the container border, like absolute children.
11 const base = { left: bounds.left + root.clientLeft, top: bounds.top + root.clientTop };
12 const rect = (anchor: string) => {
13 const node = root.querySelector(`[data-anchor="${anchor}"]`);
14 if (!node) throw new Error(`Missing diagram anchor: ${anchor}`);
15 return node.getBoundingClientRect();
16 };
17 const point = (anchor: string, edge: 'left' | 'right' | 'top'): Point => {
18 const box = rect(anchor);
19 return {
20 x:
21 (edge === 'top' ? box.left + box.width / 2 : edge === 'left' ? box.left : box.right) -
22 base.left,
23 y: (edge === 'top' ? box.top : box.top + box.height / 2) - base.top,
24 };
25 };
26 if (!desktop)
27 return {
28 mode: 'mobile',
29 source: point(anchors.wifi, 'right'),
30 relay: point(anchors.sfuInput, 'top'),
31 boardBottom: rect(anchors.board).bottom - base.top,
32 };
33 const branch = (name: Branch): BranchPoints => ({
34 source: point(anchors.sources[name], 'right'),
35 relayIn: point(anchors.relay[name], 'left'),
36 relayOut: point(anchors.relay[name], 'right'),
37 receiver: point(anchors.receivers[name], 'left'),
38 });
39 return {
40 mode: 'desktop',
41 branches: { audio: branch('audio'), spectrum: branch('spectrum'), robot: branch('robot') },
42 };
43}
44 
45export function Wires({
46 container,
47 active,
48 selected,
49 returning,
50}: {
51 container: RefObject<HTMLDivElement | null>;
52 active: Record<Branch, boolean>;
53 selected: Selection | null;
54 returning: boolean;
55}) {
56 const [wires, setWires] = useState<Wire[]>([]);
57 useEffect(() => {
58 const root = container.current;
59 if (!root) return;
60 // Keep the drawing's orientation aligned with Tailwind's lg breakpoint.
61 const desktop = matchMedia('(min-width: 64rem)');
62 const measure = () => setWires(layoutWires(measureLayout(root, desktop.matches)));
63 const observer = new ResizeObserver(measure);
64 observer.observe(root);
65 root.querySelectorAll('[data-anchor]').forEach((node) => observer.observe(node));
66 desktop.addEventListener('change', measure);
67 measure();
68 return () => {
69 observer.disconnect();
70 desktop.removeEventListener('change', measure);
71 };
72 }, [container]);
73 return (
74 <svg className="wires" aria-hidden="true">
75 {wires.map((wire) => {
76 const lit =
77 wire.branch === 'wifi' ? Object.values(active).some(Boolean) : active[wire.branch];
78 const reverse = returning && (wire.branch === 'robot' || wire.branch === 'wifi');
79 const dim =
80 selected &&
81 selected !== wire.branch &&
82 wire.branch !== 'wifi' &&
83 selected !== 'sfu' &&
84 selected !== 'board';
85 return (
86 <g
87 key={`${wire.branch}-${wire.segment}`}
88 className={`wire wire-${wire.branch} ${dim ? 'wire-dim' : ''}`}
89 data-wire={`${wire.branch}-${wire.segment}`}
90 >
91 <path d={wire.d} className="wire-track" />
92 {lit || reverse ? (
93 <path d={wire.d} className={`wire-flow ${reverse ? 'wire-return' : ''}`} />
94 ) : null}
95 </g>
96 );
97 })}
98 </svg>
99 );
100}