@1agh/maude 1.4.5 → 1.5.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/apps/studio/annotations/ai-read.ts +288 -0
- package/apps/studio/annotations/ai-write.ts +533 -0
- package/apps/studio/annotations/board-io.ts +94 -0
- package/apps/studio/annotations/board-text.ts +45 -0
- package/apps/studio/annotations/constants.ts +58 -0
- package/apps/studio/annotations/elements/_shared.ts +125 -0
- package/apps/studio/annotations/elements/arrow.model.ts +196 -0
- package/apps/studio/annotations/elements/media.model.ts +79 -0
- package/apps/studio/annotations/elements/pen.model.ts +82 -0
- package/apps/studio/annotations/elements/section.model.ts +70 -0
- package/apps/studio/annotations/elements/shape.model.ts +141 -0
- package/apps/studio/annotations/elements/sticky.model.ts +48 -0
- package/apps/studio/annotations/elements/text.model.ts +52 -0
- package/apps/studio/annotations/fields.ts +347 -0
- package/apps/studio/annotations/fractional-index.ts +234 -0
- package/apps/studio/annotations/legacy/mini-dom.ts +207 -0
- package/apps/studio/annotations/migrate-boot.ts +172 -0
- package/apps/studio/annotations/migrate-cli.ts +37 -0
- package/apps/studio/annotations/migrate-v1.ts +383 -0
- package/apps/studio/annotations/ops.ts +474 -0
- package/apps/studio/annotations/registry.ts +159 -0
- package/apps/studio/annotations/replica.ts +264 -0
- package/apps/studio/annotations/scene.ts +235 -0
- package/apps/studio/annotations/schema.ts +188 -0
- package/apps/studio/annotations/types.ts +77 -0
- package/apps/studio/annotations/ui/board.ts +126 -0
- package/apps/studio/annotations/ui/containment.ts +118 -0
- package/apps/studio/annotations/ui/edit-actions.ts +551 -0
- package/apps/studio/annotations/ui/editor-channel.ts +62 -0
- package/apps/studio/annotations/ui/element-node.tsx +993 -0
- package/apps/studio/annotations/ui/pipeline-context.ts +17 -0
- package/apps/studio/annotations/ui/pointer-pipeline.ts +150 -0
- package/apps/studio/annotations/ui/render-model.ts +264 -0
- package/apps/studio/annotations/ui/scene.tsx +63 -0
- package/apps/studio/annotations/ui/text-editor.tsx +420 -0
- package/apps/studio/annotations/ui/text-session.ts +140 -0
- package/apps/studio/annotations/ui/text-style.ts +111 -0
- package/apps/studio/annotations/ui/world.ts +47 -0
- package/apps/studio/annotations/v1-adapter.ts +428 -0
- package/apps/studio/annotations-align.ts +21 -6
- package/apps/studio/annotations-bindings.ts +2 -0
- package/apps/studio/annotations-context-toolbar.tsx +9 -13
- package/apps/studio/annotations-groups.ts +3 -0
- package/apps/studio/annotations-layer.tsx +923 -1925
- package/apps/studio/annotations-model.ts +97 -4
- package/apps/studio/annotations-sync.ts +4 -47
- package/apps/studio/api.ts +262 -68
- package/apps/studio/bin/_import-figma.mjs +22 -12
- package/apps/studio/bin/annotate.mjs +331 -838
- package/apps/studio/bin/annotate.sh +4 -4
- package/apps/studio/bin/perf.sh +21 -7
- package/apps/studio/bin/read-annotations.mjs +184 -666
- package/apps/studio/bin/read-annotations.sh +9 -5
- package/apps/studio/canvas-artifacts.ts +9 -0
- package/apps/studio/canvas-comment-mount.tsx +35 -2
- package/apps/studio/canvas-lib.tsx +18 -1
- package/apps/studio/canvas-shell.tsx +54 -1
- package/apps/studio/client/app.jsx +101 -35
- package/apps/studio/client/comments-overlay.css +10 -0
- package/apps/studio/client/hmr.mjs +1 -1
- package/apps/studio/client/panels/git-grouping.js +2 -2
- package/apps/studio/client/tree-expansion.js +217 -0
- package/apps/studio/collab/index.ts +58 -5
- package/apps/studio/collab/persistence.ts +80 -11
- package/apps/studio/collab/registry.ts +63 -26
- package/apps/studio/commands/annotation-ops-command.ts +72 -0
- package/apps/studio/comment-anchor.ts +116 -0
- package/apps/studio/comments-overlay.tsx +78 -35
- package/apps/studio/context.ts +1 -0
- package/apps/studio/cursors-overlay.tsx +337 -109
- package/apps/studio/dist/client.bundle.js +850 -850
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/figma/to-strokes.ts +31 -10
- package/apps/studio/git/endpoints.ts +1 -1
- package/apps/studio/git/service.ts +1 -1
- package/apps/studio/git/watch.ts +1 -1
- package/apps/studio/http.ts +90 -15
- package/apps/studio/server.ts +16 -0
- package/apps/studio/sync/accepted-cold-start.ts +72 -8
- package/apps/studio/sync/agent.ts +36 -6
- package/apps/studio/sync/codec.ts +95 -40
- package/apps/studio/sync/comment-ledger.ts +229 -0
- package/apps/studio/sync/file-membership.ts +12 -2
- package/apps/studio/sync/file-plane.ts +78 -2
- package/apps/studio/sync/index.ts +75 -16
- package/apps/studio/sync/journal-client.ts +5 -0
- package/apps/studio/sync/limits.ts +7 -2
- package/apps/studio/sync/migrate-seed.ts +17 -7
- package/apps/studio/sync/projection.ts +7 -2
- package/apps/studio/sync/remote-docs.ts +34 -0
- package/apps/studio/sync/writer-registry.ts +10 -0
- package/apps/studio/text-caret.ts +11 -2
- package/apps/studio/tree-state.ts +45 -0
- package/apps/studio/undo-stack.ts +2 -2
- package/apps/studio/use-annotation-resize.tsx +48 -23
- package/apps/studio/use-annotation-selection.tsx +9 -2
- package/apps/studio/use-collab.tsx +104 -1
- package/apps/studio/use-selection-set.tsx +4 -0
- package/apps/studio/whats-new.json +63 -0
- package/cli/lib/design-link.mjs +5 -1
- package/cli/lib/gitignore-block.mjs +1 -1
- package/cli/lib/gitignore-drift.mjs +2 -1
- package/package.json +9 -8
- package/plugins/design/templates/brief-board.tsx.template +1 -1
- package/apps/studio/annotation-edit-base.ts +0 -36
- package/apps/studio/commands/annotation-strokes-command.ts +0 -137
|
@@ -1,6 +1,69 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./whats-new.schema.json",
|
|
3
3
|
"entries": [
|
|
4
|
+
{
|
|
5
|
+
"id": "whiteboard-text-sections-live-previews",
|
|
6
|
+
"version": "1.5.0",
|
|
7
|
+
"date": "2026-09-30",
|
|
8
|
+
"kind": "improvement",
|
|
9
|
+
"title": "Smoother whiteboard editing, live together",
|
|
10
|
+
"summary": "Typing in stickies and shapes now wraps exactly as it will look, works with any input method (accents, Japanese), and is never lost — it reaches teammates within a second, and if someone deletes the note you are typing in, Enter puts it back with your text. Sections move, copy and delete together with everything in them, teammates see your drag or resize live before you let go, and undo only takes back your own changes.",
|
|
11
|
+
"surface": "design-ui"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "whiteboard-edits-sync-per-element",
|
|
15
|
+
"version": "1.5.0",
|
|
16
|
+
"date": "2026-09-30",
|
|
17
|
+
"kind": "improvement",
|
|
18
|
+
"title": "Whiteboard edits no longer overwrite each other",
|
|
19
|
+
"summary": "Each sticky, shape and arrow now saves on its own, so two people editing the same board keep both changes, and even text typed into the same sticky at once is merged. Existing boards convert automatically on start, and editing text is steadier: the cursor stays on a new line after Shift+Enter, double-clicking a word selects it, and Shift+click removes an item from a selection.",
|
|
20
|
+
"surface": "design-ui"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "file-tree-remembers-folders",
|
|
24
|
+
"version": "1.5.0",
|
|
25
|
+
"date": "2026-09-30",
|
|
26
|
+
"kind": "improvement",
|
|
27
|
+
"title": "The file tree remembers your folders",
|
|
28
|
+
"summary": "The Files panel now starts collapsed and keeps the folders you open, even after you switch panels, reload or restart the app. Opening a canvas unfolds the way to it.",
|
|
29
|
+
"surface": "design-ui"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "comments-stay-where-you-put-them",
|
|
33
|
+
"version": "1.4.6",
|
|
34
|
+
"date": "2026-09-30",
|
|
35
|
+
"kind": "fix",
|
|
36
|
+
"title": "Comments stay where you put them",
|
|
37
|
+
"summary": "A comment on a sticky, a drawing or empty canvas no longer disappears a few seconds after you save it. A comment on a sticky moves with it, and if the thing it points at is deleted the comment stays, marked as detached, until you remove it.",
|
|
38
|
+
"surface": "design-ui"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "comments-reach-every-peer",
|
|
42
|
+
"version": "1.4.6",
|
|
43
|
+
"date": "2026-09-30",
|
|
44
|
+
"kind": "fix",
|
|
45
|
+
"title": "Web comments reach the desktop app",
|
|
46
|
+
"summary": "Comments added on the web while your desktop app was closed now show up when you reopen it, and a comment deleted elsewhere no longer comes back.",
|
|
47
|
+
"surface": "design-ui"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "large-boards-pan-smoothly",
|
|
51
|
+
"version": "1.4.6",
|
|
52
|
+
"date": "2026-09-30",
|
|
53
|
+
"kind": "improvement",
|
|
54
|
+
"title": "Large boards pan smoothly again",
|
|
55
|
+
"summary": "Panning a board with many artboards stays smooth while something is selected or a teammate is on the canvas — on the desktop app it could drop to a frame a second.",
|
|
56
|
+
"surface": "design-ui"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"id": "sync-no-phantom-waiting-files",
|
|
60
|
+
"version": "1.4.6",
|
|
61
|
+
"date": "2026-09-30",
|
|
62
|
+
"kind": "fix",
|
|
63
|
+
"title": "Sync stops counting files that are done",
|
|
64
|
+
"summary": "The Sync panel no longer shows files as waiting or stuck when there is nothing left to deliver, and Resync now brings down a file the project changed while you were uploading it.",
|
|
65
|
+
"surface": "design-ui"
|
|
66
|
+
},
|
|
4
67
|
{
|
|
5
68
|
"id": "team-projects-hold-up",
|
|
6
69
|
"version": "1.4.5",
|
package/cli/lib/design-link.mjs
CHANGED
|
@@ -809,7 +809,11 @@ function collectAdoptManifest(cwd) {
|
|
|
809
809
|
const files = [];
|
|
810
810
|
try {
|
|
811
811
|
for (const f of readdirSync(base)) {
|
|
812
|
-
if (
|
|
812
|
+
if (
|
|
813
|
+
f.endsWith('.html') ||
|
|
814
|
+
f.endsWith('.annotations.json') ||
|
|
815
|
+
f.endsWith('.annotations.svg')
|
|
816
|
+
) {
|
|
813
817
|
try {
|
|
814
818
|
files.push({ rel: `.design/${f}`, bytes: statSync(resolve(base, f)).size });
|
|
815
819
|
} catch {
|
|
@@ -25,7 +25,7 @@ export function buildBlock(designRel = '.design', { s3Assets = false } = {}) {
|
|
|
25
25
|
// The canonical IGNORED set is the DDR-115 runtime-state taxonomy — kept in
|
|
26
26
|
// lockstep with `apps/studio/git/service.ts` (isMaudeRuntimeState backstop)
|
|
27
27
|
// and the repo's own `.gitignore`. VERSIONED content (canvases, `.meta.json`,
|
|
28
|
-
// `*.annotations.svg
|
|
28
|
+
// `*.annotations.json` (v1 `.svg`), `system/**`, `config.json`) is deliberately absent.
|
|
29
29
|
const lines = [
|
|
30
30
|
BEGIN_MARKER,
|
|
31
31
|
'# Maude design plugin runtime — gitignored even in linked mode (DDR-056/DDR-115).',
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
* `gitignore-block.mjs` and the taxonomy in `apps/studio/git/service.ts`.
|
|
38
38
|
*/
|
|
39
39
|
const VERSIONED_PATTERNS = [
|
|
40
|
-
{ pattern: '*.annotations.
|
|
40
|
+
{ pattern: '*.annotations.json', what: 'canvas draw layers (annotations)' },
|
|
41
|
+
{ pattern: '*.annotations.svg', what: 'canvas draw layers (annotations, v1 form)' },
|
|
41
42
|
{ pattern: '*.meta.json', what: 'canvas sidecars (layout, artboards, tags)' },
|
|
42
43
|
{ pattern: '*.tsx', what: 'canvas bodies' },
|
|
43
44
|
{ pattern: 'config.json', what: 'the project design config' },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1agh/maude",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Marketplace of Claude Code plugins by Michal Dovrtěl: `design` (canvas-first design iteration) + `flow` (generic agentic workflow loop with .ai second brain). Ships the `maude` CLI (with `mdcc` legacy alias) to scaffold workspace, run the design dev server, and manage configs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -58,13 +58,13 @@
|
|
|
58
58
|
"test:e2e:desktop:parity:cloud": "pnpm --filter @maude/desktop-e2e e2e:parity:cloud"
|
|
59
59
|
},
|
|
60
60
|
"optionalDependencies": {
|
|
61
|
-
"@1agh/maude-darwin-arm64": "1.
|
|
62
|
-
"@1agh/maude-darwin-x64": "1.
|
|
63
|
-
"@1agh/maude-linux-arm64": "1.
|
|
64
|
-
"@1agh/maude-linux-arm64-musl": "1.
|
|
65
|
-
"@1agh/maude-linux-x64": "1.
|
|
66
|
-
"@1agh/maude-linux-x64-musl": "1.
|
|
67
|
-
"@1agh/maude-win32-x64": "1.
|
|
61
|
+
"@1agh/maude-darwin-arm64": "1.5.0",
|
|
62
|
+
"@1agh/maude-darwin-x64": "1.5.0",
|
|
63
|
+
"@1agh/maude-linux-arm64": "1.5.0",
|
|
64
|
+
"@1agh/maude-linux-arm64-musl": "1.5.0",
|
|
65
|
+
"@1agh/maude-linux-x64": "1.5.0",
|
|
66
|
+
"@1agh/maude-linux-x64-musl": "1.5.0",
|
|
67
|
+
"@1agh/maude-win32-x64": "1.5.0"
|
|
68
68
|
},
|
|
69
69
|
"files": [
|
|
70
70
|
"cli",
|
|
@@ -131,6 +131,7 @@
|
|
|
131
131
|
"@decimalturn/toml-patch": "3.0.5",
|
|
132
132
|
"ajv": "^8.20.0",
|
|
133
133
|
"ajv-formats": "^3.0.1",
|
|
134
|
+
"diff": "8.0.4",
|
|
134
135
|
"happy-dom": "20.10.6",
|
|
135
136
|
"svgo": "^4.0.1",
|
|
136
137
|
"yaml": "2.9.0"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* a VERBATIM brief and inserts the matching generated artboards right here, below
|
|
14
14
|
* this frame — the board becomes a living brief you sketch and Claude fills in.
|
|
15
15
|
*
|
|
16
|
-
* The annotation layer lives in a sibling `<slug>.annotations.
|
|
16
|
+
* The annotation layer lives in a sibling `<slug>.annotations.json` (NEVER in this
|
|
17
17
|
* file), so your notes stay floating on top across every regeneration. No design
|
|
18
18
|
* system tokens or classes are wired in — the board is a neutral surface; the
|
|
19
19
|
* generated artboards bring the DS with them on ingest.
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
// Which SVG an annotation edit names as its base (DDR-241 accepted revisions).
|
|
2
|
-
//
|
|
3
|
-
// The project merges an annotation proposal element by element against the
|
|
4
|
-
// text it was derived from. The canvas used to send the CANONICAL
|
|
5
|
-
// re-serialization of the strokes it held — fine while the stored text is
|
|
6
|
-
// canonical too, and silently fatal when it is not: an element written by an
|
|
7
|
-
// older serializer, a hand edit or any other writer then looks changed on BOTH
|
|
8
|
-
// sides, the merge refuses it as a conflict, and the person's edit is reverted
|
|
9
|
-
// without a word (plan T31, L09 arrowheads on a non-canonical arrow).
|
|
10
|
-
//
|
|
11
|
-
// An edit made on exactly the state this canvas last RECEIVED names the text
|
|
12
|
-
// it received — the project then sees "nothing changed underneath", accepts
|
|
13
|
-
// the edit whole, and the stored text becomes canonical as a side effect. Any
|
|
14
|
-
// other state (edits stacked before the echo returns) is this canvas's own
|
|
15
|
-
// canonical output, which is what the project holds after accepting it.
|
|
16
|
-
|
|
17
|
-
import { type Stroke, strokesToSvg } from './annotations-model.ts';
|
|
18
|
-
|
|
19
|
-
export interface ReceivedAnnotations {
|
|
20
|
-
/** The SVG text exactly as the project delivered it. */
|
|
21
|
-
raw: string;
|
|
22
|
-
/** The same state as this client serializes it. */
|
|
23
|
-
canon: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function receivedAnnotations(raw: string, strokes: readonly Stroke[]): ReceivedAnnotations {
|
|
27
|
-
return { raw, canon: strokesToSvg(strokes) };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function annotationEditBase(
|
|
31
|
-
before: readonly Stroke[],
|
|
32
|
-
received: ReceivedAnnotations | null
|
|
33
|
-
): string {
|
|
34
|
-
const canon = strokesToSvg(before);
|
|
35
|
-
return received && received.canon === canon ? received.raw : canon;
|
|
36
|
-
}
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file commands/annotation-strokes-command.ts — undo entry for annotation
|
|
3
|
-
* stroke add / erase / batch-translate.
|
|
4
|
-
* @scope apps/studio/commands/annotation-strokes-command.ts
|
|
5
|
-
* @purpose Records a `Stroke[]` pair (before/after) and routes both
|
|
6
|
-
* directions through a single `putFn`. The server endpoint is
|
|
7
|
-
* `PUT /_api/annotations` which replaces the entire SVG; we
|
|
8
|
-
* match that shape rather than diffing.
|
|
9
|
-
*
|
|
10
|
-
* Per DDR-050 rev 2 the runtime command is rebuilt per iframe
|
|
11
|
-
* mount from a serializable `CommandRecord` so the stack
|
|
12
|
-
* survives canvas switches. The `putFn` here also updates the
|
|
13
|
-
* iframe's local React strokes state (see annotations-layer
|
|
14
|
-
* `putStrokes`) so undo/redo visibly refresh, not just PUT.
|
|
15
|
-
*
|
|
16
|
-
* Why per-stroke commands (not coalesced into a 300 ms window):
|
|
17
|
-
* matches Figma/FigJam — Cmd+Z reverts the most recent tap or
|
|
18
|
-
* pen stroke individually. Coalescing into windows led to a
|
|
19
|
-
* matoucí "why did Cmd+Z erase two of my last lines?" UX.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import type { Stroke } from '../annotations-layer.tsx';
|
|
23
|
-
import type { CommandRecord, EditCommand } from '../undo-stack.ts';
|
|
24
|
-
import { registerCommand } from '../undo-stack.ts';
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Push-once callable that submits a full `Stroke[]` to the server (or its
|
|
28
|
-
* test stub) AND updates the iframe's local strokes state. The
|
|
29
|
-
* annotations-layer `putStrokes` is the production binding.
|
|
30
|
-
*
|
|
31
|
-
* `before` is the baseline THIS command's `next` was computed from — passed
|
|
32
|
-
* through so the consumer can distinguish "an id `prev` still has that
|
|
33
|
-
* `before` already lacked too" (a genuinely concurrent addition, fold it in)
|
|
34
|
-
* from "an id `before` had that `next` deliberately dropped" (this command's
|
|
35
|
-
* own delete — must stay dropped even if React's rendered state hasn't
|
|
36
|
-
* caught up yet). See `reconcileCommit` in annotations-layer.tsx.
|
|
37
|
-
*/
|
|
38
|
-
export type StrokesPutFn = (
|
|
39
|
-
next: readonly Stroke[],
|
|
40
|
-
before: readonly Stroke[]
|
|
41
|
-
) => void | Promise<void>;
|
|
42
|
-
|
|
43
|
-
export interface AnnotationStrokesPayload {
|
|
44
|
-
before: readonly Stroke[];
|
|
45
|
-
after: readonly Stroke[];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export const ANNOTATION_STROKES_KIND = 'annotation-strokes';
|
|
49
|
-
|
|
50
|
-
export interface AnnotationStrokesCommandInit {
|
|
51
|
-
before: readonly Stroke[];
|
|
52
|
-
after: readonly Stroke[];
|
|
53
|
-
putFn: StrokesPutFn;
|
|
54
|
-
/** Optional label override (eraser → "erase 1 stroke"). */
|
|
55
|
-
label?: string;
|
|
56
|
-
/** Telemetry kind. */
|
|
57
|
-
kind?: string;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function createAnnotationStrokesCommand(init: AnnotationStrokesCommandInit): EditCommand {
|
|
61
|
-
const { putFn } = init;
|
|
62
|
-
const before = init.before.map(cloneStroke);
|
|
63
|
-
const after = init.after.map(cloneStroke);
|
|
64
|
-
const label = init.label ?? defaultLabel(before, after);
|
|
65
|
-
const kind = init.kind ?? ANNOTATION_STROKES_KIND;
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
kind,
|
|
69
|
-
label,
|
|
70
|
-
async do() {
|
|
71
|
-
await putFn(after.map(cloneStroke), before.map(cloneStroke));
|
|
72
|
-
},
|
|
73
|
-
async undo() {
|
|
74
|
-
await putFn(before.map(cloneStroke), after.map(cloneStroke));
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Build a persistable record from the same inputs as `createAnnotationStrokesCommand`.
|
|
81
|
-
* Used by the consumer alongside the EditCommand so the runtime side-effect
|
|
82
|
-
* and the stored shape share one snapshot.
|
|
83
|
-
*/
|
|
84
|
-
export function buildAnnotationStrokesRecord(opts: {
|
|
85
|
-
before: readonly Stroke[];
|
|
86
|
-
after: readonly Stroke[];
|
|
87
|
-
label?: string;
|
|
88
|
-
}): CommandRecord<AnnotationStrokesPayload> {
|
|
89
|
-
const before = opts.before.map(cloneStroke);
|
|
90
|
-
const after = opts.after.map(cloneStroke);
|
|
91
|
-
return {
|
|
92
|
-
kind: ANNOTATION_STROKES_KIND,
|
|
93
|
-
label: opts.label ?? defaultLabel(before, after),
|
|
94
|
-
payload: { before, after },
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
99
|
-
// Registry
|
|
100
|
-
|
|
101
|
-
registerCommand<AnnotationStrokesPayload>(ANNOTATION_STROKES_KIND, (record, sinks) => {
|
|
102
|
-
const putFn = sinks.strokesPutFn as StrokesPutFn | undefined;
|
|
103
|
-
if (!putFn) return null;
|
|
104
|
-
return createAnnotationStrokesCommand({
|
|
105
|
-
before: record.payload.before,
|
|
106
|
-
after: record.payload.after,
|
|
107
|
-
putFn,
|
|
108
|
-
label: record.label,
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
113
|
-
// Internals
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Structured clone — works for every Stroke variant because they're plain
|
|
117
|
-
* JSON-shaped (no Date / Map / DOM nodes). The pen `points: WorldPoint[]`
|
|
118
|
-
* array is nested-cloned too so callers can't poison the snapshot by
|
|
119
|
-
* mutating the source.
|
|
120
|
-
*/
|
|
121
|
-
function cloneStroke<T extends Stroke>(s: T): T {
|
|
122
|
-
// structuredClone is available on every runtime we ship to (Bun, modern
|
|
123
|
-
// browsers, Node 17+). Fall back to JSON for the rare older harness.
|
|
124
|
-
if (typeof structuredClone === 'function') return structuredClone(s);
|
|
125
|
-
return JSON.parse(JSON.stringify(s)) as T;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function defaultLabel(before: readonly Stroke[], after: readonly Stroke[]): string {
|
|
129
|
-
const added = after.length - before.length;
|
|
130
|
-
if (added > 0) return `add ${added} stroke${added === 1 ? '' : 's'}`;
|
|
131
|
-
if (added < 0) {
|
|
132
|
-
const erased = -added;
|
|
133
|
-
return `erase ${erased} stroke${erased === 1 ? '' : 's'}`;
|
|
134
|
-
}
|
|
135
|
-
// Same count — could be translate / edit / fill-change.
|
|
136
|
-
return `edit ${after.length} stroke${after.length === 1 ? '' : 's'}`;
|
|
137
|
-
}
|