@scrawl-board/board 0.1.0-beta.0 → 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,10 +1,44 @@
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
+ /** Hard ceiling on a reference's own wire length — independent of any resource limit below. */
4
+ var ASSET_REF_MAX_BYTES = 512;
3
5
  function isAssetRef(value) {
4
6
  return typeof value === "string" && ASSET_REF_PATTERN.test(value) && new TextEncoder().encode(value).length <= 512;
5
7
  }
8
+ /** Throws on malformed input; use `isAssetRef` where a boolean is wanted instead. */
9
+ function assetRef(value) {
10
+ if (!isAssetRef(value)) throw new TypeError(`Not a well-formed AssetRef: ${JSON.stringify(value)}`);
11
+ return value;
12
+ }
13
+ var AssetResolutionError = class extends Error {
14
+ constructor(code, retryable, message, ref) {
15
+ super(message);
16
+ this.code = code;
17
+ this.retryable = retryable;
18
+ this.ref = ref;
19
+ this.name = "AssetResolutionError";
20
+ }
21
+ };
22
+ var ASSET_MAX_ENCODED_BYTES = 20 * 1024 * 1024;
23
+ var ASSET_MAX_DIMENSION_PX = 8192;
24
+ var ASSET_MAX_DECODED_MEGAPIXELS = 40;
25
+ var ASSET_MAX_CONCURRENT_RESOLUTIONS = 6;
26
+ var ASSET_CACHE_BYTES_DEFAULT = 256 * 1024 * 1024;
27
+ var ASSET_CACHE_BYTES_MIN = 64 * 1024 * 1024;
28
+ var ASSET_CACHE_BYTES_MAX = 512 * 1024 * 1024;
29
+ var ASSET_EXPORT_MAX_ENCODED_BYTES = 100 * 1024 * 1024;
30
+ var ASSET_EXPORT_MAX_DECODED_MEGAPIXELS = 100;
31
+ var SUPPORTED_ASSET_MEDIA_TYPES = [
32
+ "image/png",
33
+ "image/jpeg",
34
+ "image/webp"
35
+ ];
36
+ function clampAssetCacheBytes(value) {
37
+ if (value === void 0 || !Number.isFinite(value)) return ASSET_CACHE_BYTES_DEFAULT;
38
+ return Math.min(ASSET_CACHE_BYTES_MAX, Math.max(ASSET_CACHE_BYTES_MIN, Math.floor(value)));
39
+ }
6
40
  //#endregion
