@scrawl-board/board 0.1.0-beta.6 → 0.1.0-beta.7

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
@@ -89,7 +89,15 @@ function migrateDocument(raw) {
89
89
  tables: validateCollection(raw, "tables", version, validateTable),
90
90
  images: validateCollection(raw, "images", version, validateImage),
91
91
  timers: validateCollection(raw, "timers", version, validateTimer),
92
- customObjects: validateCollection(raw, "customObjects", version, validateCustomObject, false)
92
+ customObjects: validateCollection(raw, "customObjects", version, validateCustomObject, false),
93
+ rectangles: validateCollection(raw, "rectangles", version, validateRectangle, false),
94
+ ellipses: validateCollection(raw, "ellipses", version, validateEllipse, false),
95
+ groups: validateCollection(raw, "groups", version, validateGroup, false),
96
+ lines: validateCollection(raw, "lines", version, validateLine, false),
97
+ arrows: validateCollection(raw, "arrows", version, validateArrow, false),
98
+ polygons: validateCollection(raw, "polygons", version, validatePolygon, false),
99
+ stars: validateCollection(raw, "stars", version, validateStar, false),
100
+ hearts: validateCollection(raw, "hearts", version, validateHeart, false)
93
101
  };
94
102
  if (collections.notes instanceof DocumentRecoveryError) return {
95
103
  ok: false,
@@ -115,6 +123,43 @@ function migrateDocument(raw) {
115
123
  ok: false,
116
124
  error: collections.customObjects
117
125
  };
126
+ if (collections.rectangles instanceof DocumentRecoveryError) return {
127
+ ok: false,
128
+ error: collections.rectangles
129
+ };
130
+ if (collections.ellipses instanceof DocumentRecoveryError) return {
131
+ ok: false,
132
+ error: collections.ellipses
133
+ };
134
+ if (collections.groups instanceof DocumentRecoveryError) return {
135
+ ok: false,
136
+ error: collections.groups
137
+ };
138
+ if (collections.lines instanceof DocumentRecoveryError) return {
139
+ ok: false,
140
+ error: collections.lines
141
+ };
142
+ if (collections.arrows instanceof DocumentRecoveryError) return {
143
+ ok: false,
144
+ error: collections.arrows
145
+ };
146
+ if (collections.polygons instanceof DocumentRecoveryError) return {
147
+ ok: false,
148
+ error: collections.polygons
149
+ };
150
+ if (collections.stars instanceof DocumentRecoveryError) return {
151
+ ok: false,
152
+ error: collections.stars
153
+ };
154
+ if (collections.hearts instanceof DocumentRecoveryError) return {
155
+ ok: false,
156
+ error: collections.hearts
157
+ };
158
+ const objectOrder = validateObjectOrder(raw.objectOrder);
159
+ if (objectOrder instanceof DocumentRecoveryError) return {
160
+ ok: false,
161
+ error: objectOrder
162
+ };
118
163
  return {
119
164
  ok: true,
120
165
  migratedFrom: version,
@@ -126,7 +171,16 @@ function migrateDocument(raw) {
126
171
  tables: collections.tables,
127
172
  images: collections.images,
128
173
  timers: collections.timers,
129
- customObjects: collections.customObjects
174
+ customObjects: collections.customObjects,
175
+ rectangles: collections.rectangles,
176
+ ellipses: collections.ellipses,
177
+ groups: collections.groups,
178
+ lines: collections.lines,
179
+ arrows: collections.arrows,
180
+ polygons: collections.polygons,
181
+ stars: collections.stars,
182
+ hearts: collections.hearts,
183
+ objectOrder
130
184
  }
131
185
  };
132
186
  }
@@ -190,6 +244,41 @@ function validateImage(v) {
190
244
  const identifiesAnImage = v.ref !== void 0 ? isAssetRef(v.ref) : nonempty(v.src);
191
245
  return basePosition(v) && identifiesAnImage && positive(v.width) && positive(v.height) && positive(v.aspectRatio) && validLock(v) && optionalString(v.name) && optionalString(v.createdAt) && optionalString(v.stamp);
192
246
  }
247
+ function validateShapeStyle(v) {
248
+ 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);
249
+ }
250
+ function validateRectangle(v) {
251
+ return basePosition(v) && positive(v.width) && positive(v.height) && validLock(v) && validateShapeStyle(v) && (v.rotation === void 0 || finite(v.rotation)) && (v.cornerRadius === void 0 || finite(v.cornerRadius) && v.cornerRadius >= 0);
252
+ }
253
+ function validateEllipse(v) {
254
+ return basePosition(v) && positive(v.width) && positive(v.height) && validLock(v) && validateShapeStyle(v) && (v.rotation === void 0 || finite(v.rotation));
255
+ }
256
+ function validatePolygon(v) {
257
+ return basePosition(v) && positive(v.width) && positive(v.height) && validLock(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);
258
+ }
259
+ function validateStar(v) {
260
+ return basePosition(v) && positive(v.width) && positive(v.height) && validLock(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;
261
+ }
262
+ function validateHeart(v) {
263
+ return basePosition(v) && positive(v.width) && positive(v.height) && validLock(v) && validateShapeStyle(v) && (v.rotation === void 0 || finite(v.rotation));
264
+ }
265
+ function validPoint(v) {
266
+ return isRecord(v) && finite(v.x) && finite(v.y);
267
+ }
268
+ function validateLine(v) {
269
+ return nonempty(v.id) && validPoint(v.start) && validPoint(v.end) && validLock(v) && validateShapeStyle(v);
270
+ }
271
+ function validateArrow(v) {
272
+ return nonempty(v.id) && validPoint(v.start) && validPoint(v.end) && validLock(v) && validateShapeStyle(v) && (v.head === void 0 || v.head === "triangle" || v.head === "none");
273
+ }
274
+ function validateGroup(v) {
275
+ return nonempty(v.id) && Array.isArray(v.children) && v.children.every((c) => nonempty(c)) && validLock(v);
276
+ }
277
+ function validateObjectOrder(v) {
278
+ if (v === void 0) return [];
279
+ if (!Array.isArray(v) || !v.every((id) => nonempty(id))) return validation("objectOrder must be an array of ids", "objectOrder");
280
+ return structuredClone(v);
281
+ }
193
282
  function validateTimer(v) {
194
283
  return basePosition(v) && positive(v.size) && positive(v.durationMs) && finite(v.remainingMs) && v.remainingMs >= 0 && (v.runningSince === void 0 || finite(v.runningSince)) && validLock(v);
195
284
  }
@@ -772,6 +861,222 @@ function serializeLock(item) {
772
861
  };
773
862
  }
774
863
  //#endregion
864
+ //#region src/core/shapes/shapeObjects.ts
865
+ var SHAPE_MIN_SIZE = .5;
866
+ var SHAPE_DEFAULT_STROKE = "#1C1C1E";
867
+ var SHAPE_DEFAULT_STROKE_WIDTH = .12;
868
+ function cloneRectangle(rect) {
869
+ return { ...rect };
870
+ }
871
+ function cloneEllipse(ellipse) {
872
+ return { ...ellipse };
873
+ }
874
+ function clonePolygon(polygon) {
875
+ return { ...polygon };
876
+ }
877
+ function cloneStar(star) {
878
+ return { ...star };
879
+ }
880
+ function cloneHeart(heart) {
881
+ return { ...heart };
882
+ }
883
+ function cloneLine(line) {
884
+ return {
885
+ ...line,
886
+ start: { ...line.start },
887
+ end: { ...line.end }
888
+ };
889
+ }
890
+ function cloneArrow(arrow) {
891
+ return {
892
+ ...arrow,
893
+ start: { ...arrow.start },
894
+ end: { ...arrow.end }
895
+ };
896
+ }
897
+ function cloneGroup(group) {
898
+ return {
899
+ ...group,
900
+ children: [...group.children]
901
+ };
902
+ }
903
+ //#endregion
904
+ //#region src/core/document/objectBounds.ts
905
+ function growAll(points) {
906
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
907
+ for (const p of points) {
908
+ if (p.x < minX) minX = p.x;
909
+ if (p.y < minY) minY = p.y;
910
+ if (p.x > maxX) maxX = p.x;
911
+ if (p.y > maxY) maxY = p.y;
912
+ }
913
+ return {
914
+ minX,
915
+ minY,
916
+ maxX,
917
+ maxY
918
+ };
919
+ }
920
+ function noteBounds(note) {
921
+ const half = note.size / 2;
922
+ return {
923
+ minX: note.x - half,
924
+ minY: note.y - half,
925
+ maxX: note.x + half,
926
+ maxY: note.y + half
927
+ };
928
+ }
929
+ /** (x, y) is the top-left corner; text flows downward via -y. */
930
+ function textBounds(block) {
931
+ const { width, height } = measureTextBlock(block.text, block.fontSize);
932
+ return {
933
+ minX: block.x,
934
+ minY: block.y - height,
935
+ maxX: block.x + width,
936
+ maxY: block.y
937
+ };
938
+ }
939
+ /** (x, y) is the top-left corner, matching TextBlock's convention. */
940
+ function tableBounds(table) {
941
+ const { width, height } = measureTable(table);
942
+ return {
943
+ minX: table.x,
944
+ minY: table.y - height,
945
+ maxX: table.x + width,
946
+ maxY: table.y
947
+ };
948
+ }
949
+ /** (x, y) is the center — ImageBlock's own, different convention. */
950
+ function imageBounds(image) {
951
+ const halfW = image.width / 2;
952
+ const halfH = image.height / 2;
953
+ return {
954
+ minX: image.x - halfW,
955
+ minY: image.y - halfH,
956
+ maxX: image.x + halfW,
957
+ maxY: image.y + halfH
958
+ };
959
+ }
960
+ function timerBounds(timer) {
961
+ const half = timer.size / 2;
962
+ return {
963
+ minX: timer.x - half,
964
+ minY: timer.y - half,
965
+ maxX: timer.x + half,
966
+ maxY: timer.y + half
967
+ };
968
+ }
969
+ /** 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. */
970
+ function customObjectBounds(object) {
971
+ const b = object.fallback.bounds;
972
+ return growAll([
973
+ {
974
+ x: b.x,
975
+ y: b.y
976
+ },
977
+ {
978
+ x: b.x + b.width,
979
+ y: b.y
980
+ },
981
+ {
982
+ x: b.x,
983
+ y: b.y + b.height
984
+ },
985
+ {
986
+ x: b.x + b.width,
987
+ y: b.y + b.height
988
+ }
989
+ ].map((corner) => apply(object.transform, corner)));
990
+ }
991
+ /**
992
+ * Shared by Rectangle/Ellipse: both use the top-edge, Y-up, extends-downward
993
+ * convention (see `shapeObjects.ts`'s own header comment), and both rotate
994
+ * (Phase 3) about the same center `(x + width/2, y - height/2)`. An ellipse's
995
+ * axis-aligned bounds equal its bounding rectangle's — an ellipse never
996
+ * exceeds the box it's inscribed in — so one corner-rotation helper covers
997
+ * both shapes exactly.
998
+ */
999
+ function topEdgeRotatedBounds(x, y, width, height, rotation) {
1000
+ if (!rotation) return {
1001
+ minX: x,
1002
+ minY: y - height,
1003
+ maxX: x + width,
1004
+ maxY: y
1005
+ };
1006
+ const m = rotationAbout(rotation, x + width / 2, y - height / 2);
1007
+ return growAll([
1008
+ {
1009
+ x,
1010
+ y
1011
+ },
1012
+ {
1013
+ x: x + width,
1014
+ y
1015
+ },
1016
+ {
1017
+ x,
1018
+ y: y - height
1019
+ },
1020
+ {
1021
+ x: x + width,
1022
+ y: y - height
1023
+ }
1024
+ ].map((corner) => apply(m, corner)));
1025
+ }
1026
+ function rectangleBounds(rect) {
1027
+ return topEdgeRotatedBounds(rect.x, rect.y, rect.width, rect.height, rect.rotation);
1028
+ }
1029
+ function ellipseBounds(ellipse) {
1030
+ return topEdgeRotatedBounds(ellipse.x, ellipse.y, ellipse.width, ellipse.height, ellipse.rotation);
1031
+ }
1032
+ /**
1033
+ * Same top-edge/Y-up/rotate-about-center convention as Rectangle — a
1034
+ * regular polygon's own outline is normalized (`polygonGeometry.ts`'s
1035
+ * `fitToBox`) to exactly fill its declared box, so this is exact at
1036
+ * rotation 0 and a safe (never-too-small) superset once rotated, matching
1037
+ * how `ellipseBounds` already treats its own inscribed shape.
1038
+ */
1039
+ function polygonBounds(polygon) {
1040
+ return topEdgeRotatedBounds(polygon.x, polygon.y, polygon.width, polygon.height, polygon.rotation);
1041
+ }
1042
+ /** Same convention and same safe-superset reasoning as `polygonBounds` — a star's points are normalized to touch its declared box (`polygonGeometry.ts`'s `starPoints`). */
1043
+ function starBounds(star) {
1044
+ return topEdgeRotatedBounds(star.x, star.y, star.width, star.height, star.rotation);
1045
+ }
1046
+ /** Same convention and same safe-superset reasoning as `polygonBounds` — a heart's curve is normalized to touch its declared box (`polygonGeometry.ts`'s `heartPoints`). */
1047
+ function heartBounds(heart) {
1048
+ return topEdgeRotatedBounds(heart.x, heart.y, heart.width, heart.height, heart.rotation);
1049
+ }
1050
+ /** No rotation concept (see `shapeObjects.ts`'s header) — the two endpoints already are the bounds. */
1051
+ function lineBounds(line) {
1052
+ return growAll([line.start, line.end]);
1053
+ }
1054
+ /** 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). */
1055
+ function arrowBounds(arrow) {
1056
+ return growAll([arrow.start, arrow.end]);
1057
+ }
1058
+ /**
1059
+ * Union of one or more child bounds, or `null` when every child resolved to
1060
+ * nothing (an empty or entirely-dangling group). Used for `GroupObject`
1061
+ * bounds — the caller (`BoardDocument.bbox`) supplies `resolve` as its own
1062
+ * `bbox(id)` so this stays a pure fold with no document dependency here.
1063
+ */
1064
+ function unionBounds(boxes) {
1065
+ let result;
1066
+ for (const box of boxes) {
1067
+ if (!box) continue;
1068
+ if (!result) {
1069
+ result = { ...box };
1070
+ continue;
1071
+ }
1072
+ result.minX = Math.min(result.minX, box.minX);
1073
+ result.minY = Math.min(result.minY, box.minY);
1074
+ result.maxX = Math.max(result.maxX, box.maxX);
1075
+ result.maxY = Math.max(result.maxY, box.maxY);
1076
+ }
1077
+ return result;
1078
+ }
1079
+ //#endregion
775
1080
  //#region src/core/document/document.ts
