@graview/primitives 0.1.0
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/LICENSE +96 -0
- package/README.md +25 -0
- package/dist/arrange-bar.d.ts +49 -0
- package/dist/arrange-bar.d.ts.map +1 -0
- package/dist/arrange-bar.js +264 -0
- package/dist/arrange-bar.js.map +1 -0
- package/dist/chat.d.ts +46 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +343 -0
- package/dist/chat.js.map +1 -0
- package/dist/companion.d.ts +58 -0
- package/dist/companion.d.ts.map +1 -0
- package/dist/companion.js +433 -0
- package/dist/companion.js.map +1 -0
- package/dist/connections.d.ts +23 -0
- package/dist/connections.d.ts.map +1 -0
- package/dist/connections.js +100 -0
- package/dist/connections.js.map +1 -0
- package/dist/default-views.d.ts +35 -0
- package/dist/default-views.d.ts.map +1 -0
- package/dist/default-views.js +605 -0
- package/dist/default-views.js.map +1 -0
- package/dist/editable.d.ts +71 -0
- package/dist/editable.d.ts.map +1 -0
- package/dist/editable.js +225 -0
- package/dist/editable.js.map +1 -0
- package/dist/figure.d.ts +30 -0
- package/dist/figure.d.ts.map +1 -0
- package/dist/figure.js +66 -0
- package/dist/figure.js.map +1 -0
- package/dist/find.d.ts +5 -0
- package/dist/find.d.ts.map +1 -0
- package/dist/find.js +252 -0
- package/dist/find.js.map +1 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/installation.d.ts +17 -0
- package/dist/installation.d.ts.map +1 -0
- package/dist/installation.js +37 -0
- package/dist/installation.js.map +1 -0
- package/dist/ladder.d.ts +2 -0
- package/dist/ladder.d.ts.map +1 -0
- package/dist/ladder.js +53 -0
- package/dist/ladder.js.map +1 -0
- package/dist/lens/arranging.d.ts +41 -0
- package/dist/lens/arranging.d.ts.map +1 -0
- package/dist/lens/arranging.js +38 -0
- package/dist/lens/arranging.js.map +1 -0
- package/dist/lens/board.d.ts +146 -0
- package/dist/lens/board.d.ts.map +1 -0
- package/dist/lens/board.js +658 -0
- package/dist/lens/board.js.map +1 -0
- package/dist/lens/calendar-dates.d.ts +19 -0
- package/dist/lens/calendar-dates.d.ts.map +1 -0
- package/dist/lens/calendar-dates.js +55 -0
- package/dist/lens/calendar-dates.js.map +1 -0
- package/dist/lens/calendar-drawing.d.ts +59 -0
- package/dist/lens/calendar-drawing.d.ts.map +1 -0
- package/dist/lens/calendar-drawing.js +229 -0
- package/dist/lens/calendar-drawing.js.map +1 -0
- package/dist/lens/calendar-options.d.ts +107 -0
- package/dist/lens/calendar-options.d.ts.map +1 -0
- package/dist/lens/calendar-options.js +19 -0
- package/dist/lens/calendar-options.js.map +1 -0
- package/dist/lens/calendar-placing.d.ts +35 -0
- package/dist/lens/calendar-placing.d.ts.map +1 -0
- package/dist/lens/calendar-placing.js +113 -0
- package/dist/lens/calendar-placing.js.map +1 -0
- package/dist/lens/calendar-spans.d.ts +48 -0
- package/dist/lens/calendar-spans.d.ts.map +1 -0
- package/dist/lens/calendar-spans.js +146 -0
- package/dist/lens/calendar-spans.js.map +1 -0
- package/dist/lens/calendar-view.d.ts +34 -0
- package/dist/lens/calendar-view.d.ts.map +1 -0
- package/dist/lens/calendar-view.js +228 -0
- package/dist/lens/calendar-view.js.map +1 -0
- package/dist/lens/calendar.d.ts +43 -0
- package/dist/lens/calendar.d.ts.map +1 -0
- package/dist/lens/calendar.js +43 -0
- package/dist/lens/calendar.js.map +1 -0
- package/dist/lens/coverage.d.ts +183 -0
- package/dist/lens/coverage.d.ts.map +1 -0
- package/dist/lens/coverage.js +732 -0
- package/dist/lens/coverage.js.map +1 -0
- package/dist/lens/horizon.d.ts +14 -0
- package/dist/lens/horizon.d.ts.map +1 -0
- package/dist/lens/horizon.js +20 -0
- package/dist/lens/horizon.js.map +1 -0
- package/dist/lens/more.d.ts +16 -0
- package/dist/lens/more.d.ts.map +1 -0
- package/dist/lens/more.js +17 -0
- package/dist/lens/more.js.map +1 -0
- package/dist/lens/plan-lens.d.ts +17 -0
- package/dist/lens/plan-lens.d.ts.map +1 -0
- package/dist/lens/plan-lens.js +24 -0
- package/dist/lens/plan-lens.js.map +1 -0
- package/dist/lens/plan-state.d.ts +122 -0
- package/dist/lens/plan-state.d.ts.map +1 -0
- package/dist/lens/plan-state.js +152 -0
- package/dist/lens/plan-state.js.map +1 -0
- package/dist/lens/plan-view.d.ts +10 -0
- package/dist/lens/plan-view.d.ts.map +1 -0
- package/dist/lens/plan-view.js +576 -0
- package/dist/lens/plan-view.js.map +1 -0
- package/dist/lens/plan.d.ts +25 -0
- package/dist/lens/plan.d.ts.map +1 -0
- package/dist/lens/plan.js +25 -0
- package/dist/lens/plan.js.map +1 -0
- package/dist/lens/reach.d.ts +47 -0
- package/dist/lens/reach.d.ts.map +1 -0
- package/dist/lens/reach.js +67 -0
- package/dist/lens/reach.js.map +1 -0
- package/dist/lens/timeline.d.ts +137 -0
- package/dist/lens/timeline.d.ts.map +1 -0
- package/dist/lens/timeline.js +426 -0
- package/dist/lens/timeline.js.map +1 -0
- package/dist/markup.d.ts +23 -0
- package/dist/markup.d.ts.map +1 -0
- package/dist/markup.js +24 -0
- package/dist/markup.js.map +1 -0
- package/dist/places.d.ts +24 -0
- package/dist/places.d.ts.map +1 -0
- package/dist/places.js +227 -0
- package/dist/places.js.map +1 -0
- package/dist/popover.d.ts +28 -0
- package/dist/popover.d.ts.map +1 -0
- package/dist/popover.js +59 -0
- package/dist/popover.js.map +1 -0
- package/dist/primitives/index.d.ts +214 -0
- package/dist/primitives/index.d.ts.map +1 -0
- package/dist/primitives/index.js +381 -0
- package/dist/primitives/index.js.map +1 -0
- package/dist/profile.d.ts +65 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +233 -0
- package/dist/profile.js.map +1 -0
- package/dist/quick-relations.d.ts +16 -0
- package/dist/quick-relations.d.ts.map +1 -0
- package/dist/quick-relations.js +183 -0
- package/dist/quick-relations.js.map +1 -0
- package/dist/relation-key.d.ts +44 -0
- package/dist/relation-key.d.ts.map +1 -0
- package/dist/relation-key.js +196 -0
- package/dist/relation-key.js.map +1 -0
- package/dist/seat.d.ts +137 -0
- package/dist/seat.d.ts.map +1 -0
- package/dist/seat.js +162 -0
- package/dist/seat.js.map +1 -0
- package/dist/seats.d.ts +21 -0
- package/dist/seats.d.ts.map +1 -0
- package/dist/seats.js +62 -0
- package/dist/seats.js.map +1 -0
- package/dist/seeding.d.ts +150 -0
- package/dist/seeding.d.ts.map +1 -0
- package/dist/seeding.js +538 -0
- package/dist/seeding.js.map +1 -0
- package/dist/shell.d.ts +74 -0
- package/dist/shell.d.ts.map +1 -0
- package/dist/shell.js +86 -0
- package/dist/shell.js.map +1 -0
- package/dist/theme.d.ts +50 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +1692 -0
- package/dist/theme.js.map +1 -0
- package/dist/wordmark.d.ts +15 -0
- package/dist/wordmark.d.ts.map +1 -0
- package/dist/wordmark.js +55 -0
- package/dist/wordmark.js.map +1 -0
- package/dist/workbench/activity.d.ts +75 -0
- package/dist/workbench/activity.d.ts.map +1 -0
- package/dist/workbench/activity.js +259 -0
- package/dist/workbench/activity.js.map +1 -0
- package/dist/workbench/agent-seat.d.ts +51 -0
- package/dist/workbench/agent-seat.d.ts.map +1 -0
- package/dist/workbench/agent-seat.js +112 -0
- package/dist/workbench/agent-seat.js.map +1 -0
- package/dist/workbench/answer-args.d.ts +31 -0
- package/dist/workbench/answer-args.d.ts.map +1 -0
- package/dist/workbench/answer-args.js +257 -0
- package/dist/workbench/answer-args.js.map +1 -0
- package/dist/workbench/back-out.d.ts +71 -0
- package/dist/workbench/back-out.d.ts.map +1 -0
- package/dist/workbench/back-out.js +238 -0
- package/dist/workbench/back-out.js.map +1 -0
- package/dist/workbench/following.d.ts +7 -0
- package/dist/workbench/following.d.ts.map +1 -0
- package/dist/workbench/following.js +26 -0
- package/dist/workbench/following.js.map +1 -0
- package/dist/workbench/index.d.ts +23 -0
- package/dist/workbench/index.d.ts.map +1 -0
- package/dist/workbench/index.js +23 -0
- package/dist/workbench/index.js.map +1 -0
- package/dist/workbench/inspector.d.ts +27 -0
- package/dist/workbench/inspector.d.ts.map +1 -0
- package/dist/workbench/inspector.js +745 -0
- package/dist/workbench/inspector.js.map +1 -0
- package/dist/workbench/standing.d.ts +16 -0
- package/dist/workbench/standing.d.ts.map +1 -0
- package/dist/workbench/standing.js +102 -0
- package/dist/workbench/standing.js.map +1 -0
- package/dist/workbench/trail.d.ts +35 -0
- package/dist/workbench/trail.d.ts.map +1 -0
- package/dist/workbench/trail.js +150 -0
- package/dist/workbench/trail.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { areaOf, boxOf, fitLabel, overlaps, spanAt } from "@graview/layout";
|
|
3
|
+
import { hueFor } from "../default-views.js";
|
|
4
|
+
import { Chip, Panel } from "../primitives/index.js";
|
|
5
|
+
import { useDrawnSize, useGraview, useTextMeasure } from "@graview/react";
|
|
6
|
+
import { useMemo, useRef, useState } from "react";
|
|
7
|
+
import { buildPlanLens } from "./plan-state.js";
|
|
8
|
+
/**
|
|
9
|
+
* THE SAME GROUND AS A LIST — what a map says when it cannot be drawn.
|
|
10
|
+
*
|
|
11
|
+
* A picture is not the only honest way to show ground, and below a certain
|
|
12
|
+
* size it stops being one at all. This is what the summary fidelity draws,
|
|
13
|
+
* and what the full one falls back to when the scene hands it a hundred and
|
|
14
|
+
* thirty-six pixels: every area, what is standing in it, and what was named
|
|
15
|
+
* and never drawn. Denser than the map, and at that size, more.
|
|
16
|
+
*/
|
|
17
|
+
function Roster({ map, options, implicated, flagged, }) {
|
|
18
|
+
return (_jsxs("ul", { style: { margin: 0, padding: 0, listStyle: "none", display: "grid", gap: ".25rem" }, children: [map.regions.map((region) => {
|
|
19
|
+
const emphasis = emphasisOf(region.id, implicated, flagged);
|
|
20
|
+
return (_jsxs("li", { "data-graview-pick": region.id, ...(emphasis ? { "data-graview-emphasis": emphasis } : {}), style: {
|
|
21
|
+
display: "flex",
|
|
22
|
+
flexWrap: "wrap",
|
|
23
|
+
gap: ".5rem",
|
|
24
|
+
justifyContent: "space-between",
|
|
25
|
+
minWidth: 0,
|
|
26
|
+
padding: ".25rem .4rem",
|
|
27
|
+
borderRadius: ".35rem",
|
|
28
|
+
background: emphasis === "flagged" ? fillFor(region.what, emphasis, options.hues) : undefined,
|
|
29
|
+
}, children: [_jsx("span", { style: { minWidth: 0, overflowWrap: "anywhere" }, children: region.label }), _jsxs("span", { style: { opacity: 0.7, whiteSpace: "nowrap" }, children: [region.what ? `${region.what} · ` : "", region.markers.length] })] }, region.id));
|
|
30
|
+
}), map.undrawn.map((region) => (_jsxs("li", { "data-graview-pick": region.id, style: { display: "flex", gap: ".5rem", opacity: 0.7, padding: ".25rem .4rem" }, children: [_jsx("span", { style: { minWidth: 0, overflowWrap: "anywhere" }, children: region.label }), _jsx("span", { style: { whiteSpace: "nowrap" }, children: options.undrawnLabel ?? "not drawn" })] }, region.id)))] }));
|
|
31
|
+
}
|
|
32
|
+
/** The viewBox is in hundredths, so every number below reads as a percent. */
|
|
33
|
+
const W = 1000;
|
|
34
|
+
/** A marker's hit radius, in canvas units. Twice the dot it draws. */
|
|
35
|
+
const HIT = 0.022;
|
|
36
|
+
/** A tool button: a control, never a pick target, and never under 24px. */
|
|
37
|
+
const TOOL = {
|
|
38
|
+
minHeight: "1.9rem",
|
|
39
|
+
padding: ".25rem .65rem",
|
|
40
|
+
borderRadius: ".35rem",
|
|
41
|
+
border: "1px solid var(--graview-edge)",
|
|
42
|
+
background: "var(--graview-panel)",
|
|
43
|
+
color: "var(--graview-ink)",
|
|
44
|
+
font: "inherit",
|
|
45
|
+
fontSize: "0.9375rem",
|
|
46
|
+
cursor: "pointer",
|
|
47
|
+
};
|
|
48
|
+
const emphasisOf = (id, implicated, flagged) => {
|
|
49
|
+
if (flagged?.includes(id))
|
|
50
|
+
return "flagged";
|
|
51
|
+
if (implicated?.includes(id))
|
|
52
|
+
return "implicated";
|
|
53
|
+
return undefined;
|
|
54
|
+
};
|
|
55
|
+
/** A hue for a category: the app's declared degrees, or the framework's stable hash (degrees since F-014). */
|
|
56
|
+
const degrees = (what, hues) => {
|
|
57
|
+
const named = what === undefined ? undefined : hues?.[what];
|
|
58
|
+
return named ?? hueFor(what ?? "region");
|
|
59
|
+
};
|
|
60
|
+
const fillFor = (what, emphasis, hues) => emphasis === "flagged"
|
|
61
|
+
? "color-mix(in oklab, var(--graview-warn) 26%, var(--graview-panel))"
|
|
62
|
+
: `color-mix(in oklab, hsl(${degrees(what, hues)} 52% 48%) 24%, var(--graview-panel))`;
|
|
63
|
+
const strokeFor = (what, emphasis, hues) => emphasis === "flagged"
|
|
64
|
+
? "var(--graview-warn)"
|
|
65
|
+
: `color-mix(in oklab, hsl(${degrees(what, hues)} 48% 42%) 78%, var(--graview-ink))`;
|
|
66
|
+
/** Where a click landed, 0..1 across and down the drawn site. */
|
|
67
|
+
function pointOf(event) {
|
|
68
|
+
const box = event.currentTarget.getBoundingClientRect();
|
|
69
|
+
const clamp = (n) => Math.round(Math.max(0, Math.min(1, n)) * 1000) / 1000;
|
|
70
|
+
return {
|
|
71
|
+
x: clamp(box.width === 0 ? 0 : (event.clientX - box.left) / box.width),
|
|
72
|
+
y: clamp(box.height === 0 ? 0 : (event.clientY - box.top) / box.height),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function PlanView({ schema, options, nodes, label, fidelity, selected, implicated, flagged, }) {
|
|
76
|
+
const { store } = useGraview();
|
|
77
|
+
const edges = store.graph.allEdges();
|
|
78
|
+
/*
|
|
79
|
+
* REGIONS COME FROM THE GROUP; MARKERS CANNOT.
|
|
80
|
+
*
|
|
81
|
+
* Registered over the region kind's `many` cell, this view is handed
|
|
82
|
+
* `nodes` — that kind's members, already filtered by the horizon. The
|
|
83
|
+
* markers are a DIFFERENT kind, so they are never in that array, and
|
|
84
|
+
* building from `nodes` alone drew every piece of ground with nothing
|
|
85
|
+
* standing in it. Reading the whole graph and narrowing the regions back
|
|
86
|
+
* to what the group gave keeps both: the scene's filtering, and the
|
|
87
|
+
* things that stand on the filtered ground.
|
|
88
|
+
*/
|
|
89
|
+
const everything = store.graph.allNodes();
|
|
90
|
+
const shown = nodes === undefined ? undefined : new Set(nodes.map((node) => node.id));
|
|
91
|
+
const visible = shown === undefined
|
|
92
|
+
? everything
|
|
93
|
+
: everything.filter((node) => node.kind !== options.regions || shown.has(node.id));
|
|
94
|
+
const map = buildPlanLens(visible, edges, options, schema);
|
|
95
|
+
const drawn = map.regions.length;
|
|
96
|
+
const things = map.regions.reduce((n, region) => n + region.markers.length, 0);
|
|
97
|
+
/*
|
|
98
|
+
* A CHIP. Standing in for the whole map at the smallest fidelity, the
|
|
99
|
+
* useful thing to say is how much ground there is — and, when some of it
|
|
100
|
+
* is not drawn, that there is ground this picture is not showing.
|
|
101
|
+
*/
|
|
102
|
+
if (fidelity === "glyph") {
|
|
103
|
+
return (_jsx(Chip, { label: `${label ?? "Ground"} · ${drawn}`, ...(map.undrawn.length > 0 ? { meta: `+${map.undrawn.length} undrawn` } : {}), selected: selected }));
|
|
104
|
+
}
|
|
105
|
+
/*
|
|
106
|
+
* A SUMMARY IS DENSER CONTENT, NOT THE PICTURE SCALED DOWN. A map at a
|
|
107
|
+
* third of its size is a map with no legible labels, which answers
|
|
108
|
+
* nothing; a roster of the same ground with what stands in it and what is
|
|
109
|
+
* wrong with it answers several things in the same space.
|
|
110
|
+
*/
|
|
111
|
+
if (fidelity === "summary") {
|
|
112
|
+
return (_jsx(Panel, { title: label ?? "The plan", subtitle: `${drawn} drawn · ${things} standing in them`, children: _jsx(Roster, { map: map, options: options, ...(implicated ? { implicated } : {}), ...(flagged ? { flagged } : {}) }) }));
|
|
113
|
+
}
|
|
114
|
+
/*
|
|
115
|
+
* THE DRAWING TAKES ITS PROPORTIONS FROM WHAT WAS DRAWN.
|
|
116
|
+
*
|
|
117
|
+
* The canvas used to be a fixed 1.45 to 1 and the coordinate space was
|
|
118
|
+
* "0–1 across and 0–1 down" — which is not a space, it is two independent
|
|
119
|
+
* scales, and it cannot say that a property is three times as long as it
|
|
120
|
+
* is wide. A surveyed town lot, deep and narrow, came back as squat
|
|
121
|
+
* horizontal bands: every shape stretched sideways to fill a canvas whose
|
|
122
|
+
* shape had nothing to do with the ground.
|
|
123
|
+
*
|
|
124
|
+
* So one unit across is one unit down — a SQUARE space, in which a long
|
|
125
|
+
* lot simply uses less of one axis — and the picture is cropped to what
|
|
126
|
+
* is actually in it. The canvas is then the shape of the property, and a
|
|
127
|
+
* lawn twice as long as it is wide is drawn twice as long as it is wide.
|
|
128
|
+
*
|
|
129
|
+
* `aspect` remains as an override for a domain that knows better, and is
|
|
130
|
+
* no longer a default that quietly lies.
|
|
131
|
+
*/
|
|
132
|
+
const extent = useMemo(() => {
|
|
133
|
+
let x0 = Infinity;
|
|
134
|
+
let y0 = Infinity;
|
|
135
|
+
let x1 = -Infinity;
|
|
136
|
+
let y1 = -Infinity;
|
|
137
|
+
const see = (p) => {
|
|
138
|
+
if (p.x < x0)
|
|
139
|
+
x0 = p.x;
|
|
140
|
+
if (p.x > x1)
|
|
141
|
+
x1 = p.x;
|
|
142
|
+
if (p.y < y0)
|
|
143
|
+
y0 = p.y;
|
|
144
|
+
if (p.y > y1)
|
|
145
|
+
y1 = p.y;
|
|
146
|
+
};
|
|
147
|
+
for (const region of map.regions) {
|
|
148
|
+
for (const corner of region.outline)
|
|
149
|
+
see(corner);
|
|
150
|
+
for (const marker of region.markers)
|
|
151
|
+
see(marker.at);
|
|
152
|
+
}
|
|
153
|
+
if (!Number.isFinite(x0) || x1 - x0 <= 0 || y1 - y0 <= 0)
|
|
154
|
+
return { x0: 0, y0: 0, x1: 1, y1: 1 };
|
|
155
|
+
/* A margin, so nothing is drawn against the frame. */
|
|
156
|
+
const pad = Math.max(x1 - x0, y1 - y0) * 0.04;
|
|
157
|
+
return { x0: x0 - pad, y0: y0 - pad, x1: x1 + pad, y1: y1 + pad };
|
|
158
|
+
}, [map.regions]);
|
|
159
|
+
/*
|
|
160
|
+
* Clamped, because a property really can be ten to one and a picture that
|
|
161
|
+
* is ten to one is a ribbon nothing is legible in. Past the clamp the
|
|
162
|
+
* drawing stops being true rather than stops being readable, and the true
|
|
163
|
+
* one nobody can read is the worse of the two.
|
|
164
|
+
*/
|
|
165
|
+
const raw = (extent.x1 - extent.x0) / (extent.y1 - extent.y0);
|
|
166
|
+
const aspect = options.aspect ?? Math.min(3.2, Math.max(0.55, raw));
|
|
167
|
+
/* U is the scale of the square space; the view is the window onto it. */
|
|
168
|
+
const U = Math.round(W / (extent.x1 - extent.x0));
|
|
169
|
+
const viewW = Math.round(U * (extent.x1 - extent.x0));
|
|
170
|
+
const viewH = Math.round(viewW / aspect);
|
|
171
|
+
const view = {
|
|
172
|
+
x: extent.x0 * U,
|
|
173
|
+
y: extent.y0 * U - (viewH - U * (extent.y1 - extent.y0)) / 2,
|
|
174
|
+
w: viewW,
|
|
175
|
+
h: viewH,
|
|
176
|
+
};
|
|
177
|
+
/*
|
|
178
|
+
* Two scales, and keeping them apart is the whole of this.
|
|
179
|
+
*
|
|
180
|
+
* `U` turns a coordinate into a place: it is how many units of the
|
|
181
|
+
* drawing one unit of the square space is worth, and it grows as the
|
|
182
|
+
* window narrows. `S` is how big a thing should LOOK — a font, a dot, a
|
|
183
|
+
* margin — and it is a fraction of the window, so a name on a narrow lot
|
|
184
|
+
* is the same size on screen as a name on a square one. Using the space's
|
|
185
|
+
* scale for sizes is how a long thin property ends up with enormous type.
|
|
186
|
+
*/
|
|
187
|
+
const H = U;
|
|
188
|
+
const S = view.w;
|
|
189
|
+
const anyEmphasis = (implicated?.length ?? 0) > 0 || (flagged?.length ?? 0) > 0;
|
|
190
|
+
/*
|
|
191
|
+
* WHAT CAN BE WRITTEN ON THE PICTURE, worked out before any of it is
|
|
192
|
+
* drawn — because whether a name fits decides whether the key below has
|
|
193
|
+
* to say it, and a component cannot find that out while rendering.
|
|
194
|
+
*
|
|
195
|
+
* Regions first and markers into the gaps left over: a marker is a dot
|
|
196
|
+
* with a name beside it, and the dot is the thing that carries the
|
|
197
|
+
* meaning. A name that will not fit is dropped rather than overlapped,
|
|
198
|
+
* which is the whole difference between a plan and a pile of words.
|
|
199
|
+
*/
|
|
200
|
+
/*
|
|
201
|
+
* WHETHER THIS IS A CONTROL SURFACE, A PICTURE, OR NEITHER.
|
|
202
|
+
*
|
|
203
|
+
* The framework asks this now — `useDrawnSize` — because it turned out
|
|
204
|
+
* not to be a question about grounds. A drawing written in its own units
|
|
205
|
+
* and handed whatever room the page has hides it: at THIS size, is what
|
|
206
|
+
* I am drawing still a thing a person can read, or press? The scene lays
|
|
207
|
+
* this panel out at 136 pixels on a phone, where the answer is no twice.
|
|
208
|
+
*/
|
|
209
|
+
const canvas = useRef(null);
|
|
210
|
+
const { touchable, legible: drawable } = useDrawnSize(canvas, { units: view.w, target: S * HIT * 2 });
|
|
211
|
+
const display = useTextMeasure("--graview-font-display", "serif");
|
|
212
|
+
const body = useTextMeasure("--graview-font-body", "sans-serif");
|
|
213
|
+
const { named, placed, tagged } = useMemo(() => {
|
|
214
|
+
const taken = [];
|
|
215
|
+
/*
|
|
216
|
+
* THE NAME GOES IN THE CORNER, NOT ACROSS THE MIDDLE.
|
|
217
|
+
*
|
|
218
|
+
* A name set large and centred looks like a title and behaves like a
|
|
219
|
+
* wall: it owns the widest part of the shape, which is exactly where
|
|
220
|
+
* the things standing in that shape are, so twenty-two markers had
|
|
221
|
+
* nowhere to put their own names and went unlabelled. Every site plan
|
|
222
|
+
* ever drawn does the opposite — the area is named quietly along its
|
|
223
|
+
* top edge, and the middle is left for what is in it.
|
|
224
|
+
*/
|
|
225
|
+
const inset = S * 0.012;
|
|
226
|
+
/* A hair of air: two names that merely touch read as one long word. */
|
|
227
|
+
const air = W * 0.004;
|
|
228
|
+
const clear = (box, mine) => !taken.some((other) => other !== mine &&
|
|
229
|
+
overlaps({ x0: box.x0 - air, y0: box.y0 - air, x1: box.x1 + air, y1: box.y1 + air }, other));
|
|
230
|
+
/*
|
|
231
|
+
* BIGGEST GROUND FIRST. Where two names cannot both be drawn, the one
|
|
232
|
+
* that keeps its place should be the one with more room to keep it in —
|
|
233
|
+
* and the one that loses goes to the key, which is a demotion a small
|
|
234
|
+
* shape can afford and a large one cannot.
|
|
235
|
+
*/
|
|
236
|
+
const byRoom = [...map.regions].sort((a, b) => areaOf(b.outline) - areaOf(a.outline));
|
|
237
|
+
const named = new Map();
|
|
238
|
+
const placed = new Map();
|
|
239
|
+
for (const region of byRoom) {
|
|
240
|
+
const box = boxOf(region.outline);
|
|
241
|
+
const height = Math.max(0, (box.bottom - box.top) * H * 0.55);
|
|
242
|
+
/*
|
|
243
|
+
* Down the inside of the top edge, looking for a line with room. Two
|
|
244
|
+
* areas that overlap — which a survey produces more often than
|
|
245
|
+
* anybody would like — have their top edges close together, and a
|
|
246
|
+
* name drawn over another name belongs to neither of them.
|
|
247
|
+
*/
|
|
248
|
+
let put = null;
|
|
249
|
+
for (const down of [0.12, 0.26, 0.42, 0.6, 0.78]) {
|
|
250
|
+
const y = box.top + down * (box.bottom - box.top);
|
|
251
|
+
const span = spanAt(region.outline, y);
|
|
252
|
+
const room = Math.max(0, (span.x1 - span.x0) * U - inset * 2);
|
|
253
|
+
const fitted = fitLabel(region.label, {
|
|
254
|
+
room,
|
|
255
|
+
height,
|
|
256
|
+
size: S * 0.023,
|
|
257
|
+
floor: S * 0.015,
|
|
258
|
+
measure: display,
|
|
259
|
+
});
|
|
260
|
+
if (fitted === null)
|
|
261
|
+
continue;
|
|
262
|
+
const wide = Math.max(...fitted.lines.map((line) => display(line, fitted.fontSize)));
|
|
263
|
+
const tall = fitted.lines.length * fitted.fontSize * 1.15;
|
|
264
|
+
/*
|
|
265
|
+
* OFF THE EDGE IT IS DRAWN ON. A short band's tenth-of-the-way-down
|
|
266
|
+
* is a couple of pixels, so the first row of names came back with
|
|
267
|
+
* the outline stroke ruled straight through them — a name on a
|
|
268
|
+
* boundary reads as belonging to whichever side you looked at first.
|
|
269
|
+
*/
|
|
270
|
+
const top = box.top * H + inset + tall / 2;
|
|
271
|
+
const bottom = box.bottom * H - inset - tall / 2;
|
|
272
|
+
const mid = bottom < top ? ((box.top + box.bottom) * H) / 2 : Math.min(Math.max(y * H, top), bottom);
|
|
273
|
+
/*
|
|
274
|
+
* AND MEASURED WHERE IT IS ACTUALLY DRAWN.
|
|
275
|
+
*
|
|
276
|
+
* The room was measured at `y` and the clamp above moves the label
|
|
277
|
+
* to `mid`, which is a different height — and a shape is a
|
|
278
|
+
* different width at a different height. That is how "The Tree Bed"
|
|
279
|
+
* came to be lettered across the patio next door: the widest run at
|
|
280
|
+
* the height it was measured, drawn at a height where the bed is
|
|
281
|
+
* not. So the span is re-asked where the words will land, and a
|
|
282
|
+
* label that does not fit THERE tries the next line down instead.
|
|
283
|
+
*/
|
|
284
|
+
const here = spanAt(region.outline, mid / H);
|
|
285
|
+
const x = here.x0 * U + inset;
|
|
286
|
+
if (wide > (here.x1 - here.x0) * U - inset * 2)
|
|
287
|
+
continue;
|
|
288
|
+
const candidate = { x0: x, y0: mid - tall / 2, x1: x + wide, y1: mid + tall / 2 };
|
|
289
|
+
if (!clear(candidate))
|
|
290
|
+
continue;
|
|
291
|
+
taken.push(candidate);
|
|
292
|
+
put = { fitted, x, y: mid };
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
named.set(region.id, put?.fitted ?? null);
|
|
296
|
+
if (put !== null)
|
|
297
|
+
placed.set(region.id, { x: put.x, y: put.y });
|
|
298
|
+
}
|
|
299
|
+
/*
|
|
300
|
+
* TWENTY-TWO COLOURED DOTS AND NO WORDS is not a map of anything. The
|
|
301
|
+
* first survey drew exactly that, and every one of them was reachable,
|
|
302
|
+
* announced and pickable — and unreadable, because knowing a thing is
|
|
303
|
+
* THERE is not knowing what it is.
|
|
304
|
+
*
|
|
305
|
+
* A marker's name dodges everything already on the picture, including
|
|
306
|
+
* the area names. Letting them overlap was tried and is worse than
|
|
307
|
+
* silence: "Cedar ra…" under "Perimeter raised" reads as one damaged
|
|
308
|
+
* sentence rather than as two things. What buys the room is the area
|
|
309
|
+
* names moving out of the middle, above.
|
|
310
|
+
*/
|
|
311
|
+
const size = S * 0.018;
|
|
312
|
+
const tagged = new Map();
|
|
313
|
+
/*
|
|
314
|
+
* THE DOTS ARE IN THE WAY TOO.
|
|
315
|
+
*
|
|
316
|
+
* Names dodged other names and sailed straight over the marks they
|
|
317
|
+
* belong to: "Back Fence Beds" written across the alley gate's dot,
|
|
318
|
+
* "Deck Shrub" across the drain's. A dot is the thing carrying the
|
|
319
|
+
* meaning — the name is only there to say which dot — so a name over
|
|
320
|
+
* somebody else's dot mislabels it, which is worse than the dot having
|
|
321
|
+
* no name at all. Every mark goes into the set before any name does.
|
|
322
|
+
*/
|
|
323
|
+
const dot = S * 0.011 + S * 0.004;
|
|
324
|
+
const marks = new Map();
|
|
325
|
+
for (const region of map.regions) {
|
|
326
|
+
for (const marker of region.markers) {
|
|
327
|
+
const box = {
|
|
328
|
+
x0: marker.at.x * U - dot,
|
|
329
|
+
y0: marker.at.y * H - dot,
|
|
330
|
+
x1: marker.at.x * U + dot,
|
|
331
|
+
y1: marker.at.y * H + dot,
|
|
332
|
+
};
|
|
333
|
+
marks.set(marker.id, box);
|
|
334
|
+
taken.push(box);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/*
|
|
338
|
+
* And none of them at all when the drawing is too small to press,
|
|
339
|
+
* because it is then also too small to read: at phone width this canvas
|
|
340
|
+
* lands in about a hundred and forty pixels, where an eighteen-unit
|
|
341
|
+
* name renders at two and a half. Two-and-a-half-pixel words are not a
|
|
342
|
+
* smaller version of the information; they are a texture. The key
|
|
343
|
+
* carries every marker at that size, in rows a finger can hit.
|
|
344
|
+
*/
|
|
345
|
+
for (const region of touchable ? map.regions : []) {
|
|
346
|
+
for (const marker of region.markers) {
|
|
347
|
+
const gap = S * 0.018;
|
|
348
|
+
const wide = body(marker.label, size);
|
|
349
|
+
const cx = marker.at.x * U;
|
|
350
|
+
const cy = marker.at.y * H;
|
|
351
|
+
const rise = size * 1.15;
|
|
352
|
+
/*
|
|
353
|
+
* EIGHT PLACES TO TRY, not one.
|
|
354
|
+
*
|
|
355
|
+
* The first version put every name to the right of its dot and gave
|
|
356
|
+
* up if that was taken — so on a real plan, where twenty-two things
|
|
357
|
+
* stand in eight small areas, it gave up twenty-two times and the
|
|
358
|
+
* drawing was dots. A name beside its dot is worth a good deal of
|
|
359
|
+
* looking for: right first because it reads most naturally, then
|
|
360
|
+
* left, then the diagonals, then straight above and below.
|
|
361
|
+
*/
|
|
362
|
+
const tries = [
|
|
363
|
+
{ x: cx + gap, y: cy, anchor: "start" },
|
|
364
|
+
{ x: cx - gap, y: cy, anchor: "end" },
|
|
365
|
+
{ x: cx + gap * 0.7, y: cy - rise, anchor: "start" },
|
|
366
|
+
{ x: cx - gap * 0.7, y: cy - rise, anchor: "end" },
|
|
367
|
+
{ x: cx + gap * 0.7, y: cy + rise, anchor: "start" },
|
|
368
|
+
{ x: cx - gap * 0.7, y: cy + rise, anchor: "end" },
|
|
369
|
+
{ x: cx, y: cy - rise * 1.1, anchor: "start" },
|
|
370
|
+
{ x: cx, y: cy + rise * 1.1, anchor: "start" },
|
|
371
|
+
];
|
|
372
|
+
for (const put of tries) {
|
|
373
|
+
const x0 = put.anchor === "start" ? put.x : put.x - wide;
|
|
374
|
+
const box = { x0, y0: put.y - size * 0.62, x1: x0 + wide, y1: put.y + size * 0.62 };
|
|
375
|
+
if (x0 < view.x + 4 || x0 + wide > view.x + view.w - 4)
|
|
376
|
+
continue;
|
|
377
|
+
if (box.y0 < view.y + 2 || box.y1 > view.y + view.h - 2)
|
|
378
|
+
continue;
|
|
379
|
+
/*
|
|
380
|
+
* A MARK DOES NOT BLOCK ITS OWN NAME.
|
|
381
|
+
*
|
|
382
|
+
* Every dot went into the set so that no name would be written
|
|
383
|
+
* across somebody else's — and then every name was written
|
|
384
|
+
* eighteen units from a dot whose blocked square reaches fifteen,
|
|
385
|
+
* with four units of air between labels on top. Nineteen against
|
|
386
|
+
* eighteen: each of the twenty-two names was refused by the one
|
|
387
|
+
* mark it belongs to, and the drawing came back as dots with a
|
|
388
|
+
* key of everything on it.
|
|
389
|
+
*/
|
|
390
|
+
if (!clear(box, marks.get(marker.id)))
|
|
391
|
+
continue;
|
|
392
|
+
taken.push(box);
|
|
393
|
+
tagged.set(marker.id, { at: { x: put.x, y: put.y }, anchor: put.anchor, size });
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return { named, placed, tagged };
|
|
399
|
+
}, [map.regions, W, H, touchable, display, body]);
|
|
400
|
+
/** Everything the drawing could not say for itself, in the order it is drawn. */
|
|
401
|
+
const unsaid = useMemo(() => {
|
|
402
|
+
const out = [];
|
|
403
|
+
for (const region of map.regions) {
|
|
404
|
+
if ((named.get(region.id)?.whole ?? false) === false) {
|
|
405
|
+
out.push({ id: region.id, label: region.label, what: region.what, round: false });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
for (const region of map.regions) {
|
|
409
|
+
for (const marker of region.markers) {
|
|
410
|
+
/* Everything, when the drawing is too small to be pressed: the key
|
|
411
|
+
is then not a footnote about what would not fit, it is the only
|
|
412
|
+
way to reach anything standing on the ground. */
|
|
413
|
+
if (!touchable || !tagged.has(marker.id)) {
|
|
414
|
+
out.push({ id: marker.id, label: marker.label, what: marker.what, round: true });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return out;
|
|
419
|
+
}, [map.regions, named, tagged, touchable]);
|
|
420
|
+
/*
|
|
421
|
+
* STAKING OUT, ON THE MAP ITSELF.
|
|
422
|
+
*
|
|
423
|
+
* The vision's sentence was "dragging corners on a sketch", and the first
|
|
424
|
+
* real survey — seven areas, none drawn — landed a person on an empty
|
|
425
|
+
* rectangle with a paragraph of names under it. This is where drawing
|
|
426
|
+
* belongs: choose an area, click its corners, done. Each finished outline
|
|
427
|
+
* is the domain's own act through the store, authored by whoever is in
|
|
428
|
+
* the seat, so it lands in the log and undoes like anything else.
|
|
429
|
+
*/
|
|
430
|
+
const { principal } = useGraview();
|
|
431
|
+
const [drawing, setDrawing] = useState(null);
|
|
432
|
+
const canDraw = options.draw !== undefined;
|
|
433
|
+
const canPlace = options.place !== undefined;
|
|
434
|
+
const onCanvasClick = (event) => {
|
|
435
|
+
if (drawing === null)
|
|
436
|
+
return;
|
|
437
|
+
// A click that is laying a corner must not also select or travel.
|
|
438
|
+
event.stopPropagation();
|
|
439
|
+
const point = pointOf(event);
|
|
440
|
+
if (drawing.kind === "marker") {
|
|
441
|
+
store.apply({ name: options.place.act, args: { [options.place.id]: drawing.id, [options.place.at]: point } }, { author: principal, intent: `Placed ${drawing.label} on the map` });
|
|
442
|
+
setDrawing(null);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
setDrawing({ ...drawing, corners: [...drawing.corners, point] });
|
|
446
|
+
};
|
|
447
|
+
const finishRegion = () => {
|
|
448
|
+
if (drawing === null || drawing.kind !== "region" || drawing.corners.length < 3)
|
|
449
|
+
return;
|
|
450
|
+
store.apply({
|
|
451
|
+
name: options.draw.act,
|
|
452
|
+
args: { [options.draw.id]: drawing.id, [options.draw.outline]: drawing.corners },
|
|
453
|
+
}, { author: principal, intent: `Drew ${drawing.label} on the map — ${drawing.corners.length} corners` });
|
|
454
|
+
setDrawing(null);
|
|
455
|
+
};
|
|
456
|
+
const hint = drawing === null
|
|
457
|
+
? map.regions.length === 0
|
|
458
|
+
? "Nothing drawn yet. Choose an area below, then click its corners here."
|
|
459
|
+
: null
|
|
460
|
+
: drawing.kind === "marker"
|
|
461
|
+
? `Click where ${drawing.label} stands.`
|
|
462
|
+
: drawing.corners.length < 3
|
|
463
|
+
? `Click the corners of ${drawing.label} — ${3 - drawing.corners.length} more to make an area.`
|
|
464
|
+
: `${drawing.corners.length} corners. Keep clicking, or press Done.`;
|
|
465
|
+
if (!drawable) {
|
|
466
|
+
return (_jsx(Panel, { title: label ?? "The plan", subtitle: `${drawn} drawn · ${things} standing in them`, selected: selected, children: _jsx(Roster, { map: map, options: options, ...(implicated ? { implicated } : {}), ...(flagged ? { flagged } : {}) }) }));
|
|
467
|
+
}
|
|
468
|
+
return (_jsxs(Panel, { title: label ?? "The plan", subtitle: `${drawn} drawn · ${things} standing in them`, ...(map.undrawn.length > 0
|
|
469
|
+
? { meta: `${map.undrawn.length} ${options.undrawnLabel ?? "not drawn yet"}` }
|
|
470
|
+
: {}), selected: selected, fit: true, children: [_jsx("div", { style: { position: "relative", width: "100%", aspectRatio: `${view.w} / ${view.h}`, overflow: "hidden" }, children: _jsxs("svg", { ref: canvas, viewBox: `${view.x} ${view.y} ${view.w} ${view.h}`, width: "100%", height: "100%", preserveAspectRatio: "xMidYMid meet",
|
|
471
|
+
/*
|
|
472
|
+
* A DRAWING WHOSE PARTS ARE CONTROLS IS NOT AN IMAGE.
|
|
473
|
+
*
|
|
474
|
+
* `role="img"` is a leaf: it promises a screen reader that what
|
|
475
|
+
* is inside is decoration described by the label, so a focusable
|
|
476
|
+
* shape in there is a control the reader has been told is not
|
|
477
|
+
* there. axe calls it nested-interactive and it is right. Every
|
|
478
|
+
* area here is a button, so this is a group — and an image only
|
|
479
|
+
* when the picture is too small to have given anything a target,
|
|
480
|
+
* which is exactly when it IS decoration.
|
|
481
|
+
*/
|
|
482
|
+
role: drawing !== null ? "application" : touchable ? "group" : "img", "aria-label": drawing === null
|
|
483
|
+
? `${label ?? "The plan"}: ${drawn} drawn, ${things} standing in them`
|
|
484
|
+
: hint ?? "Drawing", onClick: onCanvasClick, "data-testid": "plan-canvas", style: {
|
|
485
|
+
display: "block",
|
|
486
|
+
position: "absolute",
|
|
487
|
+
inset: 0,
|
|
488
|
+
/*
|
|
489
|
+
* THE PICTURE STAYS INSIDE ITS BOX.
|
|
490
|
+
*
|
|
491
|
+
* At phone width the scene lays this panel out at 136 pixels and
|
|
492
|
+
* something in that chain resolved the canvas to 138 by TWO — at
|
|
493
|
+
* which point the drawing did not scale down, it OVERFLOWED: a
|
|
494
|
+
* thousand units of ground painted across the page, region names
|
|
495
|
+
* ending nine hundred pixels past the right edge of a 390-pixel
|
|
496
|
+
* screen, invisible because they sat behind everything else and
|
|
497
|
+
* caught only because a harness measures where text actually
|
|
498
|
+
* lands. `aspect-ratio` makes the height follow the width even
|
|
499
|
+
* where a flex parent would rather it did not, and `overflow`
|
|
500
|
+
* makes the escape impossible rather than unlikely.
|
|
501
|
+
*/
|
|
502
|
+
cursor: drawing === null ? undefined : "crosshair",
|
|
503
|
+
// A dashed edge and a faint grid: a sheet to draw on, not a void.
|
|
504
|
+
border: `1px dashed color-mix(in oklab, var(--graview-edge) 80%, transparent)`,
|
|
505
|
+
borderRadius: ".4rem",
|
|
506
|
+
backgroundImage: "linear-gradient(color-mix(in oklab, var(--graview-edge) 35%, transparent) 1px, transparent 1px)," +
|
|
507
|
+
"linear-gradient(90deg, color-mix(in oklab, var(--graview-edge) 35%, transparent) 1px, transparent 1px)",
|
|
508
|
+
backgroundSize: "10% 10%",
|
|
509
|
+
}, children: [_jsx("text", { x: view.x + view.w - S * 0.028, y: view.y + S * 0.034, fontSize: S * 0.022, textAnchor: "middle", fill: "var(--graview-ink)", opacity: 0.55, children: "N" }), hint !== null && map.regions.length === 0 ? (_jsx("text", { x: view.x + view.w / 2, y: view.y + view.h / 2, textAnchor: "middle", dominantBaseline: "middle", fontSize: S * 0.026, fill: "var(--graview-ink)", opacity: 0.7, style: { pointerEvents: "none", fontFamily: "var(--graview-font-body)" }, children: hint })) : null, drawing !== null && drawing.kind === "region" && drawing.corners.length > 0 ? (_jsxs("g", { style: { pointerEvents: "none" }, children: [_jsx("polyline", { points: drawing.corners.map((p) => `${p.x * U},${p.y * H}`).join(" "), fill: drawing.corners.length >= 3 ? "color-mix(in oklab, var(--graview-accent) 22%, transparent)" : "none", stroke: "var(--graview-accent)", strokeWidth: 3, strokeDasharray: "8 6" }), drawing.corners.map((p, index) => (_jsx("circle", { cx: p.x * U, cy: p.y * H, r: S * 0.008, fill: "var(--graview-accent)" }, index)))] })) : null, map.regions.map((region) => {
|
|
510
|
+
const emphasis = emphasisOf(region.id, implicated, flagged);
|
|
511
|
+
const dim = anyEmphasis && emphasis === undefined ? 0.34 : 1;
|
|
512
|
+
const points = region.outline.map((p) => `${p.x * U},${p.y * H}`).join(" ");
|
|
513
|
+
return (_jsx("g", { "data-graview-pick": region.id, ...(emphasis ? { "data-graview-emphasis": emphasis } : {}),
|
|
514
|
+
/*
|
|
515
|
+
* The host's `usePickTargets` gives every pick target a tab
|
|
516
|
+
* stop, but it only grants `role="button"` to a closed set of
|
|
517
|
+
* generic HTML tags — no SVG element is on it. So an SVG
|
|
518
|
+
* target would be focusable and announced as nothing at all.
|
|
519
|
+
* Saying it here is the fix; the host leaves an attribute
|
|
520
|
+
* alone once it is set. (docs/graview-feedback.md F-013.)
|
|
521
|
+
*/
|
|
522
|
+
role: "button", "aria-label": `${region.label}${region.what ? `, ${region.what}` : ""}, ${region.markers.length} standing in it`, style: { opacity: dim, cursor: "pointer" }, children: _jsx("polygon", { points: points, fill: fillFor(region.what, emphasis, options.hues), stroke: strokeFor(region.what, emphasis, options.hues), strokeWidth: emphasis === "flagged" ? 5 : 2.5, strokeLinejoin: "round" }) }, region.id));
|
|
523
|
+
}), map.regions.flatMap((region) => region.markers.map((marker) => {
|
|
524
|
+
const emphasis = emphasisOf(marker.id, implicated, flagged);
|
|
525
|
+
const dim = anyEmphasis && emphasis === undefined ? 0.34 : 1;
|
|
526
|
+
return (_jsxs("g", { ...(touchable
|
|
527
|
+
? {
|
|
528
|
+
"data-graview-pick": marker.id,
|
|
529
|
+
role: "button",
|
|
530
|
+
"aria-label": `${marker.label}${marker.what ? `, ${marker.what}` : ""}, in ${region.label}`,
|
|
531
|
+
}
|
|
532
|
+
: { "aria-hidden": true }), ...(emphasis ? { "data-graview-emphasis": emphasis } : {}), style: { opacity: dim, cursor: touchable ? "pointer" : "default" }, children: [touchable ? (_jsx("circle", { cx: marker.at.x * U, cy: marker.at.y * H, r: S * HIT, fill: "transparent" })) : null, _jsx("circle", { cx: marker.at.x * U, cy: marker.at.y * H, r: S * 0.011, fill: emphasis === "flagged"
|
|
533
|
+
? "var(--graview-warn)"
|
|
534
|
+
: `hsl(${degrees(marker.what, options.hues)} 55% 45%)`, stroke: "var(--graview-panel)", strokeWidth: S * 0.004 }), tagged.has(marker.id) ? (_jsx("text", { x: tagged.get(marker.id).at.x, y: tagged.get(marker.id).at.y, textAnchor: tagged.get(marker.id).anchor, dominantBaseline: "middle", fontSize: tagged.get(marker.id).size, fill: "var(--graview-ink)", stroke: "var(--graview-panel)", strokeWidth: Math.max(2, tagged.get(marker.id).size * 0.3), paintOrder: "stroke", "data-testid": `marker-label-${marker.id}`, style: { pointerEvents: "none", fontFamily: "var(--graview-font-body)" }, children: marker.label })) : null] }, marker.id));
|
|
535
|
+
})), map.regions.map((region) => {
|
|
536
|
+
const fitted = named.get(region.id);
|
|
537
|
+
const at = placed.get(region.id);
|
|
538
|
+
if (!fitted || at === undefined)
|
|
539
|
+
return null;
|
|
540
|
+
const emphasis = emphasisOf(region.id, implicated, flagged);
|
|
541
|
+
return (_jsx("text", { x: at.x, y: at.y - ((fitted.lines.length - 1) * fitted.fontSize * 1.15) / 2, textAnchor: "start", dominantBaseline: "middle", fontSize: fitted.fontSize, fill: "var(--graview-ink)", opacity: anyEmphasis && emphasis === undefined ? 0.34 : 1,
|
|
542
|
+
/* Painted stroke-first so a label stays legible wherever it
|
|
543
|
+
lands — over a dark fill, over a neighbour's edge, over
|
|
544
|
+
another label. Without it the name of the ground is the
|
|
545
|
+
first thing the picture loses. */
|
|
546
|
+
stroke: "var(--graview-panel)", strokeWidth: Math.max(2, fitted.fontSize * 0.26), paintOrder: "stroke", "data-testid": `region-label-${region.id}`, style: { pointerEvents: "none", fontFamily: "var(--graview-font-display)" }, children: fitted.lines.map((line, index) => (_jsx("tspan", { x: at.x, dy: index === 0 ? 0 : fitted.fontSize * 1.15, children: line }, line + String(index)))) }, `label-${region.id}`));
|
|
547
|
+
})] }) }), unsaid.length > 0 ? (_jsx("ul", { "data-testid": "map-key", style: {
|
|
548
|
+
margin: ".5rem 0 0",
|
|
549
|
+
padding: 0,
|
|
550
|
+
listStyle: "none",
|
|
551
|
+
display: "flex",
|
|
552
|
+
flexWrap: "wrap",
|
|
553
|
+
gap: ".2rem .75rem",
|
|
554
|
+
fontSize: "0.875rem",
|
|
555
|
+
}, children: unsaid.map((one) => (_jsxs("li", { "data-graview-pick": one.id, style: {
|
|
556
|
+
display: "flex",
|
|
557
|
+
alignItems: "center",
|
|
558
|
+
gap: ".35rem",
|
|
559
|
+
minWidth: 0,
|
|
560
|
+
/* A row in the key is a real target, because for a marker
|
|
561
|
+
on a small screen it is the only one there is. */
|
|
562
|
+
minHeight: "max(1.5rem, 24px)",
|
|
563
|
+
padding: "0 .15rem",
|
|
564
|
+
cursor: "pointer",
|
|
565
|
+
}, children: [_jsx("span", { "aria-hidden": "true", style: {
|
|
566
|
+
width: one.round ? ".55rem" : ".7rem",
|
|
567
|
+
height: one.round ? ".55rem" : ".7rem",
|
|
568
|
+
flex: "0 0 auto",
|
|
569
|
+
borderRadius: one.round ? "50%" : ".15rem",
|
|
570
|
+
background: one.round
|
|
571
|
+
? `hsl(${degrees(one.what, options.hues)} 55% 45%)`
|
|
572
|
+
: fillFor(one.what, undefined, options.hues),
|
|
573
|
+
border: one.round ? undefined : `1px solid ${strokeFor(one.what, undefined, options.hues)}`,
|
|
574
|
+
} }), _jsx("span", { style: { minWidth: 0, overflowWrap: "anywhere" }, children: one.label })] }, one.id))) })) : null, drawing !== null ? (_jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: ".5rem", marginTop: ".5rem", alignItems: "center" }, children: [map.regions.length > 0 && hint !== null ? (_jsx("span", { style: { fontSize: "0.9375rem", opacity: 0.8, flexBasis: "100%" }, "aria-live": "polite", children: hint })) : null, drawing.kind === "region" ? (_jsxs("button", { type: "button", onClick: finishRegion, disabled: drawing.corners.length < 3, "data-testid": "draw-done", style: { ...TOOL, background: "var(--graview-accent)", color: "var(--graview-accent-ink)", borderColor: "transparent" }, children: ["Done \u2014 keep ", drawing.label] })) : null, drawing.kind === "region" && drawing.corners.length > 0 ? (_jsx("button", { type: "button", onClick: () => setDrawing({ ...drawing, corners: drawing.corners.slice(0, -1) }), style: TOOL, children: "Undo a corner" })) : null, _jsx("button", { type: "button", onClick: () => setDrawing(null), style: TOOL, "data-testid": "draw-cancel", children: "Stop" })] })) : (_jsxs(_Fragment, { children: [canDraw && map.undrawn.length > 0 ? (_jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: ".4rem", marginTop: ".5rem", alignItems: "center" }, children: [_jsxs("span", { style: { fontSize: "0.875rem", opacity: 0.75 }, children: [options.undrawnLabel ?? "Not drawn yet", ":"] }), map.undrawn.map((region) => (_jsxs("button", { type: "button", style: TOOL, onClick: () => setDrawing({ kind: "region", id: region.id, label: region.label, corners: [] }), "data-testid": `draw-${region.id}`, children: ["Draw ", region.label] }, region.id)))] })) : null, canPlace && map.strays.length > 0 ? (_jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: ".4rem", marginTop: ".5rem", alignItems: "center" }, children: [_jsxs("span", { style: { fontSize: "0.875rem", opacity: 0.75 }, children: [options.strayLabel ?? "Not placed", ":"] }), map.strays.map((stray) => (_jsxs("button", { type: "button", style: TOOL, onClick: () => setDrawing({ kind: "marker", id: stray.id, label: stray.label }), "data-testid": `place-${stray.id}`, children: ["Place ", stray.label] }, stray.id)))] })) : null, options.help !== undefined && (map.undrawn.length > 0 || map.strays.length > 0) ? (_jsx("p", { style: { margin: ".5rem 0 0", fontSize: "0.875rem" }, children: _jsx("a", { href: options.help.href, "data-testid": "map-help", children: options.help.label }) })) : null, !canDraw && (map.undrawn.length > 0 || map.strays.length > 0) ? (_jsxs("p", { style: { margin: ".5rem 0 0", fontSize: "0.875rem", opacity: 0.75 }, children: [map.undrawn.map((r) => r.label).join(", "), map.undrawn.length > 0 ? ` — ${options.undrawnLabel ?? "not drawn yet"}. ` : "", map.strays.map((s) => s.label).join(", "), map.strays.length > 0 ? ` — ${options.strayLabel ?? "not placed"}.` : ""] })) : null] }))] }));
|
|
575
|
+
}
|
|
576
|
+
//# sourceMappingURL=plan-view.js.map
|