7
- //#region src/core-internal/identifiers.ts
41
+ //#region src/core/document/identifiers.ts
8
42
  function documentId(value) {
9
43
  if (!value) throw new TypeError("DocumentId cannot be empty");
10
44
  return value;
@@ -14,7 +48,38 @@ function strokeId(value) {
14
48
  return value;
15
49
  }
16
50
  //#endregion
17
- //#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
18
83
  var CURRENT_DOCUMENT_SCHEMA_VERSION = 1;
19
84
  var DocumentRecoveryError = class extends Error {
20
85
  constructor(code, message, originalBytes, path) {
@@ -42,7 +107,7 @@ function loadDocumentBytes(originalBytes) {
42
107
  function migrateDocument(raw) {
43
108
  if (!isRecord(raw)) return failure("DOCUMENT_VALIDATION_FAILED", "Document must be an object");
44
109
  const version = raw.schemaVersion ?? 0;
45
- 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");
46
111
  if (!isJsonValue(raw)) return failure("DOCUMENT_VALIDATION_FAILED", "Document must contain JSON values only");
47
112
  const strokes = validateStrokes(raw.strokes);
48
113
  if (strokes instanceof DocumentRecoveryError) return {
@@ -55,7 +120,15 @@ function migrateDocument(raw) {
55
120
  tables: validateCollection(raw, "tables", version, validateTable),
56
121
  images: validateCollection(raw, "images", version, validateImage),
57
122
  timers: validateCollection(raw, "timers", version, validateTimer),
58
- 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)
59
132
  };
60
133
  if (collections.notes instanceof DocumentRecoveryError) return {
61
134
  ok: false,
@@ -81,18 +154,102 @@ function migrateDocument(raw) {
81
154
  ok: false,
82
155
  error: collections.customObjects
83
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
+ }
84
227
  return {
85
228
  ok: true,
86
229
  migratedFrom: version,
87
230
  document: {
88
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 } : {},
89
237
  strokes,
90
238
  notes: collections.notes,
91
239
  textBlocks: collections.textBlocks,
92
240
  tables: collections.tables,
93
241
  images: collections.images,
94
242
  timers: collections.timers,
95
- 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
96
253
  }
97
254
  };
98
255
  }
@@ -101,8 +258,19 @@ function serializeDocument(document) {
101
258
  if (!result.ok || result.migratedFrom !== 1) throw result.ok ? new DocumentRecoveryError("DOCUMENT_VALIDATION_FAILED", "Saving requires the current Document schema") : result.error;
102
259
  return JSON.stringify(result.document);
103
260
  }
104
- function validateCollection(raw, name, version, validator, requiredAtCurrentVersion = true) {
105
- 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);
106
274
  const value = raw[name] ?? [];
107
275
  if (!Array.isArray(value)) return validation(`${name} must be an array`, name);
108
276
  for (let index = 0; index < value.length; index += 1) if (!isRecord(value[index]) || !validator(value[index])) return validation(`invalid ${name} entry`, `${name}[${index}]`);
@@ -114,7 +282,20 @@ function validateStrokes(raw) {
114
282
  for (let index = 0; index < raw.length; index += 1) {
115
283
  const value = raw[index];
116
284
  const path = `strokes[${index}]`;
117
- 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`);
118
299
  const points = [];
119
300
  for (let pointIndex = 0; pointIndex < value.points.length; pointIndex += 1) {
120
301
  const point = normalizePoint(value.points[pointIndex]);
@@ -140,20 +321,62 @@ function normalizePoint(raw) {
140
321
  return values;
141
322
  }
142
323
  function validateNote(v) {
143
- 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)));
144
325
  }
145
326
  function validateText(v) {
146
- 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);
147
328
  }
148
329
  function validateTable(v) {
149
- 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);
150
331
  }
151
332
  function validateImage(v) {
152
333
  const identifiesAnImage = v.ref !== void 0 ? isAssetRef(v.ref) : nonempty(v.src);
153
- 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);
154
377
  }
155
378
  function validateTimer(v) {
156
- 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);
157
380
  }
158
381
  function validateCustomObject(v) {
159
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);
@@ -172,6 +395,9 @@ function basePosition(v) {
172
395
  function validLock(v) {
173
396
  return (v.locked === void 0 || typeof v.locked === "boolean") && optionalString(v.lockedBy) && optionalString(v.lockedByName);
174
397
  }
398
+ function validHidden(v) {
399
+ return v.hidden === void 0 || typeof v.hidden === "boolean";
400
+ }
175
401
  function validMatrix(v) {
176
402
  return v === void 0 || Array.isArray(v) && v.length === 6 && v.every(finite);
177
403
  }
@@ -222,7 +448,7 @@ function validation(message, path) {
222
448
  return new DocumentRecoveryError("DOCUMENT_VALIDATION_FAILED", message, void 0, path);
223
449
  }
224
450
  //#endregion
225
- //#region src/core-internal/affine2d.ts
451
+ //#region src/core/shapes/affine2d.ts
226
452
  var IDENTITY = [
227
453
  1,
228
454
  0,
@@ -306,7 +532,7 @@ function avgScale(m) {
306
532
  return (Math.hypot(m[0], m[1]) + Math.hypot(m[2], m[3])) / 2;
307
533
  }
308
534
  //#endregion
309
- //#region src/core-internal/boardSearch.ts
535
+ //#region src/core/document/boardSearch.ts
310
536
  function hay(s) {
311
537
  return s.toLowerCase();
312
538
  }
@@ -371,7 +597,7 @@ function searchBoard(query, board) {
371
597
  return hits;
372
598
  }
373
599
  //#endregion
374
- //#region src/core-internal/types.ts
600
+ //#region src/core/shapes/types.ts
375
601
  /** Erasure level at which a point counts as fully erased. */
376
602
  var ERASE_THRESHOLD = .95;
377
603
  function cloneStroke(stroke) {
@@ -388,16 +614,28 @@ var INK_COLORS = {
388
614
  green: "#16A34A"
389
615
  };
390
616
  var HIGHLIGHT_COLORS = {
391
- yellow: "#FDE047",
392
- green: "#86EFAC",
393
- pink: "#F9A8D4",
394
- blue: "#93C5FD"
617
+ yellow: "#FACC15",
618
+ green: "#4ADE80",
619
+ pink: "#F472B6",
620
+ blue: "#60A5FA"
395
621
  };
396
622
  var NOTE_COLORS = {
397
- yellow: "#FDE68A",
398
- pink: "#FBCFE8",
399
- blue: "#BFDBFE",
400
- 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"
401
639
  };
402
640
  var TEXT_DEFAULT_SIZE = 1.8;
403
641
  function cloneText(block) {
@@ -442,13 +680,12 @@ function measureTable(table) {
442
680
  height: table.rowHeights.reduce((sum, h) => sum + h, 0)
443
681
  };
444
682
  }
445
- var BOARD_COLOR = "#FFFFFF";
446
683
  var FOG_COLOR = "#FFFFFF";
447
684
  function cloneImage(img) {
448
685
  return { ...img };
449
686
  }
450
687
  //#endregion
451
- //#region src/core-internal/clusterStore.ts
688
+ //#region src/core/document/clusterStore.ts
452
689
  /** A stroke joins a cluster last written to within this window at full reach. */
453
690
  var RECENT_MS = 8e3;
454
691
  /** Older clusters are still joinable, at half the gap threshold. */
@@ -619,7 +856,7 @@ function clamp$1(v, lo, hi) {
619
856
  return Math.min(hi, Math.max(lo, v));
620
857
  }
621
858
  //#endregion
622
- //#region src/core-internal/extensionTypes.ts
859
+ //#region src/core/shapes/extensionTypes.ts
623
860
  function cloneCustomObject(object) {
624
861
  return {
625
862
  ...object,
@@ -629,7 +866,7 @@ function cloneCustomObject(object) {
629
866
  };
630
867
  }
631
868
  //#endregion
632
- //#region src/core-internal/kitchenTimer.ts
869
+ //#region src/core/shapes/kitchenTimer.ts
633
870
  var TIMER_DEFAULT_SIZE = 12;
634
871
  var TIMER_DEFAULT_DURATION_MS = 300 * 1e3;
635
872
  var TIMER_PRESETS_MS = [
@@ -686,7 +923,7 @@ function formatTimer(ms) {
686
923
  return `${Math.floor(total / 60)}:${(total % 60).toString().padStart(2, "0")}`;
687
924
  }
688
925
  //#endregion
689
- //#region src/core-internal/itemLock.ts
926
+ //#region src/core/shapes/itemLock.ts
690
927
  /**
691
928
  * Apply or clear a lock in place. Unlock always drops the holder so a stale
692
929
  * name cannot linger on an unlocked item.
@@ -723,7 +960,409 @@ function serializeLock(item) {
723
960
  };
724
961
  }
725
962
  //#endregion
726
- //#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
727
1366
  /**
728
1367
  * The one place a live Stroke becomes its wire representation — in
729
1368
  * particular, points collapse from {x,y,pressure,erase?} objects into
@@ -773,8 +1412,73 @@ var EMPTY_CHANGE = {
773
1412
  timersUpdated: [],
774
1413
  customObjectsAdded: [],
775
1414
  customObjectsRemoved: [],
776
- 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: []
777
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
+ ];
778
1482
  var BoardDocument = class {
779
1483
  constructor(id) {
780
1484
  this.id = id;
@@ -786,8 +1490,85 @@ var BoardDocument = class {
786
1490
  this.images = /* @__PURE__ */ new Map();
787
1491
  this.timers = /* @__PURE__ */ new Map();
788
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();
789
1501
  this.bboxes = /* @__PURE__ */ new Map();
790
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();
791
1572
  }
792
1573
  get(id) {
793
1574
  return this.strokes.get(id);
@@ -795,8 +1576,88 @@ var BoardDocument = class {
795
1576
  all() {
796
1577
  return this.strokes.values();
797
1578
  }
798
- bbox(id) {
799
- 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;
800
1661
  }
801
1662
  subscribe(listener) {
802
1663
  this.listeners.add(listener);
@@ -945,6 +1806,176 @@ var BoardDocument = class {
945
1806
  this.customObjects.set(object.id, object);
946
1807
  this.emit({ customObjectsUpdated: [object] });
947
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
+ }
948
1979
  setStrokeLocked(id, locked, by) {
949
1980
  const stroke = this.strokes.get(id);
950
1981
  if (!stroke) return;
@@ -992,8 +2023,56 @@ var BoardDocument = class {
992
2023
  applyItemLock(timer, locked, by);
993
2024
  this.emit({ timersUpdated: [timer] });
994
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
+ }
995
2074
  /** Replace all content (initial load). Does not touch `version`. */
996
- 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) {
997
2076
  const removed = [...this.strokes.keys()];
998
2077
  const notesRemoved = [...this.notes.keys()];
999
2078
  const textRemoved = [...this.texts.keys()];
@@ -1001,6 +2080,14 @@ var BoardDocument = class {
1001
2080
  const imagesRemoved = [...this.images.keys()];
1002
2081
  const timersRemoved = [...this.timers.keys()];
1003
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()];
1004
2091
  this.strokes.clear();
1005
2092
  this.notes.clear();
1006
2093
  this.texts.clear();
@@ -1008,6 +2095,14 @@ var BoardDocument = class {
1008
2095
  this.images.clear();
1009
2096
  this.timers.clear();
1010
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();
1011
2106
  this.bboxes.clear();
1012
2107
  for (const stroke of strokes) {
1013
2108
  this.strokes.set(stroke.id, stroke);
@@ -1019,6 +2114,14 @@ var BoardDocument = class {
1019
2114
  for (const img of images) this.images.set(img.id, img);
1020
2115
  for (const timer of timers) this.timers.set(timer.id, timer);
1021
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);
1022
2125
  this.emit({
1023
2126
  added: strokes,
1024
2127
  removed,
@@ -1033,8 +2136,25 @@ var BoardDocument = class {
1033
2136
  timersAdded: timers,
1034
2137
  timersRemoved,
1035
2138
  customObjectsAdded: customObjects,
1036
- 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
1037
2156
  });
2157
+ if (objectOrder) this.setOrder(objectOrder);
1038
2158
  }
1039
2159
  /** Apply incremental real-time change received from a remote collaborator over WebSocket. */
1040
2160
  applyRemoteChange(change) {
@@ -1146,6 +2266,114 @@ var BoardDocument = class {
1146
2266
  for (const object of change.customObjectsUpdated) this.customObjects.set(object.id, object);
1147
2267
  events.customObjectsUpdated = change.customObjectsUpdated;
1148
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
+ }
1149
2377
  this.emit(events);
1150
2378
  }
1151
2379
  toJSON() {
@@ -1157,12 +2385,45 @@ var BoardDocument = class {
1157
2385
  tables: [...this.tables.values()].map(cloneTable),
1158
2386
  images: [...this.images.values()].map(cloneImage),
1159
2387
  timers: [...this.timers.values()].map(cloneTimer),
1160
- 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]
1161
2398
  };
1162
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
+ }
1163
2418
  static deserializeCustomObjects(data) {
1164
2419
  return (data.customObjects ?? []).map(cloneCustomObject);
1165
2420
  }
2421
+ static deserializeRectangles(data) {
2422
+ return (data.rectangles ?? []).map(cloneRectangle);
2423
+ }
2424
+ static deserializeEllipses(data) {
2425
+ return (data.ellipses ?? []).map(cloneEllipse);
2426
+ }
1166
2427
  static deserializeImages(data) {
1167
2428
  return (data.images ?? []).map(cloneImage);
1168
2429
  }
@@ -1178,7 +2439,17 @@ var BoardDocument = class {
1178
2439
  static deserializeTables(data) {
1179
2440
  return (data.tables ?? []).map(cloneTable);
1180
2441
  }
1181
- 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) {
1182
2453
  const strokes = [];
1183
2454
  for (const s of data.strokes) try {
1184
2455
  strokes.push({
@@ -1192,11 +2463,79 @@ var BoardDocument = class {
1192
2463
  ...serializeLock(s)
1193
2464
  });
1194
2465
  } catch (err) {
1195
- console.error(`Skipping malformed stroke ${s?.id ?? "(no id)"}:`, err);
2466
+ onSkip?.(s?.id ?? "(no id)", err);
1196
2467
  }
1197
2468
  return strokes;
1198
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
+ */
1199
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
+ };
1200
2539
  const full = {
1201
2540
  ...EMPTY_CHANGE,
1202
2541
  ...change
@@ -1282,7 +2621,7 @@ function computeBBox(stroke) {
1282
2621
  return world;
1283
2622
  }
1284
2623
  //#endregion
1285
- //#region src/core-internal/commands.ts
2624
+ //#region src/core/commands/commands.ts
1286
2625
  /** One drawing action — a marker stroke, or a shape's strokes as one unit. */
1287
2626
  var AddStrokesCommand = class {
1288
2627
  constructor(strokes) {
@@ -1350,7 +2689,202 @@ var TransformCommand = class {
1350
2689
  if (changed.length) doc.transformStrokes(changed);
1351
2690
  }
1352
2691
  };
1353
- var AddNoteCommand = class {
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 {
1354
2888
  constructor(note) {
1355
2889
  this.label = "add note";
1356
2890
  this.note = cloneNote(note);
@@ -1551,6 +3085,342 @@ var DeleteTimerCommand = class {
1551
3085
  doc.addTimers([cloneTimer(this.timer)]);
1552
3086
  }
1553
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
+ };
1554
3424
  var LockItemsCommand = class {
1555
3425
  constructor(targets, targetState, actor) {
1556
3426
  this.label = "toggle lock";
@@ -1578,16 +3448,63 @@ var LockItemsCommand = class {
1578
3448
  else if (type === "table") doc.setTableLocked(id, locked, by);
1579
3449
  else if (type === "image") doc.setImageLocked(id, locked, by);
1580
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);
1581
3459
  }
1582
3460
  };
1583
3461
  //#endregion
1584
- //#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
+ };
1585
3483
  var LIMIT = 200;
1586
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
+ }
1587
3502
  constructor(doc) {
1588
3503
  this.doc = doc;
1589
3504
  this.undoStack = [];
1590
3505
  this.redoStack = [];
3506
+ this.scope = "default";
3507
+ this.scopes = /* @__PURE__ */ new Map();
1591
3508
  this.onCommand = null;
1592
3509
  this.undoing = false;
1593
3510
  }
@@ -1642,7 +3559,7 @@ var History = class {
1642
3559
  }
1643
3560
  };
1644
3561
  //#endregion
1645
- //#region src/core-internal/ops.ts
3562
+ //#region src/core/events/ops.ts
1646
3563
  var ADDED = [
1647
3564
  ["added", "strokes"],
1648
3565
  ["notesAdded", "notes"],
@@ -1650,6 +3567,14 @@ var ADDED = [
1650
3567
  ["tablesAdded", "tables"],
1651
3568
  ["imagesAdded", "images"],
1652
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"],
1653
3578
  ["customObjectsAdded", "customObjects"]
1654
3579
  ];
1655
3580
  var UPDATED = [
@@ -1660,6 +3585,14 @@ var UPDATED = [
1660
3585
  ["tablesUpdated", "tables"],
1661
3586
  ["imagesUpdated", "images"],
1662
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"],
1663
3596
  ["customObjectsUpdated", "customObjects"]
1664
3597
  ];
1665
3598
  var REMOVED = [
@@ -1669,6 +3602,14 @@ var REMOVED = [
1669
3602
  ["tablesRemoved", "tables"],
1670
3603
  ["imagesRemoved", "images"],
1671
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"],
1672
3613
  ["customObjectsRemoved", "customObjects"]
1673
3614
  ];
1674
3615
  function toWireObject(collection, object) {
@@ -1696,10 +3637,18 @@ function changeToOps(change, restoring = false) {
1696
3637
  return ops;
1697
3638
  }
1698
3639
  //#endregion
1699
- //#region src/core-internal/ribbonEdges.ts
3640
+ //#region src/core/shapes/ribbonEdges.ts
1700
3641
  var MIN_WIDTH_FACTOR = .35;
1701
3642
  var END_TAPER = .55;
1702
- 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) {
1703
3652
  const pts = points.length === 1 ? [points[0], {
1704
3653
  ...points[0],
1705
3654
  x: points[0].x + baseWidth * .05
@@ -1714,9 +3663,29 @@ function ribbonEdges(points, baseWidth) {
1714
3663
  const len = Math.hypot(dx, dy) || 1;
1715
3664
  dx /= len;
1716
3665
  dy /= len;
1717
- let width = baseWidth * (MIN_WIDTH_FACTOR + (1 - MIN_WIDTH_FACTOR) * pts[i].pressure);
1718
- if (i === 0 || i === n - 1) width *= END_TAPER;
1719
- 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;
1720
3689
  out[i] = {
1721
3690
  lx: pts[i].x - dy * hw,
1722
3691
  ly: pts[i].y + dx * hw,
@@ -1728,24 +3697,76 @@ function ribbonEdges(points, baseWidth) {
1728
3697
  return out;
1729
3698
  }
1730
3699
  //#endregion
1731
- //#region src/core-internal/spatialIndex.ts
3700
+ //#region src/core/document/spatialIndex.ts
1732
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
+ ];
1733
3742
  var SpatialIndex = class {
1734
3743
  constructor(doc) {
1735
3744
  this.doc = doc;
1736
3745
  this.cells = /* @__PURE__ */ new Map();
1737
- this.strokeCells = /* @__PURE__ */ new Map();
3746
+ this.objectCells = /* @__PURE__ */ new Map();
1738
3747
  this.unsubscribe = doc.subscribe((change) => {
1739
- for (const id of change.removed) this.remove(id);
1740
- 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);
1741
3750
  for (const stroke of change.transformed) {
1742
3751
  this.remove(stroke.id);
1743
3752
  this.insert(stroke.id);
1744
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
+ }
1745
3758
  });
1746
3759
  for (const stroke of doc.all()) this.insert(stroke.id);
1747
- }
1748
- /** 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. */
1749
3770
  query(minX, minY, maxX, maxY) {
1750
3771
  const result = /* @__PURE__ */ new Set();
1751
3772
  for (const key of cellsOf({
@@ -1762,13 +3783,13 @@ var SpatialIndex = class {
1762
3783
  dispose() {
1763
3784
  this.unsubscribe();
1764
3785
  this.cells.clear();
1765
- this.strokeCells.clear();
3786
+ this.objectCells.clear();
1766
3787
  }
1767
3788
  insert(id) {
1768
3789
  const box = this.doc.bbox(id);
1769
3790
  if (!box) return;
1770
3791
  const keys = cellsOf(box);
1771
- this.strokeCells.set(id, keys);
3792
+ this.objectCells.set(id, keys);
1772
3793
  for (const key of keys) {
1773
3794
  let bucket = this.cells.get(key);
1774
3795
  if (!bucket) {
@@ -1779,9 +3800,9 @@ var SpatialIndex = class {
1779
3800
  }
1780
3801
  }
1781
3802
  remove(id) {
1782
- const keys = this.strokeCells.get(id);
3803
+ const keys = this.objectCells.get(id);
1783
3804
  if (!keys) return;
1784
- this.strokeCells.delete(id);
3805
+ this.objectCells.delete(id);
1785
3806
  for (const key of keys) {
1786
3807
  const bucket = this.cells.get(key);
1787
3808
  if (bucket) {
@@ -1801,7 +3822,7 @@ function cellsOf(box) {
1801
3822
  return keys;
1802
3823
  }
1803
3824
  //#endregion
1804
- //#region src/core-internal/stamps.ts
3825
+ //#region src/core/shapes/stamps.ts
1805
3826
  var STAMP_SIZE = 6;
1806
3827
  var STAMPS = [
1807
3828
  {
@@ -1850,7 +3871,7 @@ function isStampKind(value) {
1850
3871
  return typeof value === "string" && STAMPS.some((s) => s.kind === value);
1851
3872
  }
1852
3873
  //#endregion
1853
- //#region src/core-internal/presence.ts
3874
+ //#region src/collaboration/presence/presence.ts
1854
3875
  /** Keep beacons clear of typical chrome at the top and bottom of the Board. */
1855
3876
  var BEACON_INSET = {
1856
3877
  top: 72,
@@ -1886,7 +3907,766 @@ function clamp(value, min, max) {
1886
3907
  return Math.min(max, Math.max(min, value));
1887
3908
  }
1888
3909
  //#endregion
1889
- //#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
1890
4670
  /** Matches the key light used by the board shader and note shadows. */
1891
4671
  var LIGHT = {
1892
4672
  x: -.25,
@@ -1896,12 +4676,19 @@ var LIGHT = {
1896
4676
  var MARGIN = 4;
1897
4677
  var NOTE_FONT_RATIO = 44 / 512;
1898
4678
  var NOTE_PAD_RATIO = 40 / 512;
4679
+ /** Matches the theme system's light-preset `boardSurface` default (theme.ts) — kept in sync manually, since core-internal stays decoupled from the theme layer. */
4680
+ var DEFAULT_BACKGROUND_COLOR = "#f5f5f3";
1899
4681
  /**
1900
4682
  * `registry` is optional and only enables rendering Custom objects through
1901
4683
  * their own `describe()` — without it (or for an object whose extension
1902
4684
  * isn't in it), Custom objects still export via the standard fallback
1903
4685
  * placeholder (`fallback.bounds`/`label`), never silently dropped.
1904
4686
  *
4687
+ * `backgroundColor` should be the Host's actual resolved `boardTheme.surface`
4688
+ * so an export matches what was on screen; defaults to the theme system's
4689
+ * own light-preset default when the caller doesn't have one on hand. The
4690
+ * board's reference grid, if any, is a screen-only aid and never exported.
4691
+ *
1905
4692
  * `resolvedAssets` (ticket #23) maps an Asset reference to an already-
1906
4693
  * resolved `data:` URI — see `assetExport.ts`'s `exportDocumentSVGWithAssets`,
1907
4694
  * which is the only intended caller that ever passes one. Without it, every
@@ -1909,22 +4696,36 @@ var NOTE_PAD_RATIO = 40 / 512;
1909
4696
  * placeholder instead of guessing at a URL; a legacy `src`-backed image is
1910
4697
  * unaffected either way.
1911
4698
  */
1912
- function documentToSVG(doc, now = 0, registry, resolvedAssets) {
4699
+ function documentToSVG(doc, now = 0, registry, backgroundColor = DEFAULT_BACKGROUND_COLOR, resolvedAssets) {
1913
4700
  const notes = doc.notes ?? [];
1914
4701
  const texts = doc.textBlocks ?? [];
1915
4702
  const tables = doc.tables ?? [];
1916
4703
  const images = doc.images ?? [];
1917
4704
  const timers = doc.timers ?? [];
1918
4705
  const customObjects = doc.customObjects ?? [];
1919
- 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);
1920
4714
  const view = `${fmt(bounds.minX)} ${fmt(bounds.minY)} ${fmt(bounds.width)} ${fmt(bounds.height)}`;
1921
4715
  const defs = notes.map(noteShadowFilter).join("\n");
1922
4716
  const highlights = doc.strokes.filter((s) => s.tool === "highlighter");
1923
4717
  const ink = doc.strokes.filter((s) => s.tool !== "highlighter");
1924
4718
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${view}">\n<defs>\n${defs}\n</defs>\n${[
1925
- `<rect x="${fmt(bounds.minX)}" y="${fmt(bounds.minY)}" width="${fmt(bounds.width)}" height="${fmt(bounds.height)}" fill="${BOARD_COLOR}"/>`,
4719
+ `<rect x="${fmt(bounds.minX)}" y="${fmt(bounds.minY)}" width="${fmt(bounds.width)}" height="${fmt(bounds.height)}" fill="${escapeXML(backgroundColor)}"/>`,
1926
4720
  ...images.map((image) => imageToElement(image, resolvedAssets)),
1927
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),
1928
4729
  ...highlights.flatMap(strokeToPaths),
1929
4730
  ...ink.flatMap(strokeToPaths),
1930
4731
  ...texts.map(textToElement),
@@ -1943,7 +4744,7 @@ function customObjectToGroup(object, registry, resolvedAssets) {
1943
4744
  const definition = registry?.objectType(object.type);
1944
4745
  if (definition) try {
1945
4746
  const scene = definition.describe(object, { selected: false });
1946
- 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>`;
1947
4748
  } catch {}
1948
4749
  return fallbackToGroup(object);
1949
4750
  }
@@ -1967,17 +4768,17 @@ function sceneNodeToSVG(node, parent, resolvedAssets) {
1967
4768
  case "rect": {
1968
4769
  const svgY = -node.y - node.height;
1969
4770
  const rx = node.cornerRadius ? ` rx="${fmt(node.cornerRadius)}"` : "";
1970
- const stroke = node.stroke ? ` stroke="${node.stroke}" stroke-width="${fmt(node.strokeWidth ?? .1)}"` : "";
1971
- 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}/>`;
1972
4773
  }
1973
4774
  case "text": {
1974
4775
  const anchor = node.align === "center" ? ` text-anchor="middle"` : node.align === "end" ? ` text-anchor="end"` : "";
1975
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("");
1976
- 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>`;
1977
4778
  }
1978
4779
  case "ellipse": {
1979
- const stroke = node.stroke ? ` stroke="${node.stroke}" stroke-width="${fmt(node.strokeWidth ?? .1)}"` : "";
1980
- 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}/>`;
1981
4782
  }
1982
4783
  case "image": {
1983
4784
  const svgY = -node.y - node.height;
@@ -2008,7 +4809,7 @@ function fallbackToGroup(object) {
2008
4809
  const label = object.fallback.label;
2009
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>` : "";
2010
4811
  return [
2011
- `<g data-id="${object.id}" data-fallback="true"${svgTransformAttr(object.transform)}>`,
4812
+ `<g data-id="${escapeXML(object.id)}" data-fallback="true"${svgTransformAttr(object.transform)}>`,
2012
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"/>`,
2013
4814
  labelEl,
2014
4815
  `</g>`
@@ -2021,7 +4822,7 @@ function strokeToPaths(stroke) {
2021
4822
  y,
2022
4823
  pressure,
2023
4824
  ...erase > 0 ? { erase } : {}
2024
- })), stroke.baseWidth);
4825
+ })), stroke.baseWidth, stroke.tool !== "shape");
2025
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])})"` : "";
2026
4827
  const baseOpacity = stroke.tool === "highlighter" ? .4 : 1;
