@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/README.md +4 -2
- package/dist/browser.d.ts +261 -2
- package/dist/browser.js +7390 -882
- package/dist/core.d.ts +618 -8
- package/dist/core.js +1848 -20
- package/dist/index.d.ts +740 -14
- package/dist/index.js +7592 -807
- package/dist/react.d.ts +274 -6
- package/dist/react.js +7540 -824
- package/package.json +1 -1
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
|
-
|
|
848
|
-
|
|
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.
|
|
3355
|
+
this.objectCells = /* @__PURE__ */ new Map();
|
|
1795
3356
|
this.unsubscribe = doc.subscribe((change) => {
|
|
1796
|
-
for (const id of change
|
|
1797
|
-
for (const
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
3412
|
+
const keys = this.objectCells.get(id);
|
|
1840
3413
|
if (!keys) return;
|
|
1841
|
-
this.
|
|
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
|
|
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
|
-
|
|
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 };
|