776
1081
  /**
777
1082
  * The one place a live Stroke becomes its wire representation — in
@@ -822,8 +1127,73 @@ var EMPTY_CHANGE = {
822
1127
  timersUpdated: [],
823
1128
  customObjectsAdded: [],
824
1129
  customObjectsRemoved: [],
825
- customObjectsUpdated: []
1130
+ customObjectsUpdated: [],
1131
+ rectanglesAdded: [],
1132
+ rectanglesRemoved: [],
1133
+ rectanglesUpdated: [],
1134
+ ellipsesAdded: [],
1135
+ ellipsesRemoved: [],
1136
+ ellipsesUpdated: [],
1137
+ groupsAdded: [],
1138
+ groupsRemoved: [],
1139
+ groupsUpdated: [],
1140
+ linesAdded: [],
1141
+ linesRemoved: [],
1142
+ linesUpdated: [],
1143
+ arrowsAdded: [],
1144
+ arrowsRemoved: [],
1145
+ arrowsUpdated: [],
1146
+ polygonsAdded: [],
1147
+ polygonsRemoved: [],
1148
+ polygonsUpdated: [],
1149
+ starsAdded: [],
1150
+ starsRemoved: [],
1151
+ starsUpdated: [],
1152
+ heartsAdded: [],
1153
+ heartsRemoved: [],
1154
+ heartsUpdated: [],
1155
+ orderChanged: []
826
1156
  };
1157
+ /**
1158
+ * Flat content types that participate in the unified paint order — see
1159
+ * `DocumentChange.orderChanged`'s own doc comment for why notes/timers are
1160
+ * excluded. Listed back-to-front in the exact sequence the old fixed
1161
+ * per-renderer Z constants already used (rectangles/ellipses lowest, then
1162
+ * tables, then images, then ink/text, then custom objects) — this is what
1163
+ * `replaceAll`/`emit`'s automatic append-on-add tracking uses to synthesize
1164
+ * a default order for a legacy document with no persisted `objectOrder`, so
1165
+ * an existing document's visual stacking never changes just from loading it.
1166
+ */
1167
+ var ORDERED_ADDED_FIELDS = [
1168
+ "rectanglesAdded",
1169
+ "ellipsesAdded",
1170
+ "tablesAdded",
1171
+ "imagesAdded",
1172
+ "added",
1173
+ "textAdded",
1174
+ "customObjectsAdded",
1175
+ "groupsAdded",
1176
+ "linesAdded",
1177
+ "arrowsAdded",
1178
+ "polygonsAdded",
1179
+ "starsAdded",
1180
+ "heartsAdded"
1181
+ ];
1182
+ var ORDERED_REMOVED_FIELDS = [
1183
+ "rectanglesRemoved",
1184
+ "ellipsesRemoved",
1185
+ "tablesRemoved",
1186
+ "imagesRemoved",
1187
+ "removed",
1188
+ "textRemoved",
1189
+ "customObjectsRemoved",
1190
+ "groupsRemoved",
1191
+ "linesRemoved",
1192
+ "arrowsRemoved",
1193
+ "polygonsRemoved",
1194
+ "starsRemoved",
1195
+ "heartsRemoved"
1196
+ ];
827
1197
  var BoardDocument = class {
828
1198
  constructor(id) {
829
1199
  this.id = id;
@@ -835,8 +1205,83 @@ var BoardDocument = class {
835
1205
  this.images = /* @__PURE__ */ new Map();
836
1206
  this.timers = /* @__PURE__ */ new Map();
837
1207
  this.customObjects = /* @__PURE__ */ new Map();
1208
+ this.rectangles = /* @__PURE__ */ new Map();
1209
+ this.ellipses = /* @__PURE__ */ new Map();
1210
+ this.groups = /* @__PURE__ */ new Map();
1211
+ this.lines = /* @__PURE__ */ new Map();
1212
+ this.arrows = /* @__PURE__ */ new Map();
1213
+ this.polygons = /* @__PURE__ */ new Map();
1214
+ this.stars = /* @__PURE__ */ new Map();
1215
+ this.hearts = /* @__PURE__ */ new Map();
838
1216
  this.bboxes = /* @__PURE__ */ new Map();
839
1217
  this.listeners = /* @__PURE__ */ new Set();
1218
+ this.objectOrder = [];
1219
+ this.orderIndex = /* @__PURE__ */ new Map();
1220
+ }
1221
+ reindexOrder() {
1222
+ this.orderIndex = new Map(this.objectOrder.map((id, index) => [id, index]));
1223
+ }
1224
+ /** The full current paint order, back to front. */
1225
+ order() {
1226
+ return this.objectOrder;
1227
+ }
1228
+ /** This object's rank in the paint order, or -1 if it doesn't participate (unknown id, a note, or a timer). */
1229
+ orderRank(id) {
1230
+ return this.orderIndex.get(id) ?? -1;
1231
+ }
1232
+ bringForward(id) {
1233
+ const i = this.orderIndex.get(id);
1234
+ if (i === void 0 || i >= this.objectOrder.length - 1) return;
1235
+ [this.objectOrder[i], this.objectOrder[i + 1]] = [this.objectOrder[i + 1], this.objectOrder[i]];
1236
+ this.reindexOrder();
1237
+ this.emit({ orderChanged: [...this.objectOrder] });
1238
+ }
1239
+ sendBackward(id) {
1240
+ const i = this.orderIndex.get(id);
1241
+ if (i === void 0 || i <= 0) return;
1242
+ [this.objectOrder[i], this.objectOrder[i - 1]] = [this.objectOrder[i - 1], this.objectOrder[i]];
1243
+ this.reindexOrder();
1244
+ this.emit({ orderChanged: [...this.objectOrder] });
1245
+ }
1246
+ bringToFront(id) {
1247
+ const i = this.orderIndex.get(id);
1248
+ if (i === void 0 || i >= this.objectOrder.length - 1) return;
1249
+ this.objectOrder.splice(i, 1);
1250
+ this.objectOrder.push(id);
1251
+ this.reindexOrder();
1252
+ this.emit({ orderChanged: [...this.objectOrder] });
1253
+ }
1254
+ sendToBack(id) {
1255
+ const i = this.orderIndex.get(id);
1256
+ if (i === void 0 || i <= 0) return;
1257
+ this.objectOrder.splice(i, 1);
1258
+ this.objectOrder.unshift(id);
1259
+ this.reindexOrder();
1260
+ this.emit({ orderChanged: [...this.objectOrder] });
1261
+ }
1262
+ /** Reorders `id` relative to its current neighbors. A no-op for an id that doesn't participate in paint order (see `orderRank`). */
1263
+ reorder(id, direction) {
1264
+ if (this.orderIndex.get(id) === void 0) return;
1265
+ if (direction === "forward") this.bringForward(id);
1266
+ else if (direction === "backward") this.sendBackward(id);
1267
+ else if (direction === "front") this.bringToFront(id);
1268
+ else this.sendToBack(id);
1269
+ }
1270
+ /**
1271
+ * Overwrites the paint order directly — used only when loading a document
1272
+ * that already carries a persisted `objectOrder`; every other order
1273
+ * mutation goes through `reorder`/the automatic append-on-add tracking in
1274
+ * `emit`. Ids not present in the document are dropped; ids present in the
1275
+ * document but missing from `order` are appended at the back, so a
1276
+ * partially-stale order (e.g. from a schema migration) never silently
1277
+ * drops an object from paint order entirely.
1278
+ */
1279
+ setOrder(order) {
1280
+ const known = new Set(this.objectOrder);
1281
+ const next = order.filter((id) => known.has(id));
1282
+ for (const id of this.objectOrder) if (!next.includes(id)) next.push(id);
1283
+ this.objectOrder = next;
1284
+ this.reindexOrder();
840
1285
  }
841
1286
  get(id) {
842
1287
  return this.strokes.get(id);
@@ -844,8 +1289,63 @@ var BoardDocument = class {
844
1289
  all() {
845
1290
  return this.strokes.values();
846
1291
  }
847
- bbox(id) {
848
- return this.bboxes.get(id);
1292
+ /**
1293
+ * World-space bounds for any content object, of any type. Strokes hit
1294
+ * their cached-on-mutation fast path (`bboxes`, populated by
1295
+ * `addStrokes`/`transformStrokes` — many points, worth caching); every
1296
+ * other type computes on demand via `objectBounds.ts` (cheap arithmetic,
1297
+ * no caching needed). A group's bounds are the union of its (recursively
1298
+ * resolved) children — `seen` guards against a cycle in nested groups.
1299
+ */
1300
+ bbox(id, seen) {
1301
+ const cached = this.bboxes.get(id);
1302
+ if (cached) return cached;
1303
+ const note = this.notes.get(id);
1304
+ if (note) return noteBounds(note);
1305
+ const text = this.texts.get(id);
1306
+ if (text) return textBounds(text);
1307
+ const table = this.tables.get(id);
1308
+ if (table) return tableBounds(table);
1309
+ const image = this.images.get(id);
1310
+ if (image) return imageBounds(image);
1311
+ const timer = this.timers.get(id);
1312
+ if (timer) return timerBounds(timer);
1313
+ const custom = this.customObjects.get(id);
1314
+ if (custom) return customObjectBounds(custom);
1315
+ const rect = this.rectangles.get(id);
1316
+ if (rect) return rectangleBounds(rect);
1317
+ const ellipse = this.ellipses.get(id);
1318
+ if (ellipse) return ellipseBounds(ellipse);
1319
+ const line = this.lines.get(id);
1320
+ if (line) return lineBounds(line);
1321
+ const arrow = this.arrows.get(id);
1322
+ if (arrow) return arrowBounds(arrow);
1323
+ const polygon = this.polygons.get(id);
1324
+ if (polygon) return polygonBounds(polygon);
1325
+ const star = this.stars.get(id);
1326
+ if (star) return starBounds(star);
1327
+ const heart = this.hearts.get(id);
1328
+ if (heart) return heartBounds(heart);
1329
+ const group = this.groups.get(id);
1330
+ if (group) {
1331
+ const visited = seen ?? /* @__PURE__ */ new Set();
1332
+ if (visited.has(id)) return void 0;
1333
+ visited.add(id);
1334
+ return unionBounds(group.children.map((childId) => this.bbox(childId, visited)));
1335
+ }
1336
+ }
1337
+ /**
1338
+ * True if `id` exists and is locked, for any type — the same per-type
1339
+ * probe pattern as `bbox`, for interactive gestures (drag/transform) that
1340
+ * need to gate on lock state regardless of what's selected. Custom
1341
+ * objects are deliberately excluded: their `lock` field is a different
1342
+ * shape (`{holderId, acquiredAt}`, no display name) with no interactive
1343
+ * lock UI yet, matching the existing, deliberate "always unlockable,
1344
+ * never gates a drag" treatment already established elsewhere (e.g.
1345
+ * `getSelectedItemInfo`'s custom branch hardcodes `isLocked: false`).
1346
+ */
1347
+ isLocked(id) {
1348
+ 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;
849
1349
  }
850
1350
  subscribe(listener) {
851
1351
  this.listeners.add(listener);
@@ -994,6 +1494,166 @@ var BoardDocument = class {
994
1494
  this.customObjects.set(object.id, object);
995
1495
  this.emit({ customObjectsUpdated: [object] });
996
1496
  }
1497
+ getRectangle(id) {
1498
+ return this.rectangles.get(id);
1499
+ }
1500
+ allRectangles() {
1501
+ return this.rectangles.values();
1502
+ }
1503
+ addRectangles(rectangles) {
1504
+ for (const rect of rectangles) this.rectangles.set(rect.id, rect);
1505
+ this.emit({ rectanglesAdded: rectangles });
1506
+ }
1507
+ removeRectangles(ids) {
1508
+ const rectanglesRemoved = [];
1509
+ for (const id of ids) if (this.rectangles.delete(id)) rectanglesRemoved.push(id);
1510
+ if (rectanglesRemoved.length) this.emit({ rectanglesRemoved });
1511
+ }
1512
+ /** Replace a rectangle's contents (move, resize, restyle) under the same id. */
1513
+ setRectangle(rect) {
1514
+ this.rectangles.set(rect.id, rect);
1515
+ this.emit({ rectanglesUpdated: [rect] });
1516
+ }
1517
+ getEllipse(id) {
1518
+ return this.ellipses.get(id);
1519
+ }
1520
+ allEllipses() {
1521
+ return this.ellipses.values();
1522
+ }
1523
+ addEllipses(ellipses) {
1524
+ for (const ellipse of ellipses) this.ellipses.set(ellipse.id, ellipse);
1525
+ this.emit({ ellipsesAdded: ellipses });
1526
+ }
1527
+ removeEllipses(ids) {
1528
+ const ellipsesRemoved = [];
1529
+ for (const id of ids) if (this.ellipses.delete(id)) ellipsesRemoved.push(id);
1530
+ if (ellipsesRemoved.length) this.emit({ ellipsesRemoved });
1531
+ }
1532
+ /** Replace an ellipse's contents (move, resize, restyle) under the same id. */
1533
+ setEllipse(ellipse) {
1534
+ this.ellipses.set(ellipse.id, ellipse);
1535
+ this.emit({ ellipsesUpdated: [ellipse] });
1536
+ }
1537
+ getGroup(id) {
1538
+ return this.groups.get(id);
1539
+ }
1540
+ allGroups() {
1541
+ return this.groups.values();
1542
+ }
1543
+ addGroups(groups) {
1544
+ for (const group of groups) this.groups.set(group.id, group);
1545
+ this.emit({ groupsAdded: groups });
1546
+ }
1547
+ removeGroups(ids) {
1548
+ const groupsRemoved = [];
1549
+ for (const id of ids) if (this.groups.delete(id)) groupsRemoved.push(id);
1550
+ if (groupsRemoved.length) this.emit({ groupsRemoved });
1551
+ }
1552
+ /** Replace a group's contents (its children list) under the same id. */
1553
+ setGroup(group) {
1554
+ this.groups.set(group.id, group);
1555
+ this.emit({ groupsUpdated: [group] });
1556
+ }
1557
+ getLine(id) {
1558
+ return this.lines.get(id);
1559
+ }
1560
+ allLines() {
1561
+ return this.lines.values();
1562
+ }
1563
+ addLines(lines) {
1564
+ for (const line of lines) this.lines.set(line.id, line);
1565
+ this.emit({ linesAdded: lines });
1566
+ }
1567
+ removeLines(ids) {
1568
+ const linesRemoved = [];
1569
+ for (const id of ids) if (this.lines.delete(id)) linesRemoved.push(id);
1570
+ if (linesRemoved.length) this.emit({ linesRemoved });
1571
+ }
1572
+ /** Replace a line's contents (move, restyle) under the same id. */
1573
+ setLine(line) {
1574
+ this.lines.set(line.id, line);
1575
+ this.emit({ linesUpdated: [line] });
1576
+ }
1577
+ getArrow(id) {
1578
+ return this.arrows.get(id);
1579
+ }
1580
+ allArrows() {
1581
+ return this.arrows.values();
1582
+ }
1583
+ addArrows(arrows) {
1584
+ for (const arrow of arrows) this.arrows.set(arrow.id, arrow);
1585
+ this.emit({ arrowsAdded: arrows });
1586
+ }
1587
+ removeArrows(ids) {
1588
+ const arrowsRemoved = [];
1589
+ for (const id of ids) if (this.arrows.delete(id)) arrowsRemoved.push(id);
1590
+ if (arrowsRemoved.length) this.emit({ arrowsRemoved });
1591
+ }
1592
+ /** Replace an arrow's contents (move, restyle, change head) under the same id. */
1593
+ setArrow(arrow) {
1594
+ this.arrows.set(arrow.id, arrow);
1595
+ this.emit({ arrowsUpdated: [arrow] });
1596
+ }
1597
+ getPolygon(id) {
1598
+ return this.polygons.get(id);
1599
+ }
1600
+ allPolygons() {
1601
+ return this.polygons.values();
1602
+ }
1603
+ addPolygons(polygons) {
1604
+ for (const polygon of polygons) this.polygons.set(polygon.id, polygon);
1605
+ this.emit({ polygonsAdded: polygons });
1606
+ }
1607
+ removePolygons(ids) {
1608
+ const polygonsRemoved = [];
1609
+ for (const id of ids) if (this.polygons.delete(id)) polygonsRemoved.push(id);
1610
+ if (polygonsRemoved.length) this.emit({ polygonsRemoved });
1611
+ }
1612
+ /** Replace a polygon's contents (move, resize, rotate, restyle) under the same id. */
1613
+ setPolygon(polygon) {
1614
+ this.polygons.set(polygon.id, polygon);
1615
+ this.emit({ polygonsUpdated: [polygon] });
1616
+ }
1617
+ getStar(id) {
1618
+ return this.stars.get(id);
1619
+ }
1620
+ allStars() {
1621
+ return this.stars.values();
1622
+ }
1623
+ addStars(stars) {
1624
+ for (const star of stars) this.stars.set(star.id, star);
1625
+ this.emit({ starsAdded: stars });
1626
+ }
1627
+ removeStars(ids) {
1628
+ const starsRemoved = [];
1629
+ for (const id of ids) if (this.stars.delete(id)) starsRemoved.push(id);
1630
+ if (starsRemoved.length) this.emit({ starsRemoved });
1631
+ }
1632
+ /** Replace a star's contents (move, resize, rotate, restyle) under the same id. */
1633
+ setStar(star) {
1634
+ this.stars.set(star.id, star);
1635
+ this.emit({ starsUpdated: [star] });
1636
+ }
1637
+ getHeart(id) {
1638
+ return this.hearts.get(id);
1639
+ }
1640
+ allHearts() {
1641
+ return this.hearts.values();
1642
+ }
1643
+ addHearts(hearts) {
1644
+ for (const heart of hearts) this.hearts.set(heart.id, heart);
1645
+ this.emit({ heartsAdded: hearts });
1646
+ }
1647
+ removeHearts(ids) {
1648
+ const heartsRemoved = [];
1649
+ for (const id of ids) if (this.hearts.delete(id)) heartsRemoved.push(id);
1650
+ if (heartsRemoved.length) this.emit({ heartsRemoved });
1651
+ }
1652
+ /** Replace a heart's contents (move, resize, rotate, restyle) under the same id. */
1653
+ setHeart(heart) {
1654
+ this.hearts.set(heart.id, heart);
1655
+ this.emit({ heartsUpdated: [heart] });
1656
+ }
997
1657
  setStrokeLocked(id, locked, by) {
998
1658
  const stroke = this.strokes.get(id);
999
1659
  if (!stroke) return;
@@ -1041,8 +1701,56 @@ var BoardDocument = class {
1041
1701
  applyItemLock(timer, locked, by);
1042
1702
  this.emit({ timersUpdated: [timer] });
1043
1703
  }
1704
+ setRectangleLocked(id, locked, by) {
1705
+ const rect = this.rectangles.get(id);
1706
+ if (!rect) return;
1707
+ applyItemLock(rect, locked, by);
1708
+ this.emit({ rectanglesUpdated: [rect] });
1709
+ }
1710
+ setEllipseLocked(id, locked, by) {
1711
+ const ellipse = this.ellipses.get(id);
1712
+ if (!ellipse) return;
1713
+ applyItemLock(ellipse, locked, by);
1714
+ this.emit({ ellipsesUpdated: [ellipse] });
1715
+ }
1716
+ setGroupLocked(id, locked, by) {
1717
+ const group = this.groups.get(id);
1718
+ if (!group) return;
1719
+ applyItemLock(group, locked, by);
1720
+ this.emit({ groupsUpdated: [group] });
1721
+ }
1722
+ setLineLocked(id, locked, by) {
1723
+ const line = this.lines.get(id);
1724
+ if (!line) return;
1725
+ applyItemLock(line, locked, by);
1726
+ this.emit({ linesUpdated: [line] });
1727
+ }
1728
+ setArrowLocked(id, locked, by) {
1729
+ const arrow = this.arrows.get(id);
1730
+ if (!arrow) return;
1731
+ applyItemLock(arrow, locked, by);
1732
+ this.emit({ arrowsUpdated: [arrow] });
1733
+ }
1734
+ setPolygonLocked(id, locked, by) {
1735
+ const polygon = this.polygons.get(id);
1736
+ if (!polygon) return;
1737
+ applyItemLock(polygon, locked, by);
1738
+ this.emit({ polygonsUpdated: [polygon] });
1739
+ }
1740
+ setStarLocked(id, locked, by) {
1741
+ const star = this.stars.get(id);
1742
+ if (!star) return;
1743
+ applyItemLock(star, locked, by);
1744
+ this.emit({ starsUpdated: [star] });
1745
+ }
1746
+ setHeartLocked(id, locked, by) {
1747
+ const heart = this.hearts.get(id);
1748
+ if (!heart) return;
1749
+ applyItemLock(heart, locked, by);
1750
+ this.emit({ heartsUpdated: [heart] });
1751
+ }
1044
1752
  /** Replace all content (initial load). Does not touch `version`. */
1045
- replaceAll(strokes, notes, texts, tables = [], images = [], timers = [], customObjects = []) {
1753
+ replaceAll(strokes, notes, texts, tables = [], images = [], timers = [], customObjects = [], rectangles = [], ellipses = [], groups = [], lines = [], arrows = [], polygons = [], stars = [], hearts = [], objectOrder) {
1046
1754
  const removed = [...this.strokes.keys()];
1047
1755
  const notesRemoved = [...this.notes.keys()];
1048
1756
  const textRemoved = [...this.texts.keys()];
@@ -1050,6 +1758,14 @@ var BoardDocument = class {
1050
1758
  const imagesRemoved = [...this.images.keys()];
1051
1759
  const timersRemoved = [...this.timers.keys()];
1052
1760
  const customObjectsRemoved = [...this.customObjects.keys()];
1761
+ const rectanglesRemoved = [...this.rectangles.keys()];
1762
+ const ellipsesRemoved = [...this.ellipses.keys()];
1763
+ const groupsRemoved = [...this.groups.keys()];
1764
+ const linesRemoved = [...this.lines.keys()];
1765
+ const arrowsRemoved = [...this.arrows.keys()];
1766
+ const polygonsRemoved = [...this.polygons.keys()];
1767
+ const starsRemoved = [...this.stars.keys()];
1768
+ const heartsRemoved = [...this.hearts.keys()];
1053
1769
  this.strokes.clear();
1054
1770
  this.notes.clear();
1055
1771
  this.texts.clear();
@@ -1057,6 +1773,14 @@ var BoardDocument = class {
1057
1773
  this.images.clear();
1058
1774
  this.timers.clear();
1059
1775
  this.customObjects.clear();
1776
+ this.rectangles.clear();
1777
+ this.ellipses.clear();
1778
+ this.groups.clear();
1779
+ this.lines.clear();
1780
+ this.arrows.clear();
1781
+ this.polygons.clear();
1782
+ this.stars.clear();
1783
+ this.hearts.clear();
1060
1784
  this.bboxes.clear();
1061
1785
  for (const stroke of strokes) {
1062
1786
  this.strokes.set(stroke.id, stroke);
@@ -1068,6 +1792,14 @@ var BoardDocument = class {
1068
1792
  for (const img of images) this.images.set(img.id, img);
1069
1793
  for (const timer of timers) this.timers.set(timer.id, timer);
1070
1794
  for (const object of customObjects) this.customObjects.set(object.id, object);
1795
+ for (const rect of rectangles) this.rectangles.set(rect.id, rect);
1796
+ for (const ellipse of ellipses) this.ellipses.set(ellipse.id, ellipse);
1797
+ for (const group of groups) this.groups.set(group.id, group);
1798
+ for (const line of lines) this.lines.set(line.id, line);
1799
+ for (const arrow of arrows) this.arrows.set(arrow.id, arrow);
1800
+ for (const polygon of polygons) this.polygons.set(polygon.id, polygon);
1801
+ for (const star of stars) this.stars.set(star.id, star);
1802
+ for (const heart of hearts) this.hearts.set(heart.id, heart);
1071
1803
  this.emit({
1072
1804
  added: strokes,
1073
1805
  removed,
@@ -1082,8 +1814,25 @@ var BoardDocument = class {
1082
1814
  timersAdded: timers,
1083
1815
  timersRemoved,
1084
1816
  customObjectsAdded: customObjects,
1085
- customObjectsRemoved
1817
+ customObjectsRemoved,
1818
+ rectanglesAdded: rectangles,
1819
+ rectanglesRemoved,
1820
+ ellipsesAdded: ellipses,
1821
+ ellipsesRemoved,
1822
+ groupsAdded: groups,
1823
+ groupsRemoved,
1824
+ linesAdded: lines,
1825
+ linesRemoved,
1826
+ arrowsAdded: arrows,
1827
+ arrowsRemoved,
1828
+ polygonsAdded: polygons,
1829
+ polygonsRemoved,
1830
+ starsAdded: stars,
1831
+ starsRemoved,
1832
+ heartsAdded: hearts,
1833
+ heartsRemoved
1086
1834
  });
1835
+ if (objectOrder) this.setOrder(objectOrder);
1087
1836
  }
1088
1837
  /** Apply incremental real-time change received from a remote collaborator over WebSocket. */
1089
1838
  applyRemoteChange(change) {
@@ -1195,6 +1944,114 @@ var BoardDocument = class {
1195
1944
  for (const object of change.customObjectsUpdated) this.customObjects.set(object.id, object);
1196
1945
  events.customObjectsUpdated = change.customObjectsUpdated;
1197
1946
  }
1947
+ if (change.rectanglesAdded && change.rectanglesAdded.length > 0) {
1948
+ for (const rect of change.rectanglesAdded) this.rectangles.set(rect.id, rect);
1949
+ events.rectanglesAdded = change.rectanglesAdded;
1950
+ }
1951
+ if (change.rectanglesRemoved && change.rectanglesRemoved.length > 0) {
1952
+ const rectanglesRemoved = [];
1953
+ for (const id of change.rectanglesRemoved) if (this.rectangles.delete(id)) rectanglesRemoved.push(id);
1954
+ if (rectanglesRemoved.length > 0) events.rectanglesRemoved = rectanglesRemoved;
1955
+ }
1956
+ if (change.rectanglesUpdated && change.rectanglesUpdated.length > 0) {
1957
+ for (const rect of change.rectanglesUpdated) this.rectangles.set(rect.id, rect);
1958
+ events.rectanglesUpdated = change.rectanglesUpdated;
1959
+ }
1960
+ if (change.ellipsesAdded && change.ellipsesAdded.length > 0) {
1961
+ for (const ellipse of change.ellipsesAdded) this.ellipses.set(ellipse.id, ellipse);
1962
+ events.ellipsesAdded = change.ellipsesAdded;
1963
+ }
1964
+ if (change.ellipsesRemoved && change.ellipsesRemoved.length > 0) {
1965
+ const ellipsesRemoved = [];
1966
+ for (const id of change.ellipsesRemoved) if (this.ellipses.delete(id)) ellipsesRemoved.push(id);
1967
+ if (ellipsesRemoved.length > 0) events.ellipsesRemoved = ellipsesRemoved;
1968
+ }
1969
+ if (change.ellipsesUpdated && change.ellipsesUpdated.length > 0) {
1970
+ for (const ellipse of change.ellipsesUpdated) this.ellipses.set(ellipse.id, ellipse);
1971
+ events.ellipsesUpdated = change.ellipsesUpdated;
1972
+ }
1973
+ if (change.groupsAdded && change.groupsAdded.length > 0) {
1974
+ for (const group of change.groupsAdded) this.groups.set(group.id, group);
1975
+ events.groupsAdded = change.groupsAdded;
1976
+ }
1977
+ if (change.groupsRemoved && change.groupsRemoved.length > 0) {
1978
+ const groupsRemoved = [];
1979
+ for (const id of change.groupsRemoved) if (this.groups.delete(id)) groupsRemoved.push(id);
1980
+ if (groupsRemoved.length > 0) events.groupsRemoved = groupsRemoved;
1981
+ }
1982
+ if (change.groupsUpdated && change.groupsUpdated.length > 0) {
1983
+ for (const group of change.groupsUpdated) this.groups.set(group.id, group);
1984
+ events.groupsUpdated = change.groupsUpdated;
1985
+ }
1986
+ if (change.linesAdded && change.linesAdded.length > 0) {
1987
+ for (const line of change.linesAdded) this.lines.set(line.id, line);
1988
+ events.linesAdded = change.linesAdded;
1989
+ }
1990
+ if (change.linesRemoved && change.linesRemoved.length > 0) {
1991
+ const linesRemoved = [];
1992
+ for (const id of change.linesRemoved) if (this.lines.delete(id)) linesRemoved.push(id);
1993
+ if (linesRemoved.length > 0) events.linesRemoved = linesRemoved;
1994
+ }
1995
+ if (change.linesUpdated && change.linesUpdated.length > 0) {
1996
+ for (const line of change.linesUpdated) this.lines.set(line.id, line);
1997
+ events.linesUpdated = change.linesUpdated;
1998
+ }
1999
+ if (change.arrowsAdded && change.arrowsAdded.length > 0) {
2000
+ for (const arrow of change.arrowsAdded) this.arrows.set(arrow.id, arrow);
2001
+ events.arrowsAdded = change.arrowsAdded;
2002
+ }
2003
+ if (change.arrowsRemoved && change.arrowsRemoved.length > 0) {
2004
+ const arrowsRemoved = [];
2005
+ for (const id of change.arrowsRemoved) if (this.arrows.delete(id)) arrowsRemoved.push(id);
2006
+ if (arrowsRemoved.length > 0) events.arrowsRemoved = arrowsRemoved;
2007
+ }
2008
+ if (change.arrowsUpdated && change.arrowsUpdated.length > 0) {
2009
+ for (const arrow of change.arrowsUpdated) this.arrows.set(arrow.id, arrow);
2010
+ events.arrowsUpdated = change.arrowsUpdated;
2011
+ }
2012
+ if (change.polygonsAdded && change.polygonsAdded.length > 0) {
2013
+ for (const polygon of change.polygonsAdded) this.polygons.set(polygon.id, polygon);
2014
+ events.polygonsAdded = change.polygonsAdded;
2015
+ }
2016
+ if (change.polygonsRemoved && change.polygonsRemoved.length > 0) {
2017
+ const polygonsRemoved = [];
2018
+ for (const id of change.polygonsRemoved) if (this.polygons.delete(id)) polygonsRemoved.push(id);
2019
+ if (polygonsRemoved.length > 0) events.polygonsRemoved = polygonsRemoved;
2020
+ }
2021
+ if (change.polygonsUpdated && change.polygonsUpdated.length > 0) {
2022
+ for (const polygon of change.polygonsUpdated) this.polygons.set(polygon.id, polygon);
2023
+ events.polygonsUpdated = change.polygonsUpdated;
2024
+ }
2025
+ if (change.starsAdded && change.starsAdded.length > 0) {
2026
+ for (const star of change.starsAdded) this.stars.set(star.id, star);
2027
+ events.starsAdded = change.starsAdded;
2028
+ }
2029
+ if (change.starsRemoved && change.starsRemoved.length > 0) {
2030
+ const starsRemoved = [];
2031
+ for (const id of change.starsRemoved) if (this.stars.delete(id)) starsRemoved.push(id);
2032
+ if (starsRemoved.length > 0) events.starsRemoved = starsRemoved;
2033
+ }
2034
+ if (change.starsUpdated && change.starsUpdated.length > 0) {
2035
+ for (const star of change.starsUpdated) this.stars.set(star.id, star);
2036
+ events.starsUpdated = change.starsUpdated;
2037
+ }
2038
+ if (change.heartsAdded && change.heartsAdded.length > 0) {
2039
+ for (const heart of change.heartsAdded) this.hearts.set(heart.id, heart);
2040
+ events.heartsAdded = change.heartsAdded;
2041
+ }
2042
+ if (change.heartsRemoved && change.heartsRemoved.length > 0) {
2043
+ const heartsRemoved = [];
2044
+ for (const id of change.heartsRemoved) if (this.hearts.delete(id)) heartsRemoved.push(id);
2045
+ if (heartsRemoved.length > 0) events.heartsRemoved = heartsRemoved;
2046
+ }
2047
+ if (change.heartsUpdated && change.heartsUpdated.length > 0) {
2048
+ for (const heart of change.heartsUpdated) this.hearts.set(heart.id, heart);
2049
+ events.heartsUpdated = change.heartsUpdated;
2050
+ }
2051
+ if (change.orderChanged && change.orderChanged.length > 0) {
2052
+ this.setOrder(change.orderChanged);
2053
+ events.orderChanged = change.orderChanged;
2054
+ }
1198
2055
  this.emit(events);
1199
2056
  }
1200
2057
  toJSON() {
@@ -1206,12 +2063,45 @@ var BoardDocument = class {
1206
2063
  tables: [...this.tables.values()].map(cloneTable),
1207
2064
  images: [...this.images.values()].map(cloneImage),
1208
2065
  timers: [...this.timers.values()].map(cloneTimer),
1209
- customObjects: [...this.customObjects.values()].map(cloneCustomObject)
2066
+ customObjects: [...this.customObjects.values()].map(cloneCustomObject),
2067
+ rectangles: [...this.rectangles.values()].map(cloneRectangle),
2068
+ ellipses: [...this.ellipses.values()].map(cloneEllipse),
2069
+ groups: [...this.groups.values()].map(cloneGroup),
2070
+ lines: [...this.lines.values()].map(cloneLine),
2071
+ arrows: [...this.arrows.values()].map(cloneArrow),
2072
+ polygons: [...this.polygons.values()].map(clonePolygon),
2073
+ stars: [...this.stars.values()].map(cloneStar),
2074
+ hearts: [...this.hearts.values()].map(cloneHeart),
2075
+ objectOrder: [...this.objectOrder]
1210
2076
  };
1211
2077
  }
2078
+ static deserializeGroups(data) {
2079
+ return (data.groups ?? []).map(cloneGroup);
2080
+ }
2081
+ static deserializeLines(data) {
2082
+ return (data.lines ?? []).map(cloneLine);
2083
+ }
2084
+ static deserializeArrows(data) {
2085
+ return (data.arrows ?? []).map(cloneArrow);
2086
+ }
2087
+ static deserializePolygons(data) {
2088
+ return (data.polygons ?? []).map(clonePolygon);
2089
+ }
2090
+ static deserializeStars(data) {
2091
+ return (data.stars ?? []).map(cloneStar);
2092
+ }
2093
+ static deserializeHearts(data) {
2094
+ return (data.hearts ?? []).map(cloneHeart);
2095
+ }
1212
2096
  static deserializeCustomObjects(data) {
1213
2097
  return (data.customObjects ?? []).map(cloneCustomObject);
1214
2098
  }
2099
+ static deserializeRectangles(data) {
2100
+ return (data.rectangles ?? []).map(cloneRectangle);
2101
+ }
2102
+ static deserializeEllipses(data) {
2103
+ return (data.ellipses ?? []).map(cloneEllipse);
2104
+ }
1215
2105
  static deserializeImages(data) {
1216
2106
  return (data.images ?? []).map(cloneImage);
1217
2107
  }
@@ -1245,7 +2135,56 @@ var BoardDocument = class {
1245
2135
  }
1246
2136
  return strokes;
1247
2137
  }
2138
+ /**
2139
+ * Every mutation funnels through here, so paint-order tracking lives in
2140
+ * exactly one place rather than at every individual add/remove call site
2141
+ * (18 of them, times `replaceAll`/`applyRemoteChange`) — new ids are
2142
+ * appended to the back (front-most) of `objectOrder`, removed ids are
2143
+ * spliced out. An explicit reorder (`reorder`/`setOrder`) updates
2144
+ * `objectOrder` itself before calling this, so this step is a no-op for
2145
+ * ids already tracked (idempotent by construction: `orderIndex.has` gates
2146
+ * every append).
2147
+ *
2148
+ * `orderChanged` is populated here whenever `objectOrder` actually
2149
+ * changed — not just for an explicit reorder, but for any add/remove too.
2150
+ * A pure append never shifts an existing id's rank (new ids land at the
2151
+ * tail), but a removal splices a middle id out, which *does* shift every
2152
+ * id after it down by one — a renderer that only resynced Z on an
2153
+ * explicit reorder would silently render those with a stale rank until
2154
+ * something else happened to touch them, eventually colliding with a
2155
+ * freshly-added object's freshly-computed Z. Firing this on every
2156
+ * order-touching change (not just removes) is simpler than special-
2157
+ * casing which kind of change actually needs it, at the cost of a
2158
+ * redundant same-value resync on a pure append.
2159
+ */
1248
2160
  emit(change) {
2161
+ let orderTouched = false;
2162
+ for (const field of ORDERED_ADDED_FIELDS) {
2163
+ const objects = change[field];
2164
+ if (!objects) continue;
2165
+ for (const object of objects) if (!this.orderIndex.has(object.id)) {
2166
+ this.objectOrder.push(object.id);
2167
+ orderTouched = true;
2168
+ }
2169
+ }
2170
+ for (const field of ORDERED_REMOVED_FIELDS) {
2171
+ const ids = change[field];
2172
+ if (!ids) continue;
2173
+ for (const id of ids) {
2174
+ const index = this.objectOrder.indexOf(id);
2175
+ if (index !== -1) {
2176
+ this.objectOrder.splice(index, 1);
2177
+ orderTouched = true;
2178
+ }
2179
+ }
2180
+ }
2181
+ if (orderTouched) {
2182
+ this.reindexOrder();
2183
+ change = {
2184
+ ...change,
2185
+ orderChanged: [...this.objectOrder]
2186
+ };
2187
+ }
1249
2188
  const full = {
1250
2189
  ...EMPTY_CHANGE,
1251
2190
  ...change
@@ -1399,6 +2338,200 @@ var TransformCommand = class {
1399
2338
  if (changed.length) doc.transformStrokes(changed);
1400
2339
  }
1401
2340
  };
2341
+ /**
2342
+ * One move gesture over a mixed-type selection (Phase 3) — the general
2343
+ * successor to `TransformCommand` for translation. `TransformCommand`
2344
+ * itself stays as-is (still used for scale/rotate, which remain stroke-only
2345
+ * this phase — see `interaction/tools/selectTool.ts`'s `TransformState`):
2346
+ * its own per-id `doc.get(id)` check already no-ops safely for any id that
2347
+ * isn't a stroke, so it doesn't need touching for that narrower case.
2348
+ *
2349
+ * `delta` here is always a pure translation (never scale/rotate), so
2350
+ * `apply(delta, point)` is a safe, uniform way to move every position-only
2351
+ * type's `x`/`y` — translating commutes trivially regardless of a shape's
2352
+ * own rotation. Matrix-carrying types (stroke, Custom) instead compose
2353
+ * `delta` onto their existing matrix/transform, matching `TransformCommand`.
2354
+ *
2355
+ * A group id in `ids` is expanded into its (recursively resolved, cycle-
2356
+ * safe) children every time `compose` runs — deterministic, since group
2357
+ * membership never changes mid-command — so moving a selected group means
2358
+ * moving every one of its members by the same delta. This expansion is
2359
+ * `TransformObjectsCommand`'s own responsibility precisely so a caller that
2360
+ * doesn't itself expand groups (e.g. `ScrawlEngine.nudgeSelection`) still
2361
+ * gets correct behavior; `interaction/tools/selectTool.ts`'s `TransformState`
2362
+ * separately expands for its own reason (live per-child drag preview), which
2363
+ * makes this a no-op re-expansion for that caller, not a conflict.
2364
+ */
2365
+ var TransformObjectsCommand = class {
2366
+ constructor(ids, delta) {
2367
+ this.ids = ids;
2368
+ this.delta = delta;
2369
+ this.label = "move selection";
2370
+ this.inverse = invert(delta);
2371
+ }
2372
+ apply(doc) {
2373
+ this.compose(doc, this.delta);
2374
+ }
2375
+ revert(doc) {
2376
+ this.compose(doc, this.inverse);
2377
+ }
2378
+ compose(doc, m) {
2379
+ const resolved = /* @__PURE__ */ new Set();
2380
+ const expand = (id, seen) => {
2381
+ if (seen.has(id)) return;
2382
+ seen.add(id);
2383
+ const group = doc.getGroup(id);
2384
+ if (group) {
2385
+ for (const childId of group.children) expand(childId, seen);
2386
+ return;
2387
+ }
2388
+ resolved.add(id);
2389
+ };
2390
+ for (const id of this.ids) expand(id, /* @__PURE__ */ new Set());
2391
+ const changedStrokes = [];
2392
+ for (const id of resolved) {
2393
+ const stroke = doc.get(id);
2394
+ if (stroke) {
2395
+ stroke.matrix = mul(m, stroke.matrix ?? [
2396
+ 1,
2397
+ 0,
2398
+ 0,
2399
+ 1,
2400
+ 0,
2401
+ 0
2402
+ ]);
2403
+ changedStrokes.push(stroke);
2404
+ continue;
2405
+ }
2406
+ const custom = doc.getCustomObject(id);
2407
+ if (custom) {
2408
+ doc.setCustomObject({
2409
+ ...custom,
2410
+ transform: mul(m, custom.transform)
2411
+ });
2412
+ continue;
2413
+ }
2414
+ const note = doc.getNote(id);
2415
+ if (note) {
2416
+ const p = apply(m, note);
2417
+ doc.setNote({
2418
+ ...note,
2419
+ x: p.x,
2420
+ y: p.y
2421
+ });
2422
+ continue;
2423
+ }
2424
+ const text = doc.getText(id);
2425
+ if (text) {
2426
+ const p = apply(m, text);
2427
+ doc.setText({
2428
+ ...text,
2429
+ x: p.x,
2430
+ y: p.y
2431
+ });
2432
+ continue;
2433
+ }
2434
+ const table = doc.getTable(id);
2435
+ if (table) {
2436
+ const p = apply(m, table);
2437
+ doc.setTable({
2438
+ ...table,
2439
+ x: p.x,
2440
+ y: p.y
2441
+ });
2442
+ continue;
2443
+ }
2444
+ const image = doc.getImage(id);
2445
+ if (image) {
2446
+ const p = apply(m, image);
2447
+ doc.setImage({
2448
+ ...image,
2449
+ x: p.x,
2450
+ y: p.y
2451
+ });
2452
+ continue;
2453
+ }
2454
+ const timer = doc.getTimer(id);
2455
+ if (timer) {
2456
+ const p = apply(m, timer);
2457
+ doc.setTimer({
2458
+ ...timer,
2459
+ x: p.x,
2460
+ y: p.y
2461
+ });
2462
+ continue;
2463
+ }
2464
+ const rect = doc.getRectangle(id);
2465
+ if (rect) {
2466
+ const p = apply(m, rect);
2467
+ doc.setRectangle({
2468
+ ...rect,
2469
+ x: p.x,
2470
+ y: p.y
2471
+ });
2472
+ continue;
2473
+ }
2474
+ const ellipse = doc.getEllipse(id);
2475
+ if (ellipse) {
2476
+ const p = apply(m, ellipse);
2477
+ doc.setEllipse({
2478
+ ...ellipse,
2479
+ x: p.x,
2480
+ y: p.y
2481
+ });
2482
+ continue;
2483
+ }
2484
+ const line = doc.getLine(id);
2485
+ if (line) {
2486
+ doc.setLine({
2487
+ ...line,
2488
+ start: apply(m, line.start),
2489
+ end: apply(m, line.end)
2490
+ });
2491
+ continue;
2492
+ }
2493
+ const arrow = doc.getArrow(id);
2494
+ if (arrow) {
2495
+ doc.setArrow({
2496
+ ...arrow,
2497
+ start: apply(m, arrow.start),
2498
+ end: apply(m, arrow.end)
2499
+ });
2500
+ continue;
2501
+ }
2502
+ const polygon = doc.getPolygon(id);
2503
+ if (polygon) {
2504
+ const p = apply(m, polygon);
2505
+ doc.setPolygon({
2506
+ ...polygon,
2507
+ x: p.x,
2508
+ y: p.y
2509
+ });
2510
+ continue;
2511
+ }
2512
+ const star = doc.getStar(id);
2513
+ if (star) {
2514
+ const p = apply(m, star);
2515
+ doc.setStar({
2516
+ ...star,
2517
+ x: p.x,
2518
+ y: p.y
2519
+ });
2520
+ continue;
2521
+ }
2522
+ const heart = doc.getHeart(id);
2523
+ if (heart) {
2524
+ const p = apply(m, heart);
2525
+ doc.setHeart({
2526
+ ...heart,
2527
+ x: p.x,
2528
+ y: p.y
2529
+ });
2530
+ }
2531
+ }
2532
+ if (changedStrokes.length) doc.transformStrokes(changedStrokes);
2533
+ }
2534
+ };
1402
2535
  var AddNoteCommand = class {
1403
2536
  constructor(note) {
1404
2537
  this.label = "add note";
@@ -1600,6 +2733,342 @@ var DeleteTimerCommand = class {
1600
2733
  doc.addTimers([cloneTimer(this.timer)]);
1601
2734
  }
1602
2735
  };
2736
+ /** Add/update/delete for semantic Rectangle objects (Phase 2). */
2737
+ var AddRectangleCommand = class {
2738
+ constructor(rect) {
2739
+ this.label = "add rectangle";
2740
+ this.rect = cloneRectangle(rect);
2741
+ }
2742
+ apply(doc) {
2743
+ doc.addRectangles([cloneRectangle(this.rect)]);
2744
+ }
2745
+ revert(doc) {
2746
+ doc.removeRectangles([this.rect.id]);
2747
+ }
2748
+ };
2749
+ var UpdateRectangleCommand = class {
2750
+ constructor(before, after) {
2751
+ this.label = "update rectangle";
2752
+ this.before = cloneRectangle(before);
2753
+ this.after = cloneRectangle(after);
2754
+ }
2755
+ apply(doc) {
2756
+ doc.setRectangle(cloneRectangle(this.after));
2757
+ }
2758
+ revert(doc) {
2759
+ doc.setRectangle(cloneRectangle(this.before));
2760
+ }
2761
+ };
2762
+ var DeleteRectangleCommand = class {
2763
+ constructor(rect) {
2764
+ this.label = "delete rectangle";
2765
+ this.rect = cloneRectangle(rect);
2766
+ }
2767
+ apply(doc) {
2768
+ doc.removeRectangles([this.rect.id]);
2769
+ }
2770
+ revert(doc) {
2771
+ doc.addRectangles([cloneRectangle(this.rect)]);
2772
+ }
2773
+ };
2774
+ /** Add/update/delete for semantic Ellipse objects (Phase 2). */
2775
+ var AddEllipseCommand = class {
2776
+ constructor(ellipse) {
2777
+ this.label = "add ellipse";
2778
+ this.ellipse = cloneEllipse(ellipse);
2779
+ }
2780
+ apply(doc) {
2781
+ doc.addEllipses([cloneEllipse(this.ellipse)]);
2782
+ }
2783
+ revert(doc) {
2784
+ doc.removeEllipses([this.ellipse.id]);
2785
+ }
2786
+ };
2787
+ var UpdateEllipseCommand = class {
2788
+ constructor(before, after) {
2789
+ this.label = "update ellipse";
2790
+ this.before = cloneEllipse(before);
2791
+ this.after = cloneEllipse(after);
2792
+ }
2793
+ apply(doc) {
2794
+ doc.setEllipse(cloneEllipse(this.after));
2795
+ }
2796
+ revert(doc) {
2797
+ doc.setEllipse(cloneEllipse(this.before));
2798
+ }
2799
+ };
2800
+ var DeleteEllipseCommand = class {
2801
+ constructor(ellipse) {
2802
+ this.label = "delete ellipse";
2803
+ this.ellipse = cloneEllipse(ellipse);
2804
+ }
2805
+ apply(doc) {
2806
+ doc.removeEllipses([this.ellipse.id]);
2807
+ }
2808
+ revert(doc) {
2809
+ doc.addEllipses([cloneEllipse(this.ellipse)]);
2810
+ }
2811
+ };
2812
+ /** Add/update/delete for semantic Line objects (Phase 4). */
2813
+ var AddLineCommand = class {
2814
+ constructor(line) {
2815
+ this.label = "add line";
2816
+ this.line = cloneLine(line);
2817
+ }
2818
+ apply(doc) {
2819
+ doc.addLines([cloneLine(this.line)]);
2820
+ }
2821
+ revert(doc) {
2822
+ doc.removeLines([this.line.id]);
2823
+ }
2824
+ };
2825
+ var UpdateLineCommand = class {
2826
+ constructor(before, after) {
2827
+ this.label = "update line";
2828
+ this.before = cloneLine(before);
2829
+ this.after = cloneLine(after);
2830
+ }
2831
+ apply(doc) {
2832
+ doc.setLine(cloneLine(this.after));
2833
+ }
2834
+ revert(doc) {
2835
+ doc.setLine(cloneLine(this.before));
2836
+ }
2837
+ };
2838
+ var DeleteLineCommand = class {
2839
+ constructor(line) {
2840
+ this.label = "delete line";
2841
+ this.line = cloneLine(line);
2842
+ }
2843
+ apply(doc) {
2844
+ doc.removeLines([this.line.id]);
2845
+ }
2846
+ revert(doc) {
2847
+ doc.addLines([cloneLine(this.line)]);
2848
+ }
2849
+ };
2850
+ /** Add/update/delete for semantic Arrow objects (Phase 4). */
2851
+ var AddArrowCommand = class {
2852
+ constructor(arrow) {
2853
+ this.label = "add arrow";
2854
+ this.arrow = cloneArrow(arrow);
2855
+ }
2856
+ apply(doc) {
2857
+ doc.addArrows([cloneArrow(this.arrow)]);
2858
+ }
2859
+ revert(doc) {
2860
+ doc.removeArrows([this.arrow.id]);
2861
+ }
2862
+ };
2863
+ var UpdateArrowCommand = class {
2864
+ constructor(before, after) {
2865
+ this.label = "update arrow";
2866
+ this.before = cloneArrow(before);
2867
+ this.after = cloneArrow(after);
2868
+ }
2869
+ apply(doc) {
2870
+ doc.setArrow(cloneArrow(this.after));
2871
+ }
2872
+ revert(doc) {
2873
+ doc.setArrow(cloneArrow(this.before));
2874
+ }
2875
+ };
2876
+ var DeleteArrowCommand = class {
2877
+ constructor(arrow) {
2878
+ this.label = "delete arrow";
2879
+ this.arrow = cloneArrow(arrow);
2880
+ }
2881
+ apply(doc) {
2882
+ doc.removeArrows([this.arrow.id]);
2883
+ }
2884
+ revert(doc) {
2885
+ doc.addArrows([cloneArrow(this.arrow)]);
2886
+ }
2887
+ };
2888
+ /** Add/update/delete for semantic Polygon objects (Phase 4) — Triangle/Diamond/Pentagon/Hexagon/Octagon. */
2889
+ var AddPolygonCommand = class {
2890
+ constructor(polygon) {
2891
+ this.label = "add polygon";
2892
+ this.polygon = clonePolygon(polygon);
2893
+ }
2894
+ apply(doc) {
2895
+ doc.addPolygons([clonePolygon(this.polygon)]);
2896
+ }
2897
+ revert(doc) {
2898
+ doc.removePolygons([this.polygon.id]);
2899
+ }
2900
+ };
2901
+ var UpdatePolygonCommand = class {
2902
+ constructor(before, after) {
2903
+ this.label = "update polygon";
2904
+ this.before = clonePolygon(before);
2905
+ this.after = clonePolygon(after);
2906
+ }
2907
+ apply(doc) {
2908
+ doc.setPolygon(clonePolygon(this.after));
2909
+ }
2910
+ revert(doc) {
2911
+ doc.setPolygon(clonePolygon(this.before));
2912
+ }
2913
+ };
2914
+ var DeletePolygonCommand = class {
2915
+ constructor(polygon) {
2916
+ this.label = "delete polygon";
2917
+ this.polygon = clonePolygon(polygon);
2918
+ }
2919
+ apply(doc) {
2920
+ doc.removePolygons([this.polygon.id]);
2921
+ }
2922
+ revert(doc) {
2923
+ doc.addPolygons([clonePolygon(this.polygon)]);
2924
+ }
2925
+ };
2926
+ /** Add/update/delete for semantic Star objects (Phase 4). */
2927
+ var AddStarCommand = class {
2928
+ constructor(star) {
2929
+ this.label = "add star";
2930
+ this.star = cloneStar(star);
2931
+ }
2932
+ apply(doc) {
2933
+ doc.addStars([cloneStar(this.star)]);
2934
+ }
2935
+ revert(doc) {
2936
+ doc.removeStars([this.star.id]);
2937
+ }
2938
+ };
2939
+ var UpdateStarCommand = class {
2940
+ constructor(before, after) {
2941
+ this.label = "update star";
2942
+ this.before = cloneStar(before);
2943
+ this.after = cloneStar(after);
2944
+ }
2945
+ apply(doc) {
2946
+ doc.setStar(cloneStar(this.after));
2947
+ }
2948
+ revert(doc) {
2949
+ doc.setStar(cloneStar(this.before));
2950
+ }
2951
+ };
2952
+ var DeleteStarCommand = class {
2953
+ constructor(star) {
2954
+ this.label = "delete star";
2955
+ this.star = cloneStar(star);
2956
+ }
2957
+ apply(doc) {
2958
+ doc.removeStars([this.star.id]);
2959
+ }
2960
+ revert(doc) {
2961
+ doc.addStars([cloneStar(this.star)]);
2962
+ }
2963
+ };
2964
+ /** Add/update/delete for semantic Heart objects (Phase 4). */
2965
+ var AddHeartCommand = class {
2966
+ constructor(heart) {
2967
+ this.label = "add heart";
2968
+ this.heart = cloneHeart(heart);
2969
+ }
2970
+ apply(doc) {
2971
+ doc.addHearts([cloneHeart(this.heart)]);
2972
+ }
2973
+ revert(doc) {
2974
+ doc.removeHearts([this.heart.id]);
2975
+ }
2976
+ };
2977
+ var UpdateHeartCommand = class {
2978
+ constructor(before, after) {
2979
+ this.label = "update heart";
2980
+ this.before = cloneHeart(before);
2981
+ this.after = cloneHeart(after);
2982
+ }
2983
+ apply(doc) {
2984
+ doc.setHeart(cloneHeart(this.after));
2985
+ }
2986
+ revert(doc) {
2987
+ doc.setHeart(cloneHeart(this.before));
2988
+ }
2989
+ };
2990
+ var DeleteHeartCommand = class {
2991
+ constructor(heart) {
2992
+ this.label = "delete heart";
2993
+ this.heart = cloneHeart(heart);
2994
+ }
2995
+ apply(doc) {
2996
+ doc.removeHearts([this.heart.id]);
2997
+ }
2998
+ revert(doc) {
2999
+ doc.addHearts([cloneHeart(this.heart)]);
3000
+ }
3001
+ };
3002
+ /** Add/update/delete for Groups (Phase 3). */
3003
+ var AddGroupCommand = class {
3004
+ constructor(group) {
3005
+ this.label = "group";
3006
+ this.group = cloneGroup(group);
3007
+ }
3008
+ apply(doc) {
3009
+ doc.addGroups([cloneGroup(this.group)]);
3010
+ }
3011
+ revert(doc) {
3012
+ doc.removeGroups([this.group.id]);
3013
+ }
3014
+ };
3015
+ var UpdateGroupCommand = class {
3016
+ constructor(before, after) {
3017
+ this.label = "update group";
3018
+ this.before = cloneGroup(before);
3019
+ this.after = cloneGroup(after);
3020
+ }
3021
+ apply(doc) {
3022
+ doc.setGroup(cloneGroup(this.after));
3023
+ }
3024
+ revert(doc) {
3025
+ doc.setGroup(cloneGroup(this.before));
3026
+ }
3027
+ };
3028
+ var DeleteGroupCommand = class {
3029
+ constructor(group) {
3030
+ this.label = "ungroup";
3031
+ this.group = cloneGroup(group);
3032
+ }
3033
+ apply(doc) {
3034
+ doc.removeGroups([this.group.id]);
3035
+ }
3036
+ revert(doc) {
3037
+ doc.addGroups([cloneGroup(this.group)]);
3038
+ }
3039
+ };
3040
+ var REORDER_LABELS = {
3041
+ forward: "bring forward",
3042
+ backward: "send backward",
3043
+ front: "bring to front",
3044
+ back: "send to back"
3045
+ };
3046
+ /**
3047
+ * Bring-forward / send-backward / bring-to-front / send-to-back (Phase 3) —
3048
+ * one command family covering all four directions rather than four
3049
+ * near-identical classes, since the only difference between them is which
3050
+ * `BoardDocument.reorder` direction to replay. Captures the full paint order
3051
+ * on first `apply` rather than in the constructor — `BoardDocument.order()`
3052
+ * needs the doc, which a `Command` only ever receives via `apply`/`revert` —
3053
+ * so `revert` can restore it exactly; redo re-runs the same `reorder` call,
3054
+ * which is deterministic because `revert` always restores the identical
3055
+ * starting order first.
3056
+ */
3057
+ var ReorderObjectCommand = class {
3058
+ constructor(id, direction) {
3059
+ this.id = id;
3060
+ this.direction = direction;
3061
+ this.before = null;
3062
+ this.label = REORDER_LABELS[direction];
3063
+ }
3064
+ apply(doc) {
3065
+ this.before ??= [...doc.order()];
3066
+ doc.reorder(this.id, this.direction);
3067
+ }
3068
+ revert(doc) {
3069
+ if (this.before) doc.applyRemoteChange({ orderChanged: this.before });
3070
+ }
3071
+ };
1603
3072
  var LockItemsCommand = class {
1604
3073
  constructor(targets, targetState, actor) {
1605
3074
  this.label = "toggle lock";
@@ -1627,10 +3096,38 @@ var LockItemsCommand = class {
1627
3096
  else if (type === "table") doc.setTableLocked(id, locked, by);
1628
3097
  else if (type === "image") doc.setImageLocked(id, locked, by);
1629
3098
  else if (type === "timer") doc.setTimerLocked(id, locked, by);
3099
+ else if (type === "rectangle") doc.setRectangleLocked(id, locked, by);
3100
+ else if (type === "ellipse") doc.setEllipseLocked(id, locked, by);
3101
+ else if (type === "group") doc.setGroupLocked(id, locked, by);
3102
+ else if (type === "line") doc.setLineLocked(id, locked, by);
3103
+ else if (type === "arrow") doc.setArrowLocked(id, locked, by);
3104
+ else if (type === "polygon") doc.setPolygonLocked(id, locked, by);
3105
+ else if (type === "star") doc.setStarLocked(id, locked, by);
3106
+ else if (type === "heart") doc.setHeartLocked(id, locked, by);
1630
3107
  }
1631
3108
  };
1632
3109
  //#endregion
1633
3110
  //#region src/core/history/history.ts
3111
+ /**
3112
+ * Several commands applied/reverted together as one undo step (Phase 3
3113
+ * consolidation — this exact class used to be hand-duplicated as a private
3114
+ * `CommandBatch` in `controller-internal.ts` and an exported
3115
+ * `ExtensionCommandBatch` in `interaction/tools/customTool.ts`; both now
3116
+ * import this one instead). Revert runs in reverse order, so a batch that
3117
+ * depends on ordering (e.g. add-then-reference) undoes cleanly.
3118
+ */
3119
+ var CommandBatch = class {
3120
+ constructor(label, commands) {
3121
+ this.label = label;
3122
+ this.commands = commands;
3123
+ }
3124
+ apply(doc) {
3125
+ for (const command of this.commands) command.apply(doc);
3126
+ }
3127
+ revert(doc) {
3128
+ for (const command of [...this.commands].reverse()) command.revert(doc);
3129
+ }
3130
+ };
1634
3131
  var LIMIT = 200;
1635
3132
  var History = class {
1636
3133
  constructor(doc) {
@@ -1699,6 +3196,14 @@ var ADDED = [
1699
3196
  ["tablesAdded", "tables"],
1700
3197
  ["imagesAdded", "images"],
1701
3198
  ["timersAdded", "timers"],
3199
+ ["rectanglesAdded", "rectangles"],
3200
+ ["ellipsesAdded", "ellipses"],
3201
+ ["groupsAdded", "groups"],
3202
+ ["linesAdded", "lines"],
3203
+ ["arrowsAdded", "arrows"],
3204
+ ["polygonsAdded", "polygons"],
3205
+ ["starsAdded", "stars"],
3206
+ ["heartsAdded", "hearts"],
1702
3207
  ["customObjectsAdded", "customObjects"]
1703
3208
  ];
1704
3209
  var UPDATED = [
@@ -1709,6 +3214,14 @@ var UPDATED = [
1709
3214
  ["tablesUpdated", "tables"],
1710
3215
  ["imagesUpdated", "images"],
1711
3216
  ["timersUpdated", "timers"],
3217
+ ["rectanglesUpdated", "rectangles"],
3218
+ ["ellipsesUpdated", "ellipses"],
3219
+ ["groupsUpdated", "groups"],
3220
+ ["linesUpdated", "lines"],
3221
+ ["arrowsUpdated", "arrows"],
3222
+ ["polygonsUpdated", "polygons"],
3223
+ ["starsUpdated", "stars"],
3224
+ ["heartsUpdated", "hearts"],
1712
3225
  ["customObjectsUpdated", "customObjects"]
1713
3226
  ];
1714
3227
  var REMOVED = [
@@ -1718,6 +3231,14 @@ var REMOVED = [
1718
3231
  ["tablesRemoved", "tables"],
1719
3232
  ["imagesRemoved", "images"],
1720
3233
  ["timersRemoved", "timers"],
3234
+ ["rectanglesRemoved", "rectangles"],
3235
+ ["ellipsesRemoved", "ellipses"],
3236
+ ["groupsRemoved", "groups"],
3237
+ ["linesRemoved", "lines"],
3238
+ ["arrowsRemoved", "arrows"],
3239
+ ["polygonsRemoved", "polygons"],
3240
+ ["starsRemoved", "stars"],
3241
+ ["heartsRemoved", "hearts"],
1721
3242
  ["customObjectsRemoved", "customObjects"]
1722
3243
  ];
1723
3244
  function toWireObject(collection, object) {
@@ -1787,22 +3308,74 @@ function ribbonEdges(points, baseWidth, handDrawn = true) {
1787
3308
  //#endregion
1788
3309
  //#region src/core/document/spatialIndex.ts
1789
3310
  var CELL = 8;
3311
+ /**
3312
+ * Non-stroke types have no separate "transformed" event the way strokes
3313
+ * do — any content replace (`setNote`/`setTable`/...) goes through the
3314
+ * type's single `*Updated` field regardless of whether it moved, so a
3315
+ * reindex on every update is unconditional (cheap arithmetic per object,
3316
+ * matching `objectBounds.ts`'s own "no caching needed" precedent for these
3317
+ * types — not worth distinguishing "moved" from "just retexted").
3318
+ */
3319
+ var ADDED_FIELDS = [
3320
+ "added",
3321
+ "notesAdded",
3322
+ "textAdded",
3323
+ "tablesAdded",
3324
+ "imagesAdded",
3325
+ "timersAdded",
3326
+ "customObjectsAdded",
3327
+ "rectanglesAdded",
3328
+ "ellipsesAdded"
3329
+ ];
3330
+ var REMOVED_FIELDS = [
3331
+ "removed",
3332
+ "notesRemoved",
3333
+ "textRemoved",
3334
+ "tablesRemoved",
3335
+ "imagesRemoved",
3336
+ "timersRemoved",
3337
+ "customObjectsRemoved",
3338
+ "rectanglesRemoved",
3339
+ "ellipsesRemoved"
3340
+ ];
3341
+ var UPDATED_FIELDS = [
3342
+ "notesUpdated",
3343
+ "textUpdated",
3344
+ "tablesUpdated",
3345
+ "imagesUpdated",
3346
+ "timersUpdated",
3347
+ "customObjectsUpdated",
3348
+ "rectanglesUpdated",
3349
+ "ellipsesUpdated"
3350
+ ];
1790
3351
  var SpatialIndex = class {
1791
3352
  constructor(doc) {
1792
3353
  this.doc = doc;
1793
3354
  this.cells = /* @__PURE__ */ new Map();
1794
- this.strokeCells = /* @__PURE__ */ new Map();
3355
+ this.objectCells = /* @__PURE__ */ new Map();
1795
3356
  this.unsubscribe = doc.subscribe((change) => {
1796
- for (const id of change.removed) this.remove(id);
1797
- for (const stroke of change.added) this.insert(stroke.id);
3357
+ for (const field of REMOVED_FIELDS) for (const id of change[field]) this.remove(id);
3358
+ for (const field of ADDED_FIELDS) for (const object of change[field]) this.insert(object.id);
1798
3359
  for (const stroke of change.transformed) {
1799
3360
  this.remove(stroke.id);
1800
3361
  this.insert(stroke.id);
1801
3362
  }
3363
+ for (const field of UPDATED_FIELDS) for (const object of change[field]) {
3364
+ this.remove(object.id);
3365
+ this.insert(object.id);
3366
+ }
1802
3367
  });
1803
3368
  for (const stroke of doc.all()) this.insert(stroke.id);
1804
- }
1805
- /** Ids of strokes whose bbox may overlap the query rect. */
3369
+ for (const note of doc.allNotes()) this.insert(note.id);
3370
+ for (const text of doc.allTexts()) this.insert(text.id);
3371
+ for (const table of doc.allTables()) this.insert(table.id);
3372
+ for (const image of doc.allImages()) this.insert(image.id);
3373
+ for (const timer of doc.allTimers()) this.insert(timer.id);
3374
+ for (const object of doc.allCustomObjects()) this.insert(object.id);
3375
+ for (const rect of doc.allRectangles()) this.insert(rect.id);
3376
+ for (const ellipse of doc.allEllipses()) this.insert(ellipse.id);
3377
+ }
3378
+ /** Ids of content objects (any type except groups) whose bbox may overlap the query rect. */
1806
3379
  query(minX, minY, maxX, maxY) {
1807
3380
  const result = /* @__PURE__ */ new Set();
1808
3381
  for (const key of cellsOf({
@@ -1819,13 +3392,13 @@ var SpatialIndex = class {
1819
3392
  dispose() {
1820
3393
  this.unsubscribe();
1821
3394
  this.cells.clear();
1822
- this.strokeCells.clear();
3395
+ this.objectCells.clear();
1823
3396
  }
1824
3397
  insert(id) {
1825
3398
  const box = this.doc.bbox(id);
1826
3399
  if (!box) return;
1827
3400
  const keys = cellsOf(box);
1828
- this.strokeCells.set(id, keys);
3401
+ this.objectCells.set(id, keys);
1829
3402
  for (const key of keys) {
1830
3403
  let bucket = this.cells.get(key);
1831
3404
  if (!bucket) {
@@ -1836,9 +3409,9 @@ var SpatialIndex = class {
1836
3409
  }
1837
3410
  }
1838
3411
  remove(id) {
1839
- const keys = this.strokeCells.get(id);
3412
+ const keys = this.objectCells.get(id);
1840
3413
  if (!keys) return;
1841
- this.strokeCells.delete(id);
3414
+ this.objectCells.delete(id);
1842
3415
  for (const key of keys) {
1843
3416
  const bucket = this.cells.get(key);
1844
3417
  if (bucket) {
@@ -1943,6 +3516,135 @@ function clamp(value, min, max) {
1943
3516
  return Math.min(max, Math.max(min, value));
1944
3517
  }
1945
3518
  //#endregion
3519
+ //#region src/core/shapes/lineGeometry.ts
3520
+ var BARB_ANGLE = Math.PI * (25 / 180);
3521
+ /** Two barb segments (each `[fromPoint, tip]`), or `[]` for `head: "none"`. */
3522
+ function arrowHeadBarbs(start, end, head) {
3523
+ if (head === "none") return [];
3524
+ const len = Math.hypot(end.x - start.x, end.y - start.y);
3525
+ if (len < 1e-6) return [];
3526
+ const dirX = (end.x - start.x) / len;
3527
+ const dirY = (end.y - start.y) / len;
3528
+ const headLen = Math.min(4, Math.max(1.2, len * .25));
3529
+ const barb = (sign) => {
3530
+ const cos = Math.cos(BARB_ANGLE * sign);
3531
+ const sin = Math.sin(BARB_ANGLE * sign);
3532
+ return {
3533
+ x: end.x - headLen * (dirX * cos - dirY * sin),
3534
+ y: end.y - headLen * (dirX * sin + dirY * cos)
3535
+ };
3536
+ };
3537
+ return [[barb(1), end], [barb(-1), end]];
3538
+ }
3539
+ //#endregion
3540
+ //#region src/core/shapes/polygonGeometry.ts
3541
+ /**
3542
+ * The tuned per-`sides` starting angle that makes each regular-polygon kind
3543
+ * look right inscribed in its bounding box (a point-up triangle, a
3544
+ * flat-sided diamond, a point-up pentagon/hexagon, a flat-topped octagon) —
3545
+ * extracted unchanged from the legacy shape-stroke tool's own per-call
3546
+ * arguments, which were already tuned and shipped. Any `sides` count not
3547
+ * listed falls back to the common "point at top" case.
3548
+ */
3549
+ function polygonStartAngle(sides) {
3550
+ switch (sides) {
3551
+ case 3: return Math.PI / 2;
3552
+ case 4: return 0;
3553
+ case 8: return Math.PI / 8;
3554
+ default: return Math.PI / 2;
3555
+ }
3556
+ }
3557
+ /**
3558
+ * Rescales/recenters an arbitrary point set so its own bounding box exactly
3559
+ * fills `[-width/2, width/2] × [-height/2, height/2]`, centered at the
3560
+ * origin. Needed because "inscribed on a radius-rx,ry ellipse" (the natural
3561
+ * way to place N evenly-spaced vertices) does *not* by itself guarantee a
3562
+ * full, centered fit: a shape only touches every edge of its box when it
3563
+ * has a vertex at each of the four cardinal angles (true for a square/
3564
+ * diamond/octagon-with-the-right-start-angle) — a triangle or pentagon's
3565
+ * *actual* bounding box is both smaller than and off-center from the
3566
+ * nominal rx,ry ellipse (verified: an upward triangle's own bounding box is
3567
+ * ~13% narrower than its circumradius, and vertically off-center by a
3568
+ * quarter of its own height). Every semantic shape's declared `width`/
3569
+ * `height` has to be the shape's *real* extent — `objectBounds.ts` and
3570
+ * rotate-about-center math both assume it — so every generator below routes
3571
+ * through this instead of leaving each one to reinvent (or skip) the fix.
3572
+ */
3573
+ function fitToBox(points, width, height) {
3574
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
3575
+ for (const p of points) {
3576
+ if (p.x < minX) minX = p.x;
3577
+ if (p.x > maxX) maxX = p.x;
3578
+ if (p.y < minY) minY = p.y;
3579
+ if (p.y > maxY) maxY = p.y;
3580
+ }
3581
+ const rawW = maxX - minX || 1;
3582
+ const rawH = maxY - minY || 1;
3583
+ const cx = (minX + maxX) / 2;
3584
+ const cy = (minY + maxY) / 2;
3585
+ return points.map((p) => ({
3586
+ x: (p.x - cx) / rawW * width,
3587
+ y: (p.y - cy) / rawH * height
3588
+ }));
3589
+ }
3590
+ /**
3591
+ * A regular `sides`-gon inscribed in a `width`×`height` box, centered at
3592
+ * the origin and fit exactly to it via `fitToBox` — independently scaled
3593
+ * per axis (like an ellipse inscribed in a non-square box), not a true
3594
+ * regular polygon forced square. `sides: 4` with the diamond start angle
3595
+ * (0) produces the four cardinal points of the box — a diamond is exactly
3596
+ * this case, not a separate shape.
3597
+ */
3598
+ function regularPolygonPoints(sides, width, height, startAngle) {
3599
+ const raw = [];
3600
+ for (let i = 0; i < sides; i++) {
3601
+ const theta = startAngle + i / sides * Math.PI * 2;
3602
+ raw.push({
3603
+ x: Math.cos(theta),
3604
+ y: Math.sin(theta)
3605
+ });
3606
+ }
3607
+ return fitToBox(raw, width, height);
3608
+ }
3609
+ /**
3610
+ * A `points`-pointed star inscribed in a `width`×`height` box, centered at
3611
+ * the origin and fit exactly to it via `fitToBox` — alternating outer/inner
3612
+ * vertices, `innerRatio` of the outer radius. Matches the legacy tool's own
3613
+ * tuned defaults (5 points, 0.42).
3614
+ */
3615
+ function starPoints(points, innerRatio, width, height) {
3616
+ const total = points * 2;
3617
+ const raw = [];
3618
+ for (let i = 0; i < total; i++) {
3619
+ const isOuter = i % 2 === 0;
3620
+ const theta = -Math.PI / 2 + i / total * Math.PI * 2;
3621
+ const r = isOuter ? 1 : innerRatio;
3622
+ raw.push({
3623
+ x: Math.cos(theta) * r,
3624
+ y: Math.sin(theta) * r
3625
+ });
3626
+ }
3627
+ return fitToBox(raw, width, height);
3628
+ }
3629
+ var HEART_SEGMENTS = 64;
3630
+ /**
3631
+ * A heart inscribed in a `width`×`height` box, centered at the origin and
3632
+ * fit exactly to it via `fitToBox` — the standard parametric heart curve
3633
+ * (`x = 16 sin³t`, `y = 13 cos t − 5 cos 2t − 2 cos 3t − cos 4t`, matching
3634
+ * the legacy shape-stroke tool's own formula).
3635
+ */
3636
+ function heartPoints(width, height) {
3637
+ const raw = [];
3638
+ for (let i = 0; i < HEART_SEGMENTS; i++) {
3639
+ const t = i / HEART_SEGMENTS * Math.PI * 2;
3640
+ raw.push({
3641
+ x: 16 * Math.sin(t) ** 3,
3642
+ y: -(13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t))
3643
+ });
3644
+ }
3645
+ return fitToBox(raw, width, height);
3646
+ }
3647
+ //#endregion
1946
3648
  //#region src/persistence/serialization/svg.ts
1947
3649
  /** Matches the key light used by the board shader and note shadows. */
1948
3650
  var LIGHT = {
@@ -1980,7 +3682,14 @@ function documentToSVG(doc, now = 0, registry, backgroundColor = DEFAULT_BACKGRO
1980
3682
  const images = doc.images ?? [];
1981
3683
  const timers = doc.timers ?? [];
1982
3684
  const customObjects = doc.customObjects ?? [];
1983
- const bounds = contentBounds(doc.strokes, notes, texts, tables, images, timers, customObjects);
3685
+ const rectangles = doc.rectangles ?? [];
3686
+ const ellipses = doc.ellipses ?? [];
3687
+ const lines = doc.lines ?? [];
3688
+ const arrows = doc.arrows ?? [];
3689
+ const polygons = doc.polygons ?? [];
3690
+ const stars = doc.stars ?? [];
3691
+ const hearts = doc.hearts ?? [];
3692
+ const bounds = contentBounds(doc.strokes, notes, texts, tables, images, timers, customObjects, rectangles, ellipses, lines, arrows, polygons, stars, hearts);
1984
3693
  const view = `${fmt(bounds.minX)} ${fmt(bounds.minY)} ${fmt(bounds.width)} ${fmt(bounds.height)}`;
1985
3694
  const defs = notes.map(noteShadowFilter).join("\n");
1986
3695
  const highlights = doc.strokes.filter((s) => s.tool === "highlighter");
@@ -1989,6 +3698,13 @@ function documentToSVG(doc, now = 0, registry, backgroundColor = DEFAULT_BACKGRO
1989
3698
  `<rect x="${fmt(bounds.minX)}" y="${fmt(bounds.minY)}" width="${fmt(bounds.width)}" height="${fmt(bounds.height)}" fill="${backgroundColor}"/>`,
1990
3699
  ...images.map((image) => imageToElement(image, resolvedAssets)),
1991
3700
  ...tables.map(tableToGroup),
3701
+ ...rectangles.map(rectangleToElement),
3702
+ ...ellipses.map(ellipseToElement),
3703
+ ...polygons.map(polygonToElement),
3704
+ ...stars.map(starToElement),
3705
+ ...hearts.map(heartToElement),
3706
+ ...lines.map(lineToElement),
3707
+ ...arrows.map(arrowToGroup),
1992
3708
  ...highlights.flatMap(strokeToPaths),
1993
3709
  ...ink.flatMap(strokeToPaths),
1994
3710
  ...texts.map(textToElement),
@@ -2228,7 +3944,91 @@ function imageToElement(image, resolvedAssets) {
2228
3944
  }
2229
3945
  return `<image href="${escapeXML(image.src)}" x="${fmt(left)}" y="${fmt(top)}" width="${fmt(image.width)}" height="${fmt(image.height)}" preserveAspectRatio="none"/>`;
2230
3946
  }
2231
- function contentBounds(strokes, notes, texts, tables = [], images = [], timers = [], customObjects = []) {
3947
+ /** (x, y) is the top edge (core/shapes/shapeObjects.ts) — same convention as TableBlock, so `svgY = -y` directly, no height offset. */
3948
+ /**
3949
+ * SVG `transform` attribute for a top-edge, Y-up shape's `rotation` (about
3950
+ * its own center, per `shapeObjects.ts`'s convention) — board-space
3951
+ * `rotationAbout` fed through the existing `svgTransformAttr` F·M·F
3952
+ * conversion, exactly how `sceneNodeToSVG`'s identical-convention `rect`
3953
+ * case already does it. Shared by every top-edge shape kind (Rectangle,
3954
+ * Ellipse, and Phase 4's Polygon/Star/Heart), not just these two.
3955
+ */
3956
+ function shapeRotationAttr(x, y, width, height, rotation) {
3957
+ if (!rotation) return "";
3958
+ return svgTransformAttr(rotationAbout(rotation, x + width / 2, y - height / 2));
3959
+ }
3960
+ function opacityAttrOf(opacity) {
3961
+ return opacity !== void 0 && opacity < 1 ? ` opacity="${fmt(opacity)}"` : "";
3962
+ }
3963
+ function rectangleToElement(rect) {
3964
+ const svgY = -rect.y;
3965
+ const stroke = rect.stroke ? ` stroke="${rect.stroke}" stroke-width="${fmt(rect.strokeWidth ?? .12)}"` : "";
3966
+ const rx = rect.cornerRadius ? ` rx="${fmt(rect.cornerRadius)}"` : "";
3967
+ const transform = shapeRotationAttr(rect.x, rect.y, rect.width, rect.height, rect.rotation);
3968
+ const opacity = opacityAttrOf(rect.opacity);
3969
+ return `<rect data-id="${rect.id}" x="${fmt(rect.x)}" y="${fmt(svgY)}" width="${fmt(rect.width)}" height="${fmt(rect.height)}" fill="${rect.fill ?? "none"}"${rx}${stroke}${opacity}${transform}/>`;
3970
+ }
3971
+ function ellipseToElement(ellipse) {
3972
+ const cx = ellipse.x + ellipse.width / 2;
3973
+ const cy = -ellipse.y + ellipse.height / 2;
3974
+ const stroke = ellipse.stroke ? ` stroke="${ellipse.stroke}" stroke-width="${fmt(ellipse.strokeWidth ?? .12)}"` : "";
3975
+ const transform = shapeRotationAttr(ellipse.x, ellipse.y, ellipse.width, ellipse.height, ellipse.rotation);
3976
+ const opacity = opacityAttrOf(ellipse.opacity);
3977
+ return `<ellipse data-id="${ellipse.id}" cx="${fmt(cx)}" cy="${fmt(cy)}" rx="${fmt(ellipse.width / 2)}" ry="${fmt(ellipse.height / 2)}" fill="${ellipse.fill ?? "none"}"${stroke}${opacity}${transform}/>`;
3978
+ }
3979
+ /**
3980
+ * A regular N-gon inscribed in the same top-edge/Y-up bounding box
3981
+ * Rectangle uses — points are authored unrotated (absolute, y-flipped),
3982
+ * with `transform` handling rotation about the shape's own center exactly
3983
+ * like `rectangleToElement`/`ellipseToElement` already do, so this reuses
3984
+ * that same proven pattern rather than rotating points by hand.
3985
+ */
3986
+ function polygonToElement(polygon) {
3987
+ const cx = polygon.x + polygon.width / 2;
3988
+ const cy = polygon.y - polygon.height / 2;
3989
+ const svgPoints = regularPolygonPoints(polygon.sides, polygon.width, polygon.height, polygonStartAngle(polygon.sides)).map((p) => `${fmt(cx + p.x)},${fmt(-(cy + p.y))}`).join(" ");
3990
+ const stroke = polygon.stroke ? ` stroke="${polygon.stroke}" stroke-width="${fmt(polygon.strokeWidth ?? .12)}"` : "";
3991
+ const transform = shapeRotationAttr(polygon.x, polygon.y, polygon.width, polygon.height, polygon.rotation);
3992
+ const opacity = opacityAttrOf(polygon.opacity);
3993
+ return `<polygon data-id="${polygon.id}" points="${svgPoints}" fill="${polygon.fill ?? "none"}"${stroke}${opacity}${transform}/>`;
3994
+ }
3995
+ /** Same convention/pattern as `polygonToElement` — a `points`-vertex star with `innerRadiusRatio` (`polygonGeometry.ts`'s `starPoints`). */
3996
+ function starToElement(star) {
3997
+ const cx = star.x + star.width / 2;
3998
+ const cy = star.y - star.height / 2;
3999
+ const svgPoints = starPoints(star.points, star.innerRadiusRatio, star.width, star.height).map((p) => `${fmt(cx + p.x)},${fmt(-(cy + p.y))}`).join(" ");
4000
+ const stroke = star.stroke ? ` stroke="${star.stroke}" stroke-width="${fmt(star.strokeWidth ?? .12)}"` : "";
4001
+ const transform = shapeRotationAttr(star.x, star.y, star.width, star.height, star.rotation);
4002
+ const opacity = opacityAttrOf(star.opacity);
4003
+ return `<polygon data-id="${star.id}" points="${svgPoints}" fill="${star.fill ?? "none"}"${stroke}${opacity}${transform}/>`;
4004
+ }
4005
+ /** 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). */
4006
+ function heartToElement(heart) {
4007
+ const cx = heart.x + heart.width / 2;
4008
+ const cy = heart.y - heart.height / 2;
4009
+ const svgPoints = heartPoints(heart.width, heart.height).map((p) => `${fmt(cx + p.x)},${fmt(-(cy + p.y))}`).join(" ");
4010
+ const stroke = heart.stroke ? ` stroke="${heart.stroke}" stroke-width="${fmt(heart.strokeWidth ?? .12)}"` : "";
4011
+ const transform = shapeRotationAttr(heart.x, heart.y, heart.width, heart.height, heart.rotation);
4012
+ const opacity = opacityAttrOf(heart.opacity);
4013
+ return `<polygon data-id="${heart.id}" points="${svgPoints}" fill="${heart.fill ?? "none"}"${stroke}${opacity}${transform}/>`;
4014
+ }
4015
+ /** No rotation/box concept (see shapeObjects.ts's header) — the two endpoints are emitted directly, y-flipped like every other coordinate here. */
4016
+ function lineToElement(line) {
4017
+ const stroke = line.stroke ?? "#1C1C1E";
4018
+ const width = line.strokeWidth ?? .12;
4019
+ const opacity = opacityAttrOf(line.opacity);
4020
+ return `<line data-id="${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}/>`;
4021
+ }
4022
+ /** Shaft plus head barbs — same `arrowHeadBarbs` the canvas renderer uses, so the two never drift. */
4023
+ function arrowToGroup(arrow) {
4024
+ const stroke = arrow.stroke ?? "#1C1C1E";
4025
+ const width = arrow.strokeWidth ?? .12;
4026
+ const opacity = opacityAttrOf(arrow.opacity);
4027
+ 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)}"/>`;
4028
+ const segments = [segment(arrow.start, arrow.end), ...arrowHeadBarbs(arrow.start, arrow.end, arrow.head).map(([a, b]) => segment(a, b))];
4029
+ return `<g data-id="${arrow.id}"${opacity}>${segments.join("")}</g>`;
4030
+ }
4031
+ function contentBounds(strokes, notes, texts, tables = [], images = [], timers = [], customObjects = [], rectangles = [], ellipses = [], lines = [], arrows = [], polygons = [], stars = [], hearts = []) {
2232
4032
  let minX = Infinity;
2233
4033
  let minY = Infinity;
2234
4034
  let maxX = -Infinity;
@@ -2290,6 +4090,34 @@ function contentBounds(strokes, notes, texts, tables = [], images = [], timers =
2290
4090
  grow(p.x, -p.y);
2291
4091
  }
2292
4092
  }
4093
+ for (const rect of rectangles) {
4094
+ grow(rect.x, -rect.y);
4095
+ grow(rect.x + rect.width, -rect.y + rect.height);
4096
+ }
4097
+ for (const ellipse of ellipses) {
4098
+ grow(ellipse.x, -ellipse.y);
4099
+ grow(ellipse.x + ellipse.width, -ellipse.y + ellipse.height);
4100
+ }
4101
+ for (const line of lines) {
4102
+ grow(line.start.x, -line.start.y);
4103
+ grow(line.end.x, -line.end.y);
4104
+ }
4105
+ for (const arrow of arrows) {
4106
+ grow(arrow.start.x, -arrow.start.y);
4107
+ grow(arrow.end.x, -arrow.end.y);
4108
+ }
4109
+ for (const polygon of polygons) {
4110
+ grow(polygon.x, -polygon.y);
4111
+ grow(polygon.x + polygon.width, -polygon.y + polygon.height);
4112
+ }
4113
+ for (const star of stars) {
4114
+ grow(star.x, -star.y);
4115
+ grow(star.x + star.width, -star.y + star.height);
4116
+ }
4117
+ for (const heart of hearts) {
4118
+ grow(heart.x, -heart.y);
4119
+ grow(heart.x + heart.width, -heart.y + heart.height);
4120
+ }
2293
4121
  if (minX > maxX) return {
2294
4122
  minX: 0,
2295
4123
  minY: 0,
@@ -2314,4 +4142,4 @@ function fmt(n) {
2314
4142
  var SDK_PACKAGE_NAME = "@scrawl-board/board";
2315
4143
  var SDK_DEVELOPMENT_VERSION = "0.0.0-development";
2316
4144
  //#endregion
2317
- export { ASSET_CACHE_BYTES_DEFAULT, ASSET_CACHE_BYTES_MAX, ASSET_CACHE_BYTES_MIN, ASSET_EXPORT_MAX_DECODED_MEGAPIXELS, ASSET_EXPORT_MAX_ENCODED_BYTES, ASSET_MAX_CONCURRENT_RESOLUTIONS, ASSET_MAX_DECODED_MEGAPIXELS, ASSET_MAX_DIMENSION_PX, ASSET_MAX_ENCODED_BYTES, ASSET_REF_MAX_BYTES, ASSET_REF_PATTERN, AddImageCommand, AddNoteCommand, AddStrokesCommand, AddTableCommand, AddTextCommand, AddTimerCommand, AssetResolutionError, BEACON_INSET, BoardDocument, CURRENT_DOCUMENT_SCHEMA_VERSION, ClusterStore, DeleteImageCommand, DeleteNoteCommand, DeleteStrokesCommand, DeleteTableCommand, DeleteTextCommand, DeleteTimerCommand, DocumentRecoveryError, END_TAPER, ERASE_THRESHOLD, EraseCommand, FOG_COLOR, HIGHLIGHT_COLORS, History, IDENTITY, INK_COLORS, LockItemsCommand, MIN_WIDTH_FACTOR, NOTE_COLORS, NOTE_DEFAULT_SIZE, NOTE_DEFAULT_Z, NOTE_MAX_Z, NOTE_MIN_Z, NOTE_PEEL_STEP, SDK_DEVELOPMENT_VERSION, SDK_PACKAGE_NAME, STAMPS, STAMP_SIZE, SUPPORTED_ASSET_MEDIA_TYPES, SpatialIndex, TABLE_DEFAULT_CELL_HEIGHT, TABLE_DEFAULT_CELL_WIDTH, TABLE_DEFAULT_FONT_SIZE, TEXT_DEFAULT_SIZE, TIMER_DEFAULT_DURATION_MS, TIMER_DEFAULT_SIZE, TIMER_PRESETS_MS, TransformCommand, UpdateImageCommand, UpdateNoteCommand, UpdateTableCommand, UpdateTextCommand, UpdateTimerCommand, apply, applyItemLock, assetRef, avgScale, canUnlockItem, changeToOps, clampAssetCacheBytes, cloneCustomObject, cloneImage, cloneNote, cloneStroke, cloneTable, cloneText, cloneTimer, documentId, documentToSVG, formatTimer, invert, isAssetRef, isIdentity, isStampKind, loadDocumentBytes, measureTable, measureTextBlock, migrateDocument, mul, pauseTimer, placePresenceBeacon, ribbonEdges, rotationAbout, scalingAbout, searchBoard, serializeDocument, serializeLock, serializeStroke, setTimerDuration, stampDataUrl, startTimer, strokeId, timerExpired, timerRemaining, toggleTimer, translation };
4145
+ 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, 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, ReorderObjectCommand, SDK_DEVELOPMENT_VERSION, SDK_PACKAGE_NAME, SHAPE_DEFAULT_STROKE, SHAPE_DEFAULT_STROKE_WIDTH, SHAPE_MIN_SIZE, 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, documentToSVG, formatTimer, invert, isAssetRef, isIdentity, isStampKind, loadDocumentBytes, measureTable, measureTextBlock, migrateDocument, mul, pauseTimer, placePresenceBeacon, ribbonEdges, rotationAbout, scalingAbout, searchBoard, serializeDocument, serializeLock, serializeStroke, setTimerDuration, stampDataUrl, startTimer, strokeId, timerExpired, timerRemaining, toggleTimer, translation };