@graview/react 0.1.28 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/affordances.d.ts +2 -1
- package/dist/affordances.d.ts.map +1 -1
- package/dist/affordances.js +2 -1
- package/dist/affordances.js.map +1 -1
- package/dist/beside-the-map.d.ts +13 -0
- package/dist/beside-the-map.d.ts.map +1 -0
- package/dist/beside-the-map.js +14 -0
- package/dist/beside-the-map.js.map +1 -0
- package/dist/channels.d.ts +2 -1
- package/dist/channels.d.ts.map +1 -1
- package/dist/channels.js +121 -22
- package/dist/channels.js.map +1 -1
- package/dist/connectors.d.ts +27 -2
- package/dist/connectors.d.ts.map +1 -1
- package/dist/connectors.js +144 -23
- package/dist/connectors.js.map +1 -1
- package/dist/context.d.ts +91 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +98 -16
- package/dist/context.js.map +1 -1
- package/dist/drawing.d.ts +2 -0
- package/dist/drawing.d.ts.map +1 -1
- package/dist/drawing.js +2 -0
- package/dist/drawing.js.map +1 -1
- package/dist/drawn.d.ts +1 -1
- package/dist/drawn.d.ts.map +1 -1
- package/dist/drawn.js +1 -1
- package/dist/drawn.js.map +1 -1
- package/dist/edge-signs.d.ts +12 -2
- package/dist/edge-signs.d.ts.map +1 -1
- package/dist/edge-signs.js +95 -9
- package/dist/edge-signs.js.map +1 -1
- package/dist/go.d.ts +28 -10
- package/dist/go.d.ts.map +1 -1
- package/dist/go.js +14 -5
- package/dist/go.js.map +1 -1
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/last-visit.d.ts +18 -0
- package/dist/last-visit.d.ts.map +1 -0
- package/dist/last-visit.js +79 -0
- package/dist/last-visit.js.map +1 -0
- package/dist/linked-reads.d.ts +3 -0
- package/dist/linked-reads.d.ts.map +1 -0
- package/dist/linked-reads.js +50 -0
- package/dist/linked-reads.js.map +1 -0
- package/dist/map-choice.d.ts +55 -0
- package/dist/map-choice.d.ts.map +1 -0
- package/dist/map-choice.js +73 -0
- package/dist/map-choice.js.map +1 -0
- package/dist/menu-sections.d.ts +67 -0
- package/dist/menu-sections.d.ts.map +1 -0
- package/dist/menu-sections.js +35 -0
- package/dist/menu-sections.js.map +1 -0
- package/dist/past.d.ts +58 -0
- package/dist/past.d.ts.map +1 -0
- package/dist/past.js +70 -0
- package/dist/past.js.map +1 -0
- package/dist/plots.d.ts +30 -24
- package/dist/plots.d.ts.map +1 -1
- package/dist/plots.js +168 -149
- package/dist/plots.js.map +1 -1
- package/dist/popover.d.ts +9 -1
- package/dist/popover.d.ts.map +1 -1
- package/dist/popover.js +3 -1
- package/dist/popover.js.map +1 -1
- package/dist/provider.d.ts +6 -2
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +5 -1
- package/dist/provider.js.map +1 -1
- package/dist/record-links-layer.d.ts +80 -0
- package/dist/record-links-layer.d.ts.map +1 -0
- package/dist/record-links-layer.js +127 -0
- package/dist/record-links-layer.js.map +1 -0
- package/dist/record-links.d.ts +58 -0
- package/dist/record-links.d.ts.map +1 -0
- package/dist/record-links.js +72 -0
- package/dist/record-links.js.map +1 -0
- package/dist/resolved-view.d.ts.map +1 -1
- package/dist/resolved-view.js +4 -0
- package/dist/resolved-view.js.map +1 -1
- package/dist/ribbon-width.d.ts +8 -0
- package/dist/ribbon-width.d.ts.map +1 -0
- package/dist/ribbon-width.js +15 -0
- package/dist/ribbon-width.js.map +1 -0
- package/dist/ribbons-layer.d.ts +56 -0
- package/dist/ribbons-layer.d.ts.map +1 -0
- package/dist/ribbons-layer.js +167 -0
- package/dist/ribbons-layer.js.map +1 -0
- package/dist/ribbons.d.ts +70 -0
- package/dist/ribbons.d.ts.map +1 -0
- package/dist/ribbons.js +400 -0
- package/dist/ribbons.js.map +1 -0
- package/dist/routes.d.ts +6 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +8 -0
- package/dist/routes.js.map +1 -1
- package/dist/scene-camera.d.ts.map +1 -1
- package/dist/scene-camera.js +10 -4
- package/dist/scene-camera.js.map +1 -1
- package/dist/scene-covered.d.ts +8 -0
- package/dist/scene-covered.d.ts.map +1 -0
- package/dist/scene-covered.js +118 -0
- package/dist/scene-covered.js.map +1 -0
- package/dist/scene-hand.d.ts +15 -0
- package/dist/scene-hand.d.ts.map +1 -1
- package/dist/scene-hand.js +45 -1
- package/dist/scene-hand.js.map +1 -1
- package/dist/scene-lines.d.ts +15 -2
- package/dist/scene-lines.d.ts.map +1 -1
- package/dist/scene-lines.js +305 -86
- package/dist/scene-lines.js.map +1 -1
- package/dist/scene-options.d.ts +10 -3
- package/dist/scene-options.d.ts.map +1 -1
- package/dist/scene-options.js +26 -9
- package/dist/scene-options.js.map +1 -1
- package/dist/scene-root.d.ts +7 -0
- package/dist/scene-root.d.ts.map +1 -1
- package/dist/scene-root.js +224 -158
- package/dist/scene-root.js.map +1 -1
- package/dist/tie-labels.d.ts +83 -0
- package/dist/tie-labels.d.ts.map +1 -0
- package/dist/tie-labels.js +98 -0
- package/dist/tie-labels.js.map +1 -0
- package/dist/view-host.d.ts.map +1 -1
- package/dist/view-host.js +7 -2
- package/dist/view-host.js.map +1 -1
- package/dist/view-registry.d.ts +5 -0
- package/dist/view-registry.d.ts.map +1 -1
- package/dist/view-registry.js +2 -6
- package/dist/view-registry.js.map +1 -1
- package/dist/where-drawn.d.ts +3 -1
- package/dist/where-drawn.d.ts.map +1 -1
- package/dist/where-drawn.js +10 -16
- package/dist/where-drawn.js.map +1 -1
- package/package.json +5 -5
package/dist/scene-lines.js
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { humanizeField, SCENE_LAYERS } from "@graview/core";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { counted, humanizeField, labelOf, SCENE_LAYERS } from "@graview/core";
|
|
3
|
+
import { edgeWords } from "@graview/core/words";
|
|
4
|
+
import { provenanceWords, provisionalLinks } from "@graview/core/provenance";
|
|
5
|
+
import { edgeOfSelection } from "@graview/layout";
|
|
6
|
+
import { edgeSelectionId } from "@graview/layout/view-moves";
|
|
7
|
+
import { CONNECTOR_DASH, PROVISIONAL_DASH, PROVISIONAL_OPACITY, connectorStroke, styleFor } from "@graview/render";
|
|
8
|
+
import { Suspense, useEffect, useMemo, useLayoutEffect, useRef, useState } from "react";
|
|
6
9
|
import { kitConnector, useKit } from "./kit.js";
|
|
7
|
-
import { orthogonalPoints, polylineD, routePoint, routedQuadratic } from "./routes.js";
|
|
8
|
-
import {
|
|
10
|
+
import { orthogonalPoints, polylineD, roundedPolylineD, routePoint, routedQuadratic } from "./routes.js";
|
|
11
|
+
import { channelRoute } from "./channels.js";
|
|
12
|
+
import { clipQuadratic, Connectors, connectorStrands, movedBy, tieRoute } from "./connectors.js";
|
|
9
13
|
import { drawnBox, measureVisible, visibleRect } from "./where-drawn.js";
|
|
10
14
|
import { captionRuns } from "./captions.js";
|
|
11
15
|
import { railInset } from "./rails.js";
|
|
16
|
+
import { placeTieLabels, tiesReached } from "./tie-labels.js";
|
|
17
|
+
import { retryingImport } from "@graview/core/retry";
|
|
18
|
+
import { lazyModule } from "./lazy-part.js";
|
|
19
|
+
import { mapChoice } from "./map-choice.js";
|
|
12
20
|
/**
|
|
13
21
|
* EVERY LINE IN THE SCENE, measured against the DOM it is drawn over.
|
|
14
22
|
*
|
|
@@ -32,7 +40,16 @@ import { railInset } from "./rails.js";
|
|
|
32
40
|
* center, so selecting the session lit three of its four drills and left
|
|
33
41
|
* the fourth to a faint line from nowhere.
|
|
34
42
|
*/
|
|
35
|
-
|
|
43
|
+
/**
|
|
44
|
+
* THE LINKS STILL PROVISIONAL, as the lines read them (FR-172): each by its
|
|
45
|
+
* selection id, with the sentence its hover says — who made it and what it
|
|
46
|
+
* was based on. Undefined when there are none, which is nearly always.
|
|
47
|
+
*/
|
|
48
|
+
export function provisionalLines(store) {
|
|
49
|
+
const links = provisionalLinks(store);
|
|
50
|
+
return links.length > 0 ? new Map(links.map((link) => [edgeSelectionId(link.edge.kind, link.edge.from, link.edge.to), provenanceWords(link)])) : undefined;
|
|
51
|
+
}
|
|
52
|
+
export function Lines({ frame, width, height, scheme, overview, selection, emphasis, stageRef, store, graphNodes, onPickEdge, liveOf, holding = false, provisional, focusId = null, beside = false, }) {
|
|
36
53
|
const [asked, remeasure] = useState(0);
|
|
37
54
|
/*
|
|
38
55
|
* MEASURED ONCE, AFTER THE DOM IS THERE. Measuring during render read the
|
|
@@ -51,11 +68,35 @@ export function Lines({ frame, width, height, scheme, overview, selection, empha
|
|
|
51
68
|
* with the world, which is not a new frame, so they follow the hand.
|
|
52
69
|
*/
|
|
53
70
|
const moving = frame.t < 1;
|
|
71
|
+
/*
|
|
72
|
+
* THE CAPTIONS ARE PLACED IN THE SAME COMMIT, measured from the same
|
|
73
|
+
* boxes, so the first measurement can come before them: a road along a
|
|
74
|
+
* gutter went behind words that were not there yet. Where they stood is
|
|
75
|
+
* kept with each measurement, and a frame later, if they stand elsewhere,
|
|
76
|
+
* the lines are measured again.
|
|
77
|
+
*/
|
|
78
|
+
const captionsAt = useRef("");
|
|
79
|
+
/* Where the layout had every card when the lines were measured: a held card's lines move by what it has moved since. */
|
|
80
|
+
const measuredAt = useRef([]);
|
|
81
|
+
const captionsNow = () => [...(stageRef.current?.querySelectorAll("[data-graview-relation] > span") ?? [])].map((el) => { const r = el.getBoundingClientRect(); return `${Math.round(r.left)},${Math.round(r.top)},${Math.round(r.width)}`; }).join(" ");
|
|
54
82
|
useLayoutEffect(() => {
|
|
55
83
|
if (moving || holding)
|
|
56
84
|
return;
|
|
85
|
+
captionsAt.current = captionsNow();
|
|
86
|
+
measuredAt.current = frame.nodes;
|
|
57
87
|
setMeasured(connectorStrands(frame.nodes, frame.connectors, stageRef.current, overview, scheme));
|
|
88
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
58
89
|
}, [frame, moving, holding, selection, overview, graphNodes, scheme, asked, stageRef]);
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (moving || holding || overview || typeof requestAnimationFrame === "undefined")
|
|
92
|
+
return;
|
|
93
|
+
const later = requestAnimationFrame(() => {
|
|
94
|
+
if (captionsNow() !== captionsAt.current)
|
|
95
|
+
remeasure((n) => n + 1);
|
|
96
|
+
});
|
|
97
|
+
return () => cancelAnimationFrame(later);
|
|
98
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
99
|
+
}, [measured]);
|
|
59
100
|
/*
|
|
60
101
|
* A LINE POINTS AT WHERE A THING IS, NOT AT WHERE IT WAS.
|
|
61
102
|
*
|
|
@@ -111,14 +152,45 @@ export function Lines({ frame, width, height, scheme, overview, selection, empha
|
|
|
111
152
|
watch?.disconnect();
|
|
112
153
|
};
|
|
113
154
|
}, [stageRef, views]);
|
|
155
|
+
/*
|
|
156
|
+
* WHICH RECORD A PERSON IS AT: the pointer on it, or the keyboard. A tie
|
|
157
|
+
* the record's card has already said is drawn for that one (`said`);
|
|
158
|
+
* nobody is listened to while no tie waits on it.
|
|
159
|
+
*/
|
|
160
|
+
const [pointed, point] = useState(null);
|
|
161
|
+
const waiting = (measured ?? []).some((strand) => strand.tie !== undefined);
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
const stage = stageRef.current;
|
|
164
|
+
if (!waiting || stage === null)
|
|
165
|
+
return;
|
|
166
|
+
const at = (event) => {
|
|
167
|
+
const mark = event.target?.closest?.("[data-graview-pick], [data-graview-view]");
|
|
168
|
+
point(mark?.getAttribute("data-graview-pick") ?? mark?.getAttribute("data-graview-view") ?? null);
|
|
169
|
+
};
|
|
170
|
+
const away = () => point(null);
|
|
171
|
+
stage.addEventListener("pointerover", at);
|
|
172
|
+
stage.addEventListener("focusin", at);
|
|
173
|
+
stage.addEventListener("pointerleave", away);
|
|
174
|
+
stage.addEventListener("focusout", away);
|
|
175
|
+
return () => {
|
|
176
|
+
stage.removeEventListener("pointerover", at);
|
|
177
|
+
stage.removeEventListener("focusin", at);
|
|
178
|
+
stage.removeEventListener("pointerleave", away);
|
|
179
|
+
stage.removeEventListener("focusout", away);
|
|
180
|
+
};
|
|
181
|
+
}, [waiting, stageRef]);
|
|
114
182
|
if (moving)
|
|
115
183
|
return null;
|
|
116
184
|
/*
|
|
117
185
|
* Without a DOM — the first render, a server — the lines stand on the
|
|
118
|
-
* layout's own boxes
|
|
119
|
-
*
|
|
186
|
+
* layout's own boxes. While a card is held they stand on what was
|
|
187
|
+
* measured, moved by as far as the hand has moved each card since: the
|
|
188
|
+
* page is not measured on every move, and a line still starts on the
|
|
189
|
+
* card's drawn edge rather than on the edge of the slot the layout gave
|
|
190
|
+
* it (a gap as tall as the slot is taller), and a tie the card has said
|
|
191
|
+
* stays waiting.
|
|
120
192
|
*/
|
|
121
|
-
const strands = (holding ?
|
|
193
|
+
const strands = (measured && holding ? movedBy(measured, measuredAt.current, frame.nodes) : measured) ?? connectorStrands(frame.nodes, frame.connectors, null, overview, scheme);
|
|
122
194
|
const drawnSingles = new Set();
|
|
123
195
|
for (const strand of strands) {
|
|
124
196
|
if (strand.edges.length !== 1)
|
|
@@ -126,9 +198,20 @@ export function Lines({ frame, width, height, scheme, overview, selection, empha
|
|
|
126
198
|
const edge = strand.edges[0];
|
|
127
199
|
drawnSingles.add(`${strand.connector.kind}|${edge.from}|${edge.to}`);
|
|
128
200
|
}
|
|
129
|
-
return (_jsxs(_Fragment, { children: [_jsx(Connectors, { strands: strands, result: frame, scheme: scheme, overview: overview, selection: selection, emphasis: emphasis, stageRef: stageRef, onPickEdge: onPickEdge, liveOf: liveOf }), _jsx(SelectionTies, { stageRef: stageRef, nodes: frame.nodes, scheme: scheme, selection: selection, store: store, graphNodes: graphNodes, overview: overview, width: width, height: height, onPickEdge: onPickEdge, alreadyDrawn: drawnSingles, tick: asked })] }));
|
|
201
|
+
return (_jsxs(_Fragment, { children: [_jsx(Connectors, { strands: strands, result: frame, scheme: scheme, overview: overview, selection: selection, emphasis: emphasis, stageRef: stageRef, onPickEdge: onPickEdge, liveOf: liveOf, pointed: pointed, ...(provisional ? { provisional } : {}) }), overview && mapChoice(selection, { overview, focusId, beside }, (id) => store.graph.getNode(id)?.kind).records.length > 0 ? (_jsx(Suspense, { fallback: null, children: _jsx(RecordLinks, { frame: frame, width: width, height: height, stageRef: stageRef, tick: asked, store: store, graphVersion: graphNodes, selection: selection, view: { overview, focusId, beside }, rulers: provisional ? { ...RULERS, unsureOf: (edge, from, to) => provisional.get(edgeSelectionId(edge, from, to)) } : RULERS }) })) : null, overview ? null : _jsx(SelectionTies, { ...(provisional ? { provisional } : {}), stageRef: stageRef, nodes: frame.nodes, scheme: scheme, selection: selection, store: store, graphNodes: graphNodes, overview: overview, width: width, height: height, onPickEdge: onPickEdge, alreadyDrawn: drawnSingles, tick: asked })] }));
|
|
130
202
|
}
|
|
131
|
-
|
|
203
|
+
/*
|
|
204
|
+
* A CHOSEN RECORD'S LINKS FROM ALTITUDE (`record-links-layer.tsx`), fetched
|
|
205
|
+
* the first time a record is chosen up there: a page nobody chooses
|
|
206
|
+
* anything on carries none of it.
|
|
207
|
+
*/
|
|
208
|
+
/* The scene's rulers, handed to the links rather than imported by them: a part fetched apart reaching into the scene's own modules splits them around it. */
|
|
209
|
+
const RULERS = { visibleRect, clipQuadratic, unsureDash: PROVISIONAL_DASH, unsureOpacity: PROVISIONAL_OPACITY };
|
|
210
|
+
const recordLinks = /* @__PURE__ */ lazyModule(retryingImport(() => import("./record-links-layer.js")));
|
|
211
|
+
const RecordLinks = /* @__PURE__ */ recordLinks.part((module, props) => _jsx(module.RecordLinks, { ...props }), { what: "The links", quiet: true });
|
|
212
|
+
/** The strip at the top of a card where its title is said, which a tie's words keep off. */
|
|
213
|
+
const TITLE_STRIP = 44;
|
|
214
|
+
function SelectionTies({ stageRef, nodes, scheme, selection, store, graphNodes, overview, width, height, onPickEdge, alreadyDrawn, tick, provisional, }) {
|
|
132
215
|
const kit = useKit();
|
|
133
216
|
const ties = useMemo(() => {
|
|
134
217
|
if (selection.length === 0 || selection.length > 4)
|
|
@@ -224,7 +307,7 @@ function SelectionTies({ stageRef, nodes, scheme, selection, store, graphNodes,
|
|
|
224
307
|
if (!box || !route)
|
|
225
308
|
continue;
|
|
226
309
|
const edge = [...store.graph.allEdges()].find((one) => (one.from === id && selection.includes(one.to)) || (one.to === id && selection.includes(one.from)));
|
|
227
|
-
joined.push({ key: `join:${id}`, kind: edge?.kind ?? "", endX: route.to.x, endY: route.to.y, ...route, fromBox: cellBox, toBox: box, proxy: false, edgeId: edge ? edgeSelectionId(edge.kind, edge.from, edge.to) : null });
|
|
310
|
+
joined.push({ key: `join:${id}`, self: id, other: "", others: 1, kind: edge?.kind ?? "", endX: route.to.x, endY: route.to.y, ...route, fromBox: cellBox, toBox: box, proxy: false, edgeId: edge ? edgeSelectionId(edge.kind, edge.from, edge.to) : null });
|
|
228
311
|
}
|
|
229
312
|
return joined;
|
|
230
313
|
}
|
|
@@ -319,7 +402,23 @@ function SelectionTies({ stageRef, nodes, scheme, selection, store, graphNodes,
|
|
|
319
402
|
}
|
|
320
403
|
return false;
|
|
321
404
|
};
|
|
322
|
-
|
|
405
|
+
/* The band's chips and the words over them, which a road from a chip goes round. */
|
|
406
|
+
const band = [];
|
|
407
|
+
if (!overview && stageRef.current) {
|
|
408
|
+
for (const node of nodes) {
|
|
409
|
+
if (Math.round(node.plane) !== 1)
|
|
410
|
+
continue;
|
|
411
|
+
const box = measureVisible(stageRef.current, node.id, false);
|
|
412
|
+
if (box)
|
|
413
|
+
band.push(box);
|
|
414
|
+
}
|
|
415
|
+
for (const caption of stageRef.current.querySelectorAll("[data-graview-relation] > span")) {
|
|
416
|
+
const r = caption.getBoundingClientRect();
|
|
417
|
+
if (r.width > 0 && r.height > 0)
|
|
418
|
+
band.push({ x: r.left - stage.left, y: r.top - stage.top, width: r.width, height: r.height });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const seen = new Map();
|
|
323
422
|
const found = [];
|
|
324
423
|
for (const tie of ties) {
|
|
325
424
|
if (someViewDrawsBoth(tie.self, tie.other))
|
|
@@ -328,7 +427,13 @@ function SelectionTies({ stageRef, nodes, scheme, selection, store, graphNodes,
|
|
|
328
427
|
const selfHostEl = selfHost
|
|
329
428
|
? stageRef.current?.querySelector(`[data-graview-view="${CSS.escape(selfHost.id)}"]`)
|
|
330
429
|
: null;
|
|
331
|
-
|
|
430
|
+
/*
|
|
431
|
+
* A CHIP OF THE BAND IS THE THING: chosen there, its tie leaves the chip,
|
|
432
|
+
* not a mention of it in the record's card above, whose line then ran
|
|
433
|
+
* down through the card's own chips and across the band.
|
|
434
|
+
*/
|
|
435
|
+
const ownChip = !overview && selfHost?.id === tie.self && Math.round(selfHost.plane) === 1 ? measureVisible(stageRef.current, selfHost.id, false) : null;
|
|
436
|
+
const fromCandidates = ownChip ? [ownChip] : elementBoxes(tie.self, () => false);
|
|
332
437
|
const hasFromEl = fromCandidates.length > 0;
|
|
333
438
|
if (!hasFromEl && selfHost) {
|
|
334
439
|
const box = measureVisible(stageRef.current, selfHost.id, false) ?? drawnBox(selfHost, scheme);
|
|
@@ -430,19 +535,28 @@ function SelectionTies({ stageRef, nodes, scheme, selection, store, graphNodes,
|
|
|
430
535
|
*/
|
|
431
536
|
const key = `${tie.kind}:${Math.round(fromBox.x)},${Math.round(fromBox.y)}` +
|
|
432
537
|
`:${Math.round(toBox.x)},${Math.round(toBox.y)}`;
|
|
433
|
-
|
|
538
|
+
const twin = seen.get(key);
|
|
539
|
+
if (twin) {
|
|
540
|
+
// Several records of one district on one line: the line counts them.
|
|
541
|
+
twin.others += 1;
|
|
434
542
|
continue;
|
|
435
|
-
|
|
543
|
+
}
|
|
436
544
|
const route = tieRoute(fromBox, toBox);
|
|
437
545
|
if (!route)
|
|
438
546
|
continue;
|
|
439
|
-
const { from,
|
|
547
|
+
const { from, control } = route;
|
|
548
|
+
// From a chip of the band, the road runs whole through its gutters, under nothing and over nothing.
|
|
549
|
+
const points = ownChip && fromBox === ownChip ? channelRoute(fromBox, toBox, band, found.length) : undefined;
|
|
550
|
+
const to = points ? points[points.length - 1] : route.to;
|
|
440
551
|
// The real edge this tie stands for, oriented the way the graph holds it.
|
|
441
552
|
const oriented = [...store.graph.allEdges()].find((edge) => edge.kind === tie.kind &&
|
|
442
553
|
((edge.from === tie.self && edge.to === tie.other) ||
|
|
443
554
|
(edge.from === tie.other && edge.to === tie.self)));
|
|
444
|
-
|
|
555
|
+
const line = {
|
|
445
556
|
key: `${tie.kind}:${tie.self}:${tie.other}`,
|
|
557
|
+
self: tie.self,
|
|
558
|
+
other: tie.other,
|
|
559
|
+
others: 1,
|
|
446
560
|
kind: tie.kind,
|
|
447
561
|
endX: to.x,
|
|
448
562
|
endY: to.y,
|
|
@@ -451,85 +565,189 @@ function SelectionTies({ stageRef, nodes, scheme, selection, store, graphNodes,
|
|
|
451
565
|
control,
|
|
452
566
|
fromBox,
|
|
453
567
|
toBox,
|
|
568
|
+
...(points ? { points } : {}),
|
|
454
569
|
proxy: toProxy,
|
|
455
570
|
edgeId: oriented ? edgeSelectionId(oriented.kind, oriented.from, oriented.to) : null,
|
|
456
|
-
}
|
|
571
|
+
};
|
|
572
|
+
seen.set(key, line);
|
|
573
|
+
found.push(line);
|
|
574
|
+
}
|
|
575
|
+
// EVERY LINE SAYS ITS RELATION at its far end, in the chosen record's kind's words, as the same record's links do from altitude.
|
|
576
|
+
for (const line of found) {
|
|
577
|
+
const edge = [...store.graph.allEdges()].find((one) => one.kind === line.kind && ((one.from === line.self && one.to === line.other) || (one.from === line.other && one.to === line.self)));
|
|
578
|
+
const selfNode = store.graph.getNode(line.self);
|
|
579
|
+
const otherNode = store.graph.getNode(line.other);
|
|
580
|
+
if (!edge || !selfNode || !otherNode)
|
|
581
|
+
continue;
|
|
582
|
+
const direction = edge.from === line.self ? "out" : "in";
|
|
583
|
+
const words = edgeWords(store.schema, direction === "out" ? selfNode.kind : otherNode.kind, line.kind, direction);
|
|
584
|
+
line.said = line.proxy ? `${words}: ${counted(store.schema, otherNode.kind, line.others)}` : `${words}: ${labelOf(store.schema.tryDefinition(otherNode.kind), otherNode)}`;
|
|
457
585
|
}
|
|
458
586
|
return found;
|
|
459
587
|
};
|
|
460
588
|
const [lines, setLines] = useState([]);
|
|
589
|
+
const [labels, setLabels] = useState([]);
|
|
461
590
|
useLayoutEffect(() => {
|
|
462
|
-
|
|
591
|
+
const measured = measure();
|
|
592
|
+
setLines(measured);
|
|
593
|
+
setLabels(measured.some((line) => line.said) ? labelsFor(measured) : []);
|
|
463
594
|
// The measurement depends on the DOM the frame committed, which these
|
|
464
595
|
// name; `measure` itself closes over nothing that changes without them.
|
|
465
596
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
466
597
|
}, [ties, nodes, overview, scheme, width, height, alreadyDrawn, tick, stageRef]);
|
|
598
|
+
/* Each tie's words beside its far end, once, clear of the cards' titles (`tie-labels.ts`). */
|
|
599
|
+
const labelsFor = (measured) => {
|
|
600
|
+
const stageEl = stageRef.current;
|
|
601
|
+
if (!stageEl)
|
|
602
|
+
return [];
|
|
603
|
+
const cards = [];
|
|
604
|
+
for (const host of stageEl.querySelectorAll("[data-graview-view]")) {
|
|
605
|
+
const frame = [...host.children].find((child) => !child.classList.contains("graview-kind-tag"));
|
|
606
|
+
const box = frame ? visibleRect(frame, stageEl) : null;
|
|
607
|
+
if (box)
|
|
608
|
+
cards.push({ box, title: { ...box, height: Math.min(box.height, TITLE_STRIP) } });
|
|
609
|
+
}
|
|
610
|
+
return placeTieLabels(measured.flatMap((line) => (line.said ? [{ key: line.key, said: line.said, end: { x: line.endX, y: line.endY }, far: line.toBox }] : [])), cards, { width, height });
|
|
611
|
+
};
|
|
612
|
+
/*
|
|
613
|
+
* A TIE'S WORDS WAIT TO BE ASKED FOR: shown while the tie, or a card at
|
|
614
|
+
* either end of it, is under the pointer or holds the keyboard.
|
|
615
|
+
*/
|
|
616
|
+
const [reached, setReached] = useState(() => new Set());
|
|
617
|
+
const reachable = useRef([]);
|
|
618
|
+
reachable.current = lines.map((line) => ({ key: line.key, fromBox: line.fromBox, toBox: line.toBox, ids: [line.self, line.other], edgeId: line.edgeId }));
|
|
619
|
+
const anySaid = labels.length > 0;
|
|
620
|
+
useEffect(() => {
|
|
621
|
+
const stageEl = stageRef.current;
|
|
622
|
+
if (!stageEl || !anySaid)
|
|
623
|
+
return;
|
|
624
|
+
const settle = (next) => setReached((was) => (was.size === next.size && [...next].every((key) => was.has(key)) ? was : next));
|
|
625
|
+
let queued = 0;
|
|
626
|
+
let last = null;
|
|
627
|
+
const pointed = () => {
|
|
628
|
+
queued = 0;
|
|
629
|
+
if (!last)
|
|
630
|
+
return;
|
|
631
|
+
const stage = stageEl.getBoundingClientRect();
|
|
632
|
+
const target = last.target instanceof Element ? last.target : null;
|
|
633
|
+
settle(tiesReached(reachable.current, {
|
|
634
|
+
point: { x: last.clientX - stage.left, y: last.clientY - stage.top },
|
|
635
|
+
edgeId: target?.closest("[data-graview-edge]")?.getAttribute("data-graview-edge") ?? null,
|
|
636
|
+
}));
|
|
637
|
+
};
|
|
638
|
+
const onMove = (event) => {
|
|
639
|
+
last = event;
|
|
640
|
+
if (!queued)
|
|
641
|
+
queued = requestAnimationFrame(pointed);
|
|
642
|
+
};
|
|
643
|
+
const onLeave = () => {
|
|
644
|
+
last = null;
|
|
645
|
+
settle(new Set());
|
|
646
|
+
};
|
|
647
|
+
const onFocus = (event) => {
|
|
648
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
649
|
+
if (!target)
|
|
650
|
+
return;
|
|
651
|
+
const stage = stageEl.getBoundingClientRect();
|
|
652
|
+
const rect = target.getBoundingClientRect();
|
|
653
|
+
const id = target.closest("[data-graview-pick]")?.getAttribute("data-graview-pick") ?? target.closest("[data-graview-view]")?.getAttribute("data-graview-view");
|
|
654
|
+
settle(tiesReached(reachable.current, {
|
|
655
|
+
...(id ? { ids: [id] } : {}),
|
|
656
|
+
box: { x: rect.left - stage.left, y: rect.top - stage.top, width: rect.width, height: rect.height },
|
|
657
|
+
}));
|
|
658
|
+
};
|
|
659
|
+
const onBlur = (event) => {
|
|
660
|
+
if (!(event.relatedTarget instanceof Node && stageEl.contains(event.relatedTarget)))
|
|
661
|
+
settle(new Set());
|
|
662
|
+
};
|
|
663
|
+
stageEl.addEventListener("pointermove", onMove, { passive: true });
|
|
664
|
+
stageEl.addEventListener("pointerleave", onLeave);
|
|
665
|
+
stageEl.addEventListener("focusin", onFocus);
|
|
666
|
+
stageEl.addEventListener("focusout", onBlur);
|
|
667
|
+
return () => {
|
|
668
|
+
if (queued)
|
|
669
|
+
cancelAnimationFrame(queued);
|
|
670
|
+
stageEl.removeEventListener("pointermove", onMove);
|
|
671
|
+
stageEl.removeEventListener("pointerleave", onLeave);
|
|
672
|
+
stageEl.removeEventListener("focusin", onFocus);
|
|
673
|
+
stageEl.removeEventListener("focusout", onBlur);
|
|
674
|
+
};
|
|
675
|
+
}, [stageRef, anySaid]);
|
|
467
676
|
if (lines.length === 0)
|
|
468
677
|
return null;
|
|
469
|
-
return (_jsx("svg", { "aria-hidden": lines.some((line) => line.edgeId) ? undefined : true, "data-graview-ties": lines.length, "data-graview-world": "", width: width, height: height, style: {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
.
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
678
|
+
return (_jsxs(_Fragment, { children: [_jsx("svg", { "aria-hidden": lines.some((line) => line.edgeId) ? undefined : true, "data-graview-ties": lines.length, "data-graview-world": "", width: width, height: height, style: {
|
|
679
|
+
position: "absolute",
|
|
680
|
+
left: 0,
|
|
681
|
+
top: 0,
|
|
682
|
+
pointerEvents: "none",
|
|
683
|
+
// Over the views: these lines START on an element inside one, and a
|
|
684
|
+
// line into the shelf that dives behind the focus card en route says
|
|
685
|
+
// nothing.
|
|
686
|
+
zIndex: SCENE_LAYERS.lines,
|
|
687
|
+
}, children: lines.map((line) => {
|
|
688
|
+
const { connector, style } = kitConnector(kit, line.kind);
|
|
689
|
+
// A kind the kit keeps quiet is not drawn — still selectable from the inspector.
|
|
690
|
+
if (!connector.visible)
|
|
691
|
+
return null;
|
|
692
|
+
// A link made less than sure, not yet kept by a person (FR-172): short dashes, quieter, its basis on hover.
|
|
693
|
+
const unsure = line.edgeId ? provisional?.get(line.edgeId) : undefined;
|
|
694
|
+
const quadratic = { p0: line.from, c: line.control, p1: line.to };
|
|
695
|
+
// The route is the kit's call: the arc the layout chose, its chord, or two elbows.
|
|
696
|
+
const d = line.points
|
|
697
|
+
? roundedPolylineD(line.points, 10)
|
|
698
|
+
: connector.route === "orthogonal"
|
|
699
|
+
? polylineD([orthogonalPoints(line.from, line.to)])
|
|
700
|
+
: (({ p0, c, p1 }) => `M ${p0.x} ${p0.y} Q ${c.x} ${c.y} ${p1.x} ${p1.y}`)(routedQuadratic(connector.route, quadratic));
|
|
701
|
+
/*
|
|
702
|
+
* A tie that stands for one edge takes the pointer, like any line:
|
|
703
|
+
* the hit run is the visible stretch between its two endpoint
|
|
704
|
+
* boxes, so pressing a line never steals a card's click.
|
|
705
|
+
*/
|
|
706
|
+
let hit = null;
|
|
707
|
+
// A stand-in line takes no pointer: a 14px corridor across half
|
|
708
|
+
// the scene stole clicks from every card it crossed, to select a
|
|
709
|
+
// relation whose far end is not even drawn. The edge stays
|
|
710
|
+
// selectable where it is really drawn, and from the inspector.
|
|
711
|
+
if (line.edgeId && onPickEdge && !line.proxy) {
|
|
712
|
+
const inside = (box, p) => p.x >= box.x && p.x <= box.x + box.width && p.y >= box.y && p.y <= box.y + box.height;
|
|
713
|
+
const points = line.points ? [...line.points] : [];
|
|
714
|
+
for (let i = 0; i <= (line.points ? -1 : 40); i++) {
|
|
715
|
+
const point = routePoint(connector.route, quadratic, i / 40);
|
|
716
|
+
if (inside(line.fromBox, point) || inside(line.toBox, point))
|
|
717
|
+
continue;
|
|
718
|
+
points.push(point);
|
|
719
|
+
}
|
|
720
|
+
if (points.length >= 2) {
|
|
721
|
+
hit = (_jsx("path", { "data-graview-edge": line.edgeId, className: "graview-edge-hit", d: `M ${points[0].x} ${points[0].y} ${points
|
|
722
|
+
.slice(1)
|
|
723
|
+
.map((point) => `L ${point.x} ${point.y}`)
|
|
724
|
+
.join(" ")}`, fill: "none",
|
|
725
|
+
// The ends stay clear: a line lands ON a chip, and a hit stroke that reached
|
|
726
|
+
// the chip took the click meant for it. Seven percent off each end.
|
|
727
|
+
pathLength: 1, strokeDasharray: "0.86", strokeDashoffset: -0.07, stroke: "transparent", strokeWidth: 14, style: { pointerEvents: "stroke", cursor: "pointer" }, onClick: (event) => {
|
|
728
|
+
event.stopPropagation();
|
|
729
|
+
onPickEdge(line.edgeId, { x: event.clientX, y: event.clientY });
|
|
730
|
+
}, onContextMenu: (event) => {
|
|
731
|
+
event.preventDefault();
|
|
732
|
+
event.stopPropagation();
|
|
733
|
+
onPickEdge(line.edgeId, { x: event.clientX, y: event.clientY });
|
|
734
|
+
}, children: unsure !== undefined ? _jsx("title", { children: unsure }) : null }));
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return (
|
|
738
|
+
/*
|
|
739
|
+
* A line to a STAND-IN recedes: the far end is the kind's
|
|
740
|
+
* district, not the thing itself, and five of those at full
|
|
741
|
+
* strength crossing the scene is a hairball. Present enough to
|
|
742
|
+
* say "the rest live over there", never louder than a real tie.
|
|
743
|
+
*/
|
|
744
|
+
_jsxs("g", { opacity: (line.proxy ? 0.32 : 0.72) * (unsure !== undefined ? PROVISIONAL_OPACITY : 1), children: [_jsx("path", { "data-graview-tie": line.kind, "data-graview-tie-proxy": line.proxy || undefined, "data-graview-provisional": unsure !== undefined || undefined, d: d, fill: "none", stroke: connectorStroke(style), strokeWidth: line.proxy ? 1.1 : 1.6, strokeDasharray: unsure !== undefined ? PROVISIONAL_DASH : CONNECTOR_DASH[style.pattern], strokeLinecap: "round" }), _jsx("circle", { cx: line.endX, cy: line.endY, r: line.proxy ? 2 : 2.6, fill: connectorStroke(style) }), hit] }, line.key));
|
|
745
|
+
}) }), _jsx("div", { "aria-hidden": "true", "data-graview-world": "", style: { position: "absolute", inset: 0, pointerEvents: "none", zIndex: SCENE_LAYERS.tieLabels }, children: labels.map((label) => {
|
|
746
|
+
const first = lines.find((line) => line.key === label.keys[0]);
|
|
747
|
+
return (_jsx("span", { className: "graview-record-link-said", "data-graview-link-said": first?.kind, "data-graview-lands": first?.proxy ? "district" : "record", "data-graview-said-for": label.keys.length, "data-graview-tie-said": label.keys.some((key) => reached.has(key)) ? "shown" : "waiting",
|
|
748
|
+
/* Kept on the screen: landing near a phone's edge, the words step back by as much as they would run over (the tenth walk). */
|
|
749
|
+
style: { position: "absolute", left: label.x, top: label.y, transform: `translateX(min(0px, calc(${Math.round(width - label.x - 8)}px - 100%)))` }, children: label.said }, label.said));
|
|
750
|
+
}) })] }));
|
|
533
751
|
}
|
|
534
752
|
/**
|
|
535
753
|
* What plane 1 is, said in words, over the run of cards it applies to.
|
|
@@ -564,7 +782,8 @@ export function RelationCaptions({ nodes, scheme, width: stageWidth, stageRef, m
|
|
|
564
782
|
* from the host's top floated in open ground half a band above the
|
|
565
783
|
* cards it captions.
|
|
566
784
|
*/
|
|
567
|
-
|
|
785
|
+
// By the slot, as the layout made the rows: a chip sits lower in its slot than a card of several in the same row.
|
|
786
|
+
const measured = measureVisible(stageRef.current, node.id, false, true);
|
|
568
787
|
const { scale } = styleFor(1, scheme);
|
|
569
788
|
entries.push({
|
|
570
789
|
key: `${node.via.edgeKind}|${node.via.direction}`,
|