2027
4828
  const paths = [];
@@ -2032,7 +4833,7 @@ function strokeToPaths(stroke) {
2032
4833
  const d = `M ${left.join(" L ")} L ${right.join(" L ")} Z`;
2033
4834
  const opacity = run.opacity * baseOpacity;
2034
4835
  const opacityAttr = opacity < 1 ? ` fill-opacity="${fmt(opacity)}"` : "";
2035
- 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}/>`);
2036
4837
  }
2037
4838
  return paths;
2038
4839
  }
@@ -2054,14 +4855,14 @@ function splitByOpacity(edges) {
2054
4855
  function textToElement(block) {
2055
4856
  const top = -block.y;
2056
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("");
2057
- 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>`;
2058
4859
  }
2059
4860
  function noteShadowFilter(note) {
2060
4861
  const dx = -LIGHT.x / LIGHT.z * note.zOffset;
2061
4862
  const dy = LIGHT.y / LIGHT.z * note.zOffset;
2062
4863
  const blur = (.35 + note.zOffset * .55) / 2;
2063
4864
  const opacity = Math.max(.05, .26 - note.zOffset * .028);
2064
- 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>`;
2065
4866
  }
2066
4867
  function noteToGroup(note) {
2067
4868
  const left = note.x - note.size / 2;
@@ -2071,7 +4872,7 @@ function noteToGroup(note) {
2071
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("");
2072
4873
  return [
2073
4874
  `<g>`,
2074
- `<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)})"/>`,
2075
4876
  `<text font-family="ui-rounded, 'Segoe UI', sans-serif" font-size="${fmt(fontSize)}" font-weight="500" fill="#37352F">${tspans}</text>`,
