@1agh/maude 1.4.6 → 1.5.1
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 +534 -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-merge.ts +15 -0
- package/apps/studio/annotations/ops.ts +499 -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 +65 -4
- package/apps/studio/annotations-sync.ts +4 -47
- package/apps/studio/api.ts +249 -69
- 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-lib.tsx +13 -1
- package/apps/studio/canvas-shell.tsx +6 -0
- package/apps/studio/client/app.jsx +98 -35
- 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 +49 -5
- package/apps/studio/collab/persistence.ts +31 -10
- package/apps/studio/collab/registry.ts +64 -26
- package/apps/studio/commands/annotation-ops-command.ts +72 -0
- package/apps/studio/cursors-overlay.tsx +158 -4
- 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 +12 -4
- package/apps/studio/sync/agent.ts +8 -4
- package/apps/studio/sync/codec.ts +96 -40
- package/apps/studio/sync/file-membership.ts +12 -2
- package/apps/studio/sync/file-plane.ts +1 -1
- package/apps/studio/sync/index.ts +38 -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 +8 -5
- 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 +76 -0
- package/apps/studio/whats-new.json +27 -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
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file annotations/migrate-v1.ts — v1 strokes → v2 elements (DDR-242 §6)
|
|
3
|
+
* @scope apps/studio/annotations/migrate-v1.ts
|
|
4
|
+
* @purpose The pure transform behind every migration path (studio boot,
|
|
5
|
+
* hub checkout, hub document load, lazy history upconvert). Same
|
|
6
|
+
* input → same output on every host, so two machines migrating the
|
|
7
|
+
* same board converge on identical records.
|
|
8
|
+
*
|
|
9
|
+
* What changes shape (and why the board still LOOKS the same):
|
|
10
|
+
* • anchored text → the host shape's embedded `label`
|
|
11
|
+
* • rect/ellipse/polygon → `shape` + `kind` (ellipse becomes a box)
|
|
12
|
+
* • section membership → computed ONCE with the v1 drag rule
|
|
13
|
+
* (bbox centre inside, inclusive; the
|
|
14
|
+
* smallest containing section wins, so
|
|
15
|
+
* visually nested sections nest), then
|
|
16
|
+
* coordinates become parent-relative
|
|
17
|
+
* • array order → fractional `index` per parent
|
|
18
|
+
* • bound arrow ends → `{el}` (auto) / `{el,nx,ny}` (pinned);
|
|
19
|
+
* the stored v1 endpoints are dropped
|
|
20
|
+
* • standalone text box → v1's estimated bbox, stored (the editor
|
|
21
|
+
* re-measures it on the next edit)
|
|
22
|
+
* Nothing is dropped silently: every element that cannot be carried
|
|
23
|
+
* over is listed in `report`.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
type AnchorHost,
|
|
28
|
+
type Stroke,
|
|
29
|
+
sanitizeAnnotationSvg,
|
|
30
|
+
strokeBBox,
|
|
31
|
+
strokesFromDocument,
|
|
32
|
+
} from '../annotations-model.ts';
|
|
33
|
+
import { MAX_PEN_POINTS } from './constants.ts';
|
|
34
|
+
import { keysBetween } from './fractional-index.ts';
|
|
35
|
+
import { parseMiniDom } from './legacy/mini-dom.ts';
|
|
36
|
+
import { defOf } from './registry.ts';
|
|
37
|
+
import { type Dropped, validateElements } from './schema.ts';
|
|
38
|
+
import type { AnnotationElement, Box } from './types.ts';
|
|
39
|
+
|
|
40
|
+
export interface MigrationResult {
|
|
41
|
+
elements: AnnotationElement[];
|
|
42
|
+
report: Dropped[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Parse a legacy `.annotations.svg` (sanitized first, DOM-free) into v1 strokes. */
|
|
46
|
+
export function parseLegacySvg(svg: string): Stroke[] {
|
|
47
|
+
const clean = sanitizeAnnotationSvg(svg ?? '');
|
|
48
|
+
if (!clean.trim()) return [];
|
|
49
|
+
return strokesFromDocument(parseMiniDom(clean));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Legacy SVG text → v2 elements. */
|
|
53
|
+
export function migrateSvg(svg: string): MigrationResult {
|
|
54
|
+
return v1ToV2(parseLegacySvg(svg));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normBox(x: number, y: number, w: number, h: number): Box {
|
|
58
|
+
return { x: Math.min(x, x + w), y: Math.min(y, y + h), w: Math.abs(w), h: Math.abs(h) };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function author(s: Stroke): Record<string, unknown> | undefined {
|
|
62
|
+
if (s.author === 'ai') return { kind: 'ai' };
|
|
63
|
+
if (s.authorName || s.authorId) {
|
|
64
|
+
return {
|
|
65
|
+
kind: 'human',
|
|
66
|
+
...(s.authorName ? { name: s.authorName } : {}),
|
|
67
|
+
...(s.authorId ? { id: s.authorId } : {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function textStyle(s: {
|
|
74
|
+
bold?: boolean;
|
|
75
|
+
italic?: boolean;
|
|
76
|
+
strike?: boolean;
|
|
77
|
+
underline?: boolean;
|
|
78
|
+
listType?: string;
|
|
79
|
+
align?: string;
|
|
80
|
+
}): Record<string, unknown> {
|
|
81
|
+
return {
|
|
82
|
+
bold: s.bold,
|
|
83
|
+
italic: s.italic,
|
|
84
|
+
strike: s.strike,
|
|
85
|
+
underline: s.underline,
|
|
86
|
+
list: s.listType,
|
|
87
|
+
align: s.align,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Pen points, decimated to the v2 cap if a v1 stroke exceeded it. */
|
|
92
|
+
function penPoints(points: ReadonlyArray<readonly [number, number]>): number[] {
|
|
93
|
+
const step = Math.ceil(points.length / MAX_PEN_POINTS) || 1;
|
|
94
|
+
const out: number[] = [];
|
|
95
|
+
for (let i = 0; i < points.length; i += step) {
|
|
96
|
+
const p = points[i] as readonly [number, number];
|
|
97
|
+
out.push(p[0], p[1]);
|
|
98
|
+
}
|
|
99
|
+
const lastP = points[points.length - 1];
|
|
100
|
+
if (step > 1 && lastP && (out[out.length - 2] !== lastP[0] || out[out.length - 1] !== lastP[1])) {
|
|
101
|
+
out.push(lastP[0], lastP[1]);
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface V1ToV2Opts {
|
|
107
|
+
/**
|
|
108
|
+
* Skip section membership: every element stays top level in WORLD
|
|
109
|
+
* coordinates. The v1 adapter (v1-adapter.ts) decides parents itself from
|
|
110
|
+
* the current board, so an unmoved element keeps its parent.
|
|
111
|
+
*/
|
|
112
|
+
flat?: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function v1ToV2(strokes: readonly Stroke[], opts: V1ToV2Opts = {}): MigrationResult {
|
|
116
|
+
const report: Dropped[] = [];
|
|
117
|
+
const byId = new Map<string, Stroke>();
|
|
118
|
+
for (const s of strokes) if (!byId.has(s.id)) byId.set(s.id, s);
|
|
119
|
+
const isShape = (s: Stroke | undefined): s is AnchorHost =>
|
|
120
|
+
!!s && (s.tool === 'rect' || s.tool === 'ellipse' || s.tool === 'polygon');
|
|
121
|
+
|
|
122
|
+
// 1. Anchored text → the host's label (first wins; extras become standalone text).
|
|
123
|
+
const labelOf = new Map<string, Extract<Stroke, { tool: 'text' }>>();
|
|
124
|
+
const extraText: Array<{ t: Extract<Stroke, { tool: 'text' }>; at: Box }> = [];
|
|
125
|
+
for (const s of strokes) {
|
|
126
|
+
if (s.tool !== 'text' || !s.anchorId) continue;
|
|
127
|
+
const host = byId.get(s.anchorId);
|
|
128
|
+
if (!isShape(host)) {
|
|
129
|
+
report.push({
|
|
130
|
+
id: s.id,
|
|
131
|
+
reason: 'anchored text without a shape host (not rendered in v1) — dropped',
|
|
132
|
+
});
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (!labelOf.has(host.id)) labelOf.set(host.id, s);
|
|
136
|
+
else {
|
|
137
|
+
const bb = strokeBBox(host) as Box;
|
|
138
|
+
extraText.push({ t: s, at: bb });
|
|
139
|
+
report.push({ id: s.id, reason: 'second label on one shape — kept as standalone text' });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 2. World boxes (v1 geometry) for membership + relative coordinates.
|
|
144
|
+
const kept = strokes.filter(
|
|
145
|
+
(s) => !(s.tool === 'text' && s.anchorId && byId.has(s.id) && s.anchorId !== '')
|
|
146
|
+
);
|
|
147
|
+
const worldBox = new Map<string, Box>();
|
|
148
|
+
for (const s of kept) {
|
|
149
|
+
const bb = strokeBBox(s);
|
|
150
|
+
if (bb) worldBox.set(s.id, normBox(bb.x, bb.y, bb.w, bb.h));
|
|
151
|
+
}
|
|
152
|
+
for (const { t, at } of extraText) worldBox.set(t.id, at);
|
|
153
|
+
|
|
154
|
+
// 3. Section membership: smallest section whose box contains the centre
|
|
155
|
+
// (inclusive) AND that the element painted ABOVE in v1. A child always
|
|
156
|
+
// paints over its container in v2, so adopting an element that sat UNDER
|
|
157
|
+
// a section (a backing paper, a big shape) would change what the user sees.
|
|
158
|
+
const sections = kept.filter((s) => s.tool === 'section');
|
|
159
|
+
const zOf = new Map(strokes.map((s, i) => [s.id, i]));
|
|
160
|
+
const area = (b: Box) => b.w * b.h;
|
|
161
|
+
const parentOf = new Map<string, string>();
|
|
162
|
+
const containerFor = (id: string, b: Box, selfArea: number | null): string | undefined => {
|
|
163
|
+
const cx = b.x + b.w / 2;
|
|
164
|
+
const cy = b.y + b.h / 2;
|
|
165
|
+
let best: { id: string; area: number } | null = null;
|
|
166
|
+
for (const sec of sections) {
|
|
167
|
+
if (sec.id === id) continue;
|
|
168
|
+
if ((zOf.get(sec.id) ?? 0) >= (zOf.get(id) ?? Number.POSITIVE_INFINITY)) continue;
|
|
169
|
+
const sb = worldBox.get(sec.id);
|
|
170
|
+
if (!sb) continue;
|
|
171
|
+
// A section only nests inside a strictly larger one (no cycles by construction).
|
|
172
|
+
if (selfArea !== null && area(sb) <= selfArea) continue;
|
|
173
|
+
if (cx < sb.x || cx > sb.x + sb.w || cy < sb.y || cy > sb.y + sb.h) continue;
|
|
174
|
+
// Equal size: the later-painted (nearest below) section wins.
|
|
175
|
+
if (!best || area(sb) <= best.area) best = { id: sec.id, area: area(sb) };
|
|
176
|
+
}
|
|
177
|
+
return best?.id;
|
|
178
|
+
};
|
|
179
|
+
const all = [...kept.map((s) => s.id), ...extraText.map((e) => e.t.id)];
|
|
180
|
+
for (const id of opts.flat ? [] : all) {
|
|
181
|
+
const b = worldBox.get(id);
|
|
182
|
+
if (!b) continue;
|
|
183
|
+
const s = byId.get(id);
|
|
184
|
+
const p = containerFor(id, b, s?.tool === 'section' ? area(b) : null);
|
|
185
|
+
if (p) parentOf.set(id, p);
|
|
186
|
+
}
|
|
187
|
+
// World origin of a parent = its own world top-left.
|
|
188
|
+
const originOf = (id: string): { x: number; y: number } => {
|
|
189
|
+
const p = parentOf.get(id);
|
|
190
|
+
if (!p) return { x: 0, y: 0 };
|
|
191
|
+
const b = worldBox.get(p) as Box;
|
|
192
|
+
return { x: b.x, y: b.y };
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// 4. Fractional indexes per parent, in v1 array (paint) order.
|
|
196
|
+
const order = new Map(strokes.map((s, i) => [s.id, i]));
|
|
197
|
+
const siblings = new Map<string, string[]>();
|
|
198
|
+
for (const id of all) {
|
|
199
|
+
const key = parentOf.get(id) ?? '';
|
|
200
|
+
const list = siblings.get(key);
|
|
201
|
+
if (list) list.push(id);
|
|
202
|
+
else siblings.set(key, [id]);
|
|
203
|
+
}
|
|
204
|
+
const indexOf = new Map<string, string>();
|
|
205
|
+
for (const list of siblings.values()) {
|
|
206
|
+
list.sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0));
|
|
207
|
+
const keys = keysBetween(null, null, list.length);
|
|
208
|
+
for (let i = 0; i < list.length; i++) indexOf.set(list[i] as string, keys[i] as string);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 5. Emit records (raw; validateElements makes them canonical).
|
|
212
|
+
const raw: Record<string, unknown>[] = [];
|
|
213
|
+
const head = (s: Stroke) => {
|
|
214
|
+
const o = originOf(s.id);
|
|
215
|
+
return {
|
|
216
|
+
o,
|
|
217
|
+
base: {
|
|
218
|
+
id: s.id,
|
|
219
|
+
parent: parentOf.get(s.id),
|
|
220
|
+
index: indexOf.get(s.id),
|
|
221
|
+
groups: s.groupIds,
|
|
222
|
+
author: author(s),
|
|
223
|
+
} as Record<string, unknown>,
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
const rel = (b: Box, o: { x: number; y: number }) => ({
|
|
227
|
+
x: b.x - o.x,
|
|
228
|
+
y: b.y - o.y,
|
|
229
|
+
w: b.w,
|
|
230
|
+
h: b.h,
|
|
231
|
+
});
|
|
232
|
+
const hostIds = new Set(
|
|
233
|
+
kept.filter((s) => s.tool !== 'arrow' && s.tool !== 'pen').map((s) => s.id)
|
|
234
|
+
);
|
|
235
|
+
const arrowEnd = (
|
|
236
|
+
bind: { hostId: string; nx: number; ny: number; pinned?: boolean } | undefined,
|
|
237
|
+
x: number,
|
|
238
|
+
y: number,
|
|
239
|
+
o: { x: number; y: number }
|
|
240
|
+
) => {
|
|
241
|
+
if (bind && hostIds.has(bind.hostId)) {
|
|
242
|
+
return bind.pinned ? { el: bind.hostId, nx: bind.nx, ny: bind.ny } : { el: bind.hostId };
|
|
243
|
+
}
|
|
244
|
+
return { x: x - o.x, y: y - o.y };
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
for (const s of kept) {
|
|
248
|
+
const { o, base } = head(s);
|
|
249
|
+
const b = worldBox.get(s.id);
|
|
250
|
+
const rot = s.rotation;
|
|
251
|
+
switch (s.tool) {
|
|
252
|
+
case 'pen':
|
|
253
|
+
raw.push({
|
|
254
|
+
...base,
|
|
255
|
+
type: 'pen',
|
|
256
|
+
points: penPoints(s.points.map(([x, y]) => [x - o.x, y - o.y] as const)),
|
|
257
|
+
color: s.color,
|
|
258
|
+
width: s.width,
|
|
259
|
+
highlighter: s.highlighter,
|
|
260
|
+
});
|
|
261
|
+
break;
|
|
262
|
+
case 'rect':
|
|
263
|
+
case 'ellipse':
|
|
264
|
+
case 'polygon': {
|
|
265
|
+
const label = labelOf.get(s.id);
|
|
266
|
+
raw.push({
|
|
267
|
+
...base,
|
|
268
|
+
type: 'shape',
|
|
269
|
+
kind: s.tool === 'polygon' ? s.shape : s.tool,
|
|
270
|
+
...rel(b as Box, o),
|
|
271
|
+
rot,
|
|
272
|
+
color: s.color,
|
|
273
|
+
width: s.width,
|
|
274
|
+
fill: s.fill ?? null,
|
|
275
|
+
radius: s.tool === 'rect' ? s.cornerRadius : undefined,
|
|
276
|
+
dashed: s.dashed,
|
|
277
|
+
label: label
|
|
278
|
+
? {
|
|
279
|
+
text: label.text,
|
|
280
|
+
fontSize: label.fontSize,
|
|
281
|
+
color: label.color,
|
|
282
|
+
...textStyle(label),
|
|
283
|
+
}
|
|
284
|
+
: undefined,
|
|
285
|
+
});
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
case 'arrow':
|
|
289
|
+
raw.push({
|
|
290
|
+
...base,
|
|
291
|
+
type: 'arrow',
|
|
292
|
+
start: arrowEnd(s.startBind, s.x1, s.y1, o),
|
|
293
|
+
end: arrowEnd(s.endBind, s.x2, s.y2, o),
|
|
294
|
+
color: s.color,
|
|
295
|
+
width: s.width,
|
|
296
|
+
startHead: s.startHead,
|
|
297
|
+
endHead: s.endHead,
|
|
298
|
+
dashed: s.dashed,
|
|
299
|
+
line: s.lineType,
|
|
300
|
+
});
|
|
301
|
+
break;
|
|
302
|
+
case 'text':
|
|
303
|
+
raw.push({
|
|
304
|
+
...base,
|
|
305
|
+
type: 'text',
|
|
306
|
+
...rel(b as Box, o),
|
|
307
|
+
rot,
|
|
308
|
+
text: s.text,
|
|
309
|
+
fontSize: s.fontSize,
|
|
310
|
+
color: s.color,
|
|
311
|
+
...textStyle(s),
|
|
312
|
+
});
|
|
313
|
+
break;
|
|
314
|
+
case 'sticky':
|
|
315
|
+
raw.push({
|
|
316
|
+
...base,
|
|
317
|
+
type: 'sticky',
|
|
318
|
+
...rel(b as Box, o),
|
|
319
|
+
rot,
|
|
320
|
+
fill: s.color,
|
|
321
|
+
text: s.text,
|
|
322
|
+
fontSize: s.fontSize,
|
|
323
|
+
radius: s.cornerRadius,
|
|
324
|
+
...textStyle(s),
|
|
325
|
+
});
|
|
326
|
+
break;
|
|
327
|
+
case 'image':
|
|
328
|
+
raw.push({ ...base, type: 'image', ...rel(b as Box, o), rot, href: s.href, alt: s.alt });
|
|
329
|
+
break;
|
|
330
|
+
case 'link':
|
|
331
|
+
raw.push({
|
|
332
|
+
...base,
|
|
333
|
+
type: 'link',
|
|
334
|
+
...rel(b as Box, o),
|
|
335
|
+
rot,
|
|
336
|
+
url: s.url,
|
|
337
|
+
title: s.title,
|
|
338
|
+
domain: s.domain,
|
|
339
|
+
});
|
|
340
|
+
break;
|
|
341
|
+
case 'mediaref':
|
|
342
|
+
raw.push({
|
|
343
|
+
...base,
|
|
344
|
+
type: 'mediaref',
|
|
345
|
+
...rel(b as Box, o),
|
|
346
|
+
rot,
|
|
347
|
+
src: s.src,
|
|
348
|
+
media: s.mediaKind,
|
|
349
|
+
title: s.title,
|
|
350
|
+
});
|
|
351
|
+
break;
|
|
352
|
+
case 'section':
|
|
353
|
+
raw.push({ ...base, type: 'section', ...rel(b as Box, o), label: s.label, color: s.color });
|
|
354
|
+
break;
|
|
355
|
+
case 'element': {
|
|
356
|
+
// A registry type with no stroke form: its own record, moved into parent space.
|
|
357
|
+
const def = defOf(s.el.type);
|
|
358
|
+
if (!def) break;
|
|
359
|
+
raw.push({ ...s.el, ...def.translate(s.el, -o.x, -o.y), ...base });
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
for (const { t, at } of extraText) {
|
|
365
|
+
const { o, base } = head(t);
|
|
366
|
+
raw.push({
|
|
367
|
+
...base,
|
|
368
|
+
type: 'text',
|
|
369
|
+
...rel(at, o),
|
|
370
|
+
text: t.text,
|
|
371
|
+
fontSize: t.fontSize,
|
|
372
|
+
color: t.color,
|
|
373
|
+
...textStyle(t),
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Strip undefined so the validator sees absent fields.
|
|
378
|
+
const clean = raw.map((r) =>
|
|
379
|
+
Object.fromEntries(Object.entries(r).filter(([, v]) => v !== undefined))
|
|
380
|
+
);
|
|
381
|
+
const v = validateElements(clean);
|
|
382
|
+
return { elements: v.elements, report: [...report, ...v.dropped] };
|
|
383
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file annotations/ops-merge.ts — wire the 3-way text merge into ops.ts
|
|
3
|
+
* @scope apps/studio/annotations/ops-merge.ts
|
|
4
|
+
* @purpose Side-effect import for SERVER entry points only (api, collab,
|
|
5
|
+
* codec, ai-write, the hub's accepted lane). `sync/source-merge.ts`
|
|
6
|
+
* needs the `diff` package, which the canvas iframe cannot resolve,
|
|
7
|
+
* so ops.ts takes the merge by injection instead of importing it.
|
|
8
|
+
* Never import this from anything in the canvas-lib graph —
|
|
9
|
+
* `test/canvas-lib-graph.test.ts` fails if `diff` gets back in.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { mergeSource } from '../sync/source-merge.ts';
|
|
13
|
+
import { setTextMerge } from './ops.ts';
|
|
14
|
+
|
|
15
|
+
setTextMerge(mergeSource);
|