@scrawl-board/board 0.1.0-beta.1 → 0.1.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core.js CHANGED
@@ -1,4 +1,4 @@
1
- //#region src/core-internal/assets.ts
1
+ //#region src/core/shapes/assets.ts
2
2
  var ASSET_REF_PATTERN = /^asset:[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?:[A-Za-z0-9._~-]+$/;
3
3
  /** Hard ceiling on a reference's own wire length — independent of any resource limit below. */
4
4
  var ASSET_REF_MAX_BYTES = 512;
@@ -38,7 +38,7 @@ function clampAssetCacheBytes(value) {
38
38
  return Math.min(ASSET_CACHE_BYTES_MAX, Math.max(ASSET_CACHE_BYTES_MIN, Math.floor(value)));
39
39
  }
40
40
  //#endregion
41
- //#region src/core-internal/identifiers.ts
41
+ //#region src/core/document/identifiers.ts
42
42
  function documentId(value) {
43
43
  if (!value) throw new TypeError("DocumentId cannot be empty");
44
44
  return value;
@@ -48,7 +48,38 @@ function strokeId(value) {
48
48
  return value;
49
49
  }
50
50
  //#endregion
51
- //#region src/document-schema.ts
51
+ //#region src/core/document/migrations/registry.ts
52
+ /** Schema versions this build can load. An unrecognized version fails safely (`DOCUMENT_VERSION_UNSUPPORTED`) rather than being silently treated as current. */
53
+ var SUPPORTED_SCHEMA_VERSIONS = /* @__PURE__ */ new Set([0, 1]);
54
+ function isSupportedSchemaVersion(version) {
55
+ return typeof version === "number" && SUPPORTED_SCHEMA_VERSIONS.has(version);
56
+ }
57
+ /**
58
+ * Document collection fields introduced after schemaVersion 1 first
59
+ * shipped real documents. A document stored before a field's introduction
60
+ * can never retroactively gain it, so requiring it at load would fail
61
+ * every pre-existing document — these stay optional even at the current
62
+ * version. New entries only ever get added here, never removed (an old
63
+ * stored document's absence of a since-introduced field must keep loading
64
+ * for as long as that document might still exist).
65
+ */
66
+ var FIELDS_OPTIONAL_AT_CURRENT_VERSION = /* @__PURE__ */ new Set([
67
+ "customObjects",
68
+ "rectangles",
69
+ "ellipses",
70
+ "groups",
71
+ "lines",
72
+ "arrows",
73
+ "polygons",
74
+ "stars",
75
+ "hearts"
76
+ ]);
77
+ /** Whether `field` must be present on a document already at the current schema version (never required on an older, migrating-forward document). */
78
+ function isFieldRequiredAtCurrentVersion(field) {
79
+ return !FIELDS_OPTIONAL_AT_CURRENT_VERSION.has(field);
80
+ }
81
+ //#endregion
82
+ //#region src/core/document/document-schema.ts
52
83
  var CURRENT_DOCUMENT_SCHEMA_VERSION = 1;
53
84
  var DocumentRecoveryError = class extends Error {
54
85
  constructor(code, message, originalBytes, path) {
@@ -76,7 +107,7 @@ function loadDocumentBytes(originalBytes) {
76
107
  function migrateDocument(raw) {
77
108
  if (!isRecord(raw)) return failure("DOCUMENT_VALIDATION_FAILED", "Document must be an object");
78
109
  const version = raw.schemaVersion ?? 0;
79
- if (version !== 0 && version !== 1) return failure("DOCUMENT_VERSION_UNSUPPORTED", `Document schema version ${String(version)} is unsupported`, void 0, "schemaVersion");
110
+ if (!isSupportedSchemaVersion(version)) return failure("DOCUMENT_VERSION_UNSUPPORTED", `Document schema version ${String(version)} is unsupported`, void 0, "schemaVersion");
80
111
  if (!isJsonValue(raw)) return failure("DOCUMENT_VALIDATION_FAILED", "Document must contain JSON values only");
81
112
  const strokes = validateStrokes(raw.strokes);
82
113
  if (strokes instanceof DocumentRecoveryError) return {
@@ -89,7 +120,15 @@ function migrateDocument(raw) {
89
120
  tables: validateCollection(raw, "tables", version, validateTable),
90
121
  images: validateCollection(raw, "images", version, validateImage),
91
122
  timers: validateCollection(raw, "timers", version, validateTimer),
92
- customObjects: validateCollection(raw, "customObjects", version, validateCustomObject, false)
123
+ customObjects: validateCollection(raw, "customObjects", version, validateCustomObject),
124
+ rectangles: validateCollection(raw, "rectangles", version, validateRectangle),
125
+ ellipses: validateCollection(raw, "ellipses", version, validateEllipse),
126
+ groups: validateCollection(raw, "groups", version, validateGroup),
127
+ lines: validateCollection(raw, "lines", version, validateLine),
128
+ arrows: validateCollection(raw, "arrows", version, validateArrow),
129
+ polygons: validateCollection(raw, "polygons", version, validatePolygon),
130
+ stars: validateCollection(raw, "stars", version, validateStar),
131
+ hearts: validateCollection(raw, "hearts", version, validateHeart)
93
132
  };
94
133
  if (collections.notes instanceof DocumentRecoveryError) return {
95
134
  ok: false,
@@ -115,18 +154,102 @@ function migrateDocument(raw) {
115
154
  ok: false,
116
155
  error: collections.customObjects
117
156
  };
157
+ if (collections.rectangles instanceof DocumentRecoveryError) return {
158
+ ok: false,
159
+ error: collections.rectangles
160
+ };
161
+ if (collections.ellipses instanceof DocumentRecoveryError) return {
162
+ ok: false,
163
+ error: collections.ellipses
164
+ };
165
+ if (collections.groups instanceof DocumentRecoveryError) return {
166
+ ok: false,
167
+ error: collections.groups
168
+ };
169
+ if (collections.lines instanceof DocumentRecoveryError) return {
170
+ ok: false,
171
+ error: collections.lines
172
+ };
173
+ if (collections.arrows instanceof DocumentRecoveryError) return {
174
+ ok: false,
175
+ error: collections.arrows
176
+ };
177
+ if (collections.polygons instanceof DocumentRecoveryError) return {
178
+ ok: false,
179
+ error: collections.polygons
180
+ };
181
+ if (collections.stars instanceof DocumentRecoveryError) return {
182
+ ok: false,
183
+ error: collections.stars
184
+ };
185
+ if (collections.hearts instanceof DocumentRecoveryError) return {
186
+ ok: false,
187
+ error: collections.hearts
188
+ };
189
+ const objectOrder = validateObjectOrder(raw.objectOrder);
190
+ if (objectOrder instanceof DocumentRecoveryError) return {
191
+ ok: false,
192
+ error: objectOrder
193
+ };
194
+ if (raw.pageName !== void 0 && (typeof raw.pageName !== "string" || !raw.pageName.trim() || raw.pageName.length > 200)) return failure("DOCUMENT_VALIDATION_FAILED", "Invalid first page name");
195
+ const notebookIds = /* @__PURE__ */ new Set(["default"]);
196
+ const sectionIds = /* @__PURE__ */ new Set(["default"]);
197
+ for (const field of ["notebooks", "sections"]) {
198
+ if (raw[field] === void 0) continue;
199
+ if (!Array.isArray(raw[field])) return failure("DOCUMENT_VALIDATION_FAILED", `${field} must be an array`);
200
+ const seen = /* @__PURE__ */ new Set();
201
+ for (const item of raw[field]) {
202
+ if (!isRecord(item) || typeof item.id !== "string" || !item.id.trim() || seen.has(item.id) || typeof item.name !== "string" || !item.name.trim() || item.name.length > 200 || typeof item.color !== "string" || !/^#[0-9a-f]{6}$/i.test(item.color)) return failure("DOCUMENT_VALIDATION_FAILED", `Invalid or duplicate ${field} entry`);
203
+ if (field === "sections" && (typeof item.notebookId !== "string" || !notebookIds.has(item.notebookId))) return failure("DOCUMENT_VALIDATION_FAILED", "Section references an unknown notebook");
204
+ seen.add(item.id);
205
+ (field === "notebooks" ? notebookIds : sectionIds).add(item.id);
206
+ }
207
+ }
208
+ if (raw.pageSectionId !== void 0 && (typeof raw.pageSectionId !== "string" || !sectionIds.has(raw.pageSectionId))) return failure("DOCUMENT_VALIDATION_FAILED", "First page references an unknown section");
209
+ const pages = [];
210
+ if (raw.pages !== void 0) {
211
+ if (!Array.isArray(raw.pages)) return failure("DOCUMENT_VALIDATION_FAILED", "Pages must be an array");
212
+ const ids = /* @__PURE__ */ new Set(["default"]);
213
+ for (const page of raw.pages) {
214
+ if (!isRecord(page) || typeof page.id !== "string" || !page.id || ids.has(page.id) || typeof page.name !== "string" || !page.name.trim() || page.name.length > 200 || !isRecord(page.content) || page.content.pages !== void 0 || page.content.pageName !== void 0 || page.content.notebooks !== void 0 || page.content.sections !== void 0 || page.content.pageSectionId !== void 0) return failure("DOCUMENT_VALIDATION_FAILED", "Invalid or duplicate page");
215
+ if (page.sectionId !== void 0 && (typeof page.sectionId !== "string" || !sectionIds.has(page.sectionId))) return failure("DOCUMENT_VALIDATION_FAILED", "Page references an unknown section");
216
+ const content = migrateDocument(page.content);
217
+ if (!content.ok) return content;
218
+ ids.add(page.id);
219
+ pages.push({
220
+ id: page.id,
221
+ name: page.name,
222
+ ...page.sectionId !== void 0 ? { sectionId: page.sectionId } : {},
223
+ content: content.document
224
+ });
225
+ }
226
+ }
118
227
  return {
119
228
  ok: true,
120
229
  migratedFrom: version,
121
230
  document: {
122
231
  schemaVersion: 1,
232
+ ...raw.notebooks !== void 0 ? { notebooks: raw.notebooks } : {},
233
+ ...raw.sections !== void 0 ? { sections: raw.sections } : {},
234
+ ...raw.pageSectionId !== void 0 ? { pageSectionId: raw.pageSectionId } : {},
235
+ ...raw.pageName !== void 0 ? { pageName: raw.pageName } : {},
236
+ ...raw.pages !== void 0 ? { pages } : {},
123
237
  strokes,
124
238
  notes: collections.notes,
125
239
  textBlocks: collections.textBlocks,
126
240
  tables: collections.tables,
127
241
  images: collections.images,
128
242
  timers: collections.timers,
129
- customObjects: collections.customObjects
243
+ customObjects: collections.customObjects,
244
+ rectangles: collections.rectangles,
245
+ ellipses: collections.ellipses,
246
+ groups: collections.groups,
247
+ lines: collections.lines,
248
+ arrows: collections.arrows,
249
+ polygons: collections.polygons,
250
+ stars: collections.stars,
251
+ hearts: collections.hearts,
252
+ objectOrder
130
253
  }
131
254
  };
132
255
  }
@@ -135,8 +258,19 @@ function serializeDocument(document) {
135
258
  if (!result.ok || result.migratedFrom !== 1) throw result.ok ? new DocumentRecoveryError("DOCUMENT_VALIDATION_FAILED", "Saving requires the current Document schema") : result.error;
136
259
  return JSON.stringify(result.document);
137
260
  }
138
- function validateCollection(raw, name, version, validator, requiredAtCurrentVersion = true) {
139
- if (requiredAtCurrentVersion && version === 1 && !(name in raw)) return validation(`${name} is required`, name);
261
+ /**
262
+ * A Document's serialized size in bytes (Phase 5) — UTF-8, not UTF-16
263
+ * `string.length`, since a Document with non-ASCII note/text content (most
264
+ * of them, eventually) would otherwise under-report. Useful for a Host
265
+ * deciding when to warn about an unusually large board, or for logging/
266
+ * telemetry around save size — not consulted by anything inside this
267
+ * package itself, which has no size limit of its own.
268
+ */
269
+ function documentSize(document) {
270
+ return new TextEncoder().encode(JSON.stringify(document)).length;
271
+ }
272
+ function validateCollection(raw, name, version, validator) {
273
+ if (isFieldRequiredAtCurrentVersion(name) && version === 1 && !(name in raw)) return validation(`${name} is required`, name);
140
274
  const value = raw[name] ?? [];
141
275
  if (!Array.isArray(value)) return validation(`${name} must be an array`, name);
142
276
  for (let index = 0; index < value.length; index += 1) if (!isRecord(value[index]) || !validator(value[index])) return validation(`invalid ${name} entry`, `${name}[${index}]`);
@@ -148,7 +282,20 @@ function validateStrokes(raw) {
148
282
  for (let index = 0; index < raw.length; index += 1) {
149
283
  const value = raw[index];
150
284
  const path = `strokes[${index}]`;
151
- if (!isRecord(value) || !nonempty(value.id) || !nonempty(value.color) || !positive(value.baseWidth) || !optionalEnum(value.tool, ["marker", "highlighter"]) || !validLock(value) || !optionalString(value.clusterId) || !validMatrix(value.matrix) || !Array.isArray(value.points)) return validation("invalid stroke", path);
285
+ if (!isRecord(value)) return validation("stroke must be an object", path);
286
+ if (!nonempty(value.id)) return validation("stroke.id must be a non-empty string", `${path}.id`);
287
+ if (!nonempty(value.color)) return validation("stroke.color must be a non-empty string", `${path}.color`);
288
+ if (!positive(value.baseWidth)) return validation("stroke.baseWidth must be a positive number", `${path}.baseWidth`);
289
+ if (!optionalEnum(value.tool, [
290
+ "marker",
291
+ "highlighter",
292
+ "shape"
293
+ ])) return validation("stroke.tool must be \"marker\", \"highlighter\", or \"shape\"", `${path}.tool`);
294
+ if (!validLock(value)) return validation("invalid stroke.lock", `${path}.lock`);
295
+ if (!validHidden(value)) return validation("invalid stroke.hidden", `${path}.hidden`);
296
+ if (!optionalString(value.clusterId)) return validation("stroke.clusterId must be a string", `${path}.clusterId`);
297
+ if (!validMatrix(value.matrix)) return validation("invalid stroke.matrix", `${path}.matrix`);
298
+ if (!Array.isArray(value.points)) return validation("stroke.points must be an array", `${path}.points`);
152
299
  const points = [];
153
300
  for (let pointIndex = 0; pointIndex < value.points.length; pointIndex += 1) {
154
301
  const point = normalizePoint(value.points[pointIndex]);
@@ -174,20 +321,62 @@ function normalizePoint(raw) {
174
321
  return values;
175
322
  }
176
323
  function validateNote(v) {
177
- return basePosition(v) && positive(v.size) && finite(v.zOffset) && nonempty(v.color) && typeof v.text === "string" && validLock(v) && (v.votes === void 0 || Array.isArray(v.votes) && v.votes.every((vote) => isRecord(vote) && nonempty(vote.userId) && nonempty(vote.name) && nonempty(vote.color)));
324
+ return basePosition(v) && positive(v.size) && finite(v.zOffset) && nonempty(v.color) && typeof v.text === "string" && validLock(v) && validHidden(v) && (v.votes === void 0 || Array.isArray(v.votes) && v.votes.every((vote) => isRecord(vote) && nonempty(vote.userId) && nonempty(vote.name) && nonempty(vote.color)));
178
325
  }
179
326
  function validateText(v) {
180
- return basePosition(v) && typeof v.text === "string" && positive(v.fontSize) && nonempty(v.color) && validLock(v) && optionalString(v.clusterId);
327
+ return basePosition(v) && typeof v.text === "string" && positive(v.fontSize) && nonempty(v.color) && validLock(v) && validHidden(v) && optionalString(v.clusterId);
181
328
  }
182
329
  function validateTable(v) {
183
- return basePosition(v) && integer(v.rows) && integer(v.cols) && numberArray(v.colWidths, v.cols) && numberArray(v.rowHeights, v.rows) && isStringRecord(v.cells) && validLock(v) && optionalString(v.color) && optionalString(v.backgroundColor) && optionalString(v.clusterId);
330
+ return basePosition(v) && integer(v.rows) && integer(v.cols) && numberArray(v.colWidths, v.cols) && numberArray(v.rowHeights, v.rows) && isStringRecord(v.cells) && validLock(v) && validHidden(v) && optionalString(v.color) && optionalString(v.backgroundColor) && optionalString(v.clusterId);
184
331
  }
185
332
  function validateImage(v) {
186
333
  const identifiesAnImage = v.ref !== void 0 ? isAssetRef(v.ref) : nonempty(v.src);
187
- return basePosition(v) && identifiesAnImage && positive(v.width) && positive(v.height) && positive(v.aspectRatio) && validLock(v) && optionalString(v.name) && optionalString(v.createdAt) && optionalString(v.stamp);
334
+ return basePosition(v) && identifiesAnImage && positive(v.width) && positive(v.height) && positive(v.aspectRatio) && validLock(v) && validHidden(v) && optionalString(v.name) && optionalString(v.createdAt) && optionalString(v.stamp);
335
+ }
336
+ function validateShapeStyle(v) {
337
+ return optionalString(v.fill) && optionalString(v.stroke) && (v.strokeWidth === void 0 || positive(v.strokeWidth)) && (v.opacity === void 0 || finite(v.opacity) && v.opacity >= 0 && v.opacity <= 1);
338
+ }
339
+ function validateRectangle(v) {
340
+ return basePosition(v) && positive(v.width) && positive(v.height) && validLock(v) && validHidden(v) && validateShapeStyle(v) && (v.rotation === void 0 || finite(v.rotation)) && (v.cornerRadius === void 0 || finite(v.cornerRadius) && v.cornerRadius >= 0);
341
+ }
342
+ function validateEllipse(v) {
343
+ return basePosition(v) && positive(v.width) && positive(v.height) && validLock(v) && validHidden(v) && validateShapeStyle(v) && (v.rotation === void 0 || finite(v.rotation));
344
+ }
345
+ function validatePolygon(v) {
346
+ return basePosition(v) && positive(v.width) && positive(v.height) && validLock(v) && validHidden(v) && validateShapeStyle(v) && (v.rotation === void 0 || finite(v.rotation)) && (v.sides === 3 || v.sides === 4 || v.sides === 5 || v.sides === 6 || v.sides === 8);
347
+ }
348
+ function validateStar(v) {
349
+ return basePosition(v) && positive(v.width) && positive(v.height) && validLock(v) && validHidden(v) && validateShapeStyle(v) && (v.rotation === void 0 || finite(v.rotation)) && finite(v.points) && v.points >= 3 && finite(v.innerRadiusRatio) && v.innerRadiusRatio > 0 && v.innerRadiusRatio < 1;
350
+ }
351
+ function validateHeart(v) {
352
+ return basePosition(v) && positive(v.width) && positive(v.height) && validLock(v) && validHidden(v) && validateShapeStyle(v) && (v.rotation === void 0 || finite(v.rotation));
353
+ }
354
+ function validPoint(v) {
355
+ return isRecord(v) && finite(v.x) && finite(v.y);
356
+ }
357
+ function validateLine(v) {
358
+ return nonempty(v.id) && validPoint(v.start) && validPoint(v.end) && validLock(v) && validHidden(v) && validateShapeStyle(v);
359
+ }
360
+ function validBinding(v) {
361
+ return v === void 0 || isRecord(v) && nonempty(v.objectId) && finite(v.x) && finite(v.y) && v.x >= 0 && v.x <= 1 && v.y >= 0 && v.y <= 1;
362
+ }
363
+ function validateArrow(v) {
364
+ return nonempty(v.id) && validPoint(v.start) && validPoint(v.end) && validLock(v) && validHidden(v) && validateShapeStyle(v) && (v.head === void 0 || v.head === "triangle" || v.head === "none") && (v.routing === void 0 || [
365
+ "straight",
366
+ "curved",
367
+ "polyline"
368
+ ].includes(v.routing)) && validBinding(v.startBinding) && validBinding(v.endBinding) && (v.waypoints === void 0 || Array.isArray(v.waypoints) && v.waypoints.length <= 256 && v.waypoints.every(validPoint));
369
+ }
370
+ function validateGroup(v) {
371
+ return nonempty(v.id) && Array.isArray(v.children) && v.children.every((c) => nonempty(c)) && validLock(v) && validHidden(v);
372
+ }
373
+ function validateObjectOrder(v) {
374
+ if (v === void 0) return [];
375
+ if (!Array.isArray(v) || !v.every((id) => nonempty(id))) return validation("objectOrder must be an array of ids", "objectOrder");
376
+ return structuredClone(v);
188
377
  }
189
378
  function validateTimer(v) {
190
- return basePosition(v) && positive(v.size) && positive(v.durationMs) && finite(v.remainingMs) && v.remainingMs >= 0 && (v.runningSince === void 0 || finite(v.runningSince)) && validLock(v);
379
+ return basePosition(v) && positive(v.size) && positive(v.durationMs) && finite(v.remainingMs) && v.remainingMs >= 0 && (v.runningSince === void 0 || finite(v.runningSince)) && validLock(v) && validHidden(v);
191
380
  }
192
381
  function validateCustomObject(v) {
193
382
  return nonempty(v.id) && nonempty(v.type) && integer(v.schemaVersion) && validMatrix(v.transform) && Array.isArray(v.transform) && validCustomFallback(v.fallback) && (v.lock === void 0 || validCustomLock(v.lock)) && isJsonValue(v.props);
@@ -206,6 +395,9 @@ function basePosition(v) {
206
395
  function validLock(v) {
207
396
  return (v.locked === void 0 || typeof v.locked === "boolean") && optionalString(v.lockedBy) && optionalString(v.lockedByName);
208
397
  }
398
+ function validHidden(v) {
399
+ return v.hidden === void 0 || typeof v.hidden === "boolean";
400
+ }
209
401
  function validMatrix(v) {
210
402
  return v === void 0 || Array.isArray(v) && v.length === 6 && v.every(finite);
211
403
  }
@@ -256,7 +448,7 @@ function validation(message, path) {
256
448
  return new DocumentRecoveryError("DOCUMENT_VALIDATION_FAILED", message, void 0, path);
257
449
  }
258
450
  //#endregion
259
- //#region src/core-internal/affine2d.ts
451
+ //#region src/core/shapes/affine2d.ts
260
452
  var IDENTITY = [
261
453
  1,
262
454
  0,
@@ -340,7 +532,7 @@ function avgScale(m) {
340
532
  return (Math.hypot(m[0], m[1]) + Math.hypot(m[2], m[3])) / 2;
341
533
  }
342
534
  //#endregion
343
- //#region src/core-internal/boardSearch.ts
535
+ //#region src/core/document/boardSearch.ts
344
536
  function hay(s) {
345
537
  return s.toLowerCase();
346
538
  }
@@ -405,7 +597,7 @@ function searchBoard(query, board) {
405
597
  return hits;
406
598
  }
407
599
  //#endregion
408
- //#region src/core-internal/types.ts
600
+ //#region src/core/shapes/types.ts
409
601
  /** Erasure level at which a point counts as fully erased. */
410
602
  var ERASE_THRESHOLD = .95;
411
603
  function cloneStroke(stroke) {
@@ -422,16 +614,28 @@ var INK_COLORS = {
422
614
  green: "#16A34A"
423
615
  };
424
616
  var HIGHLIGHT_COLORS = {
425
- yellow: "#FDE047",
426
- green: "#86EFAC",
427
- pink: "#F9A8D4",
428
- blue: "#93C5FD"
617
+ yellow: "#FACC15",
618
+ green: "#4ADE80",
619
+ pink: "#F472B6",
620
+ blue: "#60A5FA"
429
621
  };
430
622
  var NOTE_COLORS = {
431
- yellow: "#FDE68A",
432
- pink: "#FBCFE8",
433
- blue: "#BFDBFE",
434
- green: "#BBF7D0"
623
+ yellow: "#FDE047",
624
+ amber: "#FACC15",
625
+ orange: "#FB923C",
626
+ peach: "#FDBA74",
627
+ red: "#FB7185",
628
+ rose: "#FDA4AF",
629
+ pink: "#F9A8D4",
630
+ magenta: "#F472B6",
631
+ purple: "#C084FC",
632
+ lavender: "#C4B5FD",
633
+ blue: "#93C5FD",
634
+ cornflower: "#60A5FA",
635
+ sky: "#38BDF8",
636
+ cyan: "#67E8F9",
637
+ green: "#86EFAC",
638
+ mint: "#4ADE80"
435
639
  };
436
640
  var TEXT_DEFAULT_SIZE = 1.8;
437
641
  function cloneText(block) {
@@ -481,7 +685,7 @@ function cloneImage(img) {
481
685
  return { ...img };
482
686
  }
483
687
  //#endregion
484
- //#region src/core-internal/clusterStore.ts
688
+ //#region src/core/document/clusterStore.ts
485
689
  /** A stroke joins a cluster last written to within this window at full reach. */
486
690
  var RECENT_MS = 8e3;
487
691
  /** Older clusters are still joinable, at half the gap threshold. */
@@ -652,7 +856,7 @@ function clamp$1(v, lo, hi) {
652
856
  return Math.min(hi, Math.max(lo, v));
653
857
  }
654
858
  //#endregion
655
- //#region src/core-internal/extensionTypes.ts
859
+ //#region src/core/shapes/extensionTypes.ts
656
860
  function cloneCustomObject(object) {
657
861
  return {
658
862
  ...object,
@@ -662,7 +866,7 @@ function cloneCustomObject(object) {
662
866
  };
663
867
  }
664
868
  //#endregion
665
- //#region src/core-internal/kitchenTimer.ts
869
+ //#region src/core/shapes/kitchenTimer.ts
666
870
  var TIMER_DEFAULT_SIZE = 12;
667
871
  var TIMER_DEFAULT_DURATION_MS = 300 * 1e3;
668
872
  var TIMER_PRESETS_MS = [
@@ -719,7 +923,7 @@ function formatTimer(ms) {
719
923
  return `${Math.floor(total / 60)}:${(total % 60).toString().padStart(2, "0")}`;
720
924
  }
721
925
  //#endregion
722
- //#region src/core-internal/itemLock.ts
926
+ //#region src/core/shapes/itemLock.ts
723
927
  /**
724
928
  * Apply or clear a lock in place. Unlock always drops the holder so a stale
725
929
  * name cannot linger on an unlocked item.
@@ -756,7 +960,409 @@ function serializeLock(item) {
756
960
  };
757
961
  }
758
962
  //#endregion
759
- //#region src/core-internal/document.ts
963
+ //#region src/core/shapes/shapeObjects.ts
964
+ var SHAPE_MIN_SIZE = .5;
965
+ /**
966
+ * Centralized shape style defaults (Phase 8) — one object a Host can read
967
+ * to know (or, by not relying on the standalone constants below, override
968
+ * via its own UI state) what a newly drawn Rectangle/Ellipse/Line/Arrow/
969
+ * Polygon/Star/Heart starts with when the user hasn't picked a stroke/width
970
+ * yet. `shapeTool.ts` (commit-time), `renderer/shapes/lines.ts` (render-time
971
+ * fallback for an object missing these fields), and
972
+ * `persistence/serialization/svg.ts` (export-time fallback) all read from
973
+ * here — the two standalone constants below are kept for source
974
+ * compatibility and simply mirror this object's values, not a second
975
+ * source of truth.
976
+ */
977
+ var SHAPE_STYLE_DEFAULTS = {
978
+ stroke: "#1C1C1E",
979
+ strokeWidth: .3
980
+ };
981
+ var SHAPE_DEFAULT_STROKE = SHAPE_STYLE_DEFAULTS.stroke;
982
+ var SHAPE_DEFAULT_STROKE_WIDTH = SHAPE_STYLE_DEFAULTS.strokeWidth;
983
+ /**
984
+ * Normalizes a box-shaped object's `x`/`y`/`width`/`height` to non-negative
985
+ * dimensions in place of whatever an arbitrary caller supplied (Phase 8) —
986
+ * the interactive drag tools (`shapeTool.ts`) already produce
987
+ * always-non-negative geometry by scanning min/max points before ever
988
+ * constructing a shape, but a raw `content.add`/`content.update` call (Host
989
+ * code, a Custom tool, or a remote collaboration Op) has no such guarantee.
990
+ * `BoardDocument` calls this at every entry point so "dragging from
991
+ * bottom-right toward top-left" and "a negative `width`/`height` patch"
992
+ * produce the same valid geometry regardless of source. A no-op (returns
993
+ * the same reference) when both dimensions are already non-negative, so
994
+ * this never allocates on the common path.
995
+ */
996
+ function normalizeBoxGeometry(box) {
997
+ if (box.width >= 0 && box.height >= 0) return box;
998
+ const left = Math.min(box.x, box.x + box.width);
999
+ const right = Math.max(box.x, box.x + box.width);
1000
+ const top = Math.max(box.y, box.y - box.height);
1001
+ const bottom = Math.min(box.y, box.y - box.height);
1002
+ return {
1003
+ ...box,
1004
+ x: left,
1005
+ y: top,
1006
+ width: right - left,
1007
+ height: top - bottom
1008
+ };
1009
+ }
1010
+ function cloneRectangle(rect) {
1011
+ return { ...rect };
1012
+ }
1013
+ function cloneEllipse(ellipse) {
1014
+ return { ...ellipse };
1015
+ }
1016
+ function clonePolygon(polygon) {
1017
+ return { ...polygon };
1018
+ }
1019
+ function cloneStar(star) {
1020
+ return { ...star };
1021
+ }
1022
+ function cloneHeart(heart) {
1023
+ return { ...heart };
1024
+ }
1025
+ function cloneLine(line) {
1026
+ return {
1027
+ ...line,
1028
+ start: { ...line.start },
1029
+ end: { ...line.end }
1030
+ };
1031
+ }
1032
+ function cloneArrow(arrow) {
1033
+ return {
1034
+ ...arrow,
1035
+ start: { ...arrow.start },
1036
+ end: { ...arrow.end },
1037
+ ...arrow.waypoints ? { waypoints: arrow.waypoints.map((p) => ({ ...p })) } : {},
1038
+ ...arrow.startBinding ? { startBinding: { ...arrow.startBinding } } : {},
1039
+ ...arrow.endBinding ? { endBinding: { ...arrow.endBinding } } : {}
1040
+ };
1041
+ }
1042
+ function cloneGroup(group) {
1043
+ return {
1044
+ ...group,
1045
+ children: [...group.children]
1046
+ };
1047
+ }
1048
+ //#endregion
1049
+ //#region src/core/shapes/lineGeometry.ts
1050
+ var BARB_ANGLE = Math.PI * (25 / 180);
1051
+ /** Two barb segments (each `[fromPoint, tip]`), or `[]` for `head: "none"`. */
1052
+ function arrowHeadBarbs(start, end, head) {
1053
+ if (head === "none") return [];
1054
+ const len = Math.hypot(end.x - start.x, end.y - start.y);
1055
+ if (len < 1e-6) return [];
1056
+ const dirX = (end.x - start.x) / len;
1057
+ const dirY = (end.y - start.y) / len;
1058
+ const headLen = Math.min(4, Math.max(1.2, len * .25));
1059
+ const barb = (sign) => {
1060
+ const cos = Math.cos(BARB_ANGLE * sign);
1061
+ const sin = Math.sin(BARB_ANGLE * sign);
1062
+ return {
1063
+ x: end.x - headLen * (dirX * cos - dirY * sin),
1064
+ y: end.y - headLen * (dirX * sin + dirY * cos)
1065
+ };
1066
+ };
1067
+ return [[barb(1), end], [barb(-1), end]];
1068
+ }
1069
+ /** Shared routed shaft for previews, rendering, bounds and SVG export. */
1070
+ function connectorPoints(arrow) {
1071
+ const { start: a, end: b, routing } = arrow;
1072
+ if (!routing || routing === "straight") return [a, b];
1073
+ const mx = (a.x + b.x) / 2;
1074
+ if (routing === "polyline") return [
1075
+ a,
1076
+ ...arrow.waypoints ?? [],
1077
+ b
1078
+ ];
1079
+ return Array.from({ length: 49 }, (_, i) => {
1080
+ const t = i / 48, u = 1 - t;
1081
+ return {
1082
+ x: u ** 3 * a.x + 3 * u * t * mx + t ** 3 * b.x,
1083
+ y: (u ** 3 + 3 * u * u * t) * a.y + (3 * u * t * t + t ** 3) * b.y
1084
+ };
1085
+ });
1086
+ }
1087
+ /** Shaft and arrowhead paths, with the head following the final route tangent. */
1088
+ function arrowPolylines(arrow) {
1089
+ const points = connectorPoints(arrow).filter((p, i, all) => i === 0 || Math.hypot(p.x - all[i - 1].x, p.y - all[i - 1].y) > 1e-8);
1090
+ if (points.length < 2) return [];
1091
+ if (!arrow.routing || arrow.routing === "straight") return [points, ...arrowHeadBarbs(arrow.start, arrow.end, arrow.head)];
1092
+ const a = points[points.length - 2], b = points[points.length - 1];
1093
+ const tangentLength = Math.hypot(b.x - a.x, b.y - a.y);
1094
+ const length = points.slice(1).reduce((sum, p, i) => sum + Math.hypot(p.x - points[i].x, p.y - points[i].y), 0);
1095
+ return [points, ...arrowHeadBarbs({
1096
+ x: b.x - (b.x - a.x) / tangentLength * length,
1097
+ y: b.y - (b.y - a.y) / tangentLength * length
1098
+ }, b, arrow.head)];
1099
+ }
1100
+ function arrowSegments(arrow) {
1101
+ return arrowPolylines(arrow).flatMap((points) => points.slice(1).map((p, i) => [points[i], p]));
1102
+ }
1103
+ //#endregion
1104
+ //#region src/core/document/objectBounds.ts
1105
+ function growAll(points) {
1106
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1107
+ for (const p of points) {
1108
+ if (p.x < minX) minX = p.x;
1109
+ if (p.y < minY) minY = p.y;
1110
+ if (p.x > maxX) maxX = p.x;
1111
+ if (p.y > maxY) maxY = p.y;
1112
+ }
1113
+ return {
1114
+ minX,
1115
+ minY,
1116
+ maxX,
1117
+ maxY
1118
+ };
1119
+ }
1120
+ function noteBounds(note) {
1121
+ const half = note.size / 2;
1122
+ return {
1123
+ minX: note.x - half,
1124
+ minY: note.y - half,
1125
+ maxX: note.x + half,
1126
+ maxY: note.y + half
1127
+ };
1128
+ }
1129
+ /** (x, y) is the top-left corner; text flows downward via -y. */
1130
+ function textBounds(block) {
1131
+ const { width, height } = measureTextBlock(block.text, block.fontSize);
1132
+ return {
1133
+ minX: block.x,
1134
+ minY: block.y - height,
1135
+ maxX: block.x + width,
1136
+ maxY: block.y
1137
+ };
1138
+ }
1139
+ /** (x, y) is the top-left corner, matching TextBlock's convention. */
1140
+ function tableBounds(table) {
1141
+ const { width, height } = measureTable(table);
1142
+ return {
1143
+ minX: table.x,
1144
+ minY: table.y - height,
1145
+ maxX: table.x + width,
1146
+ maxY: table.y
1147
+ };
1148
+ }
1149
+ /** (x, y) is the center — ImageBlock's own, different convention. */
1150
+ function imageBounds(image) {
1151
+ const halfW = image.width / 2;
1152
+ const halfH = image.height / 2;
1153
+ return {
1154
+ minX: image.x - halfW,
1155
+ minY: image.y - halfH,
1156
+ maxX: image.x + halfW,
1157
+ maxY: image.y + halfH
1158
+ };
1159
+ }
1160
+ function timerBounds(timer) {
1161
+ const half = timer.size / 2;
1162
+ return {
1163
+ minX: timer.x - half,
1164
+ minY: timer.y - half,
1165
+ maxX: timer.x + half,
1166
+ maxY: timer.y + half
1167
+ };
1168
+ }
1169
+ /** World-space corners of the object's local `fallback.bounds`, taken through its `transform` — the same math `hitTestCustom`'s inverse already relies on being correct. */
1170
+ function customObjectBounds(object) {
1171
+ const b = object.fallback.bounds;
1172
+ return growAll([
1173
+ {
1174
+ x: b.x,
1175
+ y: b.y
1176
+ },
1177
+ {
1178
+ x: b.x + b.width,
1179
+ y: b.y
1180
+ },
1181
+ {
1182
+ x: b.x,
1183
+ y: b.y + b.height
1184
+ },
1185
+ {
1186
+ x: b.x + b.width,
1187
+ y: b.y + b.height
1188
+ }
1189
+ ].map((corner) => apply(object.transform, corner)));
1190
+ }
1191
+ /**
1192
+ * Shared by Rectangle/Ellipse: both use the top-edge, Y-up, extends-downward
1193
+ * convention (see `shapeObjects.ts`'s own header comment), and both rotate
1194
+ * (Phase 3) about the same center `(x + width/2, y - height/2)`. An ellipse's
1195
+ * axis-aligned bounds equal its bounding rectangle's — an ellipse never
1196
+ * exceeds the box it's inscribed in — so one corner-rotation helper covers
1197
+ * both shapes exactly.
1198
+ */
1199
+ function topEdgeRotatedBounds(x, y, width, height, rotation) {
1200
+ if (!rotation) return {
1201
+ minX: x,
1202
+ minY: y - height,
1203
+ maxX: x + width,
1204
+ maxY: y
1205
+ };
1206
+ const m = rotationAbout(rotation, x + width / 2, y - height / 2);
1207
+ return growAll([
1208
+ {
1209
+ x,
1210
+ y
1211
+ },
1212
+ {
1213
+ x: x + width,
1214
+ y
1215
+ },
1216
+ {
1217
+ x,
1218
+ y: y - height
1219
+ },
1220
+ {
1221
+ x: x + width,
1222
+ y: y - height
1223
+ }
1224
+ ].map((corner) => apply(m, corner)));
1225
+ }
1226
+ function rectangleBounds(rect) {
1227
+ return topEdgeRotatedBounds(rect.x, rect.y, rect.width, rect.height, rect.rotation);
1228
+ }
1229
+ function ellipseBounds(ellipse) {
1230
+ return topEdgeRotatedBounds(ellipse.x, ellipse.y, ellipse.width, ellipse.height, ellipse.rotation);
1231
+ }
1232
+ /**
1233
+ * Same top-edge/Y-up/rotate-about-center convention as Rectangle — a
1234
+ * regular polygon's own outline is normalized (`polygonGeometry.ts`'s
1235
+ * `fitToBox`) to exactly fill its declared box, so this is exact at
1236
+ * rotation 0 and a safe (never-too-small) superset once rotated, matching
1237
+ * how `ellipseBounds` already treats its own inscribed shape.
1238
+ */
1239
+ function polygonBounds(polygon) {
1240
+ return topEdgeRotatedBounds(polygon.x, polygon.y, polygon.width, polygon.height, polygon.rotation);
1241
+ }
1242
+ /** Same convention and same safe-superset reasoning as `polygonBounds` — a star's points are normalized to touch its declared box (`polygonGeometry.ts`'s `starPoints`). */
1243
+ function starBounds(star) {
1244
+ return topEdgeRotatedBounds(star.x, star.y, star.width, star.height, star.rotation);
1245
+ }
1246
+ /** Same convention and same safe-superset reasoning as `polygonBounds` — a heart's curve is normalized to touch its declared box (`polygonGeometry.ts`'s `heartPoints`). */
1247
+ function heartBounds(heart) {
1248
+ return topEdgeRotatedBounds(heart.x, heart.y, heart.width, heart.height, heart.rotation);
1249
+ }
1250
+ /** No rotation concept (see `shapeObjects.ts`'s header) — the two endpoints already are the bounds. */
1251
+ function lineBounds(line) {
1252
+ return growAll([line.start, line.end]);
1253
+ }
1254
+ /** Same as `lineBounds` — an arrow's head barbs stay within the shaft's own bounding box (they curve back toward it, never past the tip). */
1255
+ function arrowBounds(arrow) {
1256
+ return growAll([
1257
+ arrow.start,
1258
+ arrow.end,
1259
+ ...arrow.routing ? arrowSegments(arrow).flat() : []
1260
+ ]);
1261
+ }
1262
+ /**
1263
+ * Union of one or more child bounds, or `null` when every child resolved to
1264
+ * nothing (an empty or entirely-dangling group). Used for `GroupObject`
1265
+ * bounds — the caller (`BoardDocument.bbox`) supplies `resolve` as its own
1266
+ * `bbox(id)` so this stays a pure fold with no document dependency here.
1267
+ */
1268
+ function unionBounds(boxes) {
1269
+ let result;
1270
+ for (const box of boxes) {
1271
+ if (!box) continue;
1272
+ if (!result) {
1273
+ result = { ...box };
1274
+ continue;
1275
+ }
1276
+ result.minX = Math.min(result.minX, box.minX);
1277
+ result.minY = Math.min(result.minY, box.minY);
1278
+ result.maxX = Math.max(result.maxX, box.maxX);
1279
+ result.maxY = Math.max(result.maxY, box.maxY);
1280
+ }
1281
+ return result;
1282
+ }
1283
+ //#endregion
1284
+ //#region src/core/shapes/connectorGeometry.ts
1285
+ /** Space left between an arrowhead's tip and the outline it points at. */
1286
+ var CONNECTOR_HEAD_GAP = .35;
1287
+ function anchorPoint(target, binding) {
1288
+ const { box } = target;
1289
+ return {
1290
+ x: box.minX + (box.maxX - box.minX) * binding.x,
1291
+ y: box.minY + (box.maxY - box.minY) * binding.y
1292
+ };
1293
+ }
1294
+ /**
1295
+ * Where the ray from `from` (inside the target) toward `toward` leaves the
1296
+ * outline, pulled back by `gap` toward `toward`. Returns `from` when `toward`
1297
+ * is itself inside the outline — there's no meaningful exit to aim for.
1298
+ */
1299
+ function clipToOutline(target, from, toward, gap = 0) {
1300
+ const dx = toward.x - from.x;
1301
+ const dy = toward.y - from.y;
1302
+ const length = Math.hypot(dx, dy);
1303
+ if (length < 1e-9) return from;
1304
+ const t = target.outline === "ellipse" ? ellipseExit(target.box, from, dx, dy) : boxExit(target.box, from, dx, dy);
1305
+ if (t === null || t >= 1) return from;
1306
+ const along = Math.min(t + gap / length, 1);
1307
+ return {
1308
+ x: from.x + dx * along,
1309
+ y: from.y + dy * along
1310
+ };
1311
+ }
1312
+ function boxExit(box, from, dx, dy) {
1313
+ let t = Infinity;
1314
+ if (dx > 0) t = Math.min(t, (box.maxX - from.x) / dx);
1315
+ else if (dx < 0) t = Math.min(t, (box.minX - from.x) / dx);
1316
+ if (dy > 0) t = Math.min(t, (box.maxY - from.y) / dy);
1317
+ else if (dy < 0) t = Math.min(t, (box.minY - from.y) / dy);
1318
+ return Number.isFinite(t) ? Math.max(0, t) : null;
1319
+ }
1320
+ function ellipseExit(box, from, dx, dy) {
1321
+ const rx = (box.maxX - box.minX) / 2;
1322
+ const ry = (box.maxY - box.minY) / 2;
1323
+ if (rx <= 0 || ry <= 0) return boxExit(box, from, dx, dy);
1324
+ const px = from.x - (box.minX + rx);
1325
+ const py = from.y - (box.minY + ry);
1326
+ const a = dx * dx / (rx * rx) + dy * dy / (ry * ry);
1327
+ const b = 2 * (px * dx / (rx * rx) + py * dy / (ry * ry));
1328
+ const c = px * px / (rx * rx) + py * py / (ry * ry) - 1;
1329
+ const disc = b * b - 4 * a * c;
1330
+ if (disc < 0) return null;
1331
+ return Math.max(0, (-b + Math.sqrt(disc)) / (2 * a));
1332
+ }
1333
+ function isCentred(binding) {
1334
+ return binding.x === .5 && binding.y === .5;
1335
+ }
1336
+ /**
1337
+ * The endpoints a connector should have given its bindings. An unbound end
1338
+ * keeps its stored point; a precise one sits on its anchor. Centred ends on
1339
+ * curved connectors leave their shapes horizontally (matching the S-curve's
1340
+ * tangents); others aim at the next point along the route. Only an end with
1341
+ * an arrowhead is pulled back from the outline.
1342
+ */
1343
+ function resolveConnectorEnds(arrow, targetOf) {
1344
+ const startTarget = arrow.startBinding && targetOf(arrow.startBinding.objectId);
1345
+ const endTarget = arrow.endBinding && targetOf(arrow.endBinding.objectId);
1346
+ const startAnchor = startTarget ? anchorPoint(startTarget, arrow.startBinding) : arrow.start;
1347
+ const endAnchor = endTarget ? anchorPoint(endTarget, arrow.endBinding) : arrow.end;
1348
+ const afterStart = arrow.waypoints?.[0] ?? endAnchor;
1349
+ const beforeEnd = arrow.waypoints?.at(-1) ?? startAnchor;
1350
+ const aim = (from, toward) => arrow.routing === "curved" ? {
1351
+ x: from.x + Math.sign(toward.x - from.x || 1) * 1e6,
1352
+ y: from.y
1353
+ } : toward;
1354
+ const headGap = arrow.head === "none" ? 0 : CONNECTOR_HEAD_GAP;
1355
+ const place = (target, binding, anchor, toward, gap, fallback) => {
1356
+ if (!target || !binding) return fallback;
1357
+ return isCentred(binding) ? clipToOutline(target, anchor, aim(anchor, toward), gap) : anchor;
1358
+ };
1359
+ return {
1360
+ start: place(startTarget, arrow.startBinding, startAnchor, afterStart, 0, arrow.start),
1361
+ end: place(endTarget, arrow.endBinding, endAnchor, beforeEnd, headGap, arrow.end)
1362
+ };
1363
+ }
1364
+ //#endregion
1365
+ //#region src/core/document/document.ts
760
1366
  /**
761
1367
  * The one place a live Stroke becomes its wire representation — in
762
1368
  * particular, points collapse from {x,y,pressure,erase?} objects into
@@ -806,8 +1412,73 @@ var EMPTY_CHANGE = {
806
1412
  timersUpdated: [],
807
1413
  customObjectsAdded: [],
808
1414
  customObjectsRemoved: [],
809
- customObjectsUpdated: []
1415
+ customObjectsUpdated: [],
1416
+ rectanglesAdded: [],
1417
+ rectanglesRemoved: [],
1418
+ rectanglesUpdated: [],
1419
+ ellipsesAdded: [],
1420
+ ellipsesRemoved: [],
1421
+ ellipsesUpdated: [],
1422
+ groupsAdded: [],
1423
+ groupsRemoved: [],
1424
+ groupsUpdated: [],
1425
+ linesAdded: [],
1426
+ linesRemoved: [],
1427
+ linesUpdated: [],
1428
+ arrowsAdded: [],
1429
+ arrowsRemoved: [],
1430
+ arrowsUpdated: [],
1431
+ polygonsAdded: [],
1432
+ polygonsRemoved: [],
1433
+ polygonsUpdated: [],
1434
+ starsAdded: [],
1435
+ starsRemoved: [],
1436
+ starsUpdated: [],
1437
+ heartsAdded: [],
1438
+ heartsRemoved: [],
1439
+ heartsUpdated: [],
1440
+ orderChanged: []
810
1441
  };
1442
+ /**
1443
+ * Flat content types that participate in the unified paint order — see
1444
+ * `DocumentChange.orderChanged`'s own doc comment for why notes/timers are
1445
+ * excluded. Listed back-to-front in the exact sequence the old fixed
1446
+ * per-renderer Z constants already used (rectangles/ellipses lowest, then
1447
+ * tables, then images, then ink/text, then custom objects) — this is what
1448
+ * `replaceAll`/`emit`'s automatic append-on-add tracking uses to synthesize
1449
+ * a default order for a legacy document with no persisted `objectOrder`, so
1450
+ * an existing document's visual stacking never changes just from loading it.
1451
+ */
1452
+ var ORDERED_ADDED_FIELDS = [
1453
+ "rectanglesAdded",
1454
+ "ellipsesAdded",
1455
+ "tablesAdded",
1456
+ "imagesAdded",
1457
+ "added",
1458
+ "textAdded",
1459
+ "customObjectsAdded",
1460
+ "groupsAdded",
1461
+ "linesAdded",
1462
+ "arrowsAdded",
1463
+ "polygonsAdded",
1464
+ "starsAdded",
1465
+ "heartsAdded"
1466
+ ];
1467
+ var ORDERED_REMOVED_FIELDS = [
1468
+ "rectanglesRemoved",
1469
+ "ellipsesRemoved",
1470
+ "tablesRemoved",
1471
+ "imagesRemoved",
1472
+ "removed",
1473
+ "textRemoved",
1474
+ "customObjectsRemoved",
1475
+ "groupsRemoved",
1476
+ "linesRemoved",
1477
+ "arrowsRemoved",
1478
+ "polygonsRemoved",
1479
+ "starsRemoved",
1480
+ "heartsRemoved"
1481
+ ];
811
1482
  var BoardDocument = class {
812
1483
  constructor(id) {
813
1484
  this.id = id;
@@ -819,8 +1490,85 @@ var BoardDocument = class {
819
1490
  this.images = /* @__PURE__ */ new Map();
820
1491
  this.timers = /* @__PURE__ */ new Map();
821
1492
  this.customObjects = /* @__PURE__ */ new Map();
1493
+ this.rectangles = /* @__PURE__ */ new Map();
1494
+ this.ellipses = /* @__PURE__ */ new Map();
1495
+ this.groups = /* @__PURE__ */ new Map();
1496
+ this.lines = /* @__PURE__ */ new Map();
1497
+ this.arrows = /* @__PURE__ */ new Map();
1498
+ this.polygons = /* @__PURE__ */ new Map();
1499
+ this.stars = /* @__PURE__ */ new Map();
1500
+ this.hearts = /* @__PURE__ */ new Map();
822
1501
  this.bboxes = /* @__PURE__ */ new Map();
823
1502
  this.listeners = /* @__PURE__ */ new Set();
1503
+ this.objectOrder = [];
1504
+ this.orderIndex = /* @__PURE__ */ new Map();
1505
+ }
1506
+ reindexOrder() {
1507
+ this.orderIndex = new Map(this.objectOrder.map((id, index) => [id, index]));
1508
+ }
1509
+ /** The full current paint order, back to front. */
1510
+ order() {
1511
+ return this.objectOrder;
1512
+ }
1513
+ /** This object's rank in the paint order, or -1 if it doesn't participate (unknown id, a note, or a timer). */
1514
+ orderRank(id) {
1515
+ return this.orderIndex.get(id) ?? -1;
1516
+ }
1517
+ bringForward(id) {
1518
+ const i = this.orderIndex.get(id);
1519
+ if (i === void 0 || i >= this.objectOrder.length - 1) return;
1520
+ [this.objectOrder[i], this.objectOrder[i + 1]] = [this.objectOrder[i + 1], this.objectOrder[i]];
1521
+ this.orderIndex.set(this.objectOrder[i], i);
1522
+ this.orderIndex.set(this.objectOrder[i + 1], i + 1);
1523
+ this.emit({ orderChanged: [this.objectOrder[i], this.objectOrder[i + 1]] });
1524
+ }
1525
+ sendBackward(id) {
1526
+ const i = this.orderIndex.get(id);
1527
+ if (i === void 0 || i <= 0) return;
1528
+ [this.objectOrder[i], this.objectOrder[i - 1]] = [this.objectOrder[i - 1], this.objectOrder[i]];
1529
+ this.orderIndex.set(this.objectOrder[i], i);
1530
+ this.orderIndex.set(this.objectOrder[i - 1], i - 1);
1531
+ this.emit({ orderChanged: [this.objectOrder[i - 1], this.objectOrder[i]] });
1532
+ }
1533
+ bringToFront(id) {
1534
+ const i = this.orderIndex.get(id);
1535
+ if (i === void 0 || i >= this.objectOrder.length - 1) return;
1536
+ this.objectOrder.splice(i, 1);
1537
+ this.objectOrder.push(id);
1538
+ for (let k = i; k < this.objectOrder.length; k++) this.orderIndex.set(this.objectOrder[k], k);
1539
+ this.emit({ orderChanged: this.objectOrder.slice(i) });
1540
+ }
1541
+ sendToBack(id) {
1542
+ const i = this.orderIndex.get(id);
1543
+ if (i === void 0 || i <= 0) return;
1544
+ this.objectOrder.splice(i, 1);
1545
+ this.objectOrder.unshift(id);
1546
+ for (let k = 0; k <= i; k++) this.orderIndex.set(this.objectOrder[k], k);
1547
+ this.emit({ orderChanged: this.objectOrder.slice(0, i + 1) });
1548
+ }
1549
+ /** Reorders `id` relative to its current neighbors. A no-op for an id that doesn't participate in paint order (see `orderRank`). */
1550
+ reorder(id, direction) {
1551
+ if (this.orderIndex.get(id) === void 0) return;
1552
+ if (direction === "forward") this.bringForward(id);
1553
+ else if (direction === "backward") this.sendBackward(id);
1554
+ else if (direction === "front") this.bringToFront(id);
1555
+ else this.sendToBack(id);
1556
+ }
1557
+ /**
1558
+ * Overwrites the paint order directly — used only when loading a document
1559
+ * that already carries a persisted `objectOrder`; every other order
1560
+ * mutation goes through `reorder`/the automatic append-on-add tracking in
1561
+ * `emit`. Ids not present in the document are dropped; ids present in the
1562
+ * document but missing from `order` are appended at the back, so a
1563
+ * partially-stale order (e.g. from a schema migration) never silently
1564
+ * drops an object from paint order entirely.
1565
+ */
1566
+ setOrder(order) {
1567
+ const known = new Set(this.objectOrder);
1568
+ const next = order.filter((id) => known.has(id));
1569
+ for (const id of this.objectOrder) if (!next.includes(id)) next.push(id);
1570
+ this.objectOrder = next;
1571
+ this.reindexOrder();
824
1572
  }
825
1573
  get(id) {
826
1574
  return this.strokes.get(id);
@@ -828,8 +1576,88 @@ var BoardDocument = class {
828
1576
  all() {
829
1577
  return this.strokes.values();
830
1578
  }
831
- bbox(id) {
832
- return this.bboxes.get(id);
1579
+ /**
1580
+ * World-space bounds for any content object, of any type. Strokes hit
1581
+ * their cached-on-mutation fast path (`bboxes`, populated by
1582
+ * `addStrokes`/`transformStrokes` — many points, worth caching); every
1583
+ * other type computes on demand via `objectBounds.ts` (cheap arithmetic,
1584
+ * no caching needed). A group's bounds are the union of its (recursively
1585
+ * resolved) children — `seen` guards against a cycle in nested groups.
1586
+ */
1587
+ /**
1588
+ * What a connector can attach to at `id`: its bounds and outline shape.
1589
+ * Connectors, lines and groups aren't targets (that would allow cycles).
1590
+ */
1591
+ connectorTarget(id) {
1592
+ if (this.arrows.has(id) || this.lines.has(id) || this.groups.has(id)) return void 0;
1593
+ const box = this.bbox(id);
1594
+ if (!box) return void 0;
1595
+ return {
1596
+ box,
1597
+ outline: this.ellipses.has(id) ? "ellipse" : "box"
1598
+ };
1599
+ }
1600
+ bbox(id, seen) {
1601
+ const cached = this.bboxes.get(id);
1602
+ if (cached) return cached;
1603
+ const note = this.notes.get(id);
1604
+ if (note) return noteBounds(note);
1605
+ const text = this.texts.get(id);
1606
+ if (text) return textBounds(text);
1607
+ const table = this.tables.get(id);
1608
+ if (table) return tableBounds(table);
1609
+ const image = this.images.get(id);
1610
+ if (image) return imageBounds(image);
1611
+ const timer = this.timers.get(id);
1612
+ if (timer) return timerBounds(timer);
1613
+ const custom = this.customObjects.get(id);
1614
+ if (custom) return customObjectBounds(custom);
1615
+ const rect = this.rectangles.get(id);
1616
+ if (rect) return rectangleBounds(rect);
1617
+ const ellipse = this.ellipses.get(id);
1618
+ if (ellipse) return ellipseBounds(ellipse);
1619
+ const line = this.lines.get(id);
1620
+ if (line) return lineBounds(line);
1621
+ const arrow = this.arrows.get(id);
1622
+ if (arrow) return arrowBounds(arrow);
1623
+ const polygon = this.polygons.get(id);
1624
+ if (polygon) return polygonBounds(polygon);
1625
+ const star = this.stars.get(id);
1626
+ if (star) return starBounds(star);
1627
+ const heart = this.hearts.get(id);
1628
+ if (heart) return heartBounds(heart);
1629
+ const group = this.groups.get(id);
1630
+ if (group) {
1631
+ const visited = seen ?? /* @__PURE__ */ new Set();
1632
+ if (visited.has(id)) return void 0;
1633
+ visited.add(id);
1634
+ return unionBounds(group.children.map((childId) => this.bbox(childId, visited)));
1635
+ }
1636
+ }
1637
+ /**
1638
+ * True if `id` exists and is locked, for any type — the same per-type
1639
+ * probe pattern as `bbox`, for interactive gestures (drag/transform) that
1640
+ * need to gate on lock state regardless of what's selected. Custom
1641
+ * objects are deliberately excluded: their `lock` field is a different
1642
+ * shape (`{holderId, acquiredAt}`, no display name) with no interactive
1643
+ * lock UI yet, matching the existing, deliberate "always unlockable,
1644
+ * never gates a drag" treatment already established elsewhere (e.g.
1645
+ * `getSelectedItemInfo`'s custom branch hardcodes `isLocked: false`).
1646
+ */
1647
+ isLocked(id) {
1648
+ return !!this.strokes.get(id)?.locked || !!this.notes.get(id)?.locked || !!this.texts.get(id)?.locked || !!this.tables.get(id)?.locked || !!this.images.get(id)?.locked || !!this.timers.get(id)?.locked || !!this.rectangles.get(id)?.locked || !!this.ellipses.get(id)?.locked || !!this.groups.get(id)?.locked || !!this.lines.get(id)?.locked || !!this.arrows.get(id)?.locked || !!this.polygons.get(id)?.locked || !!this.stars.get(id)?.locked || !!this.hearts.get(id)?.locked;
1649
+ }
1650
+ /**
1651
+ * True if `id` exists and is hidden, for any type — the same per-type
1652
+ * probe pattern as {@link isLocked} (Phase 8). Custom objects are
1653
+ * excluded for the same reason `isLocked` excludes them: they have no
1654
+ * `Hideable` field at all, so "hidden" isn't a concept that applies to
1655
+ * them yet. Used by marquee selection (`selectTool.ts`) to keep a hidden
1656
+ * object out of a rubber-band selection even for object types whose own
1657
+ * renderer doesn't yet suppress click-based hit-testing.
1658
+ */
1659
+ isHidden(id) {
1660
+ return !!this.strokes.get(id)?.hidden || !!this.notes.get(id)?.hidden || !!this.texts.get(id)?.hidden || !!this.tables.get(id)?.hidden || !!this.images.get(id)?.hidden || !!this.timers.get(id)?.hidden || !!this.rectangles.get(id)?.hidden || !!this.ellipses.get(id)?.hidden || !!this.groups.get(id)?.hidden || !!this.lines.get(id)?.hidden || !!this.arrows.get(id)?.hidden || !!this.polygons.get(id)?.hidden || !!this.stars.get(id)?.hidden || !!this.hearts.get(id)?.hidden;
833
1661
  }
834
1662
  subscribe(listener) {
835
1663
  this.listeners.add(listener);
@@ -978,6 +1806,176 @@ var BoardDocument = class {
978
1806
  this.customObjects.set(object.id, object);
979
1807
  this.emit({ customObjectsUpdated: [object] });
980
1808
  }
1809
+ getRectangle(id) {
1810
+ return this.rectangles.get(id);
1811
+ }
1812
+ allRectangles() {
1813
+ return this.rectangles.values();
1814
+ }
1815
+ addRectangles(rectangles) {
1816
+ const normalized = rectangles.map(normalizeBoxGeometry);
1817
+ for (const rect of normalized) this.rectangles.set(rect.id, rect);
1818
+ this.emit({ rectanglesAdded: normalized });
1819
+ }
1820
+ removeRectangles(ids) {
1821
+ const rectanglesRemoved = [];
1822
+ for (const id of ids) if (this.rectangles.delete(id)) rectanglesRemoved.push(id);
1823
+ if (rectanglesRemoved.length) this.emit({ rectanglesRemoved });
1824
+ }
1825
+ /** Replace a rectangle's contents (move, resize, restyle) under the same id. */
1826
+ setRectangle(rect) {
1827
+ const normalized = normalizeBoxGeometry(rect);
1828
+ this.rectangles.set(normalized.id, normalized);
1829
+ this.emit({ rectanglesUpdated: [normalized] });
1830
+ }
1831
+ getEllipse(id) {
1832
+ return this.ellipses.get(id);
1833
+ }
1834
+ allEllipses() {
1835
+ return this.ellipses.values();
1836
+ }
1837
+ addEllipses(ellipses) {
1838
+ const normalized = ellipses.map(normalizeBoxGeometry);
1839
+ for (const ellipse of normalized) this.ellipses.set(ellipse.id, ellipse);
1840
+ this.emit({ ellipsesAdded: normalized });
1841
+ }
1842
+ removeEllipses(ids) {
1843
+ const ellipsesRemoved = [];
1844
+ for (const id of ids) if (this.ellipses.delete(id)) ellipsesRemoved.push(id);
1845
+ if (ellipsesRemoved.length) this.emit({ ellipsesRemoved });
1846
+ }
1847
+ /** Replace an ellipse's contents (move, resize, restyle) under the same id. */
1848
+ setEllipse(ellipse) {
1849
+ const normalized = normalizeBoxGeometry(ellipse);
1850
+ this.ellipses.set(normalized.id, normalized);
1851
+ this.emit({ ellipsesUpdated: [normalized] });
1852
+ }
1853
+ getGroup(id) {
1854
+ return this.groups.get(id);
1855
+ }
1856
+ allGroups() {
1857
+ return this.groups.values();
1858
+ }
1859
+ addGroups(groups) {
1860
+ for (const group of groups) this.groups.set(group.id, group);
1861
+ this.emit({ groupsAdded: groups });
1862
+ }
1863
+ removeGroups(ids) {
1864
+ const groupsRemoved = [];
1865
+ for (const id of ids) if (this.groups.delete(id)) groupsRemoved.push(id);
1866
+ if (groupsRemoved.length) this.emit({ groupsRemoved });
1867
+ }
1868
+ /** Replace a group's contents (its children list) under the same id. */
1869
+ setGroup(group) {
1870
+ this.groups.set(group.id, group);
1871
+ this.emit({ groupsUpdated: [group] });
1872
+ }
1873
+ getLine(id) {
1874
+ return this.lines.get(id);
1875
+ }
1876
+ allLines() {
1877
+ return this.lines.values();
1878
+ }
1879
+ addLines(lines) {
1880
+ for (const line of lines) this.lines.set(line.id, line);
1881
+ this.emit({ linesAdded: lines });
1882
+ }
1883
+ removeLines(ids) {
1884
+ const linesRemoved = [];
1885
+ for (const id of ids) if (this.lines.delete(id)) linesRemoved.push(id);
1886
+ if (linesRemoved.length) this.emit({ linesRemoved });
1887
+ }
1888
+ /** Replace a line's contents (move, restyle) under the same id. */
1889
+ setLine(line) {
1890
+ this.lines.set(line.id, line);
1891
+ this.emit({ linesUpdated: [line] });
1892
+ }
1893
+ getArrow(id) {
1894
+ return this.arrows.get(id);
1895
+ }
1896
+ allArrows() {
1897
+ return this.arrows.values();
1898
+ }
1899
+ addArrows(arrows) {
1900
+ for (const arrow of arrows) this.arrows.set(arrow.id, arrow);
1901
+ this.emit({ arrowsAdded: arrows });
1902
+ }
1903
+ removeArrows(ids) {
1904
+ const arrowsRemoved = [];
1905
+ for (const id of ids) if (this.arrows.delete(id)) arrowsRemoved.push(id);
1906
+ if (arrowsRemoved.length) this.emit({ arrowsRemoved });
1907
+ }
1908
+ /** Replace an arrow's contents (move, restyle, change head) under the same id. */
1909
+ setArrow(arrow) {
1910
+ this.arrows.set(arrow.id, arrow);
1911
+ this.emit({ arrowsUpdated: [arrow] });
1912
+ }
1913
+ getPolygon(id) {
1914
+ return this.polygons.get(id);
1915
+ }
1916
+ allPolygons() {
1917
+ return this.polygons.values();
1918
+ }
1919
+ addPolygons(polygons) {
1920
+ const normalized = polygons.map(normalizeBoxGeometry);
1921
+ for (const polygon of normalized) this.polygons.set(polygon.id, polygon);
1922
+ this.emit({ polygonsAdded: normalized });
1923
+ }
1924
+ removePolygons(ids) {
1925
+ const polygonsRemoved = [];
1926
+ for (const id of ids) if (this.polygons.delete(id)) polygonsRemoved.push(id);
1927
+ if (polygonsRemoved.length) this.emit({ polygonsRemoved });
1928
+ }
1929
+ /** Replace a polygon's contents (move, resize, rotate, restyle) under the same id. */
1930
+ setPolygon(polygon) {
1931
+ const normalized = normalizeBoxGeometry(polygon);
1932
+ this.polygons.set(normalized.id, normalized);
1933
+ this.emit({ polygonsUpdated: [normalized] });
1934
+ }
1935
+ getStar(id) {
1936
+ return this.stars.get(id);
1937
+ }
1938
+ allStars() {
1939
+ return this.stars.values();
1940
+ }
1941
+ addStars(stars) {
1942
+ const normalized = stars.map(normalizeBoxGeometry);
1943
+ for (const star of normalized) this.stars.set(star.id, star);
1944
+ this.emit({ starsAdded: normalized });
1945
+ }
1946
+ removeStars(ids) {
1947
+ const starsRemoved = [];
1948
+ for (const id of ids) if (this.stars.delete(id)) starsRemoved.push(id);
1949
+ if (starsRemoved.length) this.emit({ starsRemoved });
1950
+ }
1951
+ /** Replace a star's contents (move, resize, rotate, restyle) under the same id. */
1952
+ setStar(star) {
1953
+ const normalized = normalizeBoxGeometry(star);
1954
+ this.stars.set(normalized.id, normalized);
1955
+ this.emit({ starsUpdated: [normalized] });
1956
+ }
1957
+ getHeart(id) {
1958
+ return this.hearts.get(id);
1959
+ }
1960
+ allHearts() {
1961
+ return this.hearts.values();
1962
+ }
1963
+ addHearts(hearts) {
1964
+ const normalized = hearts.map(normalizeBoxGeometry);
1965
+ for (const heart of normalized) this.hearts.set(heart.id, heart);
1966
+ this.emit({ heartsAdded: normalized });
1967
+ }
1968
+ removeHearts(ids) {
1969
+ const heartsRemoved = [];
1970
+ for (const id of ids) if (this.hearts.delete(id)) heartsRemoved.push(id);
1971
+ if (heartsRemoved.length) this.emit({ heartsRemoved });
1972
+ }
1973
+ /** Replace a heart's contents (move, resize, rotate, restyle) under the same id. */
1974
+ setHeart(heart) {
1975
+ const normalized = normalizeBoxGeometry(heart);
1976
+ this.hearts.set(normalized.id, normalized);
1977
+ this.emit({ heartsUpdated: [normalized] });
1978
+ }
981
1979
  setStrokeLocked(id, locked, by) {
982
1980
  const stroke = this.strokes.get(id);
983
1981
  if (!stroke) return;
@@ -1025,8 +2023,56 @@ var BoardDocument = class {
1025
2023
  applyItemLock(timer, locked, by);
1026
2024
  this.emit({ timersUpdated: [timer] });
1027
2025
  }
2026
+ setRectangleLocked(id, locked, by) {
2027
+ const rect = this.rectangles.get(id);
2028
+ if (!rect) return;
2029
+ applyItemLock(rect, locked, by);
2030
+ this.emit({ rectanglesUpdated: [rect] });
2031
+ }
2032
+ setEllipseLocked(id, locked, by) {
2033
+ const ellipse = this.ellipses.get(id);
2034
+ if (!ellipse) return;
2035
+ applyItemLock(ellipse, locked, by);
2036
+ this.emit({ ellipsesUpdated: [ellipse] });
2037
+ }
2038
+ setGroupLocked(id, locked, by) {
2039
+ const group = this.groups.get(id);
2040
+ if (!group) return;
2041
+ applyItemLock(group, locked, by);
2042
+ this.emit({ groupsUpdated: [group] });
2043
+ }
2044
+ setLineLocked(id, locked, by) {
2045
+ const line = this.lines.get(id);
2046
+ if (!line) return;
2047
+ applyItemLock(line, locked, by);
2048
+ this.emit({ linesUpdated: [line] });
2049
+ }
2050
+ setArrowLocked(id, locked, by) {
2051
+ const arrow = this.arrows.get(id);
2052
+ if (!arrow) return;
2053
+ applyItemLock(arrow, locked, by);
2054
+ this.emit({ arrowsUpdated: [arrow] });
2055
+ }
2056
+ setPolygonLocked(id, locked, by) {
2057
+ const polygon = this.polygons.get(id);
2058
+ if (!polygon) return;
2059
+ applyItemLock(polygon, locked, by);
2060
+ this.emit({ polygonsUpdated: [polygon] });
2061
+ }
2062
+ setStarLocked(id, locked, by) {
2063
+ const star = this.stars.get(id);
2064
+ if (!star) return;
2065
+ applyItemLock(star, locked, by);
2066
+ this.emit({ starsUpdated: [star] });
2067
+ }
2068
+ setHeartLocked(id, locked, by) {
2069
+ const heart = this.hearts.get(id);
2070
+ if (!heart) return;
2071
+ applyItemLock(heart, locked, by);
2072
+ this.emit({ heartsUpdated: [heart] });
2073
+ }
1028
2074
  /** Replace all content (initial load). Does not touch `version`. */
1029
- replaceAll(strokes, notes, texts, tables = [], images = [], timers = [], customObjects = []) {
2075
+ replaceAll(strokes, notes, texts, tables = [], images = [], timers = [], customObjects = [], rectangles = [], ellipses = [], groups = [], lines = [], arrows = [], polygons = [], stars = [], hearts = [], objectOrder) {
1030
2076
  const removed = [...this.strokes.keys()];
1031
2077
  const notesRemoved = [...this.notes.keys()];
1032
2078
  const textRemoved = [...this.texts.keys()];
@@ -1034,6 +2080,14 @@ var BoardDocument = class {
1034
2080
  const imagesRemoved = [...this.images.keys()];
1035
2081
  const timersRemoved = [...this.timers.keys()];
1036
2082
  const customObjectsRemoved = [...this.customObjects.keys()];
2083
+ const rectanglesRemoved = [...this.rectangles.keys()];
2084
+ const ellipsesRemoved = [...this.ellipses.keys()];
2085
+ const groupsRemoved = [...this.groups.keys()];
2086
+ const linesRemoved = [...this.lines.keys()];
2087
+ const arrowsRemoved = [...this.arrows.keys()];
2088
+ const polygonsRemoved = [...this.polygons.keys()];
2089
+ const starsRemoved = [...this.stars.keys()];
2090
+ const heartsRemoved = [...this.hearts.keys()];
1037
2091
  this.strokes.clear();
1038
2092
  this.notes.clear();
1039
2093
  this.texts.clear();
@@ -1041,6 +2095,14 @@ var BoardDocument = class {
1041
2095
  this.images.clear();
1042
2096
  this.timers.clear();
1043
2097
  this.customObjects.clear();
2098
+ this.rectangles.clear();
2099
+ this.ellipses.clear();
2100
+ this.groups.clear();
2101
+ this.lines.clear();
2102
+ this.arrows.clear();
2103
+ this.polygons.clear();
2104
+ this.stars.clear();
2105
+ this.hearts.clear();
1044
2106
  this.bboxes.clear();
1045
2107
  for (const stroke of strokes) {
1046
2108
  this.strokes.set(stroke.id, stroke);
@@ -1052,6 +2114,14 @@ var BoardDocument = class {
1052
2114
  for (const img of images) this.images.set(img.id, img);
1053
2115
  for (const timer of timers) this.timers.set(timer.id, timer);
1054
2116
  for (const object of customObjects) this.customObjects.set(object.id, object);
2117
+ for (const rect of rectangles) this.rectangles.set(rect.id, rect);
2118
+ for (const ellipse of ellipses) this.ellipses.set(ellipse.id, ellipse);
2119
+ for (const group of groups) this.groups.set(group.id, group);
2120
+ for (const line of lines) this.lines.set(line.id, line);
2121
+ for (const arrow of arrows) this.arrows.set(arrow.id, arrow);
2122
+ for (const polygon of polygons) this.polygons.set(polygon.id, polygon);
2123
+ for (const star of stars) this.stars.set(star.id, star);
2124
+ for (const heart of hearts) this.hearts.set(heart.id, heart);
1055
2125
  this.emit({
1056
2126
  added: strokes,
1057
2127
  removed,
@@ -1066,8 +2136,25 @@ var BoardDocument = class {
1066
2136
  timersAdded: timers,
1067
2137
  timersRemoved,
1068
2138
  customObjectsAdded: customObjects,
1069
- customObjectsRemoved
2139
+ customObjectsRemoved,
2140
+ rectanglesAdded: rectangles,
2141
+ rectanglesRemoved,
2142
+ ellipsesAdded: ellipses,
2143
+ ellipsesRemoved,
2144
+ groupsAdded: groups,
2145
+ groupsRemoved,
2146
+ linesAdded: lines,
2147
+ linesRemoved,
2148
+ arrowsAdded: arrows,
2149
+ arrowsRemoved,
2150
+ polygonsAdded: polygons,
2151
+ polygonsRemoved,
2152
+ starsAdded: stars,
2153
+ starsRemoved,
2154
+ heartsAdded: hearts,
2155
+ heartsRemoved
1070
2156
  });
2157
+ if (objectOrder) this.setOrder(objectOrder);
1071
2158
  }
1072
2159
  /** Apply incremental real-time change received from a remote collaborator over WebSocket. */
1073
2160
  applyRemoteChange(change) {
@@ -1179,6 +2266,114 @@ var BoardDocument = class {
1179
2266
  for (const object of change.customObjectsUpdated) this.customObjects.set(object.id, object);
1180
2267
  events.customObjectsUpdated = change.customObjectsUpdated;
1181
2268
  }
2269
+ if (change.rectanglesAdded && change.rectanglesAdded.length > 0) {
2270
+ for (const rect of change.rectanglesAdded) this.rectangles.set(rect.id, rect);
2271
+ events.rectanglesAdded = change.rectanglesAdded;
2272
+ }
2273
+ if (change.rectanglesRemoved && change.rectanglesRemoved.length > 0) {
2274
+ const rectanglesRemoved = [];
2275
+ for (const id of change.rectanglesRemoved) if (this.rectangles.delete(id)) rectanglesRemoved.push(id);
2276
+ if (rectanglesRemoved.length > 0) events.rectanglesRemoved = rectanglesRemoved;
2277
+ }
2278
+ if (change.rectanglesUpdated && change.rectanglesUpdated.length > 0) {
2279
+ for (const rect of change.rectanglesUpdated) this.rectangles.set(rect.id, rect);
2280
+ events.rectanglesUpdated = change.rectanglesUpdated;
2281
+ }
2282
+ if (change.ellipsesAdded && change.ellipsesAdded.length > 0) {
2283
+ for (const ellipse of change.ellipsesAdded) this.ellipses.set(ellipse.id, ellipse);
2284
+ events.ellipsesAdded = change.ellipsesAdded;
2285
+ }
2286
+ if (change.ellipsesRemoved && change.ellipsesRemoved.length > 0) {
2287
+ const ellipsesRemoved = [];
2288
+ for (const id of change.ellipsesRemoved) if (this.ellipses.delete(id)) ellipsesRemoved.push(id);
2289
+ if (ellipsesRemoved.length > 0) events.ellipsesRemoved = ellipsesRemoved;
2290
+ }
2291
+ if (change.ellipsesUpdated && change.ellipsesUpdated.length > 0) {
2292
+ for (const ellipse of change.ellipsesUpdated) this.ellipses.set(ellipse.id, ellipse);
2293
+ events.ellipsesUpdated = change.ellipsesUpdated;
2294
+ }
2295
+ if (change.groupsAdded && change.groupsAdded.length > 0) {
2296
+ for (const group of change.groupsAdded) this.groups.set(group.id, group);
2297
+ events.groupsAdded = change.groupsAdded;
2298
+ }
2299
+ if (change.groupsRemoved && change.groupsRemoved.length > 0) {
2300
+ const groupsRemoved = [];
2301
+ for (const id of change.groupsRemoved) if (this.groups.delete(id)) groupsRemoved.push(id);
2302
+ if (groupsRemoved.length > 0) events.groupsRemoved = groupsRemoved;
2303
+ }
2304
+ if (change.groupsUpdated && change.groupsUpdated.length > 0) {
2305
+ for (const group of change.groupsUpdated) this.groups.set(group.id, group);
2306
+ events.groupsUpdated = change.groupsUpdated;
2307
+ }
2308
+ if (change.linesAdded && change.linesAdded.length > 0) {
2309
+ for (const line of change.linesAdded) this.lines.set(line.id, line);
2310
+ events.linesAdded = change.linesAdded;
2311
+ }
2312
+ if (change.linesRemoved && change.linesRemoved.length > 0) {
2313
+ const linesRemoved = [];
2314
+ for (const id of change.linesRemoved) if (this.lines.delete(id)) linesRemoved.push(id);
2315
+ if (linesRemoved.length > 0) events.linesRemoved = linesRemoved;
2316
+ }
2317
+ if (change.linesUpdated && change.linesUpdated.length > 0) {
2318
+ for (const line of change.linesUpdated) this.lines.set(line.id, line);
2319
+ events.linesUpdated = change.linesUpdated;
2320
+ }
2321
+ if (change.arrowsAdded && change.arrowsAdded.length > 0) {
2322
+ for (const arrow of change.arrowsAdded) this.arrows.set(arrow.id, arrow);
2323
+ events.arrowsAdded = change.arrowsAdded;
2324
+ }
2325
+ if (change.arrowsRemoved && change.arrowsRemoved.length > 0) {
2326
+ const arrowsRemoved = [];
2327
+ for (const id of change.arrowsRemoved) if (this.arrows.delete(id)) arrowsRemoved.push(id);
2328
+ if (arrowsRemoved.length > 0) events.arrowsRemoved = arrowsRemoved;
2329
+ }
2330
+ if (change.arrowsUpdated && change.arrowsUpdated.length > 0) {
2331
+ for (const arrow of change.arrowsUpdated) this.arrows.set(arrow.id, arrow);
2332
+ events.arrowsUpdated = change.arrowsUpdated;
2333
+ }
2334
+ if (change.polygonsAdded && change.polygonsAdded.length > 0) {
2335
+ for (const polygon of change.polygonsAdded) this.polygons.set(polygon.id, polygon);
2336
+ events.polygonsAdded = change.polygonsAdded;
2337
+ }
2338
+ if (change.polygonsRemoved && change.polygonsRemoved.length > 0) {
2339
+ const polygonsRemoved = [];
2340
+ for (const id of change.polygonsRemoved) if (this.polygons.delete(id)) polygonsRemoved.push(id);
2341
+ if (polygonsRemoved.length > 0) events.polygonsRemoved = polygonsRemoved;
2342
+ }
2343
+ if (change.polygonsUpdated && change.polygonsUpdated.length > 0) {
2344
+ for (const polygon of change.polygonsUpdated) this.polygons.set(polygon.id, polygon);
2345
+ events.polygonsUpdated = change.polygonsUpdated;
2346
+ }
2347
+ if (change.starsAdded && change.starsAdded.length > 0) {
2348
+ for (const star of change.starsAdded) this.stars.set(star.id, star);
2349
+ events.starsAdded = change.starsAdded;
2350
+ }
2351
+ if (change.starsRemoved && change.starsRemoved.length > 0) {
2352
+ const starsRemoved = [];
2353
+ for (const id of change.starsRemoved) if (this.stars.delete(id)) starsRemoved.push(id);
2354
+ if (starsRemoved.length > 0) events.starsRemoved = starsRemoved;
2355
+ }
2356
+ if (change.starsUpdated && change.starsUpdated.length > 0) {
2357
+ for (const star of change.starsUpdated) this.stars.set(star.id, star);
2358
+ events.starsUpdated = change.starsUpdated;
2359
+ }
2360
+ if (change.heartsAdded && change.heartsAdded.length > 0) {
2361
+ for (const heart of change.heartsAdded) this.hearts.set(heart.id, heart);
2362
+ events.heartsAdded = change.heartsAdded;
2363
+ }
2364
+ if (change.heartsRemoved && change.heartsRemoved.length > 0) {
2365
+ const heartsRemoved = [];
2366
+ for (const id of change.heartsRemoved) if (this.hearts.delete(id)) heartsRemoved.push(id);
2367
+ if (heartsRemoved.length > 0) events.heartsRemoved = heartsRemoved;
2368
+ }
2369
+ if (change.heartsUpdated && change.heartsUpdated.length > 0) {
2370
+ for (const heart of change.heartsUpdated) this.hearts.set(heart.id, heart);
2371
+ events.heartsUpdated = change.heartsUpdated;
2372
+ }
2373
+ if (change.orderChanged && change.orderChanged.length > 0) {
2374
+ this.setOrder(change.orderChanged);
2375
+ events.orderChanged = change.orderChanged;
2376
+ }
1182
2377
  this.emit(events);
1183
2378
  }
1184
2379
  toJSON() {
@@ -1190,12 +2385,45 @@ var BoardDocument = class {
1190
2385
  tables: [...this.tables.values()].map(cloneTable),
1191
2386
  images: [...this.images.values()].map(cloneImage),
1192
2387
  timers: [...this.timers.values()].map(cloneTimer),
1193
- customObjects: [...this.customObjects.values()].map(cloneCustomObject)
2388
+ customObjects: [...this.customObjects.values()].map(cloneCustomObject),
2389
+ rectangles: [...this.rectangles.values()].map(cloneRectangle),
2390
+ ellipses: [...this.ellipses.values()].map(cloneEllipse),
2391
+ groups: [...this.groups.values()].map(cloneGroup),
2392
+ lines: [...this.lines.values()].map(cloneLine),
2393
+ arrows: [...this.arrows.values()].map(cloneArrow),
2394
+ polygons: [...this.polygons.values()].map(clonePolygon),
2395
+ stars: [...this.stars.values()].map(cloneStar),
2396
+ hearts: [...this.hearts.values()].map(cloneHeart),
2397
+ objectOrder: [...this.objectOrder]
1194
2398
  };
1195
2399
  }
2400
+ static deserializeGroups(data) {
2401
+ return (data.groups ?? []).map(cloneGroup);
2402
+ }
2403
+ static deserializeLines(data) {
2404
+ return (data.lines ?? []).map(cloneLine);
2405
+ }
2406
+ static deserializeArrows(data) {
2407
+ return (data.arrows ?? []).map(cloneArrow);
2408
+ }
2409
+ static deserializePolygons(data) {
2410
+ return (data.polygons ?? []).map(clonePolygon);
2411
+ }
2412
+ static deserializeStars(data) {
2413
+ return (data.stars ?? []).map(cloneStar);
2414
+ }
2415
+ static deserializeHearts(data) {
2416
+ return (data.hearts ?? []).map(cloneHeart);
2417
+ }
1196
2418
  static deserializeCustomObjects(data) {
1197
2419
  return (data.customObjects ?? []).map(cloneCustomObject);
1198
2420
  }
2421
+ static deserializeRectangles(data) {
2422
+ return (data.rectangles ?? []).map(cloneRectangle);
2423
+ }
2424
+ static deserializeEllipses(data) {
2425
+ return (data.ellipses ?? []).map(cloneEllipse);
2426
+ }
1199
2427
  static deserializeImages(data) {
1200
2428
  return (data.images ?? []).map(cloneImage);
1201
2429
  }
@@ -1211,7 +2439,17 @@ var BoardDocument = class {
1211
2439
  static deserializeTables(data) {
1212
2440
  return (data.tables ?? []).map(cloneTable);
1213
2441
  }
1214
- static deserializeStrokes(data) {
2442
+ /**
2443
+ * `onSkip` (Phase 9) replaces an unconditional `console.error` — `core`
2444
+ * must never do raw console I/O (no dev-gate, no way for a Host to
2445
+ * suppress or redirect it), so a skipped stroke is now reported only if
2446
+ * the caller asks for it, via whatever diagnostic channel it already
2447
+ * has (e.g. `controller-internal.ts` routes this into the same typed
2448
+ * `"error"` event every other diagnostic already uses). Silent by
2449
+ * default, matching how every other `deserialize*` method here already
2450
+ * behaves (no diagnostics at all).
2451
+ */
2452
+ static deserializeStrokes(data, onSkip) {
1215
2453
  const strokes = [];
1216
2454
  for (const s of data.strokes) try {
1217
2455
  strokes.push({
@@ -1225,11 +2463,79 @@ var BoardDocument = class {
1225
2463
  ...serializeLock(s)
1226
2464
  });
1227
2465
  } catch (err) {
1228
- console.error(`Skipping malformed stroke ${s?.id ?? "(no id)"}:`, err);
2466
+ onSkip?.(s?.id ?? "(no id)", err);
1229
2467
  }
1230
2468
  return strokes;
1231
2469
  }
2470
+ /**
2471
+ * Every mutation funnels through here, so paint-order tracking lives in
2472
+ * exactly one place rather than at every individual add/remove call site
2473
+ * (18 of them, times `replaceAll`/`applyRemoteChange`) — new ids are
2474
+ * appended to the back (front-most) of `objectOrder`, removed ids are
2475
+ * spliced out. An explicit reorder (`reorder`/`setOrder`) updates
2476
+ * `objectOrder` itself before calling this, so this step is a no-op for
2477
+ * ids already tracked (idempotent by construction: `orderIndex.has` gates
2478
+ * every append).
2479
+ *
2480
+ * `orderChanged` (Phase 9) reports exactly the ids whose rank actually
2481
+ * changed, using `orderIndex` throughout instead of `indexOf` — a pure
2482
+ * append never shifts any existing id's rank (new ids land at the tail,
2483
+ * already covered by this same change's own `Added` field, so
2484
+ * `orderChanged` stays unset), while a removal shifts every id at-or-
2485
+ * after the lowest removed rank down by one, computed in a single O(n)
2486
+ * filter pass (not one `indexOf`+`splice` per removed id) regardless of
2487
+ * how many ids this one change removes. Every renderer's `onChange` now
2488
+ * looks up only the ids actually in `orderChanged` instead of walking
2489
+ * its entire mesh map on any order-touching change — a broad, unfiltered
2490
+ * `orderChanged` here would silently defeat that fix, not just waste
2491
+ * cycles here.
2492
+ */
1232
2493
  emit(change) {
2494
+ const targetOf = (objectId) => this.connectorTarget(objectId);
2495
+ for (const arrow of this.arrows.values()) {
2496
+ if (!arrow.startBinding && !arrow.endBinding) continue;
2497
+ const { start, end } = resolveConnectorEnds(arrow, targetOf);
2498
+ if (start.x === arrow.start.x && start.y === arrow.start.y && end.x === arrow.end.x && end.y === arrow.end.y) continue;
2499
+ const next = {
2500
+ ...arrow,
2501
+ start,
2502
+ end
2503
+ };
2504
+ this.arrows.set(arrow.id, next);
2505
+ if (change.arrowsAdded?.some((a) => a.id === arrow.id)) change.arrowsAdded = change.arrowsAdded.map((a) => a.id === arrow.id ? next : a);
2506
+ else change.arrowsUpdated = [...(change.arrowsUpdated ?? []).filter((a) => a.id !== arrow.id), next];
2507
+ }
2508
+ const newIds = [];
2509
+ for (const field of ORDERED_ADDED_FIELDS) {
2510
+ const objects = change[field];
2511
+ if (!objects) continue;
2512
+ for (const object of objects) if (!this.orderIndex.has(object.id)) newIds.push(object.id);
2513
+ }
2514
+ const removedIds = /* @__PURE__ */ new Set();
2515
+ for (const field of ORDERED_REMOVED_FIELDS) {
2516
+ const ids = change[field];
2517
+ if (!ids) continue;
2518
+ for (const id of ids) if (this.orderIndex.has(id)) removedIds.add(id);
2519
+ }
2520
+ let orderChanged;
2521
+ if (removedIds.size > 0) {
2522
+ let minIndex = this.objectOrder.length;
2523
+ for (const id of removedIds) minIndex = Math.min(minIndex, this.orderIndex.get(id));
2524
+ const next = [];
2525
+ for (const id of this.objectOrder) if (!removedIds.has(id)) next.push(id);
2526
+ this.objectOrder = next;
2527
+ for (let k = minIndex; k < this.objectOrder.length; k++) this.orderIndex.set(this.objectOrder[k], k);
2528
+ for (const id of removedIds) this.orderIndex.delete(id);
2529
+ orderChanged = this.objectOrder.slice(minIndex);
2530
+ }
2531
+ if (newIds.length > 0) for (const id of newIds) {
2532
+ this.orderIndex.set(id, this.objectOrder.length);
2533
+ this.objectOrder.push(id);
2534
+ }
2535
+ if (orderChanged) change = {
2536
+ ...change,
2537
+ orderChanged
2538
+ };
1233
2539
  const full = {
1234
2540
  ...EMPTY_CHANGE,
1235
2541
  ...change
@@ -1315,7 +2621,7 @@ function computeBBox(stroke) {
1315
2621
  return world;
1316
2622
  }
1317
2623
  //#endregion
1318
- //#region src/core-internal/commands.ts
2624
+ //#region src/core/commands/commands.ts
1319
2625
  /** One drawing action — a marker stroke, or a shape's strokes as one unit. */
1320
2626
  var AddStrokesCommand = class {
1321
2627
  constructor(strokes) {
@@ -1383,15 +2689,210 @@ var TransformCommand = class {
1383
2689
  if (changed.length) doc.transformStrokes(changed);
1384
2690
  }
1385
2691
  };
1386
- var AddNoteCommand = class {
1387
- constructor(note) {
1388
- this.label = "add note";
1389
- this.note = cloneNote(note);
1390
- }
1391
- apply(doc) {
1392
- doc.addNotes([cloneNote(this.note)]);
1393
- }
1394
- revert(doc) {
2692
+ /**
2693
+ * One move gesture over a mixed-type selection (Phase 3) — the general
2694
+ * successor to `TransformCommand` for translation. `TransformCommand`
2695
+ * itself stays as-is (still used for scale/rotate, which remain stroke-only
2696
+ * this phase — see `interaction/tools/selectTool.ts`'s `TransformState`):
2697
+ * its own per-id `doc.get(id)` check already no-ops safely for any id that
2698
+ * isn't a stroke, so it doesn't need touching for that narrower case.
2699
+ *
2700
+ * `delta` here is always a pure translation (never scale/rotate), so
2701
+ * `apply(delta, point)` is a safe, uniform way to move every position-only
2702
+ * type's `x`/`y` — translating commutes trivially regardless of a shape's
2703
+ * own rotation. Matrix-carrying types (stroke, Custom) instead compose
2704
+ * `delta` onto their existing matrix/transform, matching `TransformCommand`.
2705
+ *
2706
+ * A group id in `ids` is expanded into its (recursively resolved, cycle-
2707
+ * safe) children every time `compose` runs — deterministic, since group
2708
+ * membership never changes mid-command — so moving a selected group means
2709
+ * moving every one of its members by the same delta. This expansion is
2710
+ * `TransformObjectsCommand`'s own responsibility precisely so a caller that
2711
+ * doesn't itself expand groups (e.g. `ScrawlEngine.nudgeSelection`) still
2712
+ * gets correct behavior; `interaction/tools/selectTool.ts`'s `TransformState`
2713
+ * separately expands for its own reason (live per-child drag preview), which
2714
+ * makes this a no-op re-expansion for that caller, not a conflict.
2715
+ */
2716
+ var TransformObjectsCommand = class {
2717
+ constructor(ids, delta) {
2718
+ this.ids = ids;
2719
+ this.delta = delta;
2720
+ this.label = "move selection";
2721
+ this.inverse = invert(delta);
2722
+ }
2723
+ apply(doc) {
2724
+ this.compose(doc, this.delta);
2725
+ }
2726
+ revert(doc) {
2727
+ this.compose(doc, this.inverse);
2728
+ }
2729
+ compose(doc, m) {
2730
+ const resolved = /* @__PURE__ */ new Set();
2731
+ const expand = (id, seen) => {
2732
+ if (seen.has(id)) return;
2733
+ seen.add(id);
2734
+ const group = doc.getGroup(id);
2735
+ if (group) {
2736
+ for (const childId of group.children) expand(childId, seen);
2737
+ return;
2738
+ }
2739
+ resolved.add(id);
2740
+ };
2741
+ for (const id of this.ids) expand(id, /* @__PURE__ */ new Set());
2742
+ const changedStrokes = [];
2743
+ for (const id of resolved) {
2744
+ const stroke = doc.get(id);
2745
+ if (stroke) {
2746
+ stroke.matrix = mul(m, stroke.matrix ?? [
2747
+ 1,
2748
+ 0,
2749
+ 0,
2750
+ 1,
2751
+ 0,
2752
+ 0
2753
+ ]);
2754
+ changedStrokes.push(stroke);
2755
+ continue;
2756
+ }
2757
+ const custom = doc.getCustomObject(id);
2758
+ if (custom) {
2759
+ doc.setCustomObject({
2760
+ ...custom,
2761
+ transform: mul(m, custom.transform)
2762
+ });
2763
+ continue;
2764
+ }
2765
+ const note = doc.getNote(id);
2766
+ if (note) {
2767
+ const p = apply(m, note);
2768
+ doc.setNote({
2769
+ ...note,
2770
+ x: p.x,
2771
+ y: p.y
2772
+ });
2773
+ continue;
2774
+ }
2775
+ const text = doc.getText(id);
2776
+ if (text) {
2777
+ const p = apply(m, text);
2778
+ doc.setText({
2779
+ ...text,
2780
+ x: p.x,
2781
+ y: p.y
2782
+ });
2783
+ continue;
2784
+ }
2785
+ const table = doc.getTable(id);
2786
+ if (table) {
2787
+ const p = apply(m, table);
2788
+ doc.setTable({
2789
+ ...table,
2790
+ x: p.x,
2791
+ y: p.y
2792
+ });
2793
+ continue;
2794
+ }
2795
+ const image = doc.getImage(id);
2796
+ if (image) {
2797
+ const p = apply(m, image);
2798
+ doc.setImage({
2799
+ ...image,
2800
+ x: p.x,
2801
+ y: p.y
2802
+ });
2803
+ continue;
2804
+ }
2805
+ const timer = doc.getTimer(id);
2806
+ if (timer) {
2807
+ const p = apply(m, timer);
2808
+ doc.setTimer({
2809
+ ...timer,
2810
+ x: p.x,
2811
+ y: p.y
2812
+ });
2813
+ continue;
2814
+ }
2815
+ const rect = doc.getRectangle(id);
2816
+ if (rect) {
2817
+ const p = apply(m, rect);
2818
+ doc.setRectangle({
2819
+ ...rect,
2820
+ x: p.x,
2821
+ y: p.y
2822
+ });
2823
+ continue;
2824
+ }
2825
+ const ellipse = doc.getEllipse(id);
2826
+ if (ellipse) {
2827
+ const p = apply(m, ellipse);
2828
+ doc.setEllipse({
2829
+ ...ellipse,
2830
+ x: p.x,
2831
+ y: p.y
2832
+ });
2833
+ continue;
2834
+ }
2835
+ const line = doc.getLine(id);
2836
+ if (line) {
2837
+ doc.setLine({
2838
+ ...line,
2839
+ start: apply(m, line.start),
2840
+ end: apply(m, line.end)
2841
+ });
2842
+ continue;
2843
+ }
2844
+ const arrow = doc.getArrow(id);
2845
+ if (arrow) {
2846
+ doc.setArrow({
2847
+ ...arrow,
2848
+ start: apply(m, arrow.start),
2849
+ end: apply(m, arrow.end),
2850
+ ...arrow.waypoints ? { waypoints: arrow.waypoints.map((p) => apply(m, p)) } : {}
2851
+ });
2852
+ continue;
2853
+ }
2854
+ const polygon = doc.getPolygon(id);
2855
+ if (polygon) {
2856
+ const p = apply(m, polygon);
2857
+ doc.setPolygon({
2858
+ ...polygon,
2859
+ x: p.x,
2860
+ y: p.y
2861
+ });
2862
+ continue;
2863
+ }
2864
+ const star = doc.getStar(id);
2865
+ if (star) {
2866
+ const p = apply(m, star);
2867
+ doc.setStar({
2868
+ ...star,
2869
+ x: p.x,
2870
+ y: p.y
2871
+ });
2872
+ continue;
2873
+ }
2874
+ const heart = doc.getHeart(id);
2875
+ if (heart) {
2876
+ const p = apply(m, heart);
2877
+ doc.setHeart({
2878
+ ...heart,
2879
+ x: p.x,
2880
+ y: p.y
2881
+ });
2882
+ }
2883
+ }
2884
+ if (changedStrokes.length) doc.transformStrokes(changedStrokes);
2885
+ }
2886
+ };
2887
+ var AddNoteCommand = class {
2888
+ constructor(note) {
2889
+ this.label = "add note";
2890
+ this.note = cloneNote(note);
2891
+ }
2892
+ apply(doc) {
2893
+ doc.addNotes([cloneNote(this.note)]);
2894
+ }
2895
+ revert(doc) {
1395
2896
  doc.removeNotes([this.note.id]);
1396
2897
  }
1397
2898
  };
@@ -1584,6 +3085,342 @@ var DeleteTimerCommand = class {
1584
3085
  doc.addTimers([cloneTimer(this.timer)]);
1585
3086
  }
1586
3087
  };
3088
+ /** Add/update/delete for semantic Rectangle objects (Phase 2). */
3089
+ var AddRectangleCommand = class {
3090
+ constructor(rect) {
3091
+ this.label = "add rectangle";
3092
+ this.rect = cloneRectangle(rect);
3093
+ }
3094
+ apply(doc) {
3095
+ doc.addRectangles([cloneRectangle(this.rect)]);
3096
+ }
3097
+ revert(doc) {
3098
+ doc.removeRectangles([this.rect.id]);
3099
+ }
3100
+ };
3101
+ var UpdateRectangleCommand = class {
3102
+ constructor(before, after) {
3103
+ this.label = "update rectangle";
3104
+ this.before = cloneRectangle(before);
3105
+ this.after = cloneRectangle(after);
3106
+ }
3107
+ apply(doc) {
3108
+ doc.setRectangle(cloneRectangle(this.after));
3109
+ }
3110
+ revert(doc) {
3111
+ doc.setRectangle(cloneRectangle(this.before));
3112
+ }
3113
+ };
3114
+ var DeleteRectangleCommand = class {
3115
+ constructor(rect) {
3116
+ this.label = "delete rectangle";
3117
+ this.rect = cloneRectangle(rect);
3118
+ }
3119
+ apply(doc) {
3120
+ doc.removeRectangles([this.rect.id]);
3121
+ }
3122
+ revert(doc) {
3123
+ doc.addRectangles([cloneRectangle(this.rect)]);
3124
+ }
3125
+ };
3126
+ /** Add/update/delete for semantic Ellipse objects (Phase 2). */
3127
+ var AddEllipseCommand = class {
3128
+ constructor(ellipse) {
3129
+ this.label = "add ellipse";
3130
+ this.ellipse = cloneEllipse(ellipse);
3131
+ }
3132
+ apply(doc) {
3133
+ doc.addEllipses([cloneEllipse(this.ellipse)]);
3134
+ }
3135
+ revert(doc) {
3136
+ doc.removeEllipses([this.ellipse.id]);
3137
+ }
3138
+ };
3139
+ var UpdateEllipseCommand = class {
3140
+ constructor(before, after) {
3141
+ this.label = "update ellipse";
3142
+ this.before = cloneEllipse(before);
3143
+ this.after = cloneEllipse(after);
3144
+ }
3145
+ apply(doc) {
3146
+ doc.setEllipse(cloneEllipse(this.after));
3147
+ }
3148
+ revert(doc) {
3149
+ doc.setEllipse(cloneEllipse(this.before));
3150
+ }
3151
+ };
3152
+ var DeleteEllipseCommand = class {
3153
+ constructor(ellipse) {
3154
+ this.label = "delete ellipse";
3155
+ this.ellipse = cloneEllipse(ellipse);
3156
+ }
3157
+ apply(doc) {
3158
+ doc.removeEllipses([this.ellipse.id]);
3159
+ }
3160
+ revert(doc) {
3161
+ doc.addEllipses([cloneEllipse(this.ellipse)]);
3162
+ }
3163
+ };
3164
+ /** Add/update/delete for semantic Line objects (Phase 4). */
3165
+ var AddLineCommand = class {
3166
+ constructor(line) {
3167
+ this.label = "add line";
3168
+ this.line = cloneLine(line);
3169
+ }
3170
+ apply(doc) {
3171
+ doc.addLines([cloneLine(this.line)]);
3172
+ }
3173
+ revert(doc) {
3174
+ doc.removeLines([this.line.id]);
3175
+ }
3176
+ };
3177
+ var UpdateLineCommand = class {
3178
+ constructor(before, after) {
3179
+ this.label = "update line";
3180
+ this.before = cloneLine(before);
3181
+ this.after = cloneLine(after);
3182
+ }
3183
+ apply(doc) {
3184
+ doc.setLine(cloneLine(this.after));
3185
+ }
3186
+ revert(doc) {
3187
+ doc.setLine(cloneLine(this.before));
3188
+ }
3189
+ };
3190
+ var DeleteLineCommand = class {
3191
+ constructor(line) {
3192
+ this.label = "delete line";
3193
+ this.line = cloneLine(line);
3194
+ }
3195
+ apply(doc) {
3196
+ doc.removeLines([this.line.id]);
3197
+ }
3198
+ revert(doc) {
3199
+ doc.addLines([cloneLine(this.line)]);
3200
+ }
3201
+ };
3202
+ /** Add/update/delete for semantic Arrow objects (Phase 4). */
3203
+ var AddArrowCommand = class {
3204
+ constructor(arrow) {
3205
+ this.label = "add arrow";
3206
+ this.arrow = cloneArrow(arrow);
3207
+ }
3208
+ apply(doc) {
3209
+ doc.addArrows([cloneArrow(this.arrow)]);
3210
+ }
3211
+ revert(doc) {
3212
+ doc.removeArrows([this.arrow.id]);
3213
+ }
3214
+ };
3215
+ var UpdateArrowCommand = class {
3216
+ constructor(before, after) {
3217
+ this.label = "update arrow";
3218
+ this.before = cloneArrow(before);
3219
+ this.after = cloneArrow(after);
3220
+ }
3221
+ apply(doc) {
3222
+ doc.setArrow(cloneArrow(this.after));
3223
+ }
3224
+ revert(doc) {
3225
+ doc.setArrow(cloneArrow(this.before));
3226
+ }
3227
+ };
3228
+ var DeleteArrowCommand = class {
3229
+ constructor(arrow) {
3230
+ this.label = "delete arrow";
3231
+ this.arrow = cloneArrow(arrow);
3232
+ }
3233
+ apply(doc) {
3234
+ doc.removeArrows([this.arrow.id]);
3235
+ }
3236
+ revert(doc) {
3237
+ doc.addArrows([cloneArrow(this.arrow)]);
3238
+ }
3239
+ };
3240
+ /** Add/update/delete for semantic Polygon objects (Phase 4) — Triangle/Diamond/Pentagon/Hexagon/Octagon. */
3241
+ var AddPolygonCommand = class {
3242
+ constructor(polygon) {
3243
+ this.label = "add polygon";
3244
+ this.polygon = clonePolygon(polygon);
3245
+ }
3246
+ apply(doc) {
3247
+ doc.addPolygons([clonePolygon(this.polygon)]);
3248
+ }
3249
+ revert(doc) {
3250
+ doc.removePolygons([this.polygon.id]);
3251
+ }
3252
+ };
3253
+ var UpdatePolygonCommand = class {
3254
+ constructor(before, after) {
3255
+ this.label = "update polygon";
3256
+ this.before = clonePolygon(before);
3257
+ this.after = clonePolygon(after);
3258
+ }
3259
+ apply(doc) {
3260
+ doc.setPolygon(clonePolygon(this.after));
3261
+ }
3262
+ revert(doc) {
3263
+ doc.setPolygon(clonePolygon(this.before));
3264
+ }
3265
+ };
3266
+ var DeletePolygonCommand = class {
3267
+ constructor(polygon) {
3268
+ this.label = "delete polygon";
3269
+ this.polygon = clonePolygon(polygon);
3270
+ }
3271
+ apply(doc) {
3272
+ doc.removePolygons([this.polygon.id]);
3273
+ }
3274
+ revert(doc) {
3275
+ doc.addPolygons([clonePolygon(this.polygon)]);
3276
+ }
3277
+ };
3278
+ /** Add/update/delete for semantic Star objects (Phase 4). */
3279
+ var AddStarCommand = class {
3280
+ constructor(star) {
3281
+ this.label = "add star";
3282
+ this.star = cloneStar(star);
3283
+ }
3284
+ apply(doc) {
3285
+ doc.addStars([cloneStar(this.star)]);
3286
+ }
3287
+ revert(doc) {
3288
+ doc.removeStars([this.star.id]);
3289
+ }
3290
+ };
3291
+ var UpdateStarCommand = class {
3292
+ constructor(before, after) {
3293
+ this.label = "update star";
3294
+ this.before = cloneStar(before);
3295
+ this.after = cloneStar(after);
3296
+ }
3297
+ apply(doc) {
3298
+ doc.setStar(cloneStar(this.after));
3299
+ }
3300
+ revert(doc) {
3301
+ doc.setStar(cloneStar(this.before));
3302
+ }
3303
+ };
3304
+ var DeleteStarCommand = class {
3305
+ constructor(star) {
3306
+ this.label = "delete star";
3307
+ this.star = cloneStar(star);
3308
+ }
3309
+ apply(doc) {
3310
+ doc.removeStars([this.star.id]);
3311
+ }
3312
+ revert(doc) {
3313
+ doc.addStars([cloneStar(this.star)]);
3314
+ }
3315
+ };
3316
+ /** Add/update/delete for semantic Heart objects (Phase 4). */
3317
+ var AddHeartCommand = class {
3318
+ constructor(heart) {
3319
+ this.label = "add heart";
3320
+ this.heart = cloneHeart(heart);
3321
+ }
3322
+ apply(doc) {
3323
+ doc.addHearts([cloneHeart(this.heart)]);
3324
+ }
3325
+ revert(doc) {
3326
+ doc.removeHearts([this.heart.id]);
3327
+ }
3328
+ };
3329
+ var UpdateHeartCommand = class {
3330
+ constructor(before, after) {
3331
+ this.label = "update heart";
3332
+ this.before = cloneHeart(before);
3333
+ this.after = cloneHeart(after);
3334
+ }
3335
+ apply(doc) {
3336
+ doc.setHeart(cloneHeart(this.after));
3337
+ }
3338
+ revert(doc) {
3339
+ doc.setHeart(cloneHeart(this.before));
3340
+ }
3341
+ };
3342
+ var DeleteHeartCommand = class {
3343
+ constructor(heart) {
3344
+ this.label = "delete heart";
3345
+ this.heart = cloneHeart(heart);
3346
+ }
3347
+ apply(doc) {
3348
+ doc.removeHearts([this.heart.id]);
3349
+ }
3350
+ revert(doc) {
3351
+ doc.addHearts([cloneHeart(this.heart)]);
3352
+ }
3353
+ };
3354
+ /** Add/update/delete for Groups (Phase 3). */
3355
+ var AddGroupCommand = class {
3356
+ constructor(group) {
3357
+ this.label = "group";
3358
+ this.group = cloneGroup(group);
3359
+ }
3360
+ apply(doc) {
3361
+ doc.addGroups([cloneGroup(this.group)]);
3362
+ }
3363
+ revert(doc) {
3364
+ doc.removeGroups([this.group.id]);
3365
+ }
3366
+ };
3367
+ var UpdateGroupCommand = class {
3368
+ constructor(before, after) {
3369
+ this.label = "update group";
3370
+ this.before = cloneGroup(before);
3371
+ this.after = cloneGroup(after);
3372
+ }
3373
+ apply(doc) {
3374
+ doc.setGroup(cloneGroup(this.after));
3375
+ }
3376
+ revert(doc) {
3377
+ doc.setGroup(cloneGroup(this.before));
3378
+ }
3379
+ };
3380
+ var DeleteGroupCommand = class {
3381
+ constructor(group) {
3382
+ this.label = "ungroup";
3383
+ this.group = cloneGroup(group);
3384
+ }
3385
+ apply(doc) {
3386
+ doc.removeGroups([this.group.id]);
3387
+ }
3388
+ revert(doc) {
3389
+ doc.addGroups([cloneGroup(this.group)]);
3390
+ }
3391
+ };
3392
+ var REORDER_LABELS = {
3393
+ forward: "bring forward",
3394
+ backward: "send backward",
3395
+ front: "bring to front",
3396
+ back: "send to back"
3397
+ };
3398
+ /**
3399
+ * Bring-forward / send-backward / bring-to-front / send-to-back (Phase 3) —
3400
+ * one command family covering all four directions rather than four
3401
+ * near-identical classes, since the only difference between them is which
3402
+ * `BoardDocument.reorder` direction to replay. Captures the full paint order
3403
+ * on first `apply` rather than in the constructor — `BoardDocument.order()`
3404
+ * needs the doc, which a `Command` only ever receives via `apply`/`revert` —
3405
+ * so `revert` can restore it exactly; redo re-runs the same `reorder` call,
3406
+ * which is deterministic because `revert` always restores the identical
3407
+ * starting order first.
3408
+ */
3409
+ var ReorderObjectCommand = class {
3410
+ constructor(id, direction) {
3411
+ this.id = id;
3412
+ this.direction = direction;
3413
+ this.before = null;
3414
+ this.label = REORDER_LABELS[direction];
3415
+ }
3416
+ apply(doc) {
3417
+ this.before ??= [...doc.order()];
3418
+ doc.reorder(this.id, this.direction);
3419
+ }
3420
+ revert(doc) {
3421
+ if (this.before) doc.applyRemoteChange({ orderChanged: this.before });
3422
+ }
3423
+ };
1587
3424
  var LockItemsCommand = class {
1588
3425
  constructor(targets, targetState, actor) {
1589
3426
  this.label = "toggle lock";
@@ -1611,16 +3448,63 @@ var LockItemsCommand = class {
1611
3448
  else if (type === "table") doc.setTableLocked(id, locked, by);
1612
3449
  else if (type === "image") doc.setImageLocked(id, locked, by);
1613
3450
  else if (type === "timer") doc.setTimerLocked(id, locked, by);
3451
+ else if (type === "rectangle") doc.setRectangleLocked(id, locked, by);
3452
+ else if (type === "ellipse") doc.setEllipseLocked(id, locked, by);
3453
+ else if (type === "group") doc.setGroupLocked(id, locked, by);
3454
+ else if (type === "line") doc.setLineLocked(id, locked, by);
3455
+ else if (type === "arrow") doc.setArrowLocked(id, locked, by);
3456
+ else if (type === "polygon") doc.setPolygonLocked(id, locked, by);
3457
+ else if (type === "star") doc.setStarLocked(id, locked, by);
3458
+ else if (type === "heart") doc.setHeartLocked(id, locked, by);
1614
3459
  }
1615
3460
  };
1616
3461
  //#endregion
1617
- //#region src/core-internal/history.ts
3462
+ //#region src/core/history/history.ts
3463
+ /**
3464
+ * Several commands applied/reverted together as one undo step (Phase 3
3465
+ * consolidation — this exact class used to be hand-duplicated as a private
3466
+ * `CommandBatch` in `controller-internal.ts` and an exported
3467
+ * `ExtensionCommandBatch` in `interaction/tools/customTool.ts`; both now
3468
+ * import this one instead). Revert runs in reverse order, so a batch that
3469
+ * depends on ordering (e.g. add-then-reference) undoes cleanly.
3470
+ */
3471
+ var CommandBatch = class {
3472
+ constructor(label, commands) {
3473
+ this.label = label;
3474
+ this.commands = commands;
3475
+ }
3476
+ apply(doc) {
3477
+ for (const command of this.commands) command.apply(doc);
3478
+ }
3479
+ revert(doc) {
3480
+ for (const command of [...this.commands].reverse()) command.revert(doc);
3481
+ }
3482
+ };
1618
3483
  var LIMIT = 200;
1619
3484
  var History = class {
3485
+ /** Switch independent page histories while retaining the same document instance. */
3486
+ setScope(id) {
3487
+ if (id === this.scope) return;
3488
+ this.scopes.set(this.scope, {
3489
+ undo: [...this.undoStack],
3490
+ redo: [...this.redoStack]
3491
+ });
3492
+ const next = this.scopes.get(id);
3493
+ this.undoStack.splice(0, this.undoStack.length, ...next?.undo ?? []);
3494
+ this.redoStack.splice(0, this.redoStack.length, ...next?.redo ?? []);
3495
+ this.scope = id;
3496
+ }
3497
+ /** Discard all page histories after replacing a saved document. */
3498
+ clearScopes() {
3499
+ this.scopes.clear();
3500
+ this.clear();
3501
+ }
1620
3502
  constructor(doc) {
1621
3503
  this.doc = doc;
1622
3504
  this.undoStack = [];
1623
3505
  this.redoStack = [];
3506
+ this.scope = "default";
3507
+ this.scopes = /* @__PURE__ */ new Map();
1624
3508
  this.onCommand = null;
1625
3509
  this.undoing = false;
1626
3510
  }
@@ -1675,7 +3559,7 @@ var History = class {
1675
3559
  }
1676
3560
  };
1677
3561
  //#endregion
1678
- //#region src/core-internal/ops.ts
3562
+ //#region src/core/events/ops.ts
1679
3563
  var ADDED = [
1680
3564
  ["added", "strokes"],
1681
3565
  ["notesAdded", "notes"],
@@ -1683,6 +3567,14 @@ var ADDED = [
1683
3567
  ["tablesAdded", "tables"],
1684
3568
  ["imagesAdded", "images"],
1685
3569
  ["timersAdded", "timers"],
3570
+ ["rectanglesAdded", "rectangles"],
3571
+ ["ellipsesAdded", "ellipses"],
3572
+ ["groupsAdded", "groups"],
3573
+ ["linesAdded", "lines"],
3574
+ ["arrowsAdded", "arrows"],
3575
+ ["polygonsAdded", "polygons"],
3576
+ ["starsAdded", "stars"],
3577
+ ["heartsAdded", "hearts"],
1686
3578
  ["customObjectsAdded", "customObjects"]
1687
3579
  ];
1688
3580
  var UPDATED = [
@@ -1693,6 +3585,14 @@ var UPDATED = [
1693
3585
  ["tablesUpdated", "tables"],
1694
3586
  ["imagesUpdated", "images"],
1695
3587
  ["timersUpdated", "timers"],
3588
+ ["rectanglesUpdated", "rectangles"],
3589
+ ["ellipsesUpdated", "ellipses"],
3590
+ ["groupsUpdated", "groups"],
3591
+ ["linesUpdated", "lines"],
3592
+ ["arrowsUpdated", "arrows"],
3593
+ ["polygonsUpdated", "polygons"],
3594
+ ["starsUpdated", "stars"],
3595
+ ["heartsUpdated", "hearts"],
1696
3596
  ["customObjectsUpdated", "customObjects"]
1697
3597
  ];
1698
3598
  var REMOVED = [
@@ -1702,6 +3602,14 @@ var REMOVED = [
1702
3602
  ["tablesRemoved", "tables"],
1703
3603
  ["imagesRemoved", "images"],
1704
3604
  ["timersRemoved", "timers"],
3605
+ ["rectanglesRemoved", "rectangles"],
3606
+ ["ellipsesRemoved", "ellipses"],
3607
+ ["groupsRemoved", "groups"],
3608
+ ["linesRemoved", "lines"],
3609
+ ["arrowsRemoved", "arrows"],
3610
+ ["polygonsRemoved", "polygons"],
3611
+ ["starsRemoved", "stars"],
3612
+ ["heartsRemoved", "hearts"],
1705
3613
  ["customObjectsRemoved", "customObjects"]
1706
3614
  ];
1707
3615
  function toWireObject(collection, object) {
@@ -1729,10 +3637,18 @@ function changeToOps(change, restoring = false) {
1729
3637
  return ops;
1730
3638
  }
1731
3639
  //#endregion
1732
- //#region src/core-internal/ribbonEdges.ts
3640
+ //#region src/core/shapes/ribbonEdges.ts
1733
3641
  var MIN_WIDTH_FACTOR = .35;
1734
3642
  var END_TAPER = .55;
1735
- function ribbonEdges(points, baseWidth) {
3643
+ /**
3644
+ * @param handDrawn Default `true`: ink's organic feel — width follows
3645
+ * per-point pressure, and both ends taper. Pass `false` for a geometric
3646
+ * shape outline, which has no real pressure signal and isn't a pen stroke
3647
+ * with a lift-off: it renders at the exact `baseWidth`, uniformly, with no
3648
+ * taper at its start/end (which, for a closed shape, is the same point —
3649
+ * tapering it would visibly pinch just that one corner).
3650
+ */
3651
+ function ribbonEdges(points, baseWidth, handDrawn = true) {
1736
3652
  const pts = points.length === 1 ? [points[0], {
1737
3653
  ...points[0],
1738
3654
  x: points[0].x + baseWidth * .05
@@ -1747,9 +3663,29 @@ function ribbonEdges(points, baseWidth) {
1747
3663
  const len = Math.hypot(dx, dy) || 1;
1748
3664
  dx /= len;
1749
3665
  dy /= len;
1750
- let width = baseWidth * (MIN_WIDTH_FACTOR + (1 - MIN_WIDTH_FACTOR) * pts[i].pressure);
1751
- if (i === 0 || i === n - 1) width *= END_TAPER;
1752
- const hw = width / 2;
3666
+ let width = handDrawn ? baseWidth * (MIN_WIDTH_FACTOR + (1 - MIN_WIDTH_FACTOR) * pts[i].pressure) : baseWidth;
3667
+ if (handDrawn && (i === 0 || i === n - 1)) width *= END_TAPER;
3668
+ let miterScale = 1;
3669
+ if (!handDrawn && n > 2) {
3670
+ const closed = pts[0].x === pts[n - 1].x && pts[0].y === pts[n - 1].y;
3671
+ const before = pts[i === 0 && closed ? n - 2 : Math.max(0, i - 1)];
3672
+ const after = pts[i === n - 1 && closed ? 1 : Math.min(n - 1, i + 1)];
3673
+ const incomingLength = Math.hypot(pts[i].x - before.x, pts[i].y - before.y);
3674
+ const outgoingLength = Math.hypot(after.x - pts[i].x, after.y - pts[i].y);
3675
+ if (incomingLength > 0 && outgoingLength > 0) {
3676
+ const ax = (pts[i].x - before.x) / incomingLength;
3677
+ const ay = (pts[i].y - before.y) / incomingLength;
3678
+ const bx = (after.x - pts[i].x) / outgoingLength;
3679
+ const by = (after.y - pts[i].y) / outgoingLength;
3680
+ const length = Math.hypot(ax + bx, ay + by);
3681
+ if (length > 1e-4) {
3682
+ dx = (ax + bx) / length;
3683
+ dy = (ay + by) / length;
3684
+ miterScale = Math.min(4, 1 / Math.max(.25, dx * bx + dy * by));
3685
+ }
3686
+ }
3687
+ }
3688
+ const hw = width / 2 * miterScale;
1753
3689
  out[i] = {
1754
3690
  lx: pts[i].x - dy * hw,
1755
3691
  ly: pts[i].y + dx * hw,
@@ -1761,24 +3697,76 @@ function ribbonEdges(points, baseWidth) {
1761
3697
  return out;
1762
3698
  }
1763
3699
  //#endregion
1764
- //#region src/core-internal/spatialIndex.ts
3700
+ //#region src/core/document/spatialIndex.ts
1765
3701
  var CELL = 8;
3702
+ /**
3703
+ * Non-stroke types have no separate "transformed" event the way strokes
3704
+ * do — any content replace (`setNote`/`setTable`/...) goes through the
3705
+ * type's single `*Updated` field regardless of whether it moved, so a
3706
+ * reindex on every update is unconditional (cheap arithmetic per object,
3707
+ * matching `objectBounds.ts`'s own "no caching needed" precedent for these
3708
+ * types — not worth distinguishing "moved" from "just retexted").
3709
+ */
3710
+ var ADDED_FIELDS = [
3711
+ "added",
3712
+ "notesAdded",
3713
+ "textAdded",
3714
+ "tablesAdded",
3715
+ "imagesAdded",
3716
+ "timersAdded",
3717
+ "customObjectsAdded",
3718
+ "rectanglesAdded",
3719
+ "ellipsesAdded"
3720
+ ];
3721
+ var REMOVED_FIELDS = [
3722
+ "removed",
3723
+ "notesRemoved",
3724
+ "textRemoved",
3725
+ "tablesRemoved",
3726
+ "imagesRemoved",
3727
+ "timersRemoved",
3728
+ "customObjectsRemoved",
3729
+ "rectanglesRemoved",
3730
+ "ellipsesRemoved"
3731
+ ];
3732
+ var UPDATED_FIELDS = [
3733
+ "notesUpdated",
3734
+ "textUpdated",
3735
+ "tablesUpdated",
3736
+ "imagesUpdated",
3737
+ "timersUpdated",
3738
+ "customObjectsUpdated",
3739
+ "rectanglesUpdated",
3740
+ "ellipsesUpdated"
3741
+ ];
1766
3742
  var SpatialIndex = class {
1767
3743
  constructor(doc) {
1768
3744
  this.doc = doc;
1769
3745
  this.cells = /* @__PURE__ */ new Map();
1770
- this.strokeCells = /* @__PURE__ */ new Map();
3746
+ this.objectCells = /* @__PURE__ */ new Map();
1771
3747
  this.unsubscribe = doc.subscribe((change) => {
1772
- for (const id of change.removed) this.remove(id);
1773
- for (const stroke of change.added) this.insert(stroke.id);
3748
+ for (const field of REMOVED_FIELDS) for (const id of change[field]) this.remove(id);
3749
+ for (const field of ADDED_FIELDS) for (const object of change[field]) this.insert(object.id);
1774
3750
  for (const stroke of change.transformed) {
1775
3751
  this.remove(stroke.id);
1776
3752
  this.insert(stroke.id);
1777
3753
  }
3754
+ for (const field of UPDATED_FIELDS) for (const object of change[field]) {
3755
+ this.remove(object.id);
3756
+ this.insert(object.id);
3757
+ }
1778
3758
  });
1779
3759
  for (const stroke of doc.all()) this.insert(stroke.id);
1780
- }
1781
- /** Ids of strokes whose bbox may overlap the query rect. */
3760
+ for (const note of doc.allNotes()) this.insert(note.id);
3761
+ for (const text of doc.allTexts()) this.insert(text.id);
3762
+ for (const table of doc.allTables()) this.insert(table.id);
3763
+ for (const image of doc.allImages()) this.insert(image.id);
3764
+ for (const timer of doc.allTimers()) this.insert(timer.id);
3765
+ for (const object of doc.allCustomObjects()) this.insert(object.id);
3766
+ for (const rect of doc.allRectangles()) this.insert(rect.id);
3767
+ for (const ellipse of doc.allEllipses()) this.insert(ellipse.id);
3768
+ }
3769
+ /** Ids of content objects (any type except groups) whose bbox may overlap the query rect. */
1782
3770
  query(minX, minY, maxX, maxY) {
1783
3771
  const result = /* @__PURE__ */ new Set();
1784
3772
  for (const key of cellsOf({
@@ -1795,13 +3783,13 @@ var SpatialIndex = class {
1795
3783
  dispose() {
1796
3784
  this.unsubscribe();
1797
3785
  this.cells.clear();
1798
- this.strokeCells.clear();
3786
+ this.objectCells.clear();
1799
3787
  }
1800
3788
  insert(id) {
1801
3789
  const box = this.doc.bbox(id);
1802
3790
  if (!box) return;
1803
3791
  const keys = cellsOf(box);
1804
- this.strokeCells.set(id, keys);
3792
+ this.objectCells.set(id, keys);
1805
3793
  for (const key of keys) {
1806
3794
  let bucket = this.cells.get(key);
1807
3795
  if (!bucket) {
@@ -1812,9 +3800,9 @@ var SpatialIndex = class {
1812
3800
  }
1813
3801
  }
1814
3802
  remove(id) {
1815
- const keys = this.strokeCells.get(id);
3803
+ const keys = this.objectCells.get(id);
1816
3804
  if (!keys) return;
1817
- this.strokeCells.delete(id);
3805
+ this.objectCells.delete(id);
1818
3806
  for (const key of keys) {
1819
3807
  const bucket = this.cells.get(key);
1820
3808
  if (bucket) {
@@ -1834,7 +3822,7 @@ function cellsOf(box) {
1834
3822
  return keys;
1835
3823
  }
1836
3824
  //#endregion
1837
- //#region src/core-internal/stamps.ts
3825
+ //#region src/core/shapes/stamps.ts
1838
3826
  var STAMP_SIZE = 6;
1839
3827
  var STAMPS = [
1840
3828
  {
@@ -1883,7 +3871,7 @@ function isStampKind(value) {
1883
3871
  return typeof value === "string" && STAMPS.some((s) => s.kind === value);
1884
3872
  }
1885
3873
  //#endregion
1886
- //#region src/core-internal/presence.ts
3874
+ //#region src/collaboration/presence/presence.ts
1887
3875
  /** Keep beacons clear of typical chrome at the top and bottom of the Board. */
1888
3876
  var BEACON_INSET = {
1889
3877
  top: 72,
@@ -1919,7 +3907,766 @@ function clamp(value, min, max) {
1919
3907
  return Math.min(max, Math.max(min, value));
1920
3908
  }
1921
3909
  //#endregion
1922
- //#region src/core-internal/svg.ts
3910
+ //#region src/core/shapes/iconLibrary.ts
3911
+ var ICON_LIBRARY = [
3912
+ {
3913
+ name: "lucide:check",
3914
+ label: "Check",
3915
+ category: "Status",
3916
+ body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M20 6L9 17l-5-5\"/>"
3917
+ },
3918
+ {
3919
+ name: "lucide:x",
3920
+ label: "Cross",
3921
+ category: "Status",
3922
+ body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/>"
3923
+ },
3924
+ {
3925
+ name: "lucide:star",
3926
+ label: "Star",
3927
+ category: "Status",
3928
+ body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.12 2.12 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.12 2.12 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.12 2.12 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.12 2.12 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.12 2.12 0 0 0 1.597-1.16z\"/>"
3929
+ },
3930
+ {
3931
+ name: "lucide:heart",
3932
+ label: "Heart",
3933
+ category: "Status",
3934
+ body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M2 9.5a5.5 5.5 0 0 1 9.591-3.676a.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5\"/>"
3935
+ },
3936
+ {
3937
+ name: "lucide:flag",
3938
+ label: "Flag",
3939
+ category: "Status",
3940
+ body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 22V4a1 1 0 0 1 .4-.8A6 6 0 0 1 8 2c3 0 5 2 7.333 2q2 0 3.067-.8A1 1 0 0 1 20 4v10a1 1 0 0 1-.4.8A6 6 0 0 1 16 16c-3 0-5-2-8-2a6 6 0 0 0-4 1.528\"/>"
3941
+ },
3942
+ {
3943
+ name: "lucide:bookmark",
3944
+ label: "Bookmark",
3945
+ category: "Status",
3946
+ body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M17 3a2 2 0 0 1 2 2v15a1 1 0 0 1-1.496.868l-4.512-2.578a2 2 0 0 0-1.984 0l-4.512 2.578A1 1 0 0 1 5 20V5a2 2 0 0 1 2-2z\"/>"
3947
+ },
3948
+ {
3949
+ name: "lucide:bell",
3950
+ label: "Bell",
3951
+ category: "Status",
3952
+ body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10.268 21a2 2 0 0 0 3.464 0m-10.47-5.674A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326\"/>"
3953
+ },
3954
+ {
3955
+ name: "lucide:info",
3956
+ label: "Info",
3957
+ category: "Status",
3958
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4m0-4h.01\"/></g>"
3959
+ },
3960
+ {
3961
+ name: "lucide:circle-alert",
3962
+ label: "Alert",
3963
+ category: "Status",
3964
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 8v4m0 4h.01\"/></g>"
3965
+ },
3966
+ {
3967
+ name: "lucide:circle-question-mark",
3968
+ label: "Question",
3969
+ category: "Status",
3970
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3m.08 4h.01\"/></g>"
3971
+ },
3972
+ {
3973
+ name: "lucide:thumbs-up",
3974
+ label: "Thumbs up",
3975
+ category: "Status",
3976
+ body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15 5.88L14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88M7 10v12\"/>"
3977
+ },
3978
+ {
3979
+ name: "lucide:thumbs-down",
3980
+ label: "Thumbs down",
3981
+ category: "Status",
3982
+ body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 18.12L10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88M17 14V2\"/>"
3983
+ },
3984
+ {
3985
+ name: "lucide:arrow-up",
3986
+ label: "Arrow up",
3987
+ category: "Arrows",
3988
+ body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m5 12l7-7l7 7m-7 7V5\"/>"
3989
+ },
3990
+ {
3991
+ name: "lucide:arrow-down",
3992
+ label: "Arrow down",
3993
+ category: "Arrows",
3994
+ body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 5v14m7-7l-7 7l-7-7\"/>"
3995
+ },
3996
+ {
3997
+ name: "lucide:arrow-left",
3998
+ label: "Arrow left",
3999
+ category: "Arrows",
4000
+ body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m12 19l-7-7l7-7m7 7H5\"/>"
4001
+ },
4002
+ {
4003
+ name: "lucide:arrow-right",
4004
+ label: "Arrow right",
4005
+ category: "Arrows",
4006
+ body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 12h14m-7-7l7 7l-7 7\"/>"
4007
+ },
4008
+ {
4009
+ name: "lucide:arrow-up-right",
4010
+ label: "Arrow up-right",
4011
+ category: "Arrows",
4012
+ body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M7 7h10v10M7 17L17 7\"/>"
4013
+ },
4014
+ {
4015
+ name: "lucide:refresh-cw",
4016
+ label: "Refresh",
4017
+ category: "Arrows",
4018
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M3 12a9 9 0 0 1 9-9a9.75 9.75 0 0 1 6.74 2.74L21 8\"/><path d=\"M21 3v5h-5m5 4a9 9 0 0 1-9 9a9.75 9.75 0 0 1-6.74-2.74L3 16\"/><path d=\"M8 16H3v5\"/></g>"
4019
+ },
4020
+ {
4021
+ name: "lucide:move",
4022
+ label: "Move",
4023
+ category: "Arrows",
4024
+ body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 2v20m3-3l-3 3l-3-3M19 9l3 3l-3 3M2 12h20M5 9l-3 3l3 3M9 5l3-3l3 3\"/>"
4025
+ },
4026
+ {
4027
+ name: "lucide:shuffle",
4028
+ label: "Shuffle",
4029
+ category: "Arrows",
4030
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"m18 14l4 4l-4 4m0-20l4 4l-4 4\"/><path d=\"M2 18h1.973a4 4 0 0 0 3.3-1.7l5.454-8.6a4 4 0 0 1 3.3-1.7H22M2 6h1.972a4 4 0 0 1 3.6 2.2M22 18h-6.041a4 4 0 0 1-3.3-1.8l-.359-.45\"/></g>"
4031
+ },
4032
+ {
4033
+ name: "lucide:user",
4034
+ label: "User",
4035
+ category: "People",
4036
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/></g>"
4037
+ },
4038
+ {
4039
+ name: "lucide:users",
4040
+ label: "Users",
4041
+ category: "People",
4042
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2M16 3.128a4 4 0 0 1 0 7.744M22 21v-2a4 4 0 0 0-3-3.87\"/><circle cx=\"9\" cy=\"7\" r=\"4\"/></g>"
4043
+ },
4044
+ {
4045
+ name: "lucide:briefcase",
4046
+ label: "Briefcase",
4047
+ category: "People",
4048
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M16 20V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16\"/><rect width=\"20\" height=\"14\" x=\"2\" y=\"6\" rx=\"2\"/></g>"
4049
+ },
4050
+ {
4051
+ name: "lucide:building",
4052
+ label: "Building",
4053
+ category: "People",
4054
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M12 10h.01M12 14h.01M12 6h.01M16 10h.01M16 14h.01M16 6h.01M8 10h.01M8 14h.01M8 6h.01M9 22v-3a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3\"/><rect width=\"16\" height=\"20\" x=\"4\" y=\"2\" rx=\"2\"/></g>"
4055
+ },
4056
+ {
4057
+ name: "lucide:calendar",
4058
+ label: "Calendar",
4059
+ category: "People",
4060
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M8 2v3m8-3v3\"/><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/><path d=\"M3 9h18\"/></g>"
4061
+ },
4062
+ {
4063
+ name: "lucide:clock",
4064
+ label: "Clock",
4065
+ category: "People",
4066
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 6v6l4 2\"/></g>"
4067
+ },
4068
+ {
4069
+ name: "lucide:mail",
4070
+ label: "Mail",
4071
+ category: "People",
4072
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"m22 7l-8.991 5.727a2 2 0 0 1-2.009 0L2 7\"/><rect width=\"20\" height=\"16\" x=\"2\" y=\"4\" rx=\"2\"/></g>"
4073
+ },
4074
+ {
4075
+ name: "lucide:message-circle",
4076
+ label: "Message",
4077
+ category: "People",
4078
+ body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092a10 10 0 1 0-4.777-4.719\"/>"
4079
+ },
4080
+ {
4081
+ name: "lucide:file",
4082
+ label: "File",
4083
+ category: "Objects",
4084
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z\"/><path d=\"M14 2v5a1 1 0 0 0 1 1h5\"/></g>"
4085
+ },
4086
+ {
4087
+ name: "lucide:folder",
4088
+ label: "Folder",
4089
+ category: "Objects",
4090
+ body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z\"/>"
4091
+ },
4092
+ {
4093
+ name: "lucide:image",
4094
+ label: "Image",
4095
+ category: "Objects",
4096
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" ry=\"2\"/><circle cx=\"9\" cy=\"9\" r=\"2\"/><path d=\"m21 15l-3.086-3.086a2 2 0 0 0-2.828 0L6 21\"/></g>"
4097
+ },
4098
+ {
4099
+ name: "lucide:link",
4100
+ label: "Link",
4101
+ category: "Objects",
4102
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/></g>"
4103
+ },
4104
+ {
4105
+ name: "lucide:paperclip",
4106
+ label: "Paperclip",
4107
+ category: "Objects",
4108
+ body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m16 6l-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551\"/>"
4109
+ },
4110
+ {
4111
+ name: "lucide:pin",
4112
+ label: "Pin",
4113
+ category: "Objects",
4114
+ body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 17v5M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4a1 1 0 0 1 1 1z\"/>"
4115
+ },
4116
+ {
4117
+ name: "lucide:map-pin",
4118
+ label: "Map pin",
4119
+ category: "Objects",
4120
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0\"/><circle cx=\"12\" cy=\"10\" r=\"3\"/></g>"
4121
+ },
4122
+ {
4123
+ name: "lucide:search",
4124
+ label: "Search",
4125
+ category: "Objects",
4126
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"m21 21l-4.34-4.34\"/><circle cx=\"11\" cy=\"11\" r=\"8\"/></g>"
4127
+ },
4128
+ {
4129
+ name: "lucide:settings",
4130
+ label: "Settings",
4131
+ category: "Objects",
4132
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M9.671 4.136a2.34 2.34 0 0 1 4.659 0a2.34 2.34 0 0 0 3.319 1.915a2.34 2.34 0 0 1 2.33 4.033a2.34 2.34 0 0 0 0 3.831a2.34 2.34 0 0 1-2.33 4.033a2.34 2.34 0 0 0-3.319 1.915a2.34 2.34 0 0 1-4.659 0a2.34 2.34 0 0 0-3.32-1.915a2.34 2.34 0 0 1-2.33-4.033a2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></g>"
4133
+ },
4134
+ {
4135
+ name: "lucide:trash-2",
4136
+ label: "Trash",
4137
+ category: "Objects",
4138
+ body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10 11v6m4-6v6m5-11v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\"/>"
4139
+ },
4140
+ {
4141
+ name: "lucide:lock",
4142
+ label: "Lock",
4143
+ category: "Objects",
4144
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><rect width=\"18\" height=\"11\" x=\"3\" y=\"11\" rx=\"2\" ry=\"2\"/><path d=\"M7 11V7a5 5 0 0 1 10 0v4\"/></g>"
4145
+ },
4146
+ {
4147
+ name: "lucide:key",
4148
+ label: "Key",
4149
+ category: "Objects",
4150
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"m15.5 7.5l2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4m2-2l-9.6 9.6\"/><circle cx=\"7.5\" cy=\"15.5\" r=\"5.5\"/></g>"
4151
+ },
4152
+ {
4153
+ name: "lucide:code",
4154
+ label: "Code",
4155
+ category: "Tech",
4156
+ body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m16 18l6-6l-6-6M8 6l-6 6l6 6\"/>"
4157
+ },
4158
+ {
4159
+ name: "lucide:database",
4160
+ label: "Database",
4161
+ category: "Tech",
4162
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><ellipse cx=\"12\" cy=\"5\" rx=\"9\" ry=\"3\"/><path d=\"M3 5v14a9 3 0 0 0 18 0V5\"/><path d=\"M3 12a9 3 0 0 0 18 0\"/></g>"
4163
+ },
4164
+ {
4165
+ name: "lucide:server",
4166
+ label: "Server",
4167
+ category: "Tech",
4168
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><rect width=\"20\" height=\"8\" x=\"2\" y=\"2\" rx=\"2\" ry=\"2\"/><rect width=\"20\" height=\"8\" x=\"2\" y=\"14\" rx=\"2\" ry=\"2\"/><path d=\"M6 6h.01M6 18h.01\"/></g>"
4169
+ },
4170
+ {
4171
+ name: "lucide:cloud",
4172
+ label: "Cloud",
4173
+ category: "Tech",
4174
+ body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9\"/>"
4175
+ },
4176
+ {
4177
+ name: "lucide:git-branch",
4178
+ label: "Branch",
4179
+ category: "Tech",
4180
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M15 6a9 9 0 0 0-9 9V3\"/><circle cx=\"18\" cy=\"6\" r=\"3\"/><circle cx=\"6\" cy=\"18\" r=\"3\"/></g>"
4181
+ },
4182
+ {
4183
+ name: "lucide:terminal",
4184
+ label: "Terminal",
4185
+ category: "Tech",
4186
+ body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 19h8M4 17l6-6l-6-6\"/>"
4187
+ },
4188
+ {
4189
+ name: "lucide:cpu",
4190
+ label: "CPU",
4191
+ category: "Tech",
4192
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M12 20v2m0-20v2m5 16v2m0-20v2M2 12h2m-2 5h2M2 7h2m16 5h2m-2 5h2M20 7h2M7 20v2M7 2v2\"/><rect width=\"16\" height=\"16\" x=\"4\" y=\"4\" rx=\"2\"/><rect width=\"8\" height=\"8\" x=\"8\" y=\"8\" rx=\"1\"/></g>"
4193
+ },
4194
+ {
4195
+ name: "lucide:bug",
4196
+ label: "Bug",
4197
+ category: "Tech",
4198
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M12 20v-9m2-4a4 4 0 0 1 4 4v3a6 6 0 0 1-12 0v-3a4 4 0 0 1 4-4zm.12-3.12L16 2\"/><path d=\"M21 21a4 4 0 0 0-3.81-4M21 5a4 4 0 0 1-3.55 3.97M22 13h-4M3 21a4 4 0 0 1 3.81-4M3 5a4 4 0 0 0 3.55 3.97M6 13H2M8 2l1.88 1.88M9 7.13V6a3 3 0 1 1 6 0v1.13\"/></g>"
4199
+ },
4200
+ {
4201
+ name: "lucide:lightbulb",
4202
+ label: "Idea",
4203
+ category: "Ideas",
4204
+ body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15 14c.2-1 .7-1.7 1.5-2.5c1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5c.7.7 1.3 1.5 1.5 2.5m0 4h6m-5 4h4\"/>"
4205
+ },
4206
+ {
4207
+ name: "lucide:zap",
4208
+ label: "Zap",
4209
+ category: "Ideas",
4210
+ body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15.914 4a1.5 1.5 0 0 0-2.474-1.561l-9 9A1.5 1.5 0 0 0 5.5 14h4.002a.5.5 0 0 1 .471.666L8.086 20a1.5 1.5 0 0 0 2.475 1.56l9-9A1.5 1.5 0 0 0 18.5 10h-3.997a.5.5 0 0 1-.472-.667z\"/>"
4211
+ },
4212
+ {
4213
+ name: "lucide:target",
4214
+ label: "Target",
4215
+ category: "Ideas",
4216
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></g>"
4217
+ },
4218
+ {
4219
+ name: "lucide:rocket",
4220
+ label: "Rocket",
4221
+ category: "Ideas",
4222
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09\"/><path d=\"M9 12a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.4 22.4 0 0 1-4 2z\"/><path d=\"M9 12H4s.55-3.03 2-4c1.62-1.08 5 .05 5 .05\"/></g>"
4223
+ },
4224
+ {
4225
+ name: "lucide:trophy",
4226
+ label: "Trophy",
4227
+ category: "Ideas",
4228
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M10 14.66V17a1 1 0 0 1-1 1a2 2 0 0 0-2 2v2m7-7.34V17a1 1 0 0 0 1 1a2 2 0 0 1 2 2v2m.916-12H19.5A2.5 2.5 0 0 0 22 7.5V5a1 1 0 0 0-1-1h-3M4 22h16\"/><path d=\"M6 9a6 6 0 0 0 12 0V3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1z\"/><path d=\"M6.084 10H4.5A2.5 2.5 0 0 1 2 7.5V5a1 1 0 0 1 1-1h3\"/></g>"
4229
+ },
4230
+ {
4231
+ name: "lucide:gift",
4232
+ label: "Gift",
4233
+ category: "Ideas",
4234
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M12 7v14m8-10v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-8m3.5-4a1 1 0 0 1 0-5A4.8 8 0 0 1 12 7a4.8 8 0 0 1 4.5-5a1 1 0 0 1 0 5\"/><rect width=\"18\" height=\"4\" x=\"3\" y=\"7\" rx=\"1\"/></g>"
4235
+ },
4236
+ {
4237
+ name: "lucide:coffee",
4238
+ label: "Coffee",
4239
+ category: "Ideas",
4240
+ body: "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10 2v2m4-2v2m2 4a1 1 0 0 1 1 1v8a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1h14a4 4 0 1 1 0 8h-1M6 2v2\"/>"
4241
+ },
4242
+ {
4243
+ name: "lucide:smile",
4244
+ label: "Smile",
4245
+ category: "Ideas",
4246
+ body: "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M8 14s1.5 2 4 2s4-2 4-2M9 9h.01M15 9h.01\"/></g>"
4247
+ }
4248
+ ];
4249
+ //#endregion
4250
+ //#region src/core/shapes/emoji.ts
4251
+ var EMOJI_LIBRARY = [
4252
+ {
4253
+ name: "emoji:grinning",
4254
+ label: "Grinning face",
4255
+ category: "Smileys",
4256
+ char: "😀"
4257
+ },
4258
+ {
4259
+ name: "emoji:joy",
4260
+ label: "Tears of joy",
4261
+ category: "Smileys",
4262
+ char: "😂"
4263
+ },
4264
+ {
4265
+ name: "emoji:smiling",
4266
+ label: "Smiling face",
4267
+ category: "Smileys",
4268
+ char: "😊"
4269
+ },
4270
+ {
4271
+ name: "emoji:heart-eyes",
4272
+ label: "Heart eyes",
4273
+ category: "Smileys",
4274
+ char: "😍"
4275
+ },
4276
+ {
4277
+ name: "emoji:thinking",
4278
+ label: "Thinking face",
4279
+ category: "Smileys",
4280
+ char: "🤔"
4281
+ },
4282
+ {
4283
+ name: "emoji:sunglasses",
4284
+ label: "Sunglasses",
4285
+ category: "Smileys",
4286
+ char: "😎"
4287
+ },
4288
+ {
4289
+ name: "emoji:crying",
4290
+ label: "Crying face",
4291
+ category: "Smileys",
4292
+ char: "😢"
4293
+ },
4294
+ {
4295
+ name: "emoji:angry",
4296
+ label: "Angry face",
4297
+ category: "Smileys",
4298
+ char: "😡"
4299
+ },
4300
+ {
4301
+ name: "emoji:sleeping",
4302
+ label: "Sleeping face",
4303
+ category: "Smileys",
4304
+ char: "😴"
4305
+ },
4306
+ {
4307
+ name: "emoji:mind-blown",
4308
+ label: "Mind blown",
4309
+ category: "Smileys",
4310
+ char: "🤯"
4311
+ },
4312
+ {
4313
+ name: "emoji:grimacing",
4314
+ label: "Grimacing face",
4315
+ category: "Smileys",
4316
+ char: "😬"
4317
+ },
4318
+ {
4319
+ name: "emoji:partying",
4320
+ label: "Partying face",
4321
+ category: "Smileys",
4322
+ char: "🥳"
4323
+ },
4324
+ {
4325
+ name: "emoji:thumbs-up",
4326
+ label: "Thumbs up emoji",
4327
+ category: "Gestures",
4328
+ char: "👍"
4329
+ },
4330
+ {
4331
+ name: "emoji:thumbs-down",
4332
+ label: "Thumbs down emoji",
4333
+ category: "Gestures",
4334
+ char: "👎"
4335
+ },
4336
+ {
4337
+ name: "emoji:clap",
4338
+ label: "Clapping hands",
4339
+ category: "Gestures",
4340
+ char: "👏"
4341
+ },
4342
+ {
4343
+ name: "emoji:raised-hands",
4344
+ label: "Raised hands",
4345
+ category: "Gestures",
4346
+ char: "🙌"
4347
+ },
4348
+ {
4349
+ name: "emoji:folded-hands",
4350
+ label: "Folded hands",
4351
+ category: "Gestures",
4352
+ char: "🙏"
4353
+ },
4354
+ {
4355
+ name: "emoji:eyes",
4356
+ label: "Eyes",
4357
+ category: "Gestures",
4358
+ char: "👀"
4359
+ },
4360
+ {
4361
+ name: "emoji:muscle",
4362
+ label: "Flexed biceps",
4363
+ category: "Gestures",
4364
+ char: "💪"
4365
+ },
4366
+ {
4367
+ name: "emoji:wave",
4368
+ label: "Waving hand",
4369
+ category: "Gestures",
4370
+ char: "👋"
4371
+ },
4372
+ {
4373
+ name: "emoji:red-heart",
4374
+ label: "Red heart",
4375
+ category: "Symbols",
4376
+ char: "❤️"
4377
+ },
4378
+ {
4379
+ name: "emoji:fire",
4380
+ label: "Flame",
4381
+ category: "Symbols",
4382
+ char: "🔥"
4383
+ },
4384
+ {
4385
+ name: "emoji:star",
4386
+ label: "Medium star",
4387
+ category: "Symbols",
4388
+ char: "⭐"
4389
+ },
4390
+ {
4391
+ name: "emoji:check",
4392
+ label: "Check mark button",
4393
+ category: "Symbols",
4394
+ char: "✅"
4395
+ },
4396
+ {
4397
+ name: "emoji:cross",
4398
+ label: "Cross mark",
4399
+ category: "Symbols",
4400
+ char: "❌"
4401
+ },
4402
+ {
4403
+ name: "emoji:warning",
4404
+ label: "Warning sign",
4405
+ category: "Symbols",
4406
+ char: "⚠️"
4407
+ },
4408
+ {
4409
+ name: "emoji:question",
4410
+ label: "Red question mark",
4411
+ category: "Symbols",
4412
+ char: "❓"
4413
+ },
4414
+ {
4415
+ name: "emoji:hundred",
4416
+ label: "Hundred points",
4417
+ category: "Symbols",
4418
+ char: "💯"
4419
+ },
4420
+ {
4421
+ name: "emoji:light-bulb",
4422
+ label: "Light bulb",
4423
+ category: "Symbols",
4424
+ char: "💡"
4425
+ },
4426
+ {
4427
+ name: "emoji:bullseye",
4428
+ label: "Bullseye",
4429
+ category: "Symbols",
4430
+ char: "🎯"
4431
+ },
4432
+ {
4433
+ name: "emoji:party-popper",
4434
+ label: "Party popper",
4435
+ category: "Objects",
4436
+ char: "🎉"
4437
+ },
4438
+ {
4439
+ name: "emoji:rocket",
4440
+ label: "Rocket emoji",
4441
+ category: "Objects",
4442
+ char: "🚀"
4443
+ },
4444
+ {
4445
+ name: "emoji:trophy",
4446
+ label: "Trophy emoji",
4447
+ category: "Objects",
4448
+ char: "🏆"
4449
+ },
4450
+ {
4451
+ name: "emoji:pushpin",
4452
+ label: "Pushpin",
4453
+ category: "Objects",
4454
+ char: "📌"
4455
+ },
4456
+ {
4457
+ name: "emoji:memo",
4458
+ label: "Memo",
4459
+ category: "Objects",
4460
+ char: "📝"
4461
+ },
4462
+ {
4463
+ name: "emoji:calendar",
4464
+ label: "Tear-off calendar",
4465
+ category: "Objects",
4466
+ char: "📅"
4467
+ },
4468
+ {
4469
+ name: "emoji:alarm-clock",
4470
+ label: "Alarm clock",
4471
+ category: "Objects",
4472
+ char: "⏰"
4473
+ },
4474
+ {
4475
+ name: "emoji:speech-balloon",
4476
+ label: "Speech balloon",
4477
+ category: "Objects",
4478
+ char: "💬"
4479
+ }
4480
+ ];
4481
+ var BY_NAME$1 = new Map(EMOJI_LIBRARY.map((emoji) => [emoji.name, emoji]));
4482
+ function isEmojiName(value) {
4483
+ return typeof value === "string" && BY_NAME$1.has(value);
4484
+ }
4485
+ function emojiDefinition(name) {
4486
+ return BY_NAME$1.get(name);
4487
+ }
4488
+ var EMOJI_FONTS = `"Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", "Twemoji Mozilla", sans-serif`;
4489
+ /** An emoji as a standalone SVG data URL, sized to rasterise sharply (see icons.ts). */
4490
+ function emojiDataUrl(name) {
4491
+ const emoji = BY_NAME$1.get(name);
4492
+ if (!emoji) throw new Error(`Unknown emoji: ${name}`);
4493
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 64 64"><text x="32" y="34" text-anchor="middle" dominant-baseline="central" font-size="52" font-family='${EMOJI_FONTS}'>${emoji.char}</text></svg>`;
4494
+ return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
4495
+ }
4496
+ //#endregion
4497
+ //#region src/core/shapes/icons.ts
4498
+ /**
4499
+ * Rasterised size of an icon's SVG. The library's own art is a 24px grid, far
4500
+ * too small to use as a texture directly — the browser rasterises a data-URL
4501
+ * SVG at its declared size, so declare one that still looks sharp when a
4502
+ * sticker is zoomed well past the size it was placed at.
4503
+ */
4504
+ var ICON_RASTER_SIZE = 256;
4505
+ var BY_NAME = new Map(ICON_LIBRARY.map((icon) => [icon.name, icon]));
4506
+ function isIconName(value) {
4507
+ return typeof value === "string" && BY_NAME.has(value);
4508
+ }
4509
+ function iconDefinition(name) {
4510
+ return BY_NAME.get(name);
4511
+ }
4512
+ /** The vendored icons grouped for a picker, in the order they were vendored. */
4513
+ function iconCategories() {
4514
+ const groups = [];
4515
+ for (const icon of ICON_LIBRARY) {
4516
+ const existing = groups.find((group) => group.category === icon.category);
4517
+ if (existing) existing.icons.push(icon);
4518
+ else groups.push({
4519
+ category: icon.category,
4520
+ icons: [icon]
4521
+ });
4522
+ }
4523
+ return groups;
4524
+ }
4525
+ /**
4526
+ * An icon as a standalone SVG data URL, drawn in `color`.
4527
+ *
4528
+ * The library's art strokes in `currentColor`, which has no meaning once the
4529
+ * markup is detached into a data URL — an `<img>`/texture has no inherited
4530
+ * colour to resolve it against — so the colour is substituted in rather than
4531
+ * inherited.
4532
+ */
4533
+ function iconDataUrl(name, color) {
4534
+ const icon = BY_NAME.get(name);
4535
+ if (!icon) throw new Error(`Unknown icon: ${name}`);
4536
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${ICON_RASTER_SIZE}" height="${ICON_RASTER_SIZE}" viewBox="0 0 24 24">${icon.body.replaceAll("currentColor", color)}</svg>`;
4537
+ return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
4538
+ }
4539
+ function isStickerKind(value) {
4540
+ return isStampKind(value) || isEmojiName(value) || isIconName(value);
4541
+ }
4542
+ /** Human-readable name for any sticker kind — what a placed sticker is called and searched by. */
4543
+ function stickerLabel(kind) {
4544
+ if (isStampKind(kind)) return STAMPS.find((stamp) => stamp.kind === kind).label;
4545
+ return emojiDefinition(kind)?.label ?? BY_NAME.get(kind)?.label ?? kind;
4546
+ }
4547
+ /**
4548
+ * The image `src` for whichever sticker kind is selected. Stamp-pad stamps
4549
+ * and emoji carry their own colours, so only icons take the Host's colour.
4550
+ */
4551
+ function stickerDataUrl(kind, color) {
4552
+ if (isStampKind(kind)) return stampDataUrl(kind);
4553
+ if (isEmojiName(kind)) return emojiDataUrl(kind);
4554
+ return iconDataUrl(kind, color);
4555
+ }
4556
+ //#endregion
4557
+ //#region src/core/shapes/polygonGeometry.ts
4558
+ /**
4559
+ * The tuned per-`sides` starting angle that makes each regular-polygon kind
4560
+ * look right inscribed in its bounding box (a point-up triangle, a
4561
+ * flat-sided diamond, a point-up pentagon/hexagon, a flat-topped octagon) —
4562
+ * extracted unchanged from the legacy shape-stroke tool's own per-call
4563
+ * arguments, which were already tuned and shipped. Any `sides` count not
4564
+ * listed falls back to the common "point at top" case.
4565
+ */
4566
+ function polygonStartAngle(sides) {
4567
+ switch (sides) {
4568
+ case 3: return Math.PI / 2;
4569
+ case 4: return 0;
4570
+ case 8: return Math.PI / 8;
4571
+ default: return Math.PI / 2;
4572
+ }
4573
+ }
4574
+ /**
4575
+ * Rescales/recenters an arbitrary point set so its own bounding box exactly
4576
+ * fills `[-width/2, width/2] × [-height/2, height/2]`, centered at the
4577
+ * origin. Needed because "inscribed on a radius-rx,ry ellipse" (the natural
4578
+ * way to place N evenly-spaced vertices) does *not* by itself guarantee a
4579
+ * full, centered fit: a shape only touches every edge of its box when it
4580
+ * has a vertex at each of the four cardinal angles (true for a square/
4581
+ * diamond/octagon-with-the-right-start-angle) — a triangle or pentagon's
4582
+ * *actual* bounding box is both smaller than and off-center from the
4583
+ * nominal rx,ry ellipse (verified: an upward triangle's own bounding box is
4584
+ * ~13% narrower than its circumradius, and vertically off-center by a
4585
+ * quarter of its own height). Every semantic shape's declared `width`/
4586
+ * `height` has to be the shape's *real* extent — `objectBounds.ts` and
4587
+ * rotate-about-center math both assume it — so every generator below routes
4588
+ * through this instead of leaving each one to reinvent (or skip) the fix.
4589
+ */
4590
+ function fitToBox(points, width, height) {
4591
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
4592
+ for (const p of points) {
4593
+ if (p.x < minX) minX = p.x;
4594
+ if (p.x > maxX) maxX = p.x;
4595
+ if (p.y < minY) minY = p.y;
4596
+ if (p.y > maxY) maxY = p.y;
4597
+ }
4598
+ const rawW = maxX - minX || 1;
4599
+ const rawH = maxY - minY || 1;
4600
+ const cx = (minX + maxX) / 2;
4601
+ const cy = (minY + maxY) / 2;
4602
+ return points.map((p) => ({
4603
+ x: (p.x - cx) / rawW * width,
4604
+ y: (p.y - cy) / rawH * height
4605
+ }));
4606
+ }
4607
+ /**
4608
+ * A regular `sides`-gon inscribed in a `width`×`height` box, centered at
4609
+ * the origin and fit exactly to it via `fitToBox` — independently scaled
4610
+ * per axis (like an ellipse inscribed in a non-square box), not a true
4611
+ * regular polygon forced square. `sides: 4` with the diamond start angle
4612
+ * (0) produces the four cardinal points of the box — a diamond is exactly
4613
+ * this case, not a separate shape.
4614
+ */
4615
+ function regularPolygonPoints(sides, width, height, startAngle) {
4616
+ const raw = [];
4617
+ for (let i = 0; i < sides; i++) {
4618
+ const theta = startAngle + i / sides * Math.PI * 2;
4619
+ raw.push({
4620
+ x: Math.cos(theta),
4621
+ y: Math.sin(theta)
4622
+ });
4623
+ }
4624
+ return fitToBox(raw, width, height);
4625
+ }
4626
+ /**
4627
+ * A `points`-pointed star inscribed in a `width`×`height` box, centered at
4628
+ * the origin and fit exactly to it via `fitToBox` — alternating outer/inner
4629
+ * vertices, `innerRatio` of the outer radius. Matches the legacy tool's own
4630
+ * tuned defaults (5 points, 0.42).
4631
+ */
4632
+ function starPoints(points, innerRatio, width, height) {
4633
+ const total = points * 2;
4634
+ const raw = [];
4635
+ for (let i = 0; i < total; i++) {
4636
+ const isOuter = i % 2 === 0;
4637
+ const theta = -Math.PI / 2 + i / total * Math.PI * 2;
4638
+ const r = isOuter ? 1 : innerRatio;
4639
+ raw.push({
4640
+ x: Math.cos(theta) * r,
4641
+ y: Math.sin(theta) * r
4642
+ });
4643
+ }
4644
+ return fitToBox(raw, width, height);
4645
+ }
4646
+ var HEART_SEGMENTS = 64;
4647
+ /**
4648
+ * A heart inscribed in a `width`×`height` box, centered at the origin and
4649
+ * fit exactly to it via `fitToBox` — the standard parametric heart curve
4650
+ * (`x = 16 sin³t`, `y = 13 cos t − 5 cos 2t − 2 cos 3t − cos 4t`), used
4651
+ * as-is with no extra sign flip: board space is y-up (see `shapeTool.ts`'s
4652
+ * commitBoxShape), and this formula's cusp already lands at its most
4653
+ * negative y (bottom) with the two lobes above it, right side up. Negating
4654
+ * y — as an earlier version of this function did — puts the cusp at the
4655
+ * top instead, rendering an upside-down heart.
4656
+ */
4657
+ function heartPoints(width, height) {
4658
+ const raw = [];
4659
+ for (let i = 0; i < HEART_SEGMENTS; i++) {
4660
+ const t = i / HEART_SEGMENTS * Math.PI * 2;
4661
+ raw.push({
4662
+ x: 16 * Math.sin(t) ** 3,
4663
+ y: 13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t)
4664
+ });
4665
+ }
4666
+ return fitToBox(raw, width, height);
4667
+ }
4668
+ //#endregion
4669
+ //#region src/persistence/serialization/svg.ts
1923
4670
  /** Matches the key light used by the board shader and note shadows. */
1924
4671
  var LIGHT = {
1925
4672
  x: -.25,
@@ -1956,15 +4703,29 @@ function documentToSVG(doc, now = 0, registry, backgroundColor = DEFAULT_BACKGRO
1956
4703
  const images = doc.images ?? [];
1957
4704
  const timers = doc.timers ?? [];
1958
4705
  const customObjects = doc.customObjects ?? [];
1959
- const bounds = contentBounds(doc.strokes, notes, texts, tables, images, timers, customObjects);
4706
+ const rectangles = doc.rectangles ?? [];
4707
+ const ellipses = doc.ellipses ?? [];
4708
+ const lines = doc.lines ?? [];
4709
+ const arrows = doc.arrows ?? [];
4710
+ const polygons = doc.polygons ?? [];
4711
+ const stars = doc.stars ?? [];
4712
+ const hearts = doc.hearts ?? [];
4713
+ const bounds = contentBounds(doc.strokes, notes, texts, tables, images, timers, customObjects, rectangles, ellipses, lines, arrows, polygons, stars, hearts);
1960
4714
  const view = `${fmt(bounds.minX)} ${fmt(bounds.minY)} ${fmt(bounds.width)} ${fmt(bounds.height)}`;
1961
4715
  const defs = notes.map(noteShadowFilter).join("\n");
1962
4716
  const highlights = doc.strokes.filter((s) => s.tool === "highlighter");
1963
4717
  const ink = doc.strokes.filter((s) => s.tool !== "highlighter");
1964
4718
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${view}">\n<defs>\n${defs}\n</defs>\n${[
1965
- `<rect x="${fmt(bounds.minX)}" y="${fmt(bounds.minY)}" width="${fmt(bounds.width)}" height="${fmt(bounds.height)}" fill="${backgroundColor}"/>`,
4719
+ `<rect x="${fmt(bounds.minX)}" y="${fmt(bounds.minY)}" width="${fmt(bounds.width)}" height="${fmt(bounds.height)}" fill="${escapeXML(backgroundColor)}"/>`,
1966
4720
  ...images.map((image) => imageToElement(image, resolvedAssets)),
1967
4721
  ...tables.map(tableToGroup),
4722
+ ...rectangles.map(rectangleToElement),
4723
+ ...ellipses.map(ellipseToElement),
4724
+ ...polygons.map(polygonToElement),
4725
+ ...stars.map(starToElement),
4726
+ ...hearts.map(heartToElement),
4727
+ ...lines.map(lineToElement),
4728
+ ...arrows.map(arrowToGroup),
1968
4729
  ...highlights.flatMap(strokeToPaths),
1969
4730
  ...ink.flatMap(strokeToPaths),
1970
4731
  ...texts.map(textToElement),
@@ -1983,7 +4744,7 @@ function customObjectToGroup(object, registry, resolvedAssets) {
1983
4744
  const definition = registry?.objectType(object.type);
1984
4745
  if (definition) try {
1985
4746
  const scene = definition.describe(object, { selected: false });
1986
- return `<g data-id="${object.id}">${sceneNodeToSVG(scene, object.transform, resolvedAssets)}</g>`;
4747
+ return `<g data-id="${escapeXML(object.id)}">${sceneNodeToSVG(scene, object.transform, resolvedAssets)}</g>`;
1987
4748
  } catch {}
1988
4749
  return fallbackToGroup(object);
1989
4750
  }
@@ -2007,17 +4768,17 @@ function sceneNodeToSVG(node, parent, resolvedAssets) {
2007
4768
  case "rect": {
2008
4769
  const svgY = -node.y - node.height;
2009
4770
  const rx = node.cornerRadius ? ` rx="${fmt(node.cornerRadius)}"` : "";
2010
- const stroke = node.stroke ? ` stroke="${node.stroke}" stroke-width="${fmt(node.strokeWidth ?? .1)}"` : "";
2011
- return `<rect x="${fmt(node.x)}" y="${fmt(svgY)}" width="${fmt(node.width)}" height="${fmt(node.height)}" fill="${node.fill ?? "none"}"${rx}${stroke}${opacityAttr}${transformAttr}/>`;
4771
+ const stroke = node.stroke ? ` stroke="${escapeXML(node.stroke)}" stroke-width="${fmt(node.strokeWidth ?? .1)}"` : "";
4772
+ return `<rect x="${fmt(node.x)}" y="${fmt(svgY)}" width="${fmt(node.width)}" height="${fmt(node.height)}" fill="${escapeXML(node.fill ?? "none")}"${rx}${stroke}${opacityAttr}${transformAttr}/>`;
2012
4773
  }
2013
4774
  case "text": {
2014
4775
  const anchor = node.align === "center" ? ` text-anchor="middle"` : node.align === "end" ? ` text-anchor="end"` : "";
2015
4776
  const tspans = node.text.split("\n").map((line, i) => `<tspan x="${fmt(node.x)}" y="${fmt(-node.y + node.fontSize * (.8 + 1.25 * i))}">${escapeXML(line)}</tspan>`).join("");
2016
- return `<text font-family="Inter, 'Segoe UI', sans-serif" font-size="${fmt(node.fontSize)}" font-weight="500" fill="${node.color}"${anchor}${opacityAttr}${transformAttr}>${tspans}</text>`;
4777
+ return `<text font-family="Inter, 'Segoe UI', sans-serif" font-size="${fmt(node.fontSize)}" font-weight="500" fill="${escapeXML(node.color)}"${anchor}${opacityAttr}${transformAttr}>${tspans}</text>`;
2017
4778
  }
2018
4779
  case "ellipse": {
2019
- const stroke = node.stroke ? ` stroke="${node.stroke}" stroke-width="${fmt(node.strokeWidth ?? .1)}"` : "";
2020
- return `<ellipse cx="${fmt(node.cx)}" cy="${fmt(-node.cy)}" rx="${fmt(node.rx)}" ry="${fmt(node.ry)}" fill="${node.fill ?? "none"}"${stroke}${opacityAttr}${transformAttr}/>`;
4780
+ const stroke = node.stroke ? ` stroke="${escapeXML(node.stroke)}" stroke-width="${fmt(node.strokeWidth ?? .1)}"` : "";
4781
+ return `<ellipse cx="${fmt(node.cx)}" cy="${fmt(-node.cy)}" rx="${fmt(node.rx)}" ry="${fmt(node.ry)}" fill="${escapeXML(node.fill ?? "none")}"${stroke}${opacityAttr}${transformAttr}/>`;
2021
4782
  }
2022
4783
  case "image": {
2023
4784
  const svgY = -node.y - node.height;
@@ -2048,7 +4809,7 @@ function fallbackToGroup(object) {
2048
4809
  const label = object.fallback.label;
2049
4810
  const labelEl = label ? `<text x="${fmt(b.x + Math.min(.3, b.width * .1))}" y="${fmt(svgY + Math.min(1.2, b.height * .4))}" font-family="Inter, 'Segoe UI', sans-serif" font-size="${fmt(Math.min(1.1, b.height * .3))}" fill="#6B7280">${escapeXML(label)}</text>` : "";
2050
4811
  return [
2051
- `<g data-id="${object.id}" data-fallback="true"${svgTransformAttr(object.transform)}>`,
4812
+ `<g data-id="${escapeXML(object.id)}" data-fallback="true"${svgTransformAttr(object.transform)}>`,
2052
4813
  `<rect x="${fmt(b.x)}" y="${fmt(svgY)}" width="${fmt(b.width)}" height="${fmt(b.height)}" fill="#F3F4F6" stroke="#9CA3AF" stroke-width="0.15" stroke-dasharray="0.3 0.3"/>`,
2053
4814
  labelEl,
2054
4815
  `</g>`
@@ -2061,7 +4822,7 @@ function strokeToPaths(stroke) {
2061
4822
  y,
2062
4823
  pressure,
2063
4824
  ...erase > 0 ? { erase } : {}
2064
- })), stroke.baseWidth);
4825
+ })), stroke.baseWidth, stroke.tool !== "shape");
2065
4826
  const transform = stroke.matrix ? ` transform="matrix(${fmt(stroke.matrix[0])} ${fmt(-stroke.matrix[1])} ${fmt(-stroke.matrix[2])} ${fmt(stroke.matrix[3])} ${fmt(stroke.matrix[4])} ${fmt(-stroke.matrix[5])})"` : "";
2066
4827
  const baseOpacity = stroke.tool === "highlighter" ? .4 : 1;
2067
4828
  const paths = [];
@@ -2072,7 +4833,7 @@ function strokeToPaths(stroke) {
2072
4833
  const d = `M ${left.join(" L ")} L ${right.join(" L ")} Z`;
2073
4834
  const opacity = run.opacity * baseOpacity;
2074
4835
  const opacityAttr = opacity < 1 ? ` fill-opacity="${fmt(opacity)}"` : "";
2075
- paths.push(`<path data-id="${stroke.id}" d="${d}" fill="${stroke.color}"${opacityAttr}${transform}/>`);
4836
+ paths.push(`<path data-id="${escapeXML(stroke.id)}" d="${d}" fill="${escapeXML(stroke.color)}"${opacityAttr}${transform}/>`);
2076
4837
  }
2077
4838
  return paths;
2078
4839
  }
@@ -2094,14 +4855,14 @@ function splitByOpacity(edges) {
2094
4855
  function textToElement(block) {
2095
4856
  const top = -block.y;
2096
4857
  const tspans = block.text.split("\n").map((line, i) => `<tspan x="${fmt(block.x)}" y="${fmt(top + block.fontSize * (.8 + 1.25 * i))}">${escapeXML(line)}</tspan>`).join("");
2097
- return `<text font-family="Inter, 'Segoe UI', sans-serif" font-size="${fmt(block.fontSize)}" font-weight="500" fill="${block.color}">${tspans}</text>`;
4858
+ return `<text font-family="Inter, 'Segoe UI', sans-serif" font-size="${fmt(block.fontSize)}" font-weight="500" fill="${escapeXML(block.color)}">${tspans}</text>`;
2098
4859
  }
2099
4860
  function noteShadowFilter(note) {
2100
4861
  const dx = -LIGHT.x / LIGHT.z * note.zOffset;
2101
4862
  const dy = LIGHT.y / LIGHT.z * note.zOffset;
2102
4863
  const blur = (.35 + note.zOffset * .55) / 2;
2103
4864
  const opacity = Math.max(.05, .26 - note.zOffset * .028);
2104
- return `<filter id="sh-${note.id}" x="-50%" y="-50%" width="200%" height="200%"><feDropShadow dx="${fmt(dx)}" dy="${fmt(dy)}" stdDeviation="${fmt(blur)}" flood-opacity="${fmt(opacity)}"/></filter>`;
4865
+ return `<filter id="sh-${escapeXML(note.id)}" x="-50%" y="-50%" width="200%" height="200%"><feDropShadow dx="${fmt(dx)}" dy="${fmt(dy)}" stdDeviation="${fmt(blur)}" flood-opacity="${fmt(opacity)}"/></filter>`;
2105
4866
  }
2106
4867
  function noteToGroup(note) {
2107
4868
  const left = note.x - note.size / 2;
@@ -2111,7 +4872,7 @@ function noteToGroup(note) {
2111
4872
  const tspans = wrapText(note.text, note.size - pad * 2, fontSize).map((line, i) => `<tspan x="${fmt(left + pad)}" y="${fmt(top + pad + fontSize * (.8 + 1.25 * i))}">${escapeXML(line)}</tspan>`).join("");
2112
4873
  return [
2113
4874
  `<g>`,
2114
- `<rect x="${fmt(left)}" y="${fmt(top)}" width="${fmt(note.size)}" height="${fmt(note.size)}" fill="${note.color}" filter="url(#sh-${note.id})"/>`,
4875
+ `<rect x="${fmt(left)}" y="${fmt(top)}" width="${fmt(note.size)}" height="${fmt(note.size)}" fill="${escapeXML(note.color)}" filter="url(#sh-${escapeXML(note.id)})"/>`,
2115
4876
  `<text font-family="ui-rounded, 'Segoe UI', sans-serif" font-size="${fmt(fontSize)}" font-weight="500" fill="#37352F">${tspans}</text>`,
2116
4877
  `</g>`
2117
4878
  ].join("");
@@ -2142,8 +4903,8 @@ function tableToGroup(table) {
2142
4903
  const left = table.x;
2143
4904
  const top = -table.y;
2144
4905
  const headerH = table.rowHeights[0] ?? height / table.rows;
2145
- const borderColor = table.color || "#D1D5DB";
2146
- const bgColor = table.backgroundColor || "#FFFFFF";
4906
+ const borderColor = escapeXML(table.color || "#D1D5DB");
4907
+ const bgColor = escapeXML(table.backgroundColor || "#FFFFFF");
2147
4908
  const elements = [];
2148
4909
  elements.push(`<rect x="${fmt(left)}" y="${fmt(top)}" width="${fmt(width)}" height="${fmt(height)}" fill="${bgColor}" rx="0.5"/>`);
2149
4910
  elements.push(`<rect x="${fmt(left)}" y="${fmt(top)}" width="${fmt(width)}" height="${fmt(headerH)}" fill="#F3F4F6" rx="0.5"/>`);
@@ -2171,14 +4932,14 @@ function tableToGroup(table) {
2171
4932
  const padY = Math.min(.3, rowH * .12);
2172
4933
  const fontSize = Math.min(1.25, rowH * .45);
2173
4934
  const weight = r === 0 ? "bold" : "500";
2174
- const color = r === 0 ? "#111827" : table.color ?? "#374151";
4935
+ const color = escapeXML(r === 0 ? "#111827" : table.color ?? "#374151");
2175
4936
  elements.push(`<text x="${fmt(left + cellLeft + padX)}" y="${fmt(top + cellTop + padY + fontSize * .8)}" font-family="Inter, 'Segoe UI', sans-serif" font-size="${fmt(fontSize)}" font-weight="${weight}" fill="${color}">${escapeXML(text)}</text>`);
2176
4937
  }
2177
4938
  cellLeft += colW;
2178
4939
  }
2179
4940
  cellTop += rowH;
2180
4941
  }
2181
- return `<g data-id="${table.id}">\n${elements.join("\n")}\n</g>`;
4942
+ return `<g data-id="${escapeXML(table.id)}">\n${elements.join("\n")}\n</g>`;
2182
4943
  }
2183
4944
  function timerToGroup(timer, now) {
2184
4945
  const r = timer.size / 2;
@@ -2186,7 +4947,7 @@ function timerToGroup(timer, now) {
2186
4947
  const cy = -timer.y;
2187
4948
  const label = formatTimer(timerRemaining(timer, now));
2188
4949
  return [
2189
- `<g data-id="${timer.id}">`,
4950
+ `<g data-id="${escapeXML(timer.id)}">`,
2190
4951
  `<circle cx="${fmt(cx + .4)}" cy="${fmt(cy + .4)}" r="${fmt(r)}" fill="#000" fill-opacity="0.12"/>`,
2191
4952
  `<circle cx="${fmt(cx)}" cy="${fmt(cy)}" r="${fmt(r)}" fill="#E11D48"/>`,
2192
4953
  `<circle cx="${fmt(cx)}" cy="${fmt(cy)}" r="${fmt(r * .72)}" fill="#FAFAF9"/>`,
@@ -2199,12 +4960,100 @@ function imageToElement(image, resolvedAssets) {
2199
4960
  const top = -image.y - image.height / 2;
2200
4961
  if (image.ref) {
2201
4962
  const dataUri = resolvedAssets?.get(image.ref);
2202
- if (!dataUri) return `<g data-id="${image.id}" data-fallback="true">${imagePlaceholderElement(left, top, image.width, image.height, image.name)}</g>`;
2203
- return `<image data-id="${image.id}" href="${escapeXML(dataUri)}" x="${fmt(left)}" y="${fmt(top)}" width="${fmt(image.width)}" height="${fmt(image.height)}" preserveAspectRatio="none"/>`;
4963
+ if (!dataUri) return `<g data-id="${escapeXML(image.id)}" data-fallback="true">${imagePlaceholderElement(left, top, image.width, image.height, image.name)}</g>`;
4964
+ return `<image data-id="${escapeXML(image.id)}" href="${escapeXML(dataUri)}" x="${fmt(left)}" y="${fmt(top)}" width="${fmt(image.width)}" height="${fmt(image.height)}" preserveAspectRatio="none"/>`;
2204
4965
  }
2205
4966
  return `<image href="${escapeXML(image.src)}" x="${fmt(left)}" y="${fmt(top)}" width="${fmt(image.width)}" height="${fmt(image.height)}" preserveAspectRatio="none"/>`;
2206
4967
  }
2207
- function contentBounds(strokes, notes, texts, tables = [], images = [], timers = [], customObjects = []) {
4968
+ /** (x, y) is the top edge (core/shapes/shapeObjects.ts) — same convention as TableBlock, so `svgY = -y` directly, no height offset. */
4969
+ /**
4970
+ * SVG `transform` attribute for a top-edge, Y-up shape's `rotation` (about
4971
+ * its own center, per `shapeObjects.ts`'s convention) — board-space
4972
+ * `rotationAbout` fed through the existing `svgTransformAttr` F·M·F
4973
+ * conversion, exactly how `sceneNodeToSVG`'s identical-convention `rect`
4974
+ * case already does it. Shared by every top-edge shape kind (Rectangle,
4975
+ * Ellipse, and Phase 4's Polygon/Star/Heart), not just these two.
4976
+ */
4977
+ function shapeRotationAttr(x, y, width, height, rotation) {
4978
+ if (!rotation) return "";
4979
+ return svgTransformAttr(rotationAbout(rotation, x + width / 2, y - height / 2));
4980
+ }
4981
+ function opacityAttrOf(opacity) {
4982
+ return opacity !== void 0 && opacity < 1 ? ` opacity="${fmt(opacity)}"` : "";
4983
+ }
4984
+ function rectangleToElement(rect) {
4985
+ const svgY = -rect.y;
4986
+ const stroke = rect.stroke ? ` stroke="${escapeXML(rect.stroke)}" stroke-width="${fmt(rect.strokeWidth ?? .12)}"` : "";
4987
+ const rx = rect.cornerRadius ? ` rx="${fmt(rect.cornerRadius)}"` : "";
4988
+ const transform = shapeRotationAttr(rect.x, rect.y, rect.width, rect.height, rect.rotation);
4989
+ const opacity = opacityAttrOf(rect.opacity);
4990
+ return `<rect data-id="${escapeXML(rect.id)}" x="${fmt(rect.x)}" y="${fmt(svgY)}" width="${fmt(rect.width)}" height="${fmt(rect.height)}" fill="${escapeXML(rect.fill ?? "none")}"${rx}${stroke}${opacity}${transform}/>`;
4991
+ }
4992
+ function ellipseToElement(ellipse) {
4993
+ const cx = ellipse.x + ellipse.width / 2;
4994
+ const cy = -ellipse.y + ellipse.height / 2;
4995
+ const stroke = ellipse.stroke ? ` stroke="${escapeXML(ellipse.stroke)}" stroke-width="${fmt(ellipse.strokeWidth ?? .12)}"` : "";
4996
+ const transform = shapeRotationAttr(ellipse.x, ellipse.y, ellipse.width, ellipse.height, ellipse.rotation);
4997
+ const opacity = opacityAttrOf(ellipse.opacity);
4998
+ return `<ellipse data-id="${escapeXML(ellipse.id)}" cx="${fmt(cx)}" cy="${fmt(cy)}" rx="${fmt(ellipse.width / 2)}" ry="${fmt(ellipse.height / 2)}" fill="${escapeXML(ellipse.fill ?? "none")}"${stroke}${opacity}${transform}/>`;
4999
+ }
5000
+ /**
5001
+ * A regular N-gon inscribed in the same top-edge/Y-up bounding box
5002
+ * Rectangle uses — points are authored unrotated (absolute, y-flipped),
5003
+ * with `transform` handling rotation about the shape's own center exactly
5004
+ * like `rectangleToElement`/`ellipseToElement` already do, so this reuses
5005
+ * that same proven pattern rather than rotating points by hand.
5006
+ */
5007
+ function polygonToElement(polygon) {
5008
+ const cx = polygon.x + polygon.width / 2;
5009
+ const cy = polygon.y - polygon.height / 2;
5010
+ const svgPoints = regularPolygonPoints(polygon.sides, polygon.width, polygon.height, polygonStartAngle(polygon.sides)).map((p) => `${fmt(cx + p.x)},${fmt(-(cy + p.y))}`).join(" ");
5011
+ const stroke = polygon.stroke ? ` stroke="${escapeXML(polygon.stroke)}" stroke-width="${fmt(polygon.strokeWidth ?? SHAPE_DEFAULT_STROKE_WIDTH)}"` : "";
5012
+ const transform = shapeRotationAttr(polygon.x, polygon.y, polygon.width, polygon.height, polygon.rotation);
5013
+ const opacity = opacityAttrOf(polygon.opacity);
5014
+ return `<polygon data-id="${escapeXML(polygon.id)}" points="${svgPoints}" fill="${escapeXML(polygon.fill ?? "none")}"${stroke}${opacity}${transform}/>`;
5015
+ }
5016
+ /** Same convention/pattern as `polygonToElement` — a `points`-vertex star with `innerRadiusRatio` (`polygonGeometry.ts`'s `starPoints`). */
5017
+ function starToElement(star) {
5018
+ const cx = star.x + star.width / 2;
5019
+ const cy = star.y - star.height / 2;
5020
+ const svgPoints = starPoints(star.points, star.innerRadiusRatio, star.width, star.height).map((p) => `${fmt(cx + p.x)},${fmt(-(cy + p.y))}`).join(" ");
5021
+ const stroke = star.stroke ? ` stroke="${escapeXML(star.stroke)}" stroke-width="${fmt(star.strokeWidth ?? SHAPE_DEFAULT_STROKE_WIDTH)}"` : "";
5022
+ const transform = shapeRotationAttr(star.x, star.y, star.width, star.height, star.rotation);
5023
+ const opacity = opacityAttrOf(star.opacity);
5024
+ return `<polygon data-id="${escapeXML(star.id)}" points="${svgPoints}" fill="${escapeXML(star.fill ?? "none")}"${stroke}${opacity}${transform}/>`;
5025
+ }
5026
+ /** Same convention/pattern as `polygonToElement` — the standard parametric heart curve (`polygonGeometry.ts`'s `heartPoints`), emitted as a closed `<polygon>` (dense enough at `HEART_SEGMENTS` to read as a smooth curve). */
5027
+ function heartToElement(heart) {
5028
+ const cx = heart.x + heart.width / 2;
5029
+ const cy = heart.y - heart.height / 2;
5030
+ const svgPoints = heartPoints(heart.width, heart.height).map((p) => `${fmt(cx + p.x)},${fmt(-(cy + p.y))}`).join(" ");
5031
+ const stroke = heart.stroke ? ` stroke="${escapeXML(heart.stroke)}" stroke-width="${fmt(heart.strokeWidth ?? SHAPE_DEFAULT_STROKE_WIDTH)}"` : "";
5032
+ const transform = shapeRotationAttr(heart.x, heart.y, heart.width, heart.height, heart.rotation);
5033
+ const opacity = opacityAttrOf(heart.opacity);
5034
+ return `<polygon data-id="${escapeXML(heart.id)}" points="${svgPoints}" fill="${escapeXML(heart.fill ?? "none")}"${stroke}${opacity}${transform}/>`;
5035
+ }
5036
+ /** No rotation/box concept (see shapeObjects.ts's header) — the two endpoints are emitted directly, y-flipped like every other coordinate here. */
5037
+ function lineToElement(line) {
5038
+ const stroke = escapeXML(line.stroke ?? SHAPE_DEFAULT_STROKE);
5039
+ const width = line.strokeWidth ?? SHAPE_DEFAULT_STROKE_WIDTH;
5040
+ const opacity = opacityAttrOf(line.opacity);
5041
+ return `<line data-id="${escapeXML(line.id)}" x1="${fmt(line.start.x)}" y1="${fmt(-line.start.y)}" x2="${fmt(line.end.x)}" y2="${fmt(-line.end.y)}" stroke="${stroke}" stroke-width="${fmt(width)}"${opacity}/>`;
5042
+ }
5043
+ /** Shaft plus head barbs — same `arrowHeadBarbs` the canvas renderer uses, so the two never drift. */
5044
+ function arrowToGroup(arrow) {
5045
+ const stroke = escapeXML(arrow.stroke ?? SHAPE_DEFAULT_STROKE);
5046
+ if (arrow.routing) {
5047
+ const paths = arrowPolylines(arrow).map((points) => `<polyline points="${points.map((p) => `${fmt(p.x)},${fmt(-p.y)}`).join(" ")}" fill="none" stroke="${stroke}" stroke-width="${fmt(arrow.strokeWidth ?? SHAPE_DEFAULT_STROKE_WIDTH)}" stroke-linejoin="round"/>`);
5048
+ return `<g data-id="${escapeXML(arrow.id)}"${opacityAttrOf(arrow.opacity)}>${paths.join("")}</g>`;
5049
+ }
5050
+ const width = arrow.strokeWidth ?? SHAPE_DEFAULT_STROKE_WIDTH;
5051
+ const opacity = opacityAttrOf(arrow.opacity);
5052
+ const segment = (a, b) => `<line x1="${fmt(a.x)}" y1="${fmt(-a.y)}" x2="${fmt(b.x)}" y2="${fmt(-b.y)}" stroke="${stroke}" stroke-width="${fmt(width)}"/>`;
5053
+ const segments = [...arrowSegments(arrow).map(([a, b]) => segment(a, b))];
5054
+ return `<g data-id="${escapeXML(arrow.id)}"${opacity}>${segments.join("")}</g>`;
5055
+ }
5056
+ function contentBounds(strokes, notes, texts, tables = [], images = [], timers = [], customObjects = [], rectangles = [], ellipses = [], lines = [], arrows = [], polygons = [], stars = [], hearts = []) {
2208
5057
  let minX = Infinity;
2209
5058
  let minY = Infinity;
2210
5059
  let maxX = -Infinity;
@@ -2266,6 +5115,35 @@ function contentBounds(strokes, notes, texts, tables = [], images = [], timers =
2266
5115
  grow(p.x, -p.y);
2267
5116
  }
2268
5117
  }
5118
+ for (const rect of rectangles) {
5119
+ grow(rect.x, -rect.y);
5120
+ grow(rect.x + rect.width, -rect.y + rect.height);
5121
+ }
5122
+ for (const ellipse of ellipses) {
5123
+ grow(ellipse.x, -ellipse.y);
5124
+ grow(ellipse.x + ellipse.width, -ellipse.y + ellipse.height);
5125
+ }
5126
+ for (const line of lines) {
5127
+ grow(line.start.x, -line.start.y);
5128
+ grow(line.end.x, -line.end.y);
5129
+ }
5130
+ for (const arrow of arrows) for (const point of [
5131
+ arrow.start,
5132
+ arrow.end,
5133
+ ...arrowSegments(arrow).flat()
5134
+ ]) grow(point.x, -point.y);
5135
+ for (const polygon of polygons) {
5136
+ grow(polygon.x, -polygon.y);
5137
+ grow(polygon.x + polygon.width, -polygon.y + polygon.height);
5138
+ }
5139
+ for (const star of stars) {
5140
+ grow(star.x, -star.y);
5141
+ grow(star.x + star.width, -star.y + star.height);
5142
+ }
5143
+ for (const heart of hearts) {
5144
+ grow(heart.x, -heart.y);
5145
+ grow(heart.x + heart.width, -heart.y + heart.height);
5146
+ }
2269
5147
  if (minX > maxX) return {
2270
5148
  minX: 0,
2271
5149
  minY: 0,
@@ -2290,4 +5168,4 @@ function fmt(n) {
2290
5168
  var SDK_PACKAGE_NAME = "@scrawl-board/board";
2291
5169
  var SDK_DEVELOPMENT_VERSION = "0.0.0-development";
2292
5170
  //#endregion
2293
- export { ASSET_CACHE_BYTES_DEFAULT, ASSET_CACHE_BYTES_MAX, ASSET_CACHE_BYTES_MIN, ASSET_EXPORT_MAX_DECODED_MEGAPIXELS, ASSET_EXPORT_MAX_ENCODED_BYTES, ASSET_MAX_CONCURRENT_RESOLUTIONS, ASSET_MAX_DECODED_MEGAPIXELS, ASSET_MAX_DIMENSION_PX, ASSET_MAX_ENCODED_BYTES, ASSET_REF_MAX_BYTES, ASSET_REF_PATTERN, AddImageCommand, AddNoteCommand, AddStrokesCommand, AddTableCommand, AddTextCommand, AddTimerCommand, AssetResolutionError, BEACON_INSET, BoardDocument, CURRENT_DOCUMENT_SCHEMA_VERSION, ClusterStore, DeleteImageCommand, DeleteNoteCommand, DeleteStrokesCommand, DeleteTableCommand, DeleteTextCommand, DeleteTimerCommand, DocumentRecoveryError, END_TAPER, ERASE_THRESHOLD, EraseCommand, FOG_COLOR, HIGHLIGHT_COLORS, History, IDENTITY, INK_COLORS, LockItemsCommand, MIN_WIDTH_FACTOR, NOTE_COLORS, NOTE_DEFAULT_SIZE, NOTE_DEFAULT_Z, NOTE_MAX_Z, NOTE_MIN_Z, NOTE_PEEL_STEP, SDK_DEVELOPMENT_VERSION, SDK_PACKAGE_NAME, STAMPS, STAMP_SIZE, SUPPORTED_ASSET_MEDIA_TYPES, SpatialIndex, TABLE_DEFAULT_CELL_HEIGHT, TABLE_DEFAULT_CELL_WIDTH, TABLE_DEFAULT_FONT_SIZE, TEXT_DEFAULT_SIZE, TIMER_DEFAULT_DURATION_MS, TIMER_DEFAULT_SIZE, TIMER_PRESETS_MS, TransformCommand, UpdateImageCommand, UpdateNoteCommand, UpdateTableCommand, UpdateTextCommand, UpdateTimerCommand, apply, applyItemLock, assetRef, avgScale, canUnlockItem, changeToOps, clampAssetCacheBytes, cloneCustomObject, cloneImage, cloneNote, cloneStroke, cloneTable, cloneText, cloneTimer, documentId, documentToSVG, formatTimer, invert, isAssetRef, isIdentity, isStampKind, loadDocumentBytes, measureTable, measureTextBlock, migrateDocument, mul, pauseTimer, placePresenceBeacon, ribbonEdges, rotationAbout, scalingAbout, searchBoard, serializeDocument, serializeLock, serializeStroke, setTimerDuration, stampDataUrl, startTimer, strokeId, timerExpired, timerRemaining, toggleTimer, translation };
5171
+ export { ASSET_CACHE_BYTES_DEFAULT, ASSET_CACHE_BYTES_MAX, ASSET_CACHE_BYTES_MIN, ASSET_EXPORT_MAX_DECODED_MEGAPIXELS, ASSET_EXPORT_MAX_ENCODED_BYTES, ASSET_MAX_CONCURRENT_RESOLUTIONS, ASSET_MAX_DECODED_MEGAPIXELS, ASSET_MAX_DIMENSION_PX, ASSET_MAX_ENCODED_BYTES, ASSET_REF_MAX_BYTES, ASSET_REF_PATTERN, AddArrowCommand, AddEllipseCommand, AddGroupCommand, AddHeartCommand, AddImageCommand, AddLineCommand, AddNoteCommand, AddPolygonCommand, AddRectangleCommand, AddStarCommand, AddStrokesCommand, AddTableCommand, AddTextCommand, AddTimerCommand, AssetResolutionError, BEACON_INSET, BoardDocument, CURRENT_DOCUMENT_SCHEMA_VERSION, ClusterStore, CommandBatch, DeleteArrowCommand, DeleteEllipseCommand, DeleteGroupCommand, DeleteHeartCommand, DeleteImageCommand, DeleteLineCommand, DeleteNoteCommand, DeletePolygonCommand, DeleteRectangleCommand, DeleteStarCommand, DeleteStrokesCommand, DeleteTableCommand, DeleteTextCommand, DeleteTimerCommand, DocumentRecoveryError, EMOJI_LIBRARY, END_TAPER, ERASE_THRESHOLD, EraseCommand, FOG_COLOR, HIGHLIGHT_COLORS, History, ICON_LIBRARY, IDENTITY, INK_COLORS, LockItemsCommand, MIN_WIDTH_FACTOR, NOTE_COLORS, NOTE_DEFAULT_SIZE, NOTE_DEFAULT_Z, NOTE_MAX_Z, NOTE_MIN_Z, NOTE_PEEL_STEP, ReorderObjectCommand, SDK_DEVELOPMENT_VERSION, SDK_PACKAGE_NAME, SHAPE_DEFAULT_STROKE, SHAPE_DEFAULT_STROKE_WIDTH, SHAPE_MIN_SIZE, SHAPE_STYLE_DEFAULTS, STAMPS, STAMP_SIZE, SUPPORTED_ASSET_MEDIA_TYPES, SpatialIndex, TABLE_DEFAULT_CELL_HEIGHT, TABLE_DEFAULT_CELL_WIDTH, TABLE_DEFAULT_FONT_SIZE, TEXT_DEFAULT_SIZE, TIMER_DEFAULT_DURATION_MS, TIMER_DEFAULT_SIZE, TIMER_PRESETS_MS, TransformCommand, TransformObjectsCommand, UpdateArrowCommand, UpdateEllipseCommand, UpdateGroupCommand, UpdateHeartCommand, UpdateImageCommand, UpdateLineCommand, UpdateNoteCommand, UpdatePolygonCommand, UpdateRectangleCommand, UpdateStarCommand, UpdateTableCommand, UpdateTextCommand, UpdateTimerCommand, apply, applyItemLock, assetRef, avgScale, canUnlockItem, changeToOps, clampAssetCacheBytes, cloneArrow, cloneCustomObject, cloneEllipse, cloneGroup, cloneHeart, cloneImage, cloneLine, cloneNote, clonePolygon, cloneRectangle, cloneStar, cloneStroke, cloneTable, cloneText, cloneTimer, documentId, documentSize, documentToSVG, emojiDataUrl, emojiDefinition, formatTimer, iconCategories, iconDataUrl, iconDefinition, invert, isAssetRef, isEmojiName, isIconName, isIdentity, isStampKind, isStickerKind, loadDocumentBytes, measureTable, measureTextBlock, migrateDocument, mul, pauseTimer, placePresenceBeacon, ribbonEdges, rotationAbout, scalingAbout, searchBoard, serializeDocument, serializeLock, serializeStroke, setTimerDuration, stampDataUrl, startTimer, stickerDataUrl, stickerLabel, strokeId, timerExpired, timerRemaining, toggleTimer, translation };