@1agh/maude 0.24.0 → 0.25.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/package.json +8 -8
- package/plugins/design/dependencies.json +30 -2
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +481 -78
- package/plugins/design/dev-server/annotations-layer.tsx +817 -170
- package/plugins/design/dev-server/api.ts +15 -1
- package/plugins/design/dev-server/bin/_enumerate-artboards-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_html-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_pdf-playwright.mjs +25 -8
- package/plugins/design/dev-server/bin/_png-playwright.mjs +20 -20
- package/plugins/design/dev-server/bin/_pptx-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_pw-launch.mjs +61 -0
- package/plugins/design/dev-server/bin/_screenshot-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_svg-playwright.mjs +125 -19
- package/plugins/design/dev-server/bin/smoke.sh +114 -12
- package/plugins/design/dev-server/canvas-arrowheads.ts +220 -0
- package/plugins/design/dev-server/canvas-comment-mount.tsx +8 -24
- package/plugins/design/dev-server/canvas-cursors.ts +130 -82
- package/plugins/design/dev-server/canvas-icons.tsx +169 -0
- package/plugins/design/dev-server/canvas-shell.tsx +113 -89
- package/plugins/design/dev-server/client/app.jsx +1084 -417
- package/plugins/design/dev-server/dist/client.bundle.js +242 -20
- package/plugins/design/dev-server/dist/comment-mount.js +40 -62
- package/plugins/design/dev-server/export-dialog.tsx +189 -1
- package/plugins/design/dev-server/exporters/html.ts +4 -1
- package/plugins/design/dev-server/exporters/index.ts +4 -1
- package/plugins/design/dev-server/exporters/pdf.ts +7 -1
- package/plugins/design/dev-server/exporters/png.ts +21 -2
- package/plugins/design/dev-server/exporters/pptx.ts +330 -201
- package/plugins/design/dev-server/exporters/scope.ts +107 -11
- package/plugins/design/dev-server/exporters/svg.ts +4 -1
- package/plugins/design/dev-server/http.ts +40 -9
- package/plugins/design/dev-server/input-router.tsx +9 -2
- package/plugins/design/dev-server/test/annotations-draw-modifiers.test.ts +206 -0
- package/plugins/design/dev-server/test/annotations-layer.test.ts +83 -3
- package/plugins/design/dev-server/test/annotations-roundtrip.test.ts +243 -0
- package/plugins/design/dev-server/test/canvas-cursors.test.ts +95 -6
- package/plugins/design/dev-server/test/canvas-route.test.ts +4 -1
- package/plugins/design/dev-server/test/exporters/png.test.ts +24 -1
- package/plugins/design/dev-server/test/exporters/pptx-deck.test.ts +112 -0
- package/plugins/design/dev-server/test/exporters/pw-launch.test.ts +49 -0
- package/plugins/design/dev-server/test/exporters/scope.test.ts +0 -0
- package/plugins/design/dev-server/test/fixtures/phase-21-annotations.svg +1 -0
- package/plugins/design/dev-server/test/input-router.test.ts +11 -4
- package/plugins/design/dev-server/test/sanitize-annotation-svg.test.ts +32 -0
- package/plugins/design/dev-server/test/use-annotation-resize.test.ts +200 -0
- package/plugins/design/dev-server/test/use-tool-mode.test.tsx +9 -11
- package/plugins/design/dev-server/tool-palette.tsx +140 -11
- package/plugins/design/dev-server/use-annotation-resize.tsx +208 -61
- package/plugins/design/dev-server/use-tool-mode.tsx +55 -9
- package/plugins/design/templates/_shell.html +36 -9
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
# per-canvas hooks in /design:edit step 7 / /design:new step 9 — typically
|
|
5
5
|
# dev-server infra changes or bulk multi-canvas migrations. See DDR-021.
|
|
6
6
|
#
|
|
7
|
+
# Beyond "does it render", preview specimens are also gated on "does it render
|
|
8
|
+
# STYLED" (DDR-068): a static import-graph lint (every specimen reaches
|
|
9
|
+
# _layout.css — its single CSS entry, which @imports the tokens + _components.css
|
|
10
|
+
# — and every shared preview/_*.css has >=1 importer) plus a runtime
|
|
11
|
+
# computed-style check (the DS token contract `--bg-0` must resolve on the body,
|
|
12
|
+
# else the canvas's import graph lost the token CSS and every var()-driven rule
|
|
13
|
+
# is dead — the specimen mounts with content but renders unstyled).
|
|
14
|
+
#
|
|
7
15
|
# Usage:
|
|
8
16
|
# smoke.sh [--root <repo>]
|
|
9
17
|
# [--out-dir <dir>]
|
|
@@ -27,7 +35,7 @@
|
|
|
27
35
|
# Stdout: one line per canvas, tab-separated:
|
|
28
36
|
# STATUS \t FILE \t SCREENSHOT \t DETAIL
|
|
29
37
|
# Stderr: diagnostic / progress.
|
|
30
|
-
# Exit: 0 = all green / 3 = at least one blank/error / 1 = missing deps / 2 = bad args.
|
|
38
|
+
# Exit: 0 = all green / 3 = at least one blank/error/unstyled/lint-fail / 1 = missing deps / 2 = bad args.
|
|
31
39
|
|
|
32
40
|
REPO=""
|
|
33
41
|
OUT_DIR=""
|
|
@@ -176,6 +184,59 @@ urlencode_path() {
|
|
|
176
184
|
printf '%s' "$1" | sed 's/ /%20/g'
|
|
177
185
|
}
|
|
178
186
|
|
|
187
|
+
# Static import-graph lint (DDR-068). Runs once before render. The dev-server
|
|
188
|
+
# inlines ONLY the CSS a canvas's import graph produces (canvas-build.ts), so a
|
|
189
|
+
# forgotten import is a silently unstyled specimen that still "has content" and
|
|
190
|
+
# sails past the per-canvas render checks below. Asserts, per DS preview dir:
|
|
191
|
+
# (1) every specimen reaches `_layout.css` (direct `import`, or its own css
|
|
192
|
+
# `@import`s it) — _layout.css is the single CSS entry that pulls tokens
|
|
193
|
+
# (colors_and_type.css) + controls (_components.css);
|
|
194
|
+
# (2) every shared `preview/_*.css` has >=1 importer (no orphan partial — the
|
|
195
|
+
# class of bug where `.btn`/`.input` in _components.css loaded NOWHERE).
|
|
196
|
+
# Appends violations to $MD; sets LINT_FAILED. Anchored to real `^import` lines
|
|
197
|
+
# so a specimen that DISPLAYS a css filename in its content isn't a false match.
|
|
198
|
+
LINT_FAILED=0
|
|
199
|
+
lint_specimen_imports() {
|
|
200
|
+
local SQ Q preview tsx base css imp impfile reached n=0
|
|
201
|
+
SQ=$(printf '\047'); Q="[\"$SQ]" # bracket class matching either quote style
|
|
202
|
+
for preview in "$DESIGN_ROOT"/system/*/preview; do
|
|
203
|
+
[ -d "$preview" ] || continue
|
|
204
|
+
# (1) every specimen reaches _layout.css
|
|
205
|
+
for tsx in "$preview"/*.tsx; do
|
|
206
|
+
[ -e "$tsx" ] || continue
|
|
207
|
+
base=$(basename "$tsx"); case "$base" in _*) continue ;; esac
|
|
208
|
+
grep -qE "^[[:space:]]*import[^;]*${Q}[^\"$SQ]*_layout\.css" "$tsx" && continue
|
|
209
|
+
reached=0
|
|
210
|
+
while IFS= read -r imp; do
|
|
211
|
+
impfile="$preview/$(basename "$imp")"
|
|
212
|
+
[ -f "$impfile" ] || continue
|
|
213
|
+
if grep -qE "@import[^;]*_layout\.css" "$impfile"; then reached=1; break; fi
|
|
214
|
+
done < <(grep -oE "import[[:space:]]+${Q}[^\"$SQ]*\.css" "$tsx" 2>/dev/null | grep -oE "[^\"$SQ]*\.css$")
|
|
215
|
+
if [ "$reached" -eq 0 ]; then
|
|
216
|
+
printf 'LINT-FAIL %s — never reaches _layout.css (no DS tokens/layout/components)\n' "${tsx#$DESIGN_ROOT/}" >&2
|
|
217
|
+
printf '| ✗ LINT | `%s` | — | never reaches _layout.css |\n' "${tsx#$DESIGN_ROOT/}" >> "$MD"
|
|
218
|
+
n=$((n + 1))
|
|
219
|
+
fi
|
|
220
|
+
done
|
|
221
|
+
# (2) every shared _*.css partial has an importer
|
|
222
|
+
for css in "$preview"/_*.css; do
|
|
223
|
+
[ -e "$css" ] || continue
|
|
224
|
+
base=$(basename "$css")
|
|
225
|
+
grep -qsE "^[[:space:]]*import[^;]*${Q}[^\"$SQ]*${base}${Q}" "$preview"/*.tsx && continue
|
|
226
|
+
grep -qsE "@import[^;]*${base}" "$preview"/*.css "$preview"/../*.css && continue
|
|
227
|
+
printf 'LINT-FAIL %s — orphan shared partial (no specimen or css imports it)\n' "${css#$DESIGN_ROOT/}" >&2
|
|
228
|
+
printf '| ✗ LINT | `%s` | — | orphan shared partial |\n' "${css#$DESIGN_ROOT/}" >> "$MD"
|
|
229
|
+
n=$((n + 1))
|
|
230
|
+
done
|
|
231
|
+
done
|
|
232
|
+
LINT_FAILED=$n
|
|
233
|
+
if [ "$n" -eq 0 ]; then
|
|
234
|
+
echo "→ import-graph lint: clean" >&2
|
|
235
|
+
else
|
|
236
|
+
echo "✗ import-graph lint: $n violation(s) — see report" >&2
|
|
237
|
+
fi
|
|
238
|
+
}
|
|
239
|
+
|
|
179
240
|
# Probe a canvas via agent-browser. Returns three lines on stdout:
|
|
180
241
|
# <status> one of OK / BLANK / ERROR
|
|
181
242
|
# <detail> short summary string
|
|
@@ -183,6 +244,7 @@ urlencode_path() {
|
|
|
183
244
|
probe_agent_browser() {
|
|
184
245
|
local url="$1"
|
|
185
246
|
local out_png="$2"
|
|
247
|
+
local is_specimen="$3" # 1 → apply the DDR-068 computed-style gate
|
|
186
248
|
|
|
187
249
|
agent-browser open "$url" >/dev/null 2>&1 || { echo "ERROR"; echo "open-failed"; echo ""; return; }
|
|
188
250
|
|
|
@@ -254,6 +316,35 @@ probe_agent_browser() {
|
|
|
254
316
|
return
|
|
255
317
|
fi
|
|
256
318
|
|
|
319
|
+
# Computed-style gate (DDR-068) — "mounts with content" ≠ "rendered styled". A
|
|
320
|
+
# specimen whose import graph dropped the token CSS passes every check above
|
|
321
|
+
# (it has text, no error, a >2 KB screenshot) yet every var()-driven rule is
|
|
322
|
+
# dead. Confirm the DS token contract actually resolved at runtime: --bg-0 is
|
|
323
|
+
# defined on :root by colors_and_type.css (DDR-043 name contract) and inherits
|
|
324
|
+
# to <body>, so an empty value means the token CSS never loaded. Scoped to DS
|
|
325
|
+
# preview specimens — ui/ app canvases may legitimately opt out of the tokens.
|
|
326
|
+
if [ "$is_specimen" = "1" ]; then
|
|
327
|
+
local styled
|
|
328
|
+
styled=$(agent-browser eval "(() => {
|
|
329
|
+
const b = document.body; if (!b) return 'unstyled:no-body';
|
|
330
|
+
const cs = getComputedStyle(b);
|
|
331
|
+
const tok = (cs.getPropertyValue('--bg-0').trim() || cs.getPropertyValue('--fg-0').trim() || cs.getPropertyValue('--accent').trim());
|
|
332
|
+
if (tok) return 'styled';
|
|
333
|
+
return 'unstyled:no-tokens(ff=' + ((cs.fontFamily||'').replace(/[\"\\n]/g,'').slice(0,18)) + ')';
|
|
334
|
+
})()" 2>/dev/null)
|
|
335
|
+
styled=$(printf '%s' "$styled" | sed 's/^"//; s/"$//')
|
|
336
|
+
# Only an EXPLICIT unstyled* verdict fails — a blank/garbled eval (browser
|
|
337
|
+
# hiccup) falls through to OK, since the DOM already mounted without error.
|
|
338
|
+
case "$styled" in
|
|
339
|
+
unstyled*)
|
|
340
|
+
echo "UNSTYLED"
|
|
341
|
+
echo "$styled"
|
|
342
|
+
echo "$out_png"
|
|
343
|
+
return
|
|
344
|
+
;;
|
|
345
|
+
esac
|
|
346
|
+
fi
|
|
347
|
+
|
|
257
348
|
echo "OK"
|
|
258
349
|
echo "ok"
|
|
259
350
|
echo "$out_png"
|
|
@@ -274,6 +365,13 @@ printf 'status\tfile\tscreenshot\tdetail\n' > "$TSV"
|
|
|
274
365
|
echo "|---|---|---|---|"
|
|
275
366
|
} > "$MD"
|
|
276
367
|
|
|
368
|
+
# Static import-graph lint before rendering (DDR-068) — only meaningful when
|
|
369
|
+
# system specimens are in scope (they own the shared _layout.css / _components.css
|
|
370
|
+
# chain). Violations are folded into the final exit code alongside render fails.
|
|
371
|
+
if [ "$INCLUDE_SYSTEM" = "1" ] && [ -d "$DESIGN_ROOT/system" ]; then
|
|
372
|
+
lint_specimen_imports
|
|
373
|
+
fi
|
|
374
|
+
|
|
277
375
|
FAILED=0
|
|
278
376
|
N=0
|
|
279
377
|
while IFS= read -r CANVAS; do
|
|
@@ -288,11 +386,13 @@ while IFS= read -r CANVAS; do
|
|
|
288
386
|
URL="http://localhost:$PORT/_canvas-shell.html?canvas=$REL_ENC"
|
|
289
387
|
SLUG=$(bash "$SLUG_HELPER" "$REL" 2>/dev/null || printf '%s' "$REL" | tr '/ ' '__' | tr '[:upper:]' '[:lower:]')
|
|
290
388
|
OUT_PNG="$OUT_DIR/$SLUG.png"
|
|
389
|
+
# DS preview specimens get the computed-style gate; ui/ app canvases don't.
|
|
390
|
+
case "$REL" in system/*/preview/*) IS_SPECIMEN=1 ;; *) IS_SPECIMEN=0 ;; esac
|
|
291
391
|
|
|
292
392
|
printf ' [%d/%d] %s … ' "$N" "$COUNT" "$REL" >&2
|
|
293
393
|
|
|
294
394
|
if [ "$ENGINE" = "agent-browser" ]; then
|
|
295
|
-
RESULT=$(probe_agent_browser "$URL" "$OUT_PNG")
|
|
395
|
+
RESULT=$(probe_agent_browser "$URL" "$OUT_PNG" "$IS_SPECIMEN")
|
|
296
396
|
else
|
|
297
397
|
# Playwright fallback — coarser: just screenshot, accept any PNG > 2 KB as OK.
|
|
298
398
|
# See _screenshot-playwright.mjs for the underlying tool.
|
|
@@ -314,10 +414,11 @@ while IFS= read -r CANVAS; do
|
|
|
314
414
|
SHOT=$(printf '%s\n' "$RESULT" | sed -n '3p')
|
|
315
415
|
|
|
316
416
|
case "$STATUS" in
|
|
317
|
-
OK)
|
|
318
|
-
BLANK)
|
|
319
|
-
ERROR)
|
|
320
|
-
|
|
417
|
+
OK) SYM="✓"; echo "$SYM" >&2 ;;
|
|
418
|
+
BLANK) SYM="✗"; FAILED=$((FAILED + 1)); echo "$SYM blank ($DETAIL)" >&2 ;;
|
|
419
|
+
ERROR) SYM="⚠"; FAILED=$((FAILED + 1)); echo "$SYM error ($DETAIL)" >&2 ;;
|
|
420
|
+
UNSTYLED) SYM="✗"; FAILED=$((FAILED + 1)); echo "$SYM unstyled ($DETAIL)" >&2 ;;
|
|
421
|
+
*) SYM="?"; FAILED=$((FAILED + 1)); echo "? unknown ($STATUS)" >&2 ;;
|
|
321
422
|
esac
|
|
322
423
|
|
|
323
424
|
printf '%s\t%s\t%s\t%s\n' "$STATUS" "$REL" "$SHOT" "$DETAIL" >> "$TSV"
|
|
@@ -326,12 +427,13 @@ while IFS= read -r CANVAS; do
|
|
|
326
427
|
printf '%s\t%s\t%s\t%s\n' "$STATUS" "$REL" "$SHOT" "$DETAIL"
|
|
327
428
|
done <<< "$CANVASES"
|
|
328
429
|
|
|
430
|
+
TOTAL_FAIL=$((FAILED + LINT_FAILED))
|
|
329
431
|
{
|
|
330
432
|
echo
|
|
331
|
-
if [ $
|
|
332
|
-
echo "**Result:** ✓ all $COUNT canvases rendered."
|
|
433
|
+
if [ $TOTAL_FAIL -eq 0 ]; then
|
|
434
|
+
echo "**Result:** ✓ all $COUNT canvases rendered styled; import-graph lint clean."
|
|
333
435
|
else
|
|
334
|
-
echo "**Result:** ✗ $FAILED / $COUNT canvases failed."
|
|
436
|
+
echo "**Result:** ✗ $FAILED / $COUNT canvases failed render/style; $LINT_FAILED import-graph lint violation(s)."
|
|
335
437
|
fi
|
|
336
438
|
} >> "$MD"
|
|
337
439
|
|
|
@@ -347,9 +449,9 @@ if command -v git >/dev/null 2>&1 && git -C "$REPO" rev-parse HEAD >/dev/null 2>
|
|
|
347
449
|
> "$DESIGN_ROOT/_history/_smoke/.last-smoke.json"
|
|
348
450
|
fi
|
|
349
451
|
|
|
350
|
-
if [ $
|
|
351
|
-
echo "✗ smoke: $FAILED / $COUNT canvases failed" >&2
|
|
452
|
+
if [ $TOTAL_FAIL -gt 0 ]; then
|
|
453
|
+
echo "✗ smoke: $FAILED / $COUNT canvases failed render/style; $LINT_FAILED lint violation(s)" >&2
|
|
352
454
|
exit 3
|
|
353
455
|
fi
|
|
354
|
-
echo "✓ smoke: all $COUNT canvases rendered" >&2
|
|
456
|
+
echo "✓ smoke: all $COUNT canvases rendered styled; import-graph lint clean" >&2
|
|
355
457
|
exit 0
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file canvas-arrowheads.ts — Phase 24 arrow shaft + head geometry
|
|
3
|
+
* @scope plugins/design/dev-server/canvas-arrowheads.ts
|
|
4
|
+
* @purpose Single source of truth for arrow rendering. An ArrowStroke is
|
|
5
|
+
* reduced to an ordered list of `SvgPrimitive`s (the shaft, then
|
|
6
|
+
* the start head, then the end head). The serializer formats each
|
|
7
|
+
* primitive into the persisted `.annotations.svg` string; the live
|
|
8
|
+
* `StrokeNode` maps the SAME primitives to JSX — so the on-disk and
|
|
9
|
+
* on-canvas forms can never drift.
|
|
10
|
+
*
|
|
11
|
+
* Back-compat invariant (DDR-067): a default arrow (lineType
|
|
12
|
+
* 'straight', startHead 'none', endHead 'triangle', solid) MUST
|
|
13
|
+
* reduce to exactly `[<line>, <polyline fill=color>]` — the
|
|
14
|
+
* byte-identical Phase-5.1 form the canary fixtures freeze.
|
|
15
|
+
* `arrowHeadPoints` lives here (moved from annotations-layer, which
|
|
16
|
+
* re-exports it). This module owns the arrow style enums + a
|
|
17
|
+
* structural geometry input and imports NOTHING from
|
|
18
|
+
* annotations-layer — keeping the dependency one-way
|
|
19
|
+
* (annotations-layer → canvas-arrowheads) so there is no module
|
|
20
|
+
* cycle. (A `.ts` root in a re-export cycle with a react `.tsx`
|
|
21
|
+
* breaks @types/react's global `JSX` namespace project-wide under
|
|
22
|
+
* this tsconfig's `types: ["bun-types"]` — see DDR-067.)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Phase 24 — full FigJam arrowhead vocabulary, selectable per end. `none` =
|
|
27
|
+
* bare end; `line` = open chevron; `triangle` = the legacy filled head
|
|
28
|
+
* (default end); `triangle-outline` = unfilled triangle; `circle` / `diamond`
|
|
29
|
+
* = filled markers. annotations-layer re-exports this type.
|
|
30
|
+
*/
|
|
31
|
+
export type ArrowHead = 'none' | 'line' | 'triangle' | 'triangle-outline' | 'circle' | 'diamond';
|
|
32
|
+
/** The valid `ArrowHead` vocabulary as a runtime set — `svgToStrokes` clamps a
|
|
33
|
+
* parsed `data-*-head` against this so a poisoned/out-of-vocab value can't be
|
|
34
|
+
* cast through unchecked (Phase 24 security review, DDR-067). */
|
|
35
|
+
export const ARROW_HEADS: ReadonlySet<string> = new Set<ArrowHead>([
|
|
36
|
+
'none',
|
|
37
|
+
'line',
|
|
38
|
+
'triangle',
|
|
39
|
+
'triangle-outline',
|
|
40
|
+
'circle',
|
|
41
|
+
'diamond',
|
|
42
|
+
]);
|
|
43
|
+
/** Phase 24 — arrow shaft routing. `straight` is the default + legacy form. */
|
|
44
|
+
export type ArrowLineType = 'straight' | 'curved' | 'elbow';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Structural geometry input for {@link arrowPrimitives}. `ArrowStroke` (defined
|
|
48
|
+
* in annotations-layer) is assignable to this, so we depend on the shape, not
|
|
49
|
+
* the named type — no import back, no cycle.
|
|
50
|
+
*/
|
|
51
|
+
export interface ArrowGeom {
|
|
52
|
+
x1: number;
|
|
53
|
+
y1: number;
|
|
54
|
+
x2: number;
|
|
55
|
+
y2: number;
|
|
56
|
+
width: number;
|
|
57
|
+
color: string;
|
|
58
|
+
startHead?: ArrowHead;
|
|
59
|
+
endHead?: ArrowHead;
|
|
60
|
+
dashed?: boolean;
|
|
61
|
+
lineType?: ArrowLineType;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The filled-triangle head outline as an SVG `points` string. Three points —
|
|
66
|
+
* `wingA, tip, wingB` — drawn as a <polyline> (filled = solid triangle, the
|
|
67
|
+
* implicit close; unfilled = open chevron) or a <polygon> (closed outline).
|
|
68
|
+
* Parameterized by the tip + the point the shaft arrives FROM, exactly as in
|
|
69
|
+
* Phase 5.1, so the legacy serialization is byte-identical.
|
|
70
|
+
*/
|
|
71
|
+
export function arrowHeadPoints(
|
|
72
|
+
x1: number,
|
|
73
|
+
y1: number,
|
|
74
|
+
x2: number,
|
|
75
|
+
y2: number,
|
|
76
|
+
width: number
|
|
77
|
+
): string {
|
|
78
|
+
const angle = Math.atan2(y2 - y1, x2 - x1);
|
|
79
|
+
const len = 12 + width * 2;
|
|
80
|
+
const wing = Math.PI / 7;
|
|
81
|
+
const ax = x2 - Math.cos(angle - wing) * len;
|
|
82
|
+
const ay = y2 - Math.sin(angle - wing) * len;
|
|
83
|
+
const bx = x2 - Math.cos(angle + wing) * len;
|
|
84
|
+
const by = y2 - Math.sin(angle + wing) * len;
|
|
85
|
+
return `${ax},${ay} ${x2},${y2} ${bx},${by}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A small diamond head centered on the tip, one axis along the shaft. */
|
|
89
|
+
function diamondHeadPoints(
|
|
90
|
+
fromX: number,
|
|
91
|
+
fromY: number,
|
|
92
|
+
tipX: number,
|
|
93
|
+
tipY: number,
|
|
94
|
+
width: number
|
|
95
|
+
): string {
|
|
96
|
+
const angle = Math.atan2(tipY - fromY, tipX - fromX);
|
|
97
|
+
const half = 5 + width;
|
|
98
|
+
const ax = Math.cos(angle);
|
|
99
|
+
const ay = Math.sin(angle);
|
|
100
|
+
const px = -ay;
|
|
101
|
+
const py = ax;
|
|
102
|
+
const p = (dx: number, dy: number) => `${tipX + dx},${tipY + dy}`;
|
|
103
|
+
return `${p(ax * half, ay * half)} ${p(px * half, py * half)} ${p(-ax * half, -ay * half)} ${p(
|
|
104
|
+
-px * half,
|
|
105
|
+
-py * half
|
|
106
|
+
)}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Ordered SVG primitives — the shaft + heads. The renderer/serializer format these. */
|
|
110
|
+
export type SvgPrimitive =
|
|
111
|
+
| { el: 'line'; x1: number; y1: number; x2: number; y2: number; dash: boolean }
|
|
112
|
+
| { el: 'path'; d: string; dash: boolean }
|
|
113
|
+
| { el: 'polyline'; points: string; fill: string }
|
|
114
|
+
| { el: 'polygon'; points: string; fill: string }
|
|
115
|
+
| { el: 'circle'; cx: number; cy: number; r: number; fill: string };
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Shaft path for a non-straight arrow + the control point each head orients
|
|
119
|
+
* toward (its tangent source). Straight arrows never call this — they use a
|
|
120
|
+
* bare <line> so the legacy bytes are preserved.
|
|
121
|
+
*/
|
|
122
|
+
function shaftPath(
|
|
123
|
+
x1: number,
|
|
124
|
+
y1: number,
|
|
125
|
+
x2: number,
|
|
126
|
+
y2: number,
|
|
127
|
+
lineType: Exclude<ArrowLineType, 'straight'>
|
|
128
|
+
): { d: string; startFrom: [number, number]; endFrom: [number, number] } {
|
|
129
|
+
if (lineType === 'curved') {
|
|
130
|
+
const mx = (x1 + x2) / 2;
|
|
131
|
+
const my = (y1 + y2) / 2;
|
|
132
|
+
const dx = x2 - x1;
|
|
133
|
+
const dy = y2 - y1;
|
|
134
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
135
|
+
// Perpendicular bow — 22% of the chord, consistent side so curve is stable.
|
|
136
|
+
const off = len * 0.22;
|
|
137
|
+
const cx = mx + (-dy / len) * off;
|
|
138
|
+
const cy = my + (dx / len) * off;
|
|
139
|
+
return { d: `M${x1} ${y1} Q${cx} ${cy} ${x2} ${y2}`, startFrom: [cx, cy], endFrom: [cx, cy] };
|
|
140
|
+
}
|
|
141
|
+
// elbow — one orthogonal bend, along the dominant axis first.
|
|
142
|
+
const corner: [number, number] = Math.abs(x2 - x1) >= Math.abs(y2 - y1) ? [x2, y1] : [x1, y2];
|
|
143
|
+
return {
|
|
144
|
+
d: `M${x1} ${y1} L${corner[0]} ${corner[1]} L${x2} ${y2}`,
|
|
145
|
+
startFrom: corner,
|
|
146
|
+
endFrom: corner,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function headPrimitives(
|
|
151
|
+
head: ArrowHead,
|
|
152
|
+
tipX: number,
|
|
153
|
+
tipY: number,
|
|
154
|
+
fromX: number,
|
|
155
|
+
fromY: number,
|
|
156
|
+
width: number,
|
|
157
|
+
color: string
|
|
158
|
+
): SvgPrimitive[] {
|
|
159
|
+
switch (head) {
|
|
160
|
+
case 'none':
|
|
161
|
+
return [];
|
|
162
|
+
case 'triangle':
|
|
163
|
+
// Legacy filled head — byte-identical to Phase 5.1.
|
|
164
|
+
return [
|
|
165
|
+
{ el: 'polyline', points: arrowHeadPoints(fromX, fromY, tipX, tipY, width), fill: color },
|
|
166
|
+
];
|
|
167
|
+
case 'line':
|
|
168
|
+
// Open chevron — same 3 points, unfilled (no implicit close).
|
|
169
|
+
return [
|
|
170
|
+
{ el: 'polyline', points: arrowHeadPoints(fromX, fromY, tipX, tipY, width), fill: 'none' },
|
|
171
|
+
];
|
|
172
|
+
case 'triangle-outline':
|
|
173
|
+
// Closed triangle outline — polygon auto-closes the third edge.
|
|
174
|
+
return [
|
|
175
|
+
{ el: 'polygon', points: arrowHeadPoints(fromX, fromY, tipX, tipY, width), fill: 'none' },
|
|
176
|
+
];
|
|
177
|
+
case 'diamond':
|
|
178
|
+
return [
|
|
179
|
+
{ el: 'polygon', points: diamondHeadPoints(fromX, fromY, tipX, tipY, width), fill: color },
|
|
180
|
+
];
|
|
181
|
+
case 'circle':
|
|
182
|
+
return [{ el: 'circle', cx: tipX, cy: tipY, r: 4 + width, fill: color }];
|
|
183
|
+
default:
|
|
184
|
+
// Defensive: an out-of-vocabulary head (e.g. a poisoned hub-pushed value
|
|
185
|
+
// that slipped past the parse-clamp) renders NOTHING rather than throwing
|
|
186
|
+
// a TypeError mid-render — no per-stroke render DoS (Phase 24 security
|
|
187
|
+
// review, DDR-067). Parse-clamp + serialize-esc are the primary controls.
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Reduce an arrow to its ordered SVG primitives: shaft, then start head, then
|
|
194
|
+
* end head (matching the legacy serialization order). Defaults: start 'none',
|
|
195
|
+
* end 'triangle', lineType 'straight', solid.
|
|
196
|
+
*/
|
|
197
|
+
export function arrowPrimitives(s: ArrowGeom): SvgPrimitive[] {
|
|
198
|
+
const startHead = s.startHead ?? 'none';
|
|
199
|
+
const endHead = s.endHead ?? 'triangle';
|
|
200
|
+
const dashed = s.dashed ?? false;
|
|
201
|
+
const lineType = s.lineType ?? 'straight';
|
|
202
|
+
const out: SvgPrimitive[] = [];
|
|
203
|
+
|
|
204
|
+
// Default tangent sources (straight): each head points away from the far end.
|
|
205
|
+
let startFrom: [number, number] = [s.x2, s.y2];
|
|
206
|
+
let endFrom: [number, number] = [s.x1, s.y1];
|
|
207
|
+
|
|
208
|
+
if (lineType === 'straight') {
|
|
209
|
+
out.push({ el: 'line', x1: s.x1, y1: s.y1, x2: s.x2, y2: s.y2, dash: dashed });
|
|
210
|
+
} else {
|
|
211
|
+
const shaft = shaftPath(s.x1, s.y1, s.x2, s.y2, lineType);
|
|
212
|
+
out.push({ el: 'path', d: shaft.d, dash: dashed });
|
|
213
|
+
startFrom = shaft.startFrom;
|
|
214
|
+
endFrom = shaft.endFrom;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
out.push(...headPrimitives(startHead, s.x1, s.y1, startFrom[0], startFrom[1], s.width, s.color));
|
|
218
|
+
out.push(...headPrimitives(endHead, s.x2, s.y2, endFrom[0], endFrom[1], s.width, s.color));
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
@@ -48,29 +48,14 @@ import {
|
|
|
48
48
|
import { MaybeToolProvider, useToolMode } from './use-tool-mode.tsx';
|
|
49
49
|
|
|
50
50
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
const MC_CURSOR_CSS = `
|
|
60
|
-
[data-mc-host][data-active-tool="comment"] *,
|
|
61
|
-
body[data-active-tool="comment"] * {
|
|
62
|
-
cursor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'><path d='M3 3 L14 3 L14 11 L8 11 L4 14 L4 11 L3 11 Z' fill='white' stroke='%23111' stroke-width='1' stroke-linejoin='round'/></svg>") 4 4, crosshair !important;
|
|
63
|
-
}
|
|
64
|
-
`.trim();
|
|
65
|
-
|
|
66
|
-
function ensureMountCursorStyles(): void {
|
|
67
|
-
if (typeof document === 'undefined') return;
|
|
68
|
-
if (document.getElementById('mc-cursor-css')) return;
|
|
69
|
-
const s = document.createElement('style');
|
|
70
|
-
s.id = 'mc-cursor-css';
|
|
71
|
-
s.textContent = MC_CURSOR_CSS;
|
|
72
|
-
document.head.appendChild(s);
|
|
73
|
-
}
|
|
51
|
+
// Phase 24 — the comment-mode cursor (and every other tool cursor) is owned
|
|
52
|
+
// SOLELY by use-tool-mode.tsx, whose `* { cursor: <Kenney glyph> !important }`
|
|
53
|
+
// rule is injected by the ToolProvider this layer mounts (MaybeToolProvider) —
|
|
54
|
+
// so it covers bare DS specimens too. The old `[data-mc-host]…`/`body[…]`
|
|
55
|
+
// comment-cursor rule used to live here, but its higher specificity shadowed
|
|
56
|
+
// the unified Kenney cursor on UI canvases (comment-mount stamps
|
|
57
|
+
// `data-active-tool` on <body>, so `body[data-active-tool="comment"] *` matched
|
|
58
|
+
// there as well). Removed so the single source of truth wins. See DDR-067.
|
|
74
59
|
|
|
75
60
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
76
61
|
// CommentHost — owns the lite comment subsystem: the input router (comment-
|
|
@@ -116,7 +101,6 @@ function pickSpecimenEl(clientX: number, clientY: number): HTMLElement | null {
|
|
|
116
101
|
}
|
|
117
102
|
|
|
118
103
|
function CommentHost({ children, file }: { children: ReactNode; file: string | undefined }) {
|
|
119
|
-
ensureMountCursorStyles();
|
|
120
104
|
const { tool, setTool } = useToolMode();
|
|
121
105
|
const selSet = useSelectionSet();
|
|
122
106
|
const hostRef = useRef<HTMLDivElement | null>(null);
|