2076
4877
  `</g>`
2077
4878
  ].join("");
@@ -2102,8 +4903,8 @@ function tableToGroup(table) {
2102
4903
  const left = table.x;
2103
4904
  const top = -table.y;
2104
4905
  const headerH = table.rowHeights[0] ?? height / table.rows;
2105
- const borderColor = table.color || "#D1D5DB";
2106
- const bgColor = table.backgroundColor || "#FFFFFF";
4906
+ const borderColor = escapeXML(table.color || "#D1D5DB");
4907
+ const bgColor = escapeXML(table.backgroundColor || "#FFFFFF");
2107
4908
  const elements = [];
2108
4909
  elements.push(`<rect x="${fmt(left)}" y="${fmt(top)}" width="${fmt(width)}" height="${fmt(height)}" fill="${bgColor}" rx="0.5"/>`);
2109
4910
  elements.push(`<rect x="${fmt(left)}" y="${fmt(top)}" width="${fmt(width)}" height="${fmt(headerH)}" fill="#F3F4F6" rx="0.5"/>`);
@@ -2131,14 +4932,14 @@ function tableToGroup(table) {
2131
4932
  const padY = Math.min(.3, rowH * .12);
2132
4933
  const fontSize = Math.min(1.25, rowH * .45);
2133
4934
  const weight = r === 0 ? "bold" : "500";
2134
- const color = r === 0 ? "#111827" : table.color ?? "#374151";
4935
+ const color = escapeXML(r === 0 ? "#111827" : table.color ?? "#374151");
2135
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>`);
2136
4937
  }
2137
4938
  cellLeft += colW;
2138
4939
  }
2139
4940
  cellTop += rowH;
2140
4941
  }
2141
- 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>`;
2142
4943
  }
2143
4944
  function timerToGroup(timer, now) {
2144
4945
  const r = timer.size / 2;
@@ -2146,7 +4947,7 @@ function timerToGroup(timer, now) {
2146
4947
  const cy = -timer.y;
2147
4948
  const label = formatTimer(timerRemaining(timer, now));
2148
4949
  return [
2149
- `<g data-id="${timer.id}">`,
4950
+ `<g data-id="${escapeXML(timer.id)}">`,
2150
4951
  `<circle cx="${fmt(cx + .4)}" cy="${fmt(cy + .4)}" r="${fmt(r)}" fill="#000" fill-opacity="0.12"/>`,
2151
4952
  `<circle cx="${fmt(cx)}" cy="${fmt(cy)}" r="${fmt(r)}" fill="#E11D48"/>`,
2152
4953
  `<circle cx="${fmt(cx)}" cy="${fmt(cy)}" r="${fmt(r * .72)}" fill="#FAFAF9"/>`,
@@ -2159,12 +4960,100 @@ function imageToElement(image, resolvedAssets) {
2159
4960
  const top = -image.y - image.height / 2;
2160
4961
  if (image.ref) {
2161
4962
  const dataUri = resolvedAssets?.get(image.ref);
2162
- if (!dataUri) return `<g data-id="${image.id}" data-fallback="true">${imagePlaceholderElement(left, top, image.width, image.height, image.name)}</g>`;
2163
- 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"/>`;
2164
4965
  }
2165
4966
  return `<image href="${escapeXML(image.src)}" x="${fmt(left)}" y="${fmt(top)}" width="${fmt(image.width)}" height="${fmt(image.height)}" preserveAspectRatio="none"/>`;
2166
4967
  }
2167
- 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 = []) {
2168
5057
  let minX = Infinity;
2169
5058
  let minY = Infinity;
2170
5059
  let maxX = -Infinity;
@@ -2226,6 +5115,35 @@ function contentBounds(strokes, notes, texts, tables = [], images = [], timers =
2226
5115
  grow(p.x, -p.y);
2227
5116
  }
2228
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
+ }
2229
5147
  if (minX > maxX) return {
2230
5148
  minX: 0,
2231
5149
  minY: 0,
@@ -2250,4 +5168,4 @@ function fmt(n) {
2250
5168
  var SDK_PACKAGE_NAME = "@scrawl-board/board";
2251
5169
  var SDK_DEVELOPMENT_VERSION = "0.0.0-development";
2252
5170
  //#endregion
2253
- export { AddImageCommand, AddNoteCommand, AddStrokesCommand, AddTableCommand, AddTextCommand, AddTimerCommand, BEACON_INSET, BOARD_COLOR, 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, 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, avgScale, canUnlockItem, changeToOps, cloneImage, cloneNote, cloneStroke, cloneTable, cloneText, cloneTimer, documentId, documentToSVG, formatTimer, invert, 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 };