@scrawl-board/board 0.1.0-beta.6 → 0.1.0-beta.8
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 +11 -5
- package/dist/browser.d.ts +593 -15
- package/dist/browser.js +6277 -34044
- package/dist/core.d.ts +711 -16
- package/dist/core.js +2015 -51
- package/dist/index.d.ts +1171 -26
- package/dist/index.js +6209 -33607
- package/dist/local.d.ts +469 -0
- package/dist/local.js +234 -0
- package/dist/react.d.ts +635 -19
- package/dist/react.js +6142 -33785
- package/package.json +17 -3
package/dist/core.js
CHANGED
|
@@ -48,6 +48,37 @@ function strokeId(value) {
|
|
|
48
48
|
return value;
|
|
49
49
|
}
|
|
50
50
|
//#endregion
|
|
51
|
+
//#region src/core/document/migrations/registry.ts
|
|
52
|
+
/** Schema versions this build can load. An unrecognized version fails safely (`DOCUMENT_VERSION_UNSUPPORTED`) rather than being silently treated as current. */
|
|
53
|
+
var SUPPORTED_SCHEMA_VERSIONS = /* @__PURE__ */ new Set([0, 1]);
|
|
54
|
+
function isSupportedSchemaVersion(version) {
|
|
55
|
+
return typeof version === "number" && SUPPORTED_SCHEMA_VERSIONS.has(version);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Document collection fields introduced after schemaVersion 1 first
|
|
59
|
+
* shipped real documents. A document stored before a field's introduction
|
|
60
|
+
* can never retroactively gain it, so requiring it at load would fail
|
|
61
|
+
* every pre-existing document — these stay optional even at the current
|
|
62
|
+
* version. New entries only ever get added here, never removed (an old
|
|
63
|
+
* stored document's absence of a since-introduced field must keep loading
|
|
64
|
+
* for as long as that document might still exist).
|
|
65
|
+
*/
|
|
66
|
+
var FIELDS_OPTIONAL_AT_CURRENT_VERSION = /* @__PURE__ */ new Set([
|
|
67
|
+
"customObjects",
|
|
68
|
+
"rectangles",
|
|
69
|
+
"ellipses",
|
|
70
|
+
"groups",
|
|
71
|
+
"lines",
|
|
72
|
+
"arrows",
|
|
73
|
+
"polygons",
|
|
74
|
+
"stars",
|
|
75
|
+
"hearts"
|
|
76
|
+
]);
|
|
77
|
+
/** Whether `field` must be present on a document already at the current schema version (never required on an older, migrating-forward document). */
|
|
78
|
+
function isFieldRequiredAtCurrentVersion(field) {
|
|
79
|
+
return !FIELDS_OPTIONAL_AT_CURRENT_VERSION.has(field);
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
51
82
|
//#region src/core/document/document-schema.ts
|
|
52
83
|
var CURRENT_DOCUMENT_SCHEMA_VERSION = 1;
|
|
53
84
|
var DocumentRecoveryError = class extends Error {
|
|
@@ -76,7 +107,7 @@ function loadDocumentBytes(originalBytes) {
|
|
|
76
107
|
function migrateDocument(raw) {
|
|
77
108
|
if (!isRecord(raw)) return failure("DOCUMENT_VALIDATION_FAILED", "Document must be an object");
|
|
78
109
|
const version = raw.schemaVersion ?? 0;
|
|
79
|
-
if (version
|
|
110
|
+
if (!isSupportedSchemaVersion(version)) return failure("DOCUMENT_VERSION_UNSUPPORTED", `Document schema version ${String(version)} is unsupported`, void 0, "schemaVersion");
|
|
80
111
|
if (!isJsonValue(raw)) return failure("DOCUMENT_VALIDATION_FAILED", "Document must contain JSON values only");
|
|
81
112
|
const strokes = validateStrokes(raw.strokes);
|
|
82
113
|
if (strokes instanceof DocumentRecoveryError) return {
|
|
@@ -89,7 +120,15 @@ function migrateDocument(raw) {
|
|
|
89
120
|
tables: validateCollection(raw, "tables", version, validateTable),
|
|
90
121
|
images: validateCollection(raw, "images", version, validateImage),
|
|
91
122
|
timers: validateCollection(raw, "timers", version, validateTimer),
|
|
92
|
-
customObjects: validateCollection(raw, "customObjects", version, validateCustomObject,
|
|
123
|
+
customObjects: validateCollection(raw, "customObjects", version, validateCustomObject),
|
|
124
|
+
rectangles: validateCollection(raw, "rectangles", version, validateRectangle),
|
|
125
|
+
ellipses: validateCollection(raw, "ellipses", version, validateEllipse),
|
|
126
|
+
groups: validateCollection(raw, "groups", version, validateGroup),
|
|
127
|
+
lines: validateCollection(raw, "lines", version, validateLine),
|
|
128
|
+
arrows: validateCollection(raw, "arrows", version, validateArrow),
|
|
129
|
+
polygons: validateCollection(raw, "polygons", version, validatePolygon),
|
|
130
|
+
stars: validateCollection(raw, "stars", version, validateStar),
|
|
131
|
+
hearts: validateCollection(raw, "hearts", version, validateHeart)
|
|
93
132
|
};
|
|
94
133
|
if (collections.notes instanceof DocumentRecoveryError) return {
|
|
95
134
|
ok: false,
|
|
@@ -115,6 +154,43 @@ function migrateDocument(raw) {
|
|
|
115
154
|
ok: false,
|
|
116
155
|
error: collections.customObjects
|
|
117
156
|
};
|
|
157
|
+
if (collections.rectangles instanceof DocumentRecoveryError) return {
|
|
158
|
+
ok: false,
|
|
159
|
+
error: collections.rectangles
|
|
160
|
+
};
|
|
161
|
+
if (collections.ellipses instanceof DocumentRecoveryError) return {
|
|
162
|
+
ok: false,
|
|
163
|
+
error: collections.ellipses
|
|
164
|
+
};
|
|
165
|
+
if (collections.groups instanceof DocumentRecoveryError) return {
|
|
166
|
+
ok: false,
|
|
167
|
+
error: collections.groups
|
|
168
|
+
};
|
|
169
|
+
if (collections.lines instanceof DocumentRecoveryError) return {
|
|
170
|
+
ok: false,
|
|
171
|
+
error: collections.lines
|
|
172
|
+
};
|
|
173
|
+
if (collections.arrows instanceof DocumentRecoveryError) return {
|
|
174
|
+
ok: false,
|
|
175
|
+
error: collections.arrows
|
|
176
|
+
};
|
|
177
|
+
if (collections.polygons instanceof DocumentRecoveryError) return {
|
|
178
|
+
ok: false,
|
|
179
|
+
error: collections.polygons
|
|
180
|
+
};
|
|
181
|
+
if (collections.stars instanceof DocumentRecoveryError) return {
|
|
182
|
+
ok: false,
|
|
183
|
+
error: collections.stars
|
|
184
|
+
};
|
|
185
|
+
if (collections.hearts instanceof DocumentRecoveryError) return {
|
|
186
|
+
ok: false,
|
|
187
|
+
error: collections.hearts
|
|
188
|
+
};
|
|
189
|
+
const objectOrder = validateObjectOrder(raw.objectOrder);
|
|
190
|
+
if (objectOrder instanceof DocumentRecoveryError) return {
|
|
191
|
+
ok: false,
|
|
192
|
+
error: objectOrder
|
|
193
|
+
};
|
|
118
194
|
return {
|
|
119
195
|
ok: true,
|
|
120
196
|
migratedFrom: version,
|
|
@@ -126,7 +202,16 @@ function migrateDocument(raw) {
|
|
|
126
202
|
tables: collections.tables,
|
|
127
203
|
images: collections.images,
|
|
128
204
|
timers: collections.timers,
|
|
129
|
-
customObjects: collections.customObjects
|
|
205
|
+
customObjects: collections.customObjects,
|
|
206
|
+
rectangles: collections.rectangles,
|
|
207
|
+
ellipses: collections.ellipses,
|
|
208
|
+
groups: collections.groups,
|
|
209
|
+
lines: collections.lines,
|
|
210
|
+
arrows: collections.arrows,
|
|
211
|
+
polygons: collections.polygons,
|
|
212
|
+
stars: collections.stars,
|
|
213
|
+
hearts: collections.hearts,
|
|
214
|
+
objectOrder
|
|
130
215
|
}
|
|
131
216
|
};
|
|
132
217
|
}
|
|
@@ -135,8 +220,19 @@ function serializeDocument(document) {
|
|
|
135
220
|
if (!result.ok || result.migratedFrom !== 1) throw result.ok ? new DocumentRecoveryError("DOCUMENT_VALIDATION_FAILED", "Saving requires the current Document schema") : result.error;
|
|
136
221
|
return JSON.stringify(result.document);
|
|
137
222
|
}
|
|
138
|
-
|
|
139
|
-
|
|
223
|
+
/**
|
|
224
|
+
* A Document's serialized size in bytes (Phase 5) — UTF-8, not UTF-16
|
|
225
|
+
* `string.length`, since a Document with non-ASCII note/text content (most
|
|
226
|
+
* of them, eventually) would otherwise under-report. Useful for a Host
|
|
227
|
+
* deciding when to warn about an unusually large board, or for logging/
|
|
228
|
+
* telemetry around save size — not consulted by anything inside this
|
|
229
|
+
* package itself, which has no size limit of its own.
|
|
230
|
+
*/
|
|
231
|
+
function documentSize(document) {
|
|
232
|
+
return new TextEncoder().encode(JSON.stringify(document)).length;
|
|
233
|
+
}
|
|
234
|
+
function validateCollection(raw, name, version, validator) {
|
|
235
|
+
if (isFieldRequiredAtCurrentVersion(name) && version === 1 && !(name in raw)) return validation(`${name} is required`, name);
|
|
140
236
|
const value = raw[name] ?? [];
|
|
141
237
|
if (!Array.isArray(value)) return validation(`${name} must be an array`, name);
|
|
142
238
|
for (let index = 0; index < value.length; index += 1) if (!isRecord(value[index]) || !validator(value[index])) return validation(`invalid ${name} entry`, `${name}[${index}]`);
|
|
@@ -148,11 +244,20 @@ function validateStrokes(raw) {
|
|
|
148
244
|
for (let index = 0; index < raw.length; index += 1) {
|
|
149
245
|
const value = raw[index];
|
|
150
246
|
const path = `strokes[${index}]`;
|
|
151
|
-
if (!isRecord(value)
|
|
247
|
+
if (!isRecord(value)) return validation("stroke must be an object", path);
|
|
248
|
+
if (!nonempty(value.id)) return validation("stroke.id must be a non-empty string", `${path}.id`);
|
|
249
|
+
if (!nonempty(value.color)) return validation("stroke.color must be a non-empty string", `${path}.color`);
|
|
250
|
+
if (!positive(value.baseWidth)) return validation("stroke.baseWidth must be a positive number", `${path}.baseWidth`);
|
|
251
|
+
if (!optionalEnum(value.tool, [
|
|
152
252
|
"marker",
|
|
153
253
|
"highlighter",
|
|
154
254
|
"shape"
|
|
155
|
-
])
|
|
255
|
+
])) return validation("stroke.tool must be \"marker\", \"highlighter\", or \"shape\"", `${path}.tool`);
|
|
256
|
+
if (!validLock(value)) return validation("invalid stroke.lock", `${path}.lock`);
|
|
257
|
+
if (!validHidden(value)) return validation("invalid stroke.hidden", `${path}.hidden`);
|
|
258
|
+
if (!optionalString(value.clusterId)) return validation("stroke.clusterId must be a string", `${path}.clusterId`);
|
|
259
|
+
if (!validMatrix(value.matrix)) return validation("invalid stroke.matrix", `${path}.matrix`);
|
|
260
|
+
if (!Array.isArray(value.points)) return validation("stroke.points must be an array", `${path}.points`);
|
|
156
261
|
const points = [];
|
|
157
262
|
for (let pointIndex = 0; pointIndex < value.points.length; pointIndex += 1) {
|
|
158
263
|
const point = normalizePoint(value.points[pointIndex]);
|
|
@@ -178,20 +283,55 @@ function normalizePoint(raw) {
|
|
|
178
283
|
return values;
|
|
179
284
|
}
|
|
180
285
|
function validateNote(v) {
|
|
181
|
-
return basePosition(v) && positive(v.size) && finite(v.zOffset) && nonempty(v.color) && typeof v.text === "string" && validLock(v) && (v.votes === void 0 || Array.isArray(v.votes) && v.votes.every((vote) => isRecord(vote) && nonempty(vote.userId) && nonempty(vote.name) && nonempty(vote.color)));
|
|
286
|
+
return basePosition(v) && positive(v.size) && finite(v.zOffset) && nonempty(v.color) && typeof v.text === "string" && validLock(v) && validHidden(v) && (v.votes === void 0 || Array.isArray(v.votes) && v.votes.every((vote) => isRecord(vote) && nonempty(vote.userId) && nonempty(vote.name) && nonempty(vote.color)));
|
|
182
287
|
}
|
|
183
288
|
function validateText(v) {
|
|
184
|
-
return basePosition(v) && typeof v.text === "string" && positive(v.fontSize) && nonempty(v.color) && validLock(v) && optionalString(v.clusterId);
|
|
289
|
+
return basePosition(v) && typeof v.text === "string" && positive(v.fontSize) && nonempty(v.color) && validLock(v) && validHidden(v) && optionalString(v.clusterId);
|
|
185
290
|
}
|
|
186
291
|
function validateTable(v) {
|
|
187
|
-
return basePosition(v) && integer(v.rows) && integer(v.cols) && numberArray(v.colWidths, v.cols) && numberArray(v.rowHeights, v.rows) && isStringRecord(v.cells) && validLock(v) && optionalString(v.color) && optionalString(v.backgroundColor) && optionalString(v.clusterId);
|
|
292
|
+
return basePosition(v) && integer(v.rows) && integer(v.cols) && numberArray(v.colWidths, v.cols) && numberArray(v.rowHeights, v.rows) && isStringRecord(v.cells) && validLock(v) && validHidden(v) && optionalString(v.color) && optionalString(v.backgroundColor) && optionalString(v.clusterId);
|
|
188
293
|
}
|
|
189
294
|
function validateImage(v) {
|
|
190
295
|
const identifiesAnImage = v.ref !== void 0 ? isAssetRef(v.ref) : nonempty(v.src);
|
|
191
|
-
return basePosition(v) && identifiesAnImage && positive(v.width) && positive(v.height) && positive(v.aspectRatio) && validLock(v) && optionalString(v.name) && optionalString(v.createdAt) && optionalString(v.stamp);
|
|
296
|
+
return basePosition(v) && identifiesAnImage && positive(v.width) && positive(v.height) && positive(v.aspectRatio) && validLock(v) && validHidden(v) && optionalString(v.name) && optionalString(v.createdAt) && optionalString(v.stamp);
|
|
297
|
+
}
|
|
298
|
+
function validateShapeStyle(v) {
|
|
299
|
+
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);
|
|
300
|
+
}
|
|
301
|
+
function validateRectangle(v) {
|
|
302
|
+
return basePosition(v) && positive(v.width) && positive(v.height) && validLock(v) && validHidden(v) && validateShapeStyle(v) && (v.rotation === void 0 || finite(v.rotation)) && (v.cornerRadius === void 0 || finite(v.cornerRadius) && v.cornerRadius >= 0);
|
|
303
|
+
}
|
|
304
|
+
function validateEllipse(v) {
|
|
305
|
+
return basePosition(v) && positive(v.width) && positive(v.height) && validLock(v) && validHidden(v) && validateShapeStyle(v) && (v.rotation === void 0 || finite(v.rotation));
|
|
306
|
+
}
|
|
307
|
+
function validatePolygon(v) {
|
|
308
|
+
return basePosition(v) && positive(v.width) && positive(v.height) && validLock(v) && validHidden(v) && validateShapeStyle(v) && (v.rotation === void 0 || finite(v.rotation)) && (v.sides === 3 || v.sides === 4 || v.sides === 5 || v.sides === 6 || v.sides === 8);
|
|
309
|
+
}
|
|
310
|
+
function validateStar(v) {
|
|
311
|
+
return basePosition(v) && positive(v.width) && positive(v.height) && validLock(v) && validHidden(v) && validateShapeStyle(v) && (v.rotation === void 0 || finite(v.rotation)) && finite(v.points) && v.points >= 3 && finite(v.innerRadiusRatio) && v.innerRadiusRatio > 0 && v.innerRadiusRatio < 1;
|
|
312
|
+
}
|
|
313
|
+
function validateHeart(v) {
|
|
314
|
+
return basePosition(v) && positive(v.width) && positive(v.height) && validLock(v) && validHidden(v) && validateShapeStyle(v) && (v.rotation === void 0 || finite(v.rotation));
|
|
315
|
+
}
|
|
316
|
+
function validPoint(v) {
|
|
317
|
+
return isRecord(v) && finite(v.x) && finite(v.y);
|
|
318
|
+
}
|
|
319
|
+
function validateLine(v) {
|
|
320
|
+
return nonempty(v.id) && validPoint(v.start) && validPoint(v.end) && validLock(v) && validHidden(v) && validateShapeStyle(v);
|
|
321
|
+
}
|
|
322
|
+
function validateArrow(v) {
|
|
323
|
+
return nonempty(v.id) && validPoint(v.start) && validPoint(v.end) && validLock(v) && validHidden(v) && validateShapeStyle(v) && (v.head === void 0 || v.head === "triangle" || v.head === "none");
|
|
324
|
+
}
|
|
325
|
+
function validateGroup(v) {
|
|
326
|
+
return nonempty(v.id) && Array.isArray(v.children) && v.children.every((c) => nonempty(c)) && validLock(v) && validHidden(v);
|
|
327
|
+
}
|
|
328
|
+
function validateObjectOrder(v) {
|
|
329
|
+
if (v === void 0) return [];
|
|
330
|
+
if (!Array.isArray(v) || !v.every((id) => nonempty(id))) return validation("objectOrder must be an array of ids", "objectOrder");
|
|
331
|
+
return structuredClone(v);
|
|
192
332
|
}
|
|
193
333
|
function validateTimer(v) {
|
|
194
|
-
return basePosition(v) && positive(v.size) && positive(v.durationMs) && finite(v.remainingMs) && v.remainingMs >= 0 && (v.runningSince === void 0 || finite(v.runningSince)) && validLock(v);
|
|
334
|
+
return basePosition(v) && positive(v.size) && positive(v.durationMs) && finite(v.remainingMs) && v.remainingMs >= 0 && (v.runningSince === void 0 || finite(v.runningSince)) && validLock(v) && validHidden(v);
|
|
195
335
|
}
|
|
196
336
|
function validateCustomObject(v) {
|
|
197
337
|
return nonempty(v.id) && nonempty(v.type) && integer(v.schemaVersion) && validMatrix(v.transform) && Array.isArray(v.transform) && validCustomFallback(v.fallback) && (v.lock === void 0 || validCustomLock(v.lock)) && isJsonValue(v.props);
|
|
@@ -210,6 +350,9 @@ function basePosition(v) {
|
|
|
210
350
|
function validLock(v) {
|
|
211
351
|
return (v.locked === void 0 || typeof v.locked === "boolean") && optionalString(v.lockedBy) && optionalString(v.lockedByName);
|
|
212
352
|
}
|
|
353
|
+
function validHidden(v) {
|
|
354
|
+
return v.hidden === void 0 || typeof v.hidden === "boolean";
|
|
355
|
+
}
|
|
213
356
|
function validMatrix(v) {
|
|
214
357
|
return v === void 0 || Array.isArray(v) && v.length === 6 && v.every(finite);
|
|
215
358
|
}
|
|
@@ -772,6 +915,265 @@ function serializeLock(item) {
|
|
|
772
915
|
};
|
|
773
916
|
}
|
|
774
917
|
//#endregion
|
|
918
|
+
//#region src/core/shapes/shapeObjects.ts
|
|
919
|
+
var SHAPE_MIN_SIZE = .5;
|
|
920
|
+
/**
|
|
921
|
+
* Centralized shape style defaults (Phase 8) — one object a Host can read
|
|
922
|
+
* to know (or, by not relying on the standalone constants below, override
|
|
923
|
+
* via its own UI state) what a newly drawn Rectangle/Ellipse/Line/Arrow/
|
|
924
|
+
* Polygon/Star/Heart starts with when the user hasn't picked a stroke/width
|
|
925
|
+
* yet. `shapeTool.ts` (commit-time), `renderer/shapes/lines.ts` (render-time
|
|
926
|
+
* fallback for an object missing these fields), and
|
|
927
|
+
* `persistence/serialization/svg.ts` (export-time fallback) all read from
|
|
928
|
+
* here — the two standalone constants below are kept for source
|
|
929
|
+
* compatibility and simply mirror this object's values, not a second
|
|
930
|
+
* source of truth.
|
|
931
|
+
*/
|
|
932
|
+
var SHAPE_STYLE_DEFAULTS = {
|
|
933
|
+
stroke: "#1C1C1E",
|
|
934
|
+
strokeWidth: .12
|
|
935
|
+
};
|
|
936
|
+
var SHAPE_DEFAULT_STROKE = SHAPE_STYLE_DEFAULTS.stroke;
|
|
937
|
+
var SHAPE_DEFAULT_STROKE_WIDTH = SHAPE_STYLE_DEFAULTS.strokeWidth;
|
|
938
|
+
/**
|
|
939
|
+
* Normalizes a box-shaped object's `x`/`y`/`width`/`height` to non-negative
|
|
940
|
+
* dimensions in place of whatever an arbitrary caller supplied (Phase 8) —
|
|
941
|
+
* the interactive drag tools (`shapeTool.ts`) already produce
|
|
942
|
+
* always-non-negative geometry by scanning min/max points before ever
|
|
943
|
+
* constructing a shape, but a raw `content.add`/`content.update` call (Host
|
|
944
|
+
* code, a Custom tool, or a remote collaboration Op) has no such guarantee.
|
|
945
|
+
* `BoardDocument` calls this at every entry point so "dragging from
|
|
946
|
+
* bottom-right toward top-left" and "a negative `width`/`height` patch"
|
|
947
|
+
* produce the same valid geometry regardless of source. A no-op (returns
|
|
948
|
+
* the same reference) when both dimensions are already non-negative, so
|
|
949
|
+
* this never allocates on the common path.
|
|
950
|
+
*/
|
|
951
|
+
function normalizeBoxGeometry(box) {
|
|
952
|
+
if (box.width >= 0 && box.height >= 0) return box;
|
|
953
|
+
const left = Math.min(box.x, box.x + box.width);
|
|
954
|
+
const right = Math.max(box.x, box.x + box.width);
|
|
955
|
+
const top = Math.max(box.y, box.y - box.height);
|
|
956
|
+
const bottom = Math.min(box.y, box.y - box.height);
|
|
957
|
+
return {
|
|
958
|
+
...box,
|
|
959
|
+
x: left,
|
|
960
|
+
y: top,
|
|
961
|
+
width: right - left,
|
|
962
|
+
height: top - bottom
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
function cloneRectangle(rect) {
|
|
966
|
+
return { ...rect };
|
|
967
|
+
}
|
|
968
|
+
function cloneEllipse(ellipse) {
|
|
969
|
+
return { ...ellipse };
|
|
970
|
+
}
|
|
971
|
+
function clonePolygon(polygon) {
|
|
972
|
+
return { ...polygon };
|
|
973
|
+
}
|
|
974
|
+
function cloneStar(star) {
|
|
975
|
+
return { ...star };
|
|
976
|
+
}
|
|
977
|
+
function cloneHeart(heart) {
|
|
978
|
+
return { ...heart };
|
|
979
|
+
}
|
|
980
|
+
function cloneLine(line) {
|
|
981
|
+
return {
|
|
982
|
+
...line,
|
|
983
|
+
start: { ...line.start },
|
|
984
|
+
end: { ...line.end }
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
function cloneArrow(arrow) {
|
|
988
|
+
return {
|
|
989
|
+
...arrow,
|
|
990
|
+
start: { ...arrow.start },
|
|
991
|
+
end: { ...arrow.end }
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
function cloneGroup(group) {
|
|
995
|
+
return {
|
|
996
|
+
...group,
|
|
997
|
+
children: [...group.children]
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
//#endregion
|
|
1001
|
+
//#region src/core/document/objectBounds.ts
|
|
1002
|
+
function growAll(points) {
|
|
1003
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1004
|
+
for (const p of points) {
|
|
1005
|
+
if (p.x < minX) minX = p.x;
|
|
1006
|
+
if (p.y < minY) minY = p.y;
|
|
1007
|
+
if (p.x > maxX) maxX = p.x;
|
|
1008
|
+
if (p.y > maxY) maxY = p.y;
|
|
1009
|
+
}
|
|
1010
|
+
return {
|
|
1011
|
+
minX,
|
|
1012
|
+
minY,
|
|
1013
|
+
maxX,
|
|
1014
|
+
maxY
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
function noteBounds(note) {
|
|
1018
|
+
const half = note.size / 2;
|
|
1019
|
+
return {
|
|
1020
|
+
minX: note.x - half,
|
|
1021
|
+
minY: note.y - half,
|
|
1022
|
+
maxX: note.x + half,
|
|
1023
|
+
maxY: note.y + half
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
/** (x, y) is the top-left corner; text flows downward via -y. */
|
|
1027
|
+
function textBounds(block) {
|
|
1028
|
+
const { width, height } = measureTextBlock(block.text, block.fontSize);
|
|
1029
|
+
return {
|
|
1030
|
+
minX: block.x,
|
|
1031
|
+
minY: block.y - height,
|
|
1032
|
+
maxX: block.x + width,
|
|
1033
|
+
maxY: block.y
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
/** (x, y) is the top-left corner, matching TextBlock's convention. */
|
|
1037
|
+
function tableBounds(table) {
|
|
1038
|
+
const { width, height } = measureTable(table);
|
|
1039
|
+
return {
|
|
1040
|
+
minX: table.x,
|
|
1041
|
+
minY: table.y - height,
|
|
1042
|
+
maxX: table.x + width,
|
|
1043
|
+
maxY: table.y
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
/** (x, y) is the center — ImageBlock's own, different convention. */
|
|
1047
|
+
function imageBounds(image) {
|
|
1048
|
+
const halfW = image.width / 2;
|
|
1049
|
+
const halfH = image.height / 2;
|
|
1050
|
+
return {
|
|
1051
|
+
minX: image.x - halfW,
|
|
1052
|
+
minY: image.y - halfH,
|
|
1053
|
+
maxX: image.x + halfW,
|
|
1054
|
+
maxY: image.y + halfH
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
function timerBounds(timer) {
|
|
1058
|
+
const half = timer.size / 2;
|
|
1059
|
+
return {
|
|
1060
|
+
minX: timer.x - half,
|
|
1061
|
+
minY: timer.y - half,
|
|
1062
|
+
maxX: timer.x + half,
|
|
1063
|
+
maxY: timer.y + half
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
/** 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. */
|
|
1067
|
+
function customObjectBounds(object) {
|
|
1068
|
+
const b = object.fallback.bounds;
|
|
1069
|
+
return growAll([
|
|
1070
|
+
{
|
|
1071
|
+
x: b.x,
|
|
1072
|
+
y: b.y
|
|
1073
|
+
},
|
|
1074
|
+
{
|
|
1075
|
+
x: b.x + b.width,
|
|
1076
|
+
y: b.y
|
|
1077
|
+
},
|
|
1078
|
+
{
|
|
1079
|
+
x: b.x,
|
|
1080
|
+
y: b.y + b.height
|
|
1081
|
+
},
|
|
1082
|
+
{
|
|
1083
|
+
x: b.x + b.width,
|
|
1084
|
+
y: b.y + b.height
|
|
1085
|
+
}
|
|
1086
|
+
].map((corner) => apply(object.transform, corner)));
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Shared by Rectangle/Ellipse: both use the top-edge, Y-up, extends-downward
|
|
1090
|
+
* convention (see `shapeObjects.ts`'s own header comment), and both rotate
|
|
1091
|
+
* (Phase 3) about the same center `(x + width/2, y - height/2)`. An ellipse's
|
|
1092
|
+
* axis-aligned bounds equal its bounding rectangle's — an ellipse never
|
|
1093
|
+
* exceeds the box it's inscribed in — so one corner-rotation helper covers
|
|
1094
|
+
* both shapes exactly.
|
|
1095
|
+
*/
|
|
1096
|
+
function topEdgeRotatedBounds(x, y, width, height, rotation) {
|
|
1097
|
+
if (!rotation) return {
|
|
1098
|
+
minX: x,
|
|
1099
|
+
minY: y - height,
|
|
1100
|
+
maxX: x + width,
|
|
1101
|
+
maxY: y
|
|
1102
|
+
};
|
|
1103
|
+
const m = rotationAbout(rotation, x + width / 2, y - height / 2);
|
|
1104
|
+
return growAll([
|
|
1105
|
+
{
|
|
1106
|
+
x,
|
|
1107
|
+
y
|
|
1108
|
+
},
|
|
1109
|
+
{
|
|
1110
|
+
x: x + width,
|
|
1111
|
+
y
|
|
1112
|
+
},
|
|
1113
|
+
{
|
|
1114
|
+
x,
|
|
1115
|
+
y: y - height
|
|
1116
|
+
},
|
|
1117
|
+
{
|
|
1118
|
+
x: x + width,
|
|
1119
|
+
y: y - height
|
|
1120
|
+
}
|
|
1121
|
+
].map((corner) => apply(m, corner)));
|
|
1122
|
+
}
|
|
1123
|
+
function rectangleBounds(rect) {
|
|
1124
|
+
return topEdgeRotatedBounds(rect.x, rect.y, rect.width, rect.height, rect.rotation);
|
|
1125
|
+
}
|
|
1126
|
+
function ellipseBounds(ellipse) {
|
|
1127
|
+
return topEdgeRotatedBounds(ellipse.x, ellipse.y, ellipse.width, ellipse.height, ellipse.rotation);
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* Same top-edge/Y-up/rotate-about-center convention as Rectangle — a
|
|
1131
|
+
* regular polygon's own outline is normalized (`polygonGeometry.ts`'s
|
|
1132
|
+
* `fitToBox`) to exactly fill its declared box, so this is exact at
|
|
1133
|
+
* rotation 0 and a safe (never-too-small) superset once rotated, matching
|
|
1134
|
+
* how `ellipseBounds` already treats its own inscribed shape.
|
|
1135
|
+
*/
|
|
1136
|
+
function polygonBounds(polygon) {
|
|
1137
|
+
return topEdgeRotatedBounds(polygon.x, polygon.y, polygon.width, polygon.height, polygon.rotation);
|
|
1138
|
+
}
|
|
1139
|
+
/** Same convention and same safe-superset reasoning as `polygonBounds` — a star's points are normalized to touch its declared box (`polygonGeometry.ts`'s `starPoints`). */
|
|
1140
|
+
function starBounds(star) {
|
|
1141
|
+
return topEdgeRotatedBounds(star.x, star.y, star.width, star.height, star.rotation);
|
|
1142
|
+
}
|
|
1143
|
+
/** Same convention and same safe-superset reasoning as `polygonBounds` — a heart's curve is normalized to touch its declared box (`polygonGeometry.ts`'s `heartPoints`). */
|
|
1144
|
+
function heartBounds(heart) {
|
|
1145
|
+
return topEdgeRotatedBounds(heart.x, heart.y, heart.width, heart.height, heart.rotation);
|
|
1146
|
+
}
|
|
1147
|
+
/** No rotation concept (see `shapeObjects.ts`'s header) — the two endpoints already are the bounds. */
|
|
1148
|
+
function lineBounds(line) {
|
|
1149
|
+
return growAll([line.start, line.end]);
|
|
1150
|
+
}
|
|
1151
|
+
/** 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). */
|
|
1152
|
+
function arrowBounds(arrow) {
|
|
1153
|
+
return growAll([arrow.start, arrow.end]);
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Union of one or more child bounds, or `null` when every child resolved to
|
|
1157
|
+
* nothing (an empty or entirely-dangling group). Used for `GroupObject`
|
|
1158
|
+
* bounds — the caller (`BoardDocument.bbox`) supplies `resolve` as its own
|
|
1159
|
+
* `bbox(id)` so this stays a pure fold with no document dependency here.
|
|
1160
|
+
*/
|
|
1161
|
+
function unionBounds(boxes) {
|
|
1162
|
+
let result;
|
|
1163
|
+
for (const box of boxes) {
|
|
1164
|
+
if (!box) continue;
|
|
1165
|
+
if (!result) {
|
|
1166
|
+
result = { ...box };
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
result.minX = Math.min(result.minX, box.minX);
|
|
1170
|
+
result.minY = Math.min(result.minY, box.minY);
|
|
1171
|
+
result.maxX = Math.max(result.maxX, box.maxX);
|
|
1172
|
+
result.maxY = Math.max(result.maxY, box.maxY);
|
|
1173
|
+
}
|
|
1174
|
+
return result;
|
|
1175
|
+
}
|
|
1176
|
+
//#endregion
|
|
775
1177
|
//#region src/core/document/document.ts
|
|
776
1178
|
/**
|
|
777
1179
|
* The one place a live Stroke becomes its wire representation — in
|
|
@@ -822,8 +1224,73 @@ var EMPTY_CHANGE = {
|
|
|
822
1224
|
timersUpdated: [],
|
|
823
1225
|
customObjectsAdded: [],
|
|
824
1226
|
customObjectsRemoved: [],
|
|
825
|
-
customObjectsUpdated: []
|
|
1227
|
+
customObjectsUpdated: [],
|
|
1228
|
+
rectanglesAdded: [],
|
|
1229
|
+
rectanglesRemoved: [],
|
|
1230
|
+
rectanglesUpdated: [],
|
|
1231
|
+
ellipsesAdded: [],
|
|
1232
|
+
ellipsesRemoved: [],
|
|
1233
|
+
ellipsesUpdated: [],
|
|
1234
|
+
groupsAdded: [],
|
|
1235
|
+
groupsRemoved: [],
|
|
1236
|
+
groupsUpdated: [],
|
|
1237
|
+
linesAdded: [],
|
|
1238
|
+
linesRemoved: [],
|
|
1239
|
+
linesUpdated: [],
|
|
1240
|
+
arrowsAdded: [],
|
|
1241
|
+
arrowsRemoved: [],
|
|
1242
|
+
arrowsUpdated: [],
|
|
1243
|
+
polygonsAdded: [],
|
|
1244
|
+
polygonsRemoved: [],
|
|
1245
|
+
polygonsUpdated: [],
|
|
1246
|
+
starsAdded: [],
|
|
1247
|
+
starsRemoved: [],
|
|
1248
|
+
starsUpdated: [],
|
|
1249
|
+
heartsAdded: [],
|
|
1250
|
+
heartsRemoved: [],
|
|
1251
|
+
heartsUpdated: [],
|
|
1252
|
+
orderChanged: []
|
|
826
1253
|
};
|
|
1254
|
+
/**
|
|
1255
|
+
* Flat content types that participate in the unified paint order — see
|
|
1256
|
+
* `DocumentChange.orderChanged`'s own doc comment for why notes/timers are
|
|
1257
|
+
* excluded. Listed back-to-front in the exact sequence the old fixed
|
|
1258
|
+
* per-renderer Z constants already used (rectangles/ellipses lowest, then
|
|
1259
|
+
* tables, then images, then ink/text, then custom objects) — this is what
|
|
1260
|
+
* `replaceAll`/`emit`'s automatic append-on-add tracking uses to synthesize
|
|
1261
|
+
* a default order for a legacy document with no persisted `objectOrder`, so
|
|
1262
|
+
* an existing document's visual stacking never changes just from loading it.
|
|
1263
|
+
*/
|
|
1264
|
+
var ORDERED_ADDED_FIELDS = [
|
|
1265
|
+
"rectanglesAdded",
|
|
1266
|
+
"ellipsesAdded",
|
|
1267
|
+
"tablesAdded",
|
|
1268
|
+
"imagesAdded",
|
|
1269
|
+
"added",
|
|
1270
|
+
"textAdded",
|
|
1271
|
+
"customObjectsAdded",
|
|
1272
|
+
"groupsAdded",
|
|
1273
|
+
"linesAdded",
|
|
1274
|
+
"arrowsAdded",
|
|
1275
|
+
"polygonsAdded",
|
|
1276
|
+
"starsAdded",
|
|
1277
|
+
"heartsAdded"
|
|
1278
|
+
];
|
|
1279
|
+
var ORDERED_REMOVED_FIELDS = [
|
|
1280
|
+
"rectanglesRemoved",
|
|
1281
|
+
"ellipsesRemoved",
|
|
1282
|
+
"tablesRemoved",
|
|
1283
|
+
"imagesRemoved",
|
|
1284
|
+
"removed",
|
|
1285
|
+
"textRemoved",
|
|
1286
|
+
"customObjectsRemoved",
|
|
1287
|
+
"groupsRemoved",
|
|
1288
|
+
"linesRemoved",
|
|
1289
|
+
"arrowsRemoved",
|
|
1290
|
+
"polygonsRemoved",
|
|
1291
|
+
"starsRemoved",
|
|
1292
|
+
"heartsRemoved"
|
|
1293
|
+
];
|
|
827
1294
|
var BoardDocument = class {
|
|
828
1295
|
constructor(id) {
|
|
829
1296
|
this.id = id;
|
|
@@ -835,8 +1302,85 @@ var BoardDocument = class {
|
|
|
835
1302
|
this.images = /* @__PURE__ */ new Map();
|
|
836
1303
|
this.timers = /* @__PURE__ */ new Map();
|
|
837
1304
|
this.customObjects = /* @__PURE__ */ new Map();
|
|
1305
|
+
this.rectangles = /* @__PURE__ */ new Map();
|
|
1306
|
+
this.ellipses = /* @__PURE__ */ new Map();
|
|
1307
|
+
this.groups = /* @__PURE__ */ new Map();
|
|
1308
|
+
this.lines = /* @__PURE__ */ new Map();
|
|
1309
|
+
this.arrows = /* @__PURE__ */ new Map();
|
|
1310
|
+
this.polygons = /* @__PURE__ */ new Map();
|
|
1311
|
+
this.stars = /* @__PURE__ */ new Map();
|
|
1312
|
+
this.hearts = /* @__PURE__ */ new Map();
|
|
838
1313
|
this.bboxes = /* @__PURE__ */ new Map();
|
|
839
1314
|
this.listeners = /* @__PURE__ */ new Set();
|
|
1315
|
+
this.objectOrder = [];
|
|
1316
|
+
this.orderIndex = /* @__PURE__ */ new Map();
|
|
1317
|
+
}
|
|
1318
|
+
reindexOrder() {
|
|
1319
|
+
this.orderIndex = new Map(this.objectOrder.map((id, index) => [id, index]));
|
|
1320
|
+
}
|
|
1321
|
+
/** The full current paint order, back to front. */
|
|
1322
|
+
order() {
|
|
1323
|
+
return this.objectOrder;
|
|
1324
|
+
}
|
|
1325
|
+
/** This object's rank in the paint order, or -1 if it doesn't participate (unknown id, a note, or a timer). */
|
|
1326
|
+
orderRank(id) {
|
|
1327
|
+
return this.orderIndex.get(id) ?? -1;
|
|
1328
|
+
}
|
|
1329
|
+
bringForward(id) {
|
|
1330
|
+
const i = this.orderIndex.get(id);
|
|
1331
|
+
if (i === void 0 || i >= this.objectOrder.length - 1) return;
|
|
1332
|
+
[this.objectOrder[i], this.objectOrder[i + 1]] = [this.objectOrder[i + 1], this.objectOrder[i]];
|
|
1333
|
+
this.orderIndex.set(this.objectOrder[i], i);
|
|
1334
|
+
this.orderIndex.set(this.objectOrder[i + 1], i + 1);
|
|
1335
|
+
this.emit({ orderChanged: [this.objectOrder[i], this.objectOrder[i + 1]] });
|
|
1336
|
+
}
|
|
1337
|
+
sendBackward(id) {
|
|
1338
|
+
const i = this.orderIndex.get(id);
|
|
1339
|
+
if (i === void 0 || i <= 0) return;
|
|
1340
|
+
[this.objectOrder[i], this.objectOrder[i - 1]] = [this.objectOrder[i - 1], this.objectOrder[i]];
|
|
1341
|
+
this.orderIndex.set(this.objectOrder[i], i);
|
|
1342
|
+
this.orderIndex.set(this.objectOrder[i - 1], i - 1);
|
|
1343
|
+
this.emit({ orderChanged: [this.objectOrder[i - 1], this.objectOrder[i]] });
|
|
1344
|
+
}
|
|
1345
|
+
bringToFront(id) {
|
|
1346
|
+
const i = this.orderIndex.get(id);
|
|
1347
|
+
if (i === void 0 || i >= this.objectOrder.length - 1) return;
|
|
1348
|
+
this.objectOrder.splice(i, 1);
|
|
1349
|
+
this.objectOrder.push(id);
|
|
1350
|
+
for (let k = i; k < this.objectOrder.length; k++) this.orderIndex.set(this.objectOrder[k], k);
|
|
1351
|
+
this.emit({ orderChanged: this.objectOrder.slice(i) });
|
|
1352
|
+
}
|
|
1353
|
+
sendToBack(id) {
|
|
1354
|
+
const i = this.orderIndex.get(id);
|
|
1355
|
+
if (i === void 0 || i <= 0) return;
|
|
1356
|
+
this.objectOrder.splice(i, 1);
|
|
1357
|
+
this.objectOrder.unshift(id);
|
|
1358
|
+
for (let k = 0; k <= i; k++) this.orderIndex.set(this.objectOrder[k], k);
|
|
1359
|
+
this.emit({ orderChanged: this.objectOrder.slice(0, i + 1) });
|
|
1360
|
+
}
|
|
1361
|
+
/** Reorders `id` relative to its current neighbors. A no-op for an id that doesn't participate in paint order (see `orderRank`). */
|
|
1362
|
+
reorder(id, direction) {
|
|
1363
|
+
if (this.orderIndex.get(id) === void 0) return;
|
|
1364
|
+
if (direction === "forward") this.bringForward(id);
|
|
1365
|
+
else if (direction === "backward") this.sendBackward(id);
|
|
1366
|
+
else if (direction === "front") this.bringToFront(id);
|
|
1367
|
+
else this.sendToBack(id);
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Overwrites the paint order directly — used only when loading a document
|
|
1371
|
+
* that already carries a persisted `objectOrder`; every other order
|
|
1372
|
+
* mutation goes through `reorder`/the automatic append-on-add tracking in
|
|
1373
|
+
* `emit`. Ids not present in the document are dropped; ids present in the
|
|
1374
|
+
* document but missing from `order` are appended at the back, so a
|
|
1375
|
+
* partially-stale order (e.g. from a schema migration) never silently
|
|
1376
|
+
* drops an object from paint order entirely.
|
|
1377
|
+
*/
|
|
1378
|
+
setOrder(order) {
|
|
1379
|
+
const known = new Set(this.objectOrder);
|
|
1380
|
+
const next = order.filter((id) => known.has(id));
|
|
1381
|
+
for (const id of this.objectOrder) if (!next.includes(id)) next.push(id);
|
|
1382
|
+
this.objectOrder = next;
|
|
1383
|
+
this.reindexOrder();
|
|
840
1384
|
}
|
|
841
1385
|
get(id) {
|
|
842
1386
|
return this.strokes.get(id);
|
|
@@ -844,8 +1388,75 @@ var BoardDocument = class {
|
|
|
844
1388
|
all() {
|
|
845
1389
|
return this.strokes.values();
|
|
846
1390
|
}
|
|
847
|
-
|
|
848
|
-
|
|
1391
|
+
/**
|
|
1392
|
+
* World-space bounds for any content object, of any type. Strokes hit
|
|
1393
|
+
* their cached-on-mutation fast path (`bboxes`, populated by
|
|
1394
|
+
* `addStrokes`/`transformStrokes` — many points, worth caching); every
|
|
1395
|
+
* other type computes on demand via `objectBounds.ts` (cheap arithmetic,
|
|
1396
|
+
* no caching needed). A group's bounds are the union of its (recursively
|
|
1397
|
+
* resolved) children — `seen` guards against a cycle in nested groups.
|
|
1398
|
+
*/
|
|
1399
|
+
bbox(id, seen) {
|
|
1400
|
+
const cached = this.bboxes.get(id);
|
|
1401
|
+
if (cached) return cached;
|
|
1402
|
+
const note = this.notes.get(id);
|
|
1403
|
+
if (note) return noteBounds(note);
|
|
1404
|
+
const text = this.texts.get(id);
|
|
1405
|
+
if (text) return textBounds(text);
|
|
1406
|
+
const table = this.tables.get(id);
|
|
1407
|
+
if (table) return tableBounds(table);
|
|
1408
|
+
const image = this.images.get(id);
|
|
1409
|
+
if (image) return imageBounds(image);
|
|
1410
|
+
const timer = this.timers.get(id);
|
|
1411
|
+
if (timer) return timerBounds(timer);
|
|
1412
|
+
const custom = this.customObjects.get(id);
|
|
1413
|
+
if (custom) return customObjectBounds(custom);
|
|
1414
|
+
const rect = this.rectangles.get(id);
|
|
1415
|
+
if (rect) return rectangleBounds(rect);
|
|
1416
|
+
const ellipse = this.ellipses.get(id);
|
|
1417
|
+
if (ellipse) return ellipseBounds(ellipse);
|
|
1418
|
+
const line = this.lines.get(id);
|
|
1419
|
+
if (line) return lineBounds(line);
|
|
1420
|
+
const arrow = this.arrows.get(id);
|
|
1421
|
+
if (arrow) return arrowBounds(arrow);
|
|
1422
|
+
const polygon = this.polygons.get(id);
|
|
1423
|
+
if (polygon) return polygonBounds(polygon);
|
|
1424
|
+
const star = this.stars.get(id);
|
|
1425
|
+
if (star) return starBounds(star);
|
|
1426
|
+
const heart = this.hearts.get(id);
|
|
1427
|
+
if (heart) return heartBounds(heart);
|
|
1428
|
+
const group = this.groups.get(id);
|
|
1429
|
+
if (group) {
|
|
1430
|
+
const visited = seen ?? /* @__PURE__ */ new Set();
|
|
1431
|
+
if (visited.has(id)) return void 0;
|
|
1432
|
+
visited.add(id);
|
|
1433
|
+
return unionBounds(group.children.map((childId) => this.bbox(childId, visited)));
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* True if `id` exists and is locked, for any type — the same per-type
|
|
1438
|
+
* probe pattern as `bbox`, for interactive gestures (drag/transform) that
|
|
1439
|
+
* need to gate on lock state regardless of what's selected. Custom
|
|
1440
|
+
* objects are deliberately excluded: their `lock` field is a different
|
|
1441
|
+
* shape (`{holderId, acquiredAt}`, no display name) with no interactive
|
|
1442
|
+
* lock UI yet, matching the existing, deliberate "always unlockable,
|
|
1443
|
+
* never gates a drag" treatment already established elsewhere (e.g.
|
|
1444
|
+
* `getSelectedItemInfo`'s custom branch hardcodes `isLocked: false`).
|
|
1445
|
+
*/
|
|
1446
|
+
isLocked(id) {
|
|
1447
|
+
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;
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* True if `id` exists and is hidden, for any type — the same per-type
|
|
1451
|
+
* probe pattern as {@link isLocked} (Phase 8). Custom objects are
|
|
1452
|
+
* excluded for the same reason `isLocked` excludes them: they have no
|
|
1453
|
+
* `Hideable` field at all, so "hidden" isn't a concept that applies to
|
|
1454
|
+
* them yet. Used by marquee selection (`selectTool.ts`) to keep a hidden
|
|
1455
|
+
* object out of a rubber-band selection even for object types whose own
|
|
1456
|
+
* renderer doesn't yet suppress click-based hit-testing.
|
|
1457
|
+
*/
|
|
1458
|
+
isHidden(id) {
|
|
1459
|
+
return !!this.strokes.get(id)?.hidden || !!this.notes.get(id)?.hidden || !!this.texts.get(id)?.hidden || !!this.tables.get(id)?.hidden || !!this.images.get(id)?.hidden || !!this.timers.get(id)?.hidden || !!this.rectangles.get(id)?.hidden || !!this.ellipses.get(id)?.hidden || !!this.groups.get(id)?.hidden || !!this.lines.get(id)?.hidden || !!this.arrows.get(id)?.hidden || !!this.polygons.get(id)?.hidden || !!this.stars.get(id)?.hidden || !!this.hearts.get(id)?.hidden;
|
|
849
1460
|
}
|
|
850
1461
|
subscribe(listener) {
|
|
851
1462
|
this.listeners.add(listener);
|
|
@@ -994,6 +1605,176 @@ var BoardDocument = class {
|
|
|
994
1605
|
this.customObjects.set(object.id, object);
|
|
995
1606
|
this.emit({ customObjectsUpdated: [object] });
|
|
996
1607
|
}
|
|
1608
|
+
getRectangle(id) {
|
|
1609
|
+
return this.rectangles.get(id);
|
|
1610
|
+
}
|
|
1611
|
+
allRectangles() {
|
|
1612
|
+
return this.rectangles.values();
|
|
1613
|
+
}
|
|
1614
|
+
addRectangles(rectangles) {
|
|
1615
|
+
const normalized = rectangles.map(normalizeBoxGeometry);
|
|
1616
|
+
for (const rect of normalized) this.rectangles.set(rect.id, rect);
|
|
1617
|
+
this.emit({ rectanglesAdded: normalized });
|
|
1618
|
+
}
|
|
1619
|
+
removeRectangles(ids) {
|
|
1620
|
+
const rectanglesRemoved = [];
|
|
1621
|
+
for (const id of ids) if (this.rectangles.delete(id)) rectanglesRemoved.push(id);
|
|
1622
|
+
if (rectanglesRemoved.length) this.emit({ rectanglesRemoved });
|
|
1623
|
+
}
|
|
1624
|
+
/** Replace a rectangle's contents (move, resize, restyle) under the same id. */
|
|
1625
|
+
setRectangle(rect) {
|
|
1626
|
+
const normalized = normalizeBoxGeometry(rect);
|
|
1627
|
+
this.rectangles.set(normalized.id, normalized);
|
|
1628
|
+
this.emit({ rectanglesUpdated: [normalized] });
|
|
1629
|
+
}
|
|
1630
|
+
getEllipse(id) {
|
|
1631
|
+
return this.ellipses.get(id);
|
|
1632
|
+
}
|
|
1633
|
+
allEllipses() {
|
|
1634
|
+
return this.ellipses.values();
|
|
1635
|
+
}
|
|
1636
|
+
addEllipses(ellipses) {
|
|
1637
|
+
const normalized = ellipses.map(normalizeBoxGeometry);
|
|
1638
|
+
for (const ellipse of normalized) this.ellipses.set(ellipse.id, ellipse);
|
|
1639
|
+
this.emit({ ellipsesAdded: normalized });
|
|
1640
|
+
}
|
|
1641
|
+
removeEllipses(ids) {
|
|
1642
|
+
const ellipsesRemoved = [];
|
|
1643
|
+
for (const id of ids) if (this.ellipses.delete(id)) ellipsesRemoved.push(id);
|
|
1644
|
+
if (ellipsesRemoved.length) this.emit({ ellipsesRemoved });
|
|
1645
|
+
}
|
|
1646
|
+
/** Replace an ellipse's contents (move, resize, restyle) under the same id. */
|
|
1647
|
+
setEllipse(ellipse) {
|
|
1648
|
+
const normalized = normalizeBoxGeometry(ellipse);
|
|
1649
|
+
this.ellipses.set(normalized.id, normalized);
|
|
1650
|
+
this.emit({ ellipsesUpdated: [normalized] });
|
|
1651
|
+
}
|
|
1652
|
+
getGroup(id) {
|
|
1653
|
+
return this.groups.get(id);
|
|
1654
|
+
}
|
|
1655
|
+
allGroups() {
|
|
1656
|
+
return this.groups.values();
|
|
1657
|
+
}
|
|
1658
|
+
addGroups(groups) {
|
|
1659
|
+
for (const group of groups) this.groups.set(group.id, group);
|
|
1660
|
+
this.emit({ groupsAdded: groups });
|
|
1661
|
+
}
|
|
1662
|
+
removeGroups(ids) {
|
|
1663
|
+
const groupsRemoved = [];
|
|
1664
|
+
for (const id of ids) if (this.groups.delete(id)) groupsRemoved.push(id);
|
|
1665
|
+
if (groupsRemoved.length) this.emit({ groupsRemoved });
|
|
1666
|
+
}
|
|
1667
|
+
/** Replace a group's contents (its children list) under the same id. */
|
|
1668
|
+
setGroup(group) {
|
|
1669
|
+
this.groups.set(group.id, group);
|
|
1670
|
+
this.emit({ groupsUpdated: [group] });
|
|
1671
|
+
}
|
|
1672
|
+
getLine(id) {
|
|
1673
|
+
return this.lines.get(id);
|
|
1674
|
+
}
|
|
1675
|
+
allLines() {
|
|
1676
|
+
return this.lines.values();
|
|
1677
|
+
}
|
|
1678
|
+
addLines(lines) {
|
|
1679
|
+
for (const line of lines) this.lines.set(line.id, line);
|
|
1680
|
+
this.emit({ linesAdded: lines });
|
|
1681
|
+
}
|
|
1682
|
+
removeLines(ids) {
|
|
1683
|
+
const linesRemoved = [];
|
|
1684
|
+
for (const id of ids) if (this.lines.delete(id)) linesRemoved.push(id);
|
|
1685
|
+
if (linesRemoved.length) this.emit({ linesRemoved });
|
|
1686
|
+
}
|
|
1687
|
+
/** Replace a line's contents (move, restyle) under the same id. */
|
|
1688
|
+
setLine(line) {
|
|
1689
|
+
this.lines.set(line.id, line);
|
|
1690
|
+
this.emit({ linesUpdated: [line] });
|
|
1691
|
+
}
|
|
1692
|
+
getArrow(id) {
|
|
1693
|
+
return this.arrows.get(id);
|
|
1694
|
+
}
|
|
1695
|
+
allArrows() {
|
|
1696
|
+
return this.arrows.values();
|
|
1697
|
+
}
|
|
1698
|
+
addArrows(arrows) {
|
|
1699
|
+
for (const arrow of arrows) this.arrows.set(arrow.id, arrow);
|
|
1700
|
+
this.emit({ arrowsAdded: arrows });
|
|
1701
|
+
}
|
|
1702
|
+
removeArrows(ids) {
|
|
1703
|
+
const arrowsRemoved = [];
|
|
1704
|
+
for (const id of ids) if (this.arrows.delete(id)) arrowsRemoved.push(id);
|
|
1705
|
+
if (arrowsRemoved.length) this.emit({ arrowsRemoved });
|
|
1706
|
+
}
|
|
1707
|
+
/** Replace an arrow's contents (move, restyle, change head) under the same id. */
|
|
1708
|
+
setArrow(arrow) {
|
|
1709
|
+
this.arrows.set(arrow.id, arrow);
|
|
1710
|
+
this.emit({ arrowsUpdated: [arrow] });
|
|
1711
|
+
}
|
|
1712
|
+
getPolygon(id) {
|
|
1713
|
+
return this.polygons.get(id);
|
|
1714
|
+
}
|
|
1715
|
+
allPolygons() {
|
|
1716
|
+
return this.polygons.values();
|
|
1717
|
+
}
|
|
1718
|
+
addPolygons(polygons) {
|
|
1719
|
+
const normalized = polygons.map(normalizeBoxGeometry);
|
|
1720
|
+
for (const polygon of normalized) this.polygons.set(polygon.id, polygon);
|
|
1721
|
+
this.emit({ polygonsAdded: normalized });
|
|
1722
|
+
}
|
|
1723
|
+
removePolygons(ids) {
|
|
1724
|
+
const polygonsRemoved = [];
|
|
1725
|
+
for (const id of ids) if (this.polygons.delete(id)) polygonsRemoved.push(id);
|
|
1726
|
+
if (polygonsRemoved.length) this.emit({ polygonsRemoved });
|
|
1727
|
+
}
|
|
1728
|
+
/** Replace a polygon's contents (move, resize, rotate, restyle) under the same id. */
|
|
1729
|
+
setPolygon(polygon) {
|
|
1730
|
+
const normalized = normalizeBoxGeometry(polygon);
|
|
1731
|
+
this.polygons.set(normalized.id, normalized);
|
|
1732
|
+
this.emit({ polygonsUpdated: [normalized] });
|
|
1733
|
+
}
|
|
1734
|
+
getStar(id) {
|
|
1735
|
+
return this.stars.get(id);
|
|
1736
|
+
}
|
|
1737
|
+
allStars() {
|
|
1738
|
+
return this.stars.values();
|
|
1739
|
+
}
|
|
1740
|
+
addStars(stars) {
|
|
1741
|
+
const normalized = stars.map(normalizeBoxGeometry);
|
|
1742
|
+
for (const star of normalized) this.stars.set(star.id, star);
|
|
1743
|
+
this.emit({ starsAdded: normalized });
|
|
1744
|
+
}
|
|
1745
|
+
removeStars(ids) {
|
|
1746
|
+
const starsRemoved = [];
|
|
1747
|
+
for (const id of ids) if (this.stars.delete(id)) starsRemoved.push(id);
|
|
1748
|
+
if (starsRemoved.length) this.emit({ starsRemoved });
|
|
1749
|
+
}
|
|
1750
|
+
/** Replace a star's contents (move, resize, rotate, restyle) under the same id. */
|
|
1751
|
+
setStar(star) {
|
|
1752
|
+
const normalized = normalizeBoxGeometry(star);
|
|
1753
|
+
this.stars.set(normalized.id, normalized);
|
|
1754
|
+
this.emit({ starsUpdated: [normalized] });
|
|
1755
|
+
}
|
|
1756
|
+
getHeart(id) {
|
|
1757
|
+
return this.hearts.get(id);
|
|
1758
|
+
}
|
|
1759
|
+
allHearts() {
|
|
1760
|
+
return this.hearts.values();
|
|
1761
|
+
}
|
|
1762
|
+
addHearts(hearts) {
|
|
1763
|
+
const normalized = hearts.map(normalizeBoxGeometry);
|
|
1764
|
+
for (const heart of normalized) this.hearts.set(heart.id, heart);
|
|
1765
|
+
this.emit({ heartsAdded: normalized });
|
|
1766
|
+
}
|
|
1767
|
+
removeHearts(ids) {
|
|
1768
|
+
const heartsRemoved = [];
|
|
1769
|
+
for (const id of ids) if (this.hearts.delete(id)) heartsRemoved.push(id);
|
|
1770
|
+
if (heartsRemoved.length) this.emit({ heartsRemoved });
|
|
1771
|
+
}
|
|
1772
|
+
/** Replace a heart's contents (move, resize, rotate, restyle) under the same id. */
|
|
1773
|
+
setHeart(heart) {
|
|
1774
|
+
const normalized = normalizeBoxGeometry(heart);
|
|
1775
|
+
this.hearts.set(normalized.id, normalized);
|
|
1776
|
+
this.emit({ heartsUpdated: [normalized] });
|
|
1777
|
+
}
|
|
997
1778
|
setStrokeLocked(id, locked, by) {
|
|
998
1779
|
const stroke = this.strokes.get(id);
|
|
999
1780
|
if (!stroke) return;
|
|
@@ -1041,8 +1822,56 @@ var BoardDocument = class {
|
|
|
1041
1822
|
applyItemLock(timer, locked, by);
|
|
1042
1823
|
this.emit({ timersUpdated: [timer] });
|
|
1043
1824
|
}
|
|
1825
|
+
setRectangleLocked(id, locked, by) {
|
|
1826
|
+
const rect = this.rectangles.get(id);
|
|
1827
|
+
if (!rect) return;
|
|
1828
|
+
applyItemLock(rect, locked, by);
|
|
1829
|
+
this.emit({ rectanglesUpdated: [rect] });
|
|
1830
|
+
}
|
|
1831
|
+
setEllipseLocked(id, locked, by) {
|
|
1832
|
+
const ellipse = this.ellipses.get(id);
|
|
1833
|
+
if (!ellipse) return;
|
|
1834
|
+
applyItemLock(ellipse, locked, by);
|
|
1835
|
+
this.emit({ ellipsesUpdated: [ellipse] });
|
|
1836
|
+
}
|
|
1837
|
+
setGroupLocked(id, locked, by) {
|
|
1838
|
+
const group = this.groups.get(id);
|
|
1839
|
+
if (!group) return;
|
|
1840
|
+
applyItemLock(group, locked, by);
|
|
1841
|
+
this.emit({ groupsUpdated: [group] });
|
|
1842
|
+
}
|
|
1843
|
+
setLineLocked(id, locked, by) {
|
|
1844
|
+
const line = this.lines.get(id);
|
|
1845
|
+
if (!line) return;
|
|
1846
|
+
applyItemLock(line, locked, by);
|
|
1847
|
+
this.emit({ linesUpdated: [line] });
|
|
1848
|
+
}
|
|
1849
|
+
setArrowLocked(id, locked, by) {
|
|
1850
|
+
const arrow = this.arrows.get(id);
|
|
1851
|
+
if (!arrow) return;
|
|
1852
|
+
applyItemLock(arrow, locked, by);
|
|
1853
|
+
this.emit({ arrowsUpdated: [arrow] });
|
|
1854
|
+
}
|
|
1855
|
+
setPolygonLocked(id, locked, by) {
|
|
1856
|
+
const polygon = this.polygons.get(id);
|
|
1857
|
+
if (!polygon) return;
|
|
1858
|
+
applyItemLock(polygon, locked, by);
|
|
1859
|
+
this.emit({ polygonsUpdated: [polygon] });
|
|
1860
|
+
}
|
|
1861
|
+
setStarLocked(id, locked, by) {
|
|
1862
|
+
const star = this.stars.get(id);
|
|
1863
|
+
if (!star) return;
|
|
1864
|
+
applyItemLock(star, locked, by);
|
|
1865
|
+
this.emit({ starsUpdated: [star] });
|
|
1866
|
+
}
|
|
1867
|
+
setHeartLocked(id, locked, by) {
|
|
1868
|
+
const heart = this.hearts.get(id);
|
|
1869
|
+
if (!heart) return;
|
|
1870
|
+
applyItemLock(heart, locked, by);
|
|
1871
|
+
this.emit({ heartsUpdated: [heart] });
|
|
1872
|
+
}
|
|
1044
1873
|
/** Replace all content (initial load). Does not touch `version`. */
|
|
1045
|
-
replaceAll(strokes, notes, texts, tables = [], images = [], timers = [], customObjects = []) {
|
|
1874
|
+
replaceAll(strokes, notes, texts, tables = [], images = [], timers = [], customObjects = [], rectangles = [], ellipses = [], groups = [], lines = [], arrows = [], polygons = [], stars = [], hearts = [], objectOrder) {
|
|
1046
1875
|
const removed = [...this.strokes.keys()];
|
|
1047
1876
|
const notesRemoved = [...this.notes.keys()];
|
|
1048
1877
|
const textRemoved = [...this.texts.keys()];
|
|
@@ -1050,6 +1879,14 @@ var BoardDocument = class {
|
|
|
1050
1879
|
const imagesRemoved = [...this.images.keys()];
|
|
1051
1880
|
const timersRemoved = [...this.timers.keys()];
|
|
1052
1881
|
const customObjectsRemoved = [...this.customObjects.keys()];
|
|
1882
|
+
const rectanglesRemoved = [...this.rectangles.keys()];
|
|
1883
|
+
const ellipsesRemoved = [...this.ellipses.keys()];
|
|
1884
|
+
const groupsRemoved = [...this.groups.keys()];
|
|
1885
|
+
const linesRemoved = [...this.lines.keys()];
|
|
1886
|
+
const arrowsRemoved = [...this.arrows.keys()];
|
|
1887
|
+
const polygonsRemoved = [...this.polygons.keys()];
|
|
1888
|
+
const starsRemoved = [...this.stars.keys()];
|
|
1889
|
+
const heartsRemoved = [...this.hearts.keys()];
|
|
1053
1890
|
this.strokes.clear();
|
|
1054
1891
|
this.notes.clear();
|
|
1055
1892
|
this.texts.clear();
|
|
@@ -1057,6 +1894,14 @@ var BoardDocument = class {
|
|
|
1057
1894
|
this.images.clear();
|
|
1058
1895
|
this.timers.clear();
|
|
1059
1896
|
this.customObjects.clear();
|
|
1897
|
+
this.rectangles.clear();
|
|
1898
|
+
this.ellipses.clear();
|
|
1899
|
+
this.groups.clear();
|
|
1900
|
+
this.lines.clear();
|
|
1901
|
+
this.arrows.clear();
|
|
1902
|
+
this.polygons.clear();
|
|
1903
|
+
this.stars.clear();
|
|
1904
|
+
this.hearts.clear();
|
|
1060
1905
|
this.bboxes.clear();
|
|
1061
1906
|
for (const stroke of strokes) {
|
|
1062
1907
|
this.strokes.set(stroke.id, stroke);
|
|
@@ -1068,6 +1913,14 @@ var BoardDocument = class {
|
|
|
1068
1913
|
for (const img of images) this.images.set(img.id, img);
|
|
1069
1914
|
for (const timer of timers) this.timers.set(timer.id, timer);
|
|
1070
1915
|
for (const object of customObjects) this.customObjects.set(object.id, object);
|
|
1916
|
+
for (const rect of rectangles) this.rectangles.set(rect.id, rect);
|
|
1917
|
+
for (const ellipse of ellipses) this.ellipses.set(ellipse.id, ellipse);
|
|
1918
|
+
for (const group of groups) this.groups.set(group.id, group);
|
|
1919
|
+
for (const line of lines) this.lines.set(line.id, line);
|
|
1920
|
+
for (const arrow of arrows) this.arrows.set(arrow.id, arrow);
|
|
1921
|
+
for (const polygon of polygons) this.polygons.set(polygon.id, polygon);
|
|
1922
|
+
for (const star of stars) this.stars.set(star.id, star);
|
|
1923
|
+
for (const heart of hearts) this.hearts.set(heart.id, heart);
|
|
1071
1924
|
this.emit({
|
|
1072
1925
|
added: strokes,
|
|
1073
1926
|
removed,
|
|
@@ -1082,8 +1935,25 @@ var BoardDocument = class {
|
|
|
1082
1935
|
timersAdded: timers,
|
|
1083
1936
|
timersRemoved,
|
|
1084
1937
|
customObjectsAdded: customObjects,
|
|
1085
|
-
customObjectsRemoved
|
|
1938
|
+
customObjectsRemoved,
|
|
1939
|
+
rectanglesAdded: rectangles,
|
|
1940
|
+
rectanglesRemoved,
|
|
1941
|
+
ellipsesAdded: ellipses,
|
|
1942
|
+
ellipsesRemoved,
|
|
1943
|
+
groupsAdded: groups,
|
|
1944
|
+
groupsRemoved,
|
|
1945
|
+
linesAdded: lines,
|
|
1946
|
+
linesRemoved,
|
|
1947
|
+
arrowsAdded: arrows,
|
|
1948
|
+
arrowsRemoved,
|
|
1949
|
+
polygonsAdded: polygons,
|
|
1950
|
+
polygonsRemoved,
|
|
1951
|
+
starsAdded: stars,
|
|
1952
|
+
starsRemoved,
|
|
1953
|
+
heartsAdded: hearts,
|
|
1954
|
+
heartsRemoved
|
|
1086
1955
|
});
|
|
1956
|
+
if (objectOrder) this.setOrder(objectOrder);
|
|
1087
1957
|
}
|
|
1088
1958
|
/** Apply incremental real-time change received from a remote collaborator over WebSocket. */
|
|
1089
1959
|
applyRemoteChange(change) {
|
|
@@ -1195,6 +2065,114 @@ var BoardDocument = class {
|
|
|
1195
2065
|
for (const object of change.customObjectsUpdated) this.customObjects.set(object.id, object);
|
|
1196
2066
|
events.customObjectsUpdated = change.customObjectsUpdated;
|
|
1197
2067
|
}
|
|
2068
|
+
if (change.rectanglesAdded && change.rectanglesAdded.length > 0) {
|
|
2069
|
+
for (const rect of change.rectanglesAdded) this.rectangles.set(rect.id, rect);
|
|
2070
|
+
events.rectanglesAdded = change.rectanglesAdded;
|
|
2071
|
+
}
|
|
2072
|
+
if (change.rectanglesRemoved && change.rectanglesRemoved.length > 0) {
|
|
2073
|
+
const rectanglesRemoved = [];
|
|
2074
|
+
for (const id of change.rectanglesRemoved) if (this.rectangles.delete(id)) rectanglesRemoved.push(id);
|
|
2075
|
+
if (rectanglesRemoved.length > 0) events.rectanglesRemoved = rectanglesRemoved;
|
|
2076
|
+
}
|
|
2077
|
+
if (change.rectanglesUpdated && change.rectanglesUpdated.length > 0) {
|
|
2078
|
+
for (const rect of change.rectanglesUpdated) this.rectangles.set(rect.id, rect);
|
|
2079
|
+
events.rectanglesUpdated = change.rectanglesUpdated;
|
|
2080
|
+
}
|
|
2081
|
+
if (change.ellipsesAdded && change.ellipsesAdded.length > 0) {
|
|
2082
|
+
for (const ellipse of change.ellipsesAdded) this.ellipses.set(ellipse.id, ellipse);
|
|
2083
|
+
events.ellipsesAdded = change.ellipsesAdded;
|
|
2084
|
+
}
|
|
2085
|
+
if (change.ellipsesRemoved && change.ellipsesRemoved.length > 0) {
|
|
2086
|
+
const ellipsesRemoved = [];
|
|
2087
|
+
for (const id of change.ellipsesRemoved) if (this.ellipses.delete(id)) ellipsesRemoved.push(id);
|
|
2088
|
+
if (ellipsesRemoved.length > 0) events.ellipsesRemoved = ellipsesRemoved;
|
|
2089
|
+
}
|
|
2090
|
+
if (change.ellipsesUpdated && change.ellipsesUpdated.length > 0) {
|
|
2091
|
+
for (const ellipse of change.ellipsesUpdated) this.ellipses.set(ellipse.id, ellipse);
|
|
2092
|
+
events.ellipsesUpdated = change.ellipsesUpdated;
|
|
2093
|
+
}
|
|
2094
|
+
if (change.groupsAdded && change.groupsAdded.length > 0) {
|
|
2095
|
+
for (const group of change.groupsAdded) this.groups.set(group.id, group);
|
|
2096
|
+
events.groupsAdded = change.groupsAdded;
|
|
2097
|
+
}
|
|
2098
|
+
if (change.groupsRemoved && change.groupsRemoved.length > 0) {
|
|
2099
|
+
const groupsRemoved = [];
|
|
2100
|
+
for (const id of change.groupsRemoved) if (this.groups.delete(id)) groupsRemoved.push(id);
|
|
2101
|
+
if (groupsRemoved.length > 0) events.groupsRemoved = groupsRemoved;
|
|
2102
|
+
}
|
|
2103
|
+
if (change.groupsUpdated && change.groupsUpdated.length > 0) {
|
|
2104
|
+
for (const group of change.groupsUpdated) this.groups.set(group.id, group);
|
|
2105
|
+
events.groupsUpdated = change.groupsUpdated;
|
|
2106
|
+
}
|
|
2107
|
+
if (change.linesAdded && change.linesAdded.length > 0) {
|
|
2108
|
+
for (const line of change.linesAdded) this.lines.set(line.id, line);
|
|
2109
|
+
events.linesAdded = change.linesAdded;
|
|
2110
|
+
}
|
|
2111
|
+
if (change.linesRemoved && change.linesRemoved.length > 0) {
|
|
2112
|
+
const linesRemoved = [];
|
|
2113
|
+
for (const id of change.linesRemoved) if (this.lines.delete(id)) linesRemoved.push(id);
|
|
2114
|
+
if (linesRemoved.length > 0) events.linesRemoved = linesRemoved;
|
|
2115
|
+
}
|
|
2116
|
+
if (change.linesUpdated && change.linesUpdated.length > 0) {
|
|
2117
|
+
for (const line of change.linesUpdated) this.lines.set(line.id, line);
|
|
2118
|
+
events.linesUpdated = change.linesUpdated;
|
|
2119
|
+
}
|
|
2120
|
+
if (change.arrowsAdded && change.arrowsAdded.length > 0) {
|
|
2121
|
+
for (const arrow of change.arrowsAdded) this.arrows.set(arrow.id, arrow);
|
|
2122
|
+
events.arrowsAdded = change.arrowsAdded;
|
|
2123
|
+
}
|
|
2124
|
+
if (change.arrowsRemoved && change.arrowsRemoved.length > 0) {
|
|
2125
|
+
const arrowsRemoved = [];
|
|
2126
|
+
for (const id of change.arrowsRemoved) if (this.arrows.delete(id)) arrowsRemoved.push(id);
|
|
2127
|
+
if (arrowsRemoved.length > 0) events.arrowsRemoved = arrowsRemoved;
|
|
2128
|
+
}
|
|
2129
|
+
if (change.arrowsUpdated && change.arrowsUpdated.length > 0) {
|
|
2130
|
+
for (const arrow of change.arrowsUpdated) this.arrows.set(arrow.id, arrow);
|
|
2131
|
+
events.arrowsUpdated = change.arrowsUpdated;
|
|
2132
|
+
}
|
|
2133
|
+
if (change.polygonsAdded && change.polygonsAdded.length > 0) {
|
|
2134
|
+
for (const polygon of change.polygonsAdded) this.polygons.set(polygon.id, polygon);
|
|
2135
|
+
events.polygonsAdded = change.polygonsAdded;
|
|
2136
|
+
}
|
|
2137
|
+
if (change.polygonsRemoved && change.polygonsRemoved.length > 0) {
|
|
2138
|
+
const polygonsRemoved = [];
|
|
2139
|
+
for (const id of change.polygonsRemoved) if (this.polygons.delete(id)) polygonsRemoved.push(id);
|
|
2140
|
+
if (polygonsRemoved.length > 0) events.polygonsRemoved = polygonsRemoved;
|
|
2141
|
+
}
|
|
2142
|
+
if (change.polygonsUpdated && change.polygonsUpdated.length > 0) {
|
|
2143
|
+
for (const polygon of change.polygonsUpdated) this.polygons.set(polygon.id, polygon);
|
|
2144
|
+
events.polygonsUpdated = change.polygonsUpdated;
|
|
2145
|
+
}
|
|
2146
|
+
if (change.starsAdded && change.starsAdded.length > 0) {
|
|
2147
|
+
for (const star of change.starsAdded) this.stars.set(star.id, star);
|
|
2148
|
+
events.starsAdded = change.starsAdded;
|
|
2149
|
+
}
|
|
2150
|
+
if (change.starsRemoved && change.starsRemoved.length > 0) {
|
|
2151
|
+
const starsRemoved = [];
|
|
2152
|
+
for (const id of change.starsRemoved) if (this.stars.delete(id)) starsRemoved.push(id);
|
|
2153
|
+
if (starsRemoved.length > 0) events.starsRemoved = starsRemoved;
|
|
2154
|
+
}
|
|
2155
|
+
if (change.starsUpdated && change.starsUpdated.length > 0) {
|
|
2156
|
+
for (const star of change.starsUpdated) this.stars.set(star.id, star);
|
|
2157
|
+
events.starsUpdated = change.starsUpdated;
|
|
2158
|
+
}
|
|
2159
|
+
if (change.heartsAdded && change.heartsAdded.length > 0) {
|
|
2160
|
+
for (const heart of change.heartsAdded) this.hearts.set(heart.id, heart);
|
|
2161
|
+
events.heartsAdded = change.heartsAdded;
|
|
2162
|
+
}
|
|
2163
|
+
if (change.heartsRemoved && change.heartsRemoved.length > 0) {
|
|
2164
|
+
const heartsRemoved = [];
|
|
2165
|
+
for (const id of change.heartsRemoved) if (this.hearts.delete(id)) heartsRemoved.push(id);
|
|
2166
|
+
if (heartsRemoved.length > 0) events.heartsRemoved = heartsRemoved;
|
|
2167
|
+
}
|
|
2168
|
+
if (change.heartsUpdated && change.heartsUpdated.length > 0) {
|
|
2169
|
+
for (const heart of change.heartsUpdated) this.hearts.set(heart.id, heart);
|
|
2170
|
+
events.heartsUpdated = change.heartsUpdated;
|
|
2171
|
+
}
|
|
2172
|
+
if (change.orderChanged && change.orderChanged.length > 0) {
|
|
2173
|
+
this.setOrder(change.orderChanged);
|
|
2174
|
+
events.orderChanged = change.orderChanged;
|
|
2175
|
+
}
|
|
1198
2176
|
this.emit(events);
|
|
1199
2177
|
}
|
|
1200
2178
|
toJSON() {
|
|
@@ -1206,12 +2184,45 @@ var BoardDocument = class {
|
|
|
1206
2184
|
tables: [...this.tables.values()].map(cloneTable),
|
|
1207
2185
|
images: [...this.images.values()].map(cloneImage),
|
|
1208
2186
|
timers: [...this.timers.values()].map(cloneTimer),
|
|
1209
|
-
customObjects: [...this.customObjects.values()].map(cloneCustomObject)
|
|
2187
|
+
customObjects: [...this.customObjects.values()].map(cloneCustomObject),
|
|
2188
|
+
rectangles: [...this.rectangles.values()].map(cloneRectangle),
|
|
2189
|
+
ellipses: [...this.ellipses.values()].map(cloneEllipse),
|
|
2190
|
+
groups: [...this.groups.values()].map(cloneGroup),
|
|
2191
|
+
lines: [...this.lines.values()].map(cloneLine),
|
|
2192
|
+
arrows: [...this.arrows.values()].map(cloneArrow),
|
|
2193
|
+
polygons: [...this.polygons.values()].map(clonePolygon),
|
|
2194
|
+
stars: [...this.stars.values()].map(cloneStar),
|
|
2195
|
+
hearts: [...this.hearts.values()].map(cloneHeart),
|
|
2196
|
+
objectOrder: [...this.objectOrder]
|
|
1210
2197
|
};
|
|
1211
2198
|
}
|
|
2199
|
+
static deserializeGroups(data) {
|
|
2200
|
+
return (data.groups ?? []).map(cloneGroup);
|
|
2201
|
+
}
|
|
2202
|
+
static deserializeLines(data) {
|
|
2203
|
+
return (data.lines ?? []).map(cloneLine);
|
|
2204
|
+
}
|
|
2205
|
+
static deserializeArrows(data) {
|
|
2206
|
+
return (data.arrows ?? []).map(cloneArrow);
|
|
2207
|
+
}
|
|
2208
|
+
static deserializePolygons(data) {
|
|
2209
|
+
return (data.polygons ?? []).map(clonePolygon);
|
|
2210
|
+
}
|
|
2211
|
+
static deserializeStars(data) {
|
|
2212
|
+
return (data.stars ?? []).map(cloneStar);
|
|
2213
|
+
}
|
|
2214
|
+
static deserializeHearts(data) {
|
|
2215
|
+
return (data.hearts ?? []).map(cloneHeart);
|
|
2216
|
+
}
|
|
1212
2217
|
static deserializeCustomObjects(data) {
|
|
1213
2218
|
return (data.customObjects ?? []).map(cloneCustomObject);
|
|
1214
2219
|
}
|
|
2220
|
+
static deserializeRectangles(data) {
|
|
2221
|
+
return (data.rectangles ?? []).map(cloneRectangle);
|
|
2222
|
+
}
|
|
2223
|
+
static deserializeEllipses(data) {
|
|
2224
|
+
return (data.ellipses ?? []).map(cloneEllipse);
|
|
2225
|
+
}
|
|
1215
2226
|
static deserializeImages(data) {
|
|
1216
2227
|
return (data.images ?? []).map(cloneImage);
|
|
1217
2228
|
}
|
|
@@ -1227,7 +2238,17 @@ var BoardDocument = class {
|
|
|
1227
2238
|
static deserializeTables(data) {
|
|
1228
2239
|
return (data.tables ?? []).map(cloneTable);
|
|
1229
2240
|
}
|
|
1230
|
-
|
|
2241
|
+
/**
|
|
2242
|
+
* `onSkip` (Phase 9) replaces an unconditional `console.error` — `core`
|
|
2243
|
+
* must never do raw console I/O (no dev-gate, no way for a Host to
|
|
2244
|
+
* suppress or redirect it), so a skipped stroke is now reported only if
|
|
2245
|
+
* the caller asks for it, via whatever diagnostic channel it already
|
|
2246
|
+
* has (e.g. `controller-internal.ts` routes this into the same typed
|
|
2247
|
+
* `"error"` event every other diagnostic already uses). Silent by
|
|
2248
|
+
* default, matching how every other `deserialize*` method here already
|
|
2249
|
+
* behaves (no diagnostics at all).
|
|
2250
|
+
*/
|
|
2251
|
+
static deserializeStrokes(data, onSkip) {
|
|
1231
2252
|
const strokes = [];
|
|
1232
2253
|
for (const s of data.strokes) try {
|
|
1233
2254
|
strokes.push({
|
|
@@ -1241,11 +2262,65 @@ var BoardDocument = class {
|
|
|
1241
2262
|
...serializeLock(s)
|
|
1242
2263
|
});
|
|
1243
2264
|
} catch (err) {
|
|
1244
|
-
|
|
2265
|
+
onSkip?.(s?.id ?? "(no id)", err);
|
|
1245
2266
|
}
|
|
1246
2267
|
return strokes;
|
|
1247
2268
|
}
|
|
2269
|
+
/**
|
|
2270
|
+
* Every mutation funnels through here, so paint-order tracking lives in
|
|
2271
|
+
* exactly one place rather than at every individual add/remove call site
|
|
2272
|
+
* (18 of them, times `replaceAll`/`applyRemoteChange`) — new ids are
|
|
2273
|
+
* appended to the back (front-most) of `objectOrder`, removed ids are
|
|
2274
|
+
* spliced out. An explicit reorder (`reorder`/`setOrder`) updates
|
|
2275
|
+
* `objectOrder` itself before calling this, so this step is a no-op for
|
|
2276
|
+
* ids already tracked (idempotent by construction: `orderIndex.has` gates
|
|
2277
|
+
* every append).
|
|
2278
|
+
*
|
|
2279
|
+
* `orderChanged` (Phase 9) reports exactly the ids whose rank actually
|
|
2280
|
+
* changed, using `orderIndex` throughout instead of `indexOf` — a pure
|
|
2281
|
+
* append never shifts any existing id's rank (new ids land at the tail,
|
|
2282
|
+
* already covered by this same change's own `Added` field, so
|
|
2283
|
+
* `orderChanged` stays unset), while a removal shifts every id at-or-
|
|
2284
|
+
* after the lowest removed rank down by one, computed in a single O(n)
|
|
2285
|
+
* filter pass (not one `indexOf`+`splice` per removed id) regardless of
|
|
2286
|
+
* how many ids this one change removes. Every renderer's `onChange` now
|
|
2287
|
+
* looks up only the ids actually in `orderChanged` instead of walking
|
|
2288
|
+
* its entire mesh map on any order-touching change — a broad, unfiltered
|
|
2289
|
+
* `orderChanged` here would silently defeat that fix, not just waste
|
|
2290
|
+
* cycles here.
|
|
2291
|
+
*/
|
|
1248
2292
|
emit(change) {
|
|
2293
|
+
const newIds = [];
|
|
2294
|
+
for (const field of ORDERED_ADDED_FIELDS) {
|
|
2295
|
+
const objects = change[field];
|
|
2296
|
+
if (!objects) continue;
|
|
2297
|
+
for (const object of objects) if (!this.orderIndex.has(object.id)) newIds.push(object.id);
|
|
2298
|
+
}
|
|
2299
|
+
const removedIds = /* @__PURE__ */ new Set();
|
|
2300
|
+
for (const field of ORDERED_REMOVED_FIELDS) {
|
|
2301
|
+
const ids = change[field];
|
|
2302
|
+
if (!ids) continue;
|
|
2303
|
+
for (const id of ids) if (this.orderIndex.has(id)) removedIds.add(id);
|
|
2304
|
+
}
|
|
2305
|
+
let orderChanged;
|
|
2306
|
+
if (removedIds.size > 0) {
|
|
2307
|
+
let minIndex = this.objectOrder.length;
|
|
2308
|
+
for (const id of removedIds) minIndex = Math.min(minIndex, this.orderIndex.get(id));
|
|
2309
|
+
const next = [];
|
|
2310
|
+
for (const id of this.objectOrder) if (!removedIds.has(id)) next.push(id);
|
|
2311
|
+
this.objectOrder = next;
|
|
2312
|
+
for (let k = minIndex; k < this.objectOrder.length; k++) this.orderIndex.set(this.objectOrder[k], k);
|
|
2313
|
+
for (const id of removedIds) this.orderIndex.delete(id);
|
|
2314
|
+
orderChanged = this.objectOrder.slice(minIndex);
|
|
2315
|
+
}
|
|
2316
|
+
if (newIds.length > 0) for (const id of newIds) {
|
|
2317
|
+
this.orderIndex.set(id, this.objectOrder.length);
|
|
2318
|
+
this.objectOrder.push(id);
|
|
2319
|
+
}
|
|
2320
|
+
if (orderChanged) change = {
|
|
2321
|
+
...change,
|
|
2322
|
+
orderChanged
|
|
2323
|
+
};
|
|
1249
2324
|
const full = {
|
|
1250
2325
|
...EMPTY_CHANGE,
|
|
1251
2326
|
...change
|
|
@@ -1399,6 +2474,200 @@ var TransformCommand = class {
|
|
|
1399
2474
|
if (changed.length) doc.transformStrokes(changed);
|
|
1400
2475
|
}
|
|
1401
2476
|
};
|
|
2477
|
+
/**
|
|
2478
|
+
* One move gesture over a mixed-type selection (Phase 3) — the general
|
|
2479
|
+
* successor to `TransformCommand` for translation. `TransformCommand`
|
|
2480
|
+
* itself stays as-is (still used for scale/rotate, which remain stroke-only
|
|
2481
|
+
* this phase — see `interaction/tools/selectTool.ts`'s `TransformState`):
|
|
2482
|
+
* its own per-id `doc.get(id)` check already no-ops safely for any id that
|
|
2483
|
+
* isn't a stroke, so it doesn't need touching for that narrower case.
|
|
2484
|
+
*
|
|
2485
|
+
* `delta` here is always a pure translation (never scale/rotate), so
|
|
2486
|
+
* `apply(delta, point)` is a safe, uniform way to move every position-only
|
|
2487
|
+
* type's `x`/`y` — translating commutes trivially regardless of a shape's
|
|
2488
|
+
* own rotation. Matrix-carrying types (stroke, Custom) instead compose
|
|
2489
|
+
* `delta` onto their existing matrix/transform, matching `TransformCommand`.
|
|
2490
|
+
*
|
|
2491
|
+
* A group id in `ids` is expanded into its (recursively resolved, cycle-
|
|
2492
|
+
* safe) children every time `compose` runs — deterministic, since group
|
|
2493
|
+
* membership never changes mid-command — so moving a selected group means
|
|
2494
|
+
* moving every one of its members by the same delta. This expansion is
|
|
2495
|
+
* `TransformObjectsCommand`'s own responsibility precisely so a caller that
|
|
2496
|
+
* doesn't itself expand groups (e.g. `ScrawlEngine.nudgeSelection`) still
|
|
2497
|
+
* gets correct behavior; `interaction/tools/selectTool.ts`'s `TransformState`
|
|
2498
|
+
* separately expands for its own reason (live per-child drag preview), which
|
|
2499
|
+
* makes this a no-op re-expansion for that caller, not a conflict.
|
|
2500
|
+
*/
|
|
2501
|
+
var TransformObjectsCommand = class {
|
|
2502
|
+
constructor(ids, delta) {
|
|
2503
|
+
this.ids = ids;
|
|
2504
|
+
this.delta = delta;
|
|
2505
|
+
this.label = "move selection";
|
|
2506
|
+
this.inverse = invert(delta);
|
|
2507
|
+
}
|
|
2508
|
+
apply(doc) {
|
|
2509
|
+
this.compose(doc, this.delta);
|
|
2510
|
+
}
|
|
2511
|
+
revert(doc) {
|
|
2512
|
+
this.compose(doc, this.inverse);
|
|
2513
|
+
}
|
|
2514
|
+
compose(doc, m) {
|
|
2515
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
2516
|
+
const expand = (id, seen) => {
|
|
2517
|
+
if (seen.has(id)) return;
|
|
2518
|
+
seen.add(id);
|
|
2519
|
+
const group = doc.getGroup(id);
|
|
2520
|
+
if (group) {
|
|
2521
|
+
for (const childId of group.children) expand(childId, seen);
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
resolved.add(id);
|
|
2525
|
+
};
|
|
2526
|
+
for (const id of this.ids) expand(id, /* @__PURE__ */ new Set());
|
|
2527
|
+
const changedStrokes = [];
|
|
2528
|
+
for (const id of resolved) {
|
|
2529
|
+
const stroke = doc.get(id);
|
|
2530
|
+
if (stroke) {
|
|
2531
|
+
stroke.matrix = mul(m, stroke.matrix ?? [
|
|
2532
|
+
1,
|
|
2533
|
+
0,
|
|
2534
|
+
0,
|
|
2535
|
+
1,
|
|
2536
|
+
0,
|
|
2537
|
+
0
|
|
2538
|
+
]);
|
|
2539
|
+
changedStrokes.push(stroke);
|
|
2540
|
+
continue;
|
|
2541
|
+
}
|
|
2542
|
+
const custom = doc.getCustomObject(id);
|
|
2543
|
+
if (custom) {
|
|
2544
|
+
doc.setCustomObject({
|
|
2545
|
+
...custom,
|
|
2546
|
+
transform: mul(m, custom.transform)
|
|
2547
|
+
});
|
|
2548
|
+
continue;
|
|
2549
|
+
}
|
|
2550
|
+
const note = doc.getNote(id);
|
|
2551
|
+
if (note) {
|
|
2552
|
+
const p = apply(m, note);
|
|
2553
|
+
doc.setNote({
|
|
2554
|
+
...note,
|
|
2555
|
+
x: p.x,
|
|
2556
|
+
y: p.y
|
|
2557
|
+
});
|
|
2558
|
+
continue;
|
|
2559
|
+
}
|
|
2560
|
+
const text = doc.getText(id);
|
|
2561
|
+
if (text) {
|
|
2562
|
+
const p = apply(m, text);
|
|
2563
|
+
doc.setText({
|
|
2564
|
+
...text,
|
|
2565
|
+
x: p.x,
|
|
2566
|
+
y: p.y
|
|
2567
|
+
});
|
|
2568
|
+
continue;
|
|
2569
|
+
}
|
|
2570
|
+
const table = doc.getTable(id);
|
|
2571
|
+
if (table) {
|
|
2572
|
+
const p = apply(m, table);
|
|
2573
|
+
doc.setTable({
|
|
2574
|
+
...table,
|
|
2575
|
+
x: p.x,
|
|
2576
|
+
y: p.y
|
|
2577
|
+
});
|
|
2578
|
+
continue;
|
|
2579
|
+
}
|
|
2580
|
+
const image = doc.getImage(id);
|
|
2581
|
+
if (image) {
|
|
2582
|
+
const p = apply(m, image);
|
|
2583
|
+
doc.setImage({
|
|
2584
|
+
...image,
|
|
2585
|
+
x: p.x,
|
|
2586
|
+
y: p.y
|
|
2587
|
+
});
|
|
2588
|
+
continue;
|
|
2589
|
+
}
|
|
2590
|
+
const timer = doc.getTimer(id);
|
|
2591
|
+
if (timer) {
|
|
2592
|
+
const p = apply(m, timer);
|
|
2593
|
+
doc.setTimer({
|
|
2594
|
+
...timer,
|
|
2595
|
+
x: p.x,
|
|
2596
|
+
y: p.y
|
|
2597
|
+
});
|
|
2598
|
+
continue;
|
|
2599
|
+
}
|
|
2600
|
+
const rect = doc.getRectangle(id);
|
|
2601
|
+
if (rect) {
|
|
2602
|
+
const p = apply(m, rect);
|
|
2603
|
+
doc.setRectangle({
|
|
2604
|
+
...rect,
|
|
2605
|
+
x: p.x,
|
|
2606
|
+
y: p.y
|
|
2607
|
+
});
|
|
2608
|
+
continue;
|
|
2609
|
+
}
|
|
2610
|
+
const ellipse = doc.getEllipse(id);
|
|
2611
|
+
if (ellipse) {
|
|
2612
|
+
const p = apply(m, ellipse);
|
|
2613
|
+
doc.setEllipse({
|
|
2614
|
+
...ellipse,
|
|
2615
|
+
x: p.x,
|
|
2616
|
+
y: p.y
|
|
2617
|
+
});
|
|
2618
|
+
continue;
|
|
2619
|
+
}
|
|
2620
|
+
const line = doc.getLine(id);
|
|
2621
|
+
if (line) {
|
|
2622
|
+
doc.setLine({
|
|
2623
|
+
...line,
|
|
2624
|
+
start: apply(m, line.start),
|
|
2625
|
+
end: apply(m, line.end)
|
|
2626
|
+
});
|
|
2627
|
+
continue;
|
|
2628
|
+
}
|
|
2629
|
+
const arrow = doc.getArrow(id);
|
|
2630
|
+
if (arrow) {
|
|
2631
|
+
doc.setArrow({
|
|
2632
|
+
...arrow,
|
|
2633
|
+
start: apply(m, arrow.start),
|
|
2634
|
+
end: apply(m, arrow.end)
|
|
2635
|
+
});
|
|
2636
|
+
continue;
|
|
2637
|
+
}
|
|
2638
|
+
const polygon = doc.getPolygon(id);
|
|
2639
|
+
if (polygon) {
|
|
2640
|
+
const p = apply(m, polygon);
|
|
2641
|
+
doc.setPolygon({
|
|
2642
|
+
...polygon,
|
|
2643
|
+
x: p.x,
|
|
2644
|
+
y: p.y
|
|
2645
|
+
});
|
|
2646
|
+
continue;
|
|
2647
|
+
}
|
|
2648
|
+
const star = doc.getStar(id);
|
|
2649
|
+
if (star) {
|
|
2650
|
+
const p = apply(m, star);
|
|
2651
|
+
doc.setStar({
|
|
2652
|
+
...star,
|
|
2653
|
+
x: p.x,
|
|
2654
|
+
y: p.y
|
|
2655
|
+
});
|
|
2656
|
+
continue;
|
|
2657
|
+
}
|
|
2658
|
+
const heart = doc.getHeart(id);
|
|
2659
|
+
if (heart) {
|
|
2660
|
+
const p = apply(m, heart);
|
|
2661
|
+
doc.setHeart({
|
|
2662
|
+
...heart,
|
|
2663
|
+
x: p.x,
|
|
2664
|
+
y: p.y
|
|
2665
|
+
});
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
if (changedStrokes.length) doc.transformStrokes(changedStrokes);
|
|
2669
|
+
}
|
|
2670
|
+
};
|
|
1402
2671
|
var AddNoteCommand = class {
|
|
1403
2672
|
constructor(note) {
|
|
1404
2673
|
this.label = "add note";
|
|
@@ -1600,6 +2869,342 @@ var DeleteTimerCommand = class {
|
|
|
1600
2869
|
doc.addTimers([cloneTimer(this.timer)]);
|
|
1601
2870
|
}
|
|
1602
2871
|
};
|
|
2872
|
+
/** Add/update/delete for semantic Rectangle objects (Phase 2). */
|
|
2873
|
+
var AddRectangleCommand = class {
|
|
2874
|
+
constructor(rect) {
|
|
2875
|
+
this.label = "add rectangle";
|
|
2876
|
+
this.rect = cloneRectangle(rect);
|
|
2877
|
+
}
|
|
2878
|
+
apply(doc) {
|
|
2879
|
+
doc.addRectangles([cloneRectangle(this.rect)]);
|
|
2880
|
+
}
|
|
2881
|
+
revert(doc) {
|
|
2882
|
+
doc.removeRectangles([this.rect.id]);
|
|
2883
|
+
}
|
|
2884
|
+
};
|
|
2885
|
+
var UpdateRectangleCommand = class {
|
|
2886
|
+
constructor(before, after) {
|
|
2887
|
+
this.label = "update rectangle";
|
|
2888
|
+
this.before = cloneRectangle(before);
|
|
2889
|
+
this.after = cloneRectangle(after);
|
|
2890
|
+
}
|
|
2891
|
+
apply(doc) {
|
|
2892
|
+
doc.setRectangle(cloneRectangle(this.after));
|
|
2893
|
+
}
|
|
2894
|
+
revert(doc) {
|
|
2895
|
+
doc.setRectangle(cloneRectangle(this.before));
|
|
2896
|
+
}
|
|
2897
|
+
};
|
|
2898
|
+
var DeleteRectangleCommand = class {
|
|
2899
|
+
constructor(rect) {
|
|
2900
|
+
this.label = "delete rectangle";
|
|
2901
|
+
this.rect = cloneRectangle(rect);
|
|
2902
|
+
}
|
|
2903
|
+
apply(doc) {
|
|
2904
|
+
doc.removeRectangles([this.rect.id]);
|
|
2905
|
+
}
|
|
2906
|
+
revert(doc) {
|
|
2907
|
+
doc.addRectangles([cloneRectangle(this.rect)]);
|
|
2908
|
+
}
|
|
2909
|
+
};
|
|
2910
|
+
/** Add/update/delete for semantic Ellipse objects (Phase 2). */
|
|
2911
|
+
var AddEllipseCommand = class {
|
|
2912
|
+
constructor(ellipse) {
|
|
2913
|
+
this.label = "add ellipse";
|
|
2914
|
+
this.ellipse = cloneEllipse(ellipse);
|
|
2915
|
+
}
|
|
2916
|
+
apply(doc) {
|
|
2917
|
+
doc.addEllipses([cloneEllipse(this.ellipse)]);
|
|
2918
|
+
}
|
|
2919
|
+
revert(doc) {
|
|
2920
|
+
doc.removeEllipses([this.ellipse.id]);
|
|
2921
|
+
}
|
|
2922
|
+
};
|
|
2923
|
+
var UpdateEllipseCommand = class {
|
|
2924
|
+
constructor(before, after) {
|
|
2925
|
+
this.label = "update ellipse";
|
|
2926
|
+
this.before = cloneEllipse(before);
|
|
2927
|
+
this.after = cloneEllipse(after);
|
|
2928
|
+
}
|
|
2929
|
+
apply(doc) {
|
|
2930
|
+
doc.setEllipse(cloneEllipse(this.after));
|
|
2931
|
+
}
|
|
2932
|
+
revert(doc) {
|
|
2933
|
+
doc.setEllipse(cloneEllipse(this.before));
|
|
2934
|
+
}
|
|
2935
|
+
};
|
|
2936
|
+
var DeleteEllipseCommand = class {
|
|
2937
|
+
constructor(ellipse) {
|
|
2938
|
+
this.label = "delete ellipse";
|
|
2939
|
+
this.ellipse = cloneEllipse(ellipse);
|
|
2940
|
+
}
|
|
2941
|
+
apply(doc) {
|
|
2942
|
+
doc.removeEllipses([this.ellipse.id]);
|
|
2943
|
+
}
|
|
2944
|
+
revert(doc) {
|
|
2945
|
+
doc.addEllipses([cloneEllipse(this.ellipse)]);
|
|
2946
|
+
}
|
|
2947
|
+
};
|
|
2948
|
+
/** Add/update/delete for semantic Line objects (Phase 4). */
|
|
2949
|
+
var AddLineCommand = class {
|
|
2950
|
+
constructor(line) {
|
|
2951
|
+
this.label = "add line";
|
|
2952
|
+
this.line = cloneLine(line);
|
|
2953
|
+
}
|
|
2954
|
+
apply(doc) {
|
|
2955
|
+
doc.addLines([cloneLine(this.line)]);
|
|
2956
|
+
}
|
|
2957
|
+
revert(doc) {
|
|
2958
|
+
doc.removeLines([this.line.id]);
|
|
2959
|
+
}
|
|
2960
|
+
};
|
|
2961
|
+
var UpdateLineCommand = class {
|
|
2962
|
+
constructor(before, after) {
|
|
2963
|
+
this.label = "update line";
|
|
2964
|
+
this.before = cloneLine(before);
|
|
2965
|
+
this.after = cloneLine(after);
|
|
2966
|
+
}
|
|
2967
|
+
apply(doc) {
|
|
2968
|
+
doc.setLine(cloneLine(this.after));
|
|
2969
|
+
}
|
|
2970
|
+
revert(doc) {
|
|
2971
|
+
doc.setLine(cloneLine(this.before));
|
|
2972
|
+
}
|
|
2973
|
+
};
|
|
2974
|
+
var DeleteLineCommand = class {
|
|
2975
|
+
constructor(line) {
|
|
2976
|
+
this.label = "delete line";
|
|
2977
|
+
this.line = cloneLine(line);
|
|
2978
|
+
}
|
|
2979
|
+
apply(doc) {
|
|
2980
|
+
doc.removeLines([this.line.id]);
|
|
2981
|
+
}
|
|
2982
|
+
revert(doc) {
|
|
2983
|
+
doc.addLines([cloneLine(this.line)]);
|
|
2984
|
+
}
|
|
2985
|
+
};
|
|
2986
|
+
/** Add/update/delete for semantic Arrow objects (Phase 4). */
|
|
2987
|
+
var AddArrowCommand = class {
|
|
2988
|
+
constructor(arrow) {
|
|
2989
|
+
this.label = "add arrow";
|
|
2990
|
+
this.arrow = cloneArrow(arrow);
|
|
2991
|
+
}
|
|
2992
|
+
apply(doc) {
|
|
2993
|
+
doc.addArrows([cloneArrow(this.arrow)]);
|
|
2994
|
+
}
|
|
2995
|
+
revert(doc) {
|
|
2996
|
+
doc.removeArrows([this.arrow.id]);
|
|
2997
|
+
}
|
|
2998
|
+
};
|
|
2999
|
+
var UpdateArrowCommand = class {
|
|
3000
|
+
constructor(before, after) {
|
|
3001
|
+
this.label = "update arrow";
|
|
3002
|
+
this.before = cloneArrow(before);
|
|
3003
|
+
this.after = cloneArrow(after);
|
|
3004
|
+
}
|
|
3005
|
+
apply(doc) {
|
|
3006
|
+
doc.setArrow(cloneArrow(this.after));
|
|
3007
|
+
}
|
|
3008
|
+
revert(doc) {
|
|
3009
|
+
doc.setArrow(cloneArrow(this.before));
|
|
3010
|
+
}
|
|
3011
|
+
};
|
|
3012
|
+
var DeleteArrowCommand = class {
|
|
3013
|
+
constructor(arrow) {
|
|
3014
|
+
this.label = "delete arrow";
|
|
3015
|
+
this.arrow = cloneArrow(arrow);
|
|
3016
|
+
}
|
|
3017
|
+
apply(doc) {
|
|
3018
|
+
doc.removeArrows([this.arrow.id]);
|
|
3019
|
+
}
|
|
3020
|
+
revert(doc) {
|
|
3021
|
+
doc.addArrows([cloneArrow(this.arrow)]);
|
|
3022
|
+
}
|
|
3023
|
+
};
|
|
3024
|
+
/** Add/update/delete for semantic Polygon objects (Phase 4) — Triangle/Diamond/Pentagon/Hexagon/Octagon. */
|
|
3025
|
+
var AddPolygonCommand = class {
|
|
3026
|
+
constructor(polygon) {
|
|
3027
|
+
this.label = "add polygon";
|
|
3028
|
+
this.polygon = clonePolygon(polygon);
|
|
3029
|
+
}
|
|
3030
|
+
apply(doc) {
|
|
3031
|
+
doc.addPolygons([clonePolygon(this.polygon)]);
|
|
3032
|
+
}
|
|
3033
|
+
revert(doc) {
|
|
3034
|
+
doc.removePolygons([this.polygon.id]);
|
|
3035
|
+
}
|
|
3036
|
+
};
|
|
3037
|
+
var UpdatePolygonCommand = class {
|
|
3038
|
+
constructor(before, after) {
|
|
3039
|
+
this.label = "update polygon";
|
|
3040
|
+
this.before = clonePolygon(before);
|
|
3041
|
+
this.after = clonePolygon(after);
|
|
3042
|
+
}
|
|
3043
|
+
apply(doc) {
|
|
3044
|
+
doc.setPolygon(clonePolygon(this.after));
|
|
3045
|
+
}
|
|
3046
|
+
revert(doc) {
|
|
3047
|
+
doc.setPolygon(clonePolygon(this.before));
|
|
3048
|
+
}
|
|
3049
|
+
};
|
|
3050
|
+
var DeletePolygonCommand = class {
|
|
3051
|
+
constructor(polygon) {
|
|
3052
|
+
this.label = "delete polygon";
|
|
3053
|
+
this.polygon = clonePolygon(polygon);
|
|
3054
|
+
}
|
|
3055
|
+
apply(doc) {
|
|
3056
|
+
doc.removePolygons([this.polygon.id]);
|
|
3057
|
+
}
|
|
3058
|
+
revert(doc) {
|
|
3059
|
+
doc.addPolygons([clonePolygon(this.polygon)]);
|
|
3060
|
+
}
|
|
3061
|
+
};
|
|
3062
|
+
/** Add/update/delete for semantic Star objects (Phase 4). */
|
|
3063
|
+
var AddStarCommand = class {
|
|
3064
|
+
constructor(star) {
|
|
3065
|
+
this.label = "add star";
|
|
3066
|
+
this.star = cloneStar(star);
|
|
3067
|
+
}
|
|
3068
|
+
apply(doc) {
|
|
3069
|
+
doc.addStars([cloneStar(this.star)]);
|
|
3070
|
+
}
|
|
3071
|
+
revert(doc) {
|
|
3072
|
+
doc.removeStars([this.star.id]);
|
|
3073
|
+
}
|
|
3074
|
+
};
|
|
3075
|
+
var UpdateStarCommand = class {
|
|
3076
|
+
constructor(before, after) {
|
|
3077
|
+
this.label = "update star";
|
|
3078
|
+
this.before = cloneStar(before);
|
|
3079
|
+
this.after = cloneStar(after);
|
|
3080
|
+
}
|
|
3081
|
+
apply(doc) {
|
|
3082
|
+
doc.setStar(cloneStar(this.after));
|
|
3083
|
+
}
|
|
3084
|
+
revert(doc) {
|
|
3085
|
+
doc.setStar(cloneStar(this.before));
|
|
3086
|
+
}
|
|
3087
|
+
};
|
|
3088
|
+
var DeleteStarCommand = class {
|
|
3089
|
+
constructor(star) {
|
|
3090
|
+
this.label = "delete star";
|
|
3091
|
+
this.star = cloneStar(star);
|
|
3092
|
+
}
|
|
3093
|
+
apply(doc) {
|
|
3094
|
+
doc.removeStars([this.star.id]);
|
|
3095
|
+
}
|
|
3096
|
+
revert(doc) {
|
|
3097
|
+
doc.addStars([cloneStar(this.star)]);
|
|
3098
|
+
}
|
|
3099
|
+
};
|
|
3100
|
+
/** Add/update/delete for semantic Heart objects (Phase 4). */
|
|
3101
|
+
var AddHeartCommand = class {
|
|
3102
|
+
constructor(heart) {
|
|
3103
|
+
this.label = "add heart";
|
|
3104
|
+
this.heart = cloneHeart(heart);
|
|
3105
|
+
}
|
|
3106
|
+
apply(doc) {
|
|
3107
|
+
doc.addHearts([cloneHeart(this.heart)]);
|
|
3108
|
+
}
|
|
3109
|
+
revert(doc) {
|
|
3110
|
+
doc.removeHearts([this.heart.id]);
|
|
3111
|
+
}
|
|
3112
|
+
};
|
|
3113
|
+
var UpdateHeartCommand = class {
|
|
3114
|
+
constructor(before, after) {
|
|
3115
|
+
this.label = "update heart";
|
|
3116
|
+
this.before = cloneHeart(before);
|
|
3117
|
+
this.after = cloneHeart(after);
|
|
3118
|
+
}
|
|
3119
|
+
apply(doc) {
|
|
3120
|
+
doc.setHeart(cloneHeart(this.after));
|
|
3121
|
+
}
|
|
3122
|
+
revert(doc) {
|
|
3123
|
+
doc.setHeart(cloneHeart(this.before));
|
|
3124
|
+
}
|
|
3125
|
+
};
|
|
3126
|
+
var DeleteHeartCommand = class {
|
|
3127
|
+
constructor(heart) {
|
|
3128
|
+
this.label = "delete heart";
|
|
3129
|
+
this.heart = cloneHeart(heart);
|
|
3130
|
+
}
|
|
3131
|
+
apply(doc) {
|
|
3132
|
+
doc.removeHearts([this.heart.id]);
|
|
3133
|
+
}
|
|
3134
|
+
revert(doc) {
|
|
3135
|
+
doc.addHearts([cloneHeart(this.heart)]);
|
|
3136
|
+
}
|
|
3137
|
+
};
|
|
3138
|
+
/** Add/update/delete for Groups (Phase 3). */
|
|
3139
|
+
var AddGroupCommand = class {
|
|
3140
|
+
constructor(group) {
|
|
3141
|
+
this.label = "group";
|
|
3142
|
+
this.group = cloneGroup(group);
|
|
3143
|
+
}
|
|
3144
|
+
apply(doc) {
|
|
3145
|
+
doc.addGroups([cloneGroup(this.group)]);
|
|
3146
|
+
}
|
|
3147
|
+
revert(doc) {
|
|
3148
|
+
doc.removeGroups([this.group.id]);
|
|
3149
|
+
}
|
|
3150
|
+
};
|
|
3151
|
+
var UpdateGroupCommand = class {
|
|
3152
|
+
constructor(before, after) {
|
|
3153
|
+
this.label = "update group";
|
|
3154
|
+
this.before = cloneGroup(before);
|
|
3155
|
+
this.after = cloneGroup(after);
|
|
3156
|
+
}
|
|
3157
|
+
apply(doc) {
|
|
3158
|
+
doc.setGroup(cloneGroup(this.after));
|
|
3159
|
+
}
|
|
3160
|
+
revert(doc) {
|
|
3161
|
+
doc.setGroup(cloneGroup(this.before));
|
|
3162
|
+
}
|
|
3163
|
+
};
|
|
3164
|
+
var DeleteGroupCommand = class {
|
|
3165
|
+
constructor(group) {
|
|
3166
|
+
this.label = "ungroup";
|
|
3167
|
+
this.group = cloneGroup(group);
|
|
3168
|
+
}
|
|
3169
|
+
apply(doc) {
|
|
3170
|
+
doc.removeGroups([this.group.id]);
|
|
3171
|
+
}
|
|
3172
|
+
revert(doc) {
|
|
3173
|
+
doc.addGroups([cloneGroup(this.group)]);
|
|
3174
|
+
}
|
|
3175
|
+
};
|
|
3176
|
+
var REORDER_LABELS = {
|
|
3177
|
+
forward: "bring forward",
|
|
3178
|
+
backward: "send backward",
|
|
3179
|
+
front: "bring to front",
|
|
3180
|
+
back: "send to back"
|
|
3181
|
+
};
|
|
3182
|
+
/**
|
|
3183
|
+
* Bring-forward / send-backward / bring-to-front / send-to-back (Phase 3) —
|
|
3184
|
+
* one command family covering all four directions rather than four
|
|
3185
|
+
* near-identical classes, since the only difference between them is which
|
|
3186
|
+
* `BoardDocument.reorder` direction to replay. Captures the full paint order
|
|
3187
|
+
* on first `apply` rather than in the constructor — `BoardDocument.order()`
|
|
3188
|
+
* needs the doc, which a `Command` only ever receives via `apply`/`revert` —
|
|
3189
|
+
* so `revert` can restore it exactly; redo re-runs the same `reorder` call,
|
|
3190
|
+
* which is deterministic because `revert` always restores the identical
|
|
3191
|
+
* starting order first.
|
|
3192
|
+
*/
|
|
3193
|
+
var ReorderObjectCommand = class {
|
|
3194
|
+
constructor(id, direction) {
|
|
3195
|
+
this.id = id;
|
|
3196
|
+
this.direction = direction;
|
|
3197
|
+
this.before = null;
|
|
3198
|
+
this.label = REORDER_LABELS[direction];
|
|
3199
|
+
}
|
|
3200
|
+
apply(doc) {
|
|
3201
|
+
this.before ??= [...doc.order()];
|
|
3202
|
+
doc.reorder(this.id, this.direction);
|
|
3203
|
+
}
|
|
3204
|
+
revert(doc) {
|
|
3205
|
+
if (this.before) doc.applyRemoteChange({ orderChanged: this.before });
|
|
3206
|
+
}
|
|
3207
|
+
};
|
|
1603
3208
|
var LockItemsCommand = class {
|
|
1604
3209
|
constructor(targets, targetState, actor) {
|
|
1605
3210
|
this.label = "toggle lock";
|
|
@@ -1627,10 +3232,38 @@ var LockItemsCommand = class {
|
|
|
1627
3232
|
else if (type === "table") doc.setTableLocked(id, locked, by);
|
|
1628
3233
|
else if (type === "image") doc.setImageLocked(id, locked, by);
|
|
1629
3234
|
else if (type === "timer") doc.setTimerLocked(id, locked, by);
|
|
3235
|
+
else if (type === "rectangle") doc.setRectangleLocked(id, locked, by);
|
|
3236
|
+
else if (type === "ellipse") doc.setEllipseLocked(id, locked, by);
|
|
3237
|
+
else if (type === "group") doc.setGroupLocked(id, locked, by);
|
|
3238
|
+
else if (type === "line") doc.setLineLocked(id, locked, by);
|
|
3239
|
+
else if (type === "arrow") doc.setArrowLocked(id, locked, by);
|
|
3240
|
+
else if (type === "polygon") doc.setPolygonLocked(id, locked, by);
|
|
3241
|
+
else if (type === "star") doc.setStarLocked(id, locked, by);
|
|
3242
|
+
else if (type === "heart") doc.setHeartLocked(id, locked, by);
|
|
1630
3243
|
}
|
|
1631
3244
|
};
|
|
1632
3245
|
//#endregion
|
|
1633
3246
|
//#region src/core/history/history.ts
|
|
3247
|
+
/**
|
|
3248
|
+
* Several commands applied/reverted together as one undo step (Phase 3
|
|
3249
|
+
* consolidation — this exact class used to be hand-duplicated as a private
|
|
3250
|
+
* `CommandBatch` in `controller-internal.ts` and an exported
|
|
3251
|
+
* `ExtensionCommandBatch` in `interaction/tools/customTool.ts`; both now
|
|
3252
|
+
* import this one instead). Revert runs in reverse order, so a batch that
|
|
3253
|
+
* depends on ordering (e.g. add-then-reference) undoes cleanly.
|
|
3254
|
+
*/
|
|
3255
|
+
var CommandBatch = class {
|
|
3256
|
+
constructor(label, commands) {
|
|
3257
|
+
this.label = label;
|
|
3258
|
+
this.commands = commands;
|
|
3259
|
+
}
|
|
3260
|
+
apply(doc) {
|
|
3261
|
+
for (const command of this.commands) command.apply(doc);
|
|
3262
|
+
}
|
|
3263
|
+
revert(doc) {
|
|
3264
|
+
for (const command of [...this.commands].reverse()) command.revert(doc);
|
|
3265
|
+
}
|
|
3266
|
+
};
|
|
1634
3267
|
var LIMIT = 200;
|
|
1635
3268
|
var History = class {
|
|
1636
3269
|
constructor(doc) {
|
|
@@ -1699,6 +3332,14 @@ var ADDED = [
|
|
|
1699
3332
|
["tablesAdded", "tables"],
|
|
1700
3333
|
["imagesAdded", "images"],
|
|
1701
3334
|
["timersAdded", "timers"],
|
|
3335
|
+
["rectanglesAdded", "rectangles"],
|
|
3336
|
+
["ellipsesAdded", "ellipses"],
|
|
3337
|
+
["groupsAdded", "groups"],
|
|
3338
|
+
["linesAdded", "lines"],
|
|
3339
|
+
["arrowsAdded", "arrows"],
|
|
3340
|
+
["polygonsAdded", "polygons"],
|
|
3341
|
+
["starsAdded", "stars"],
|
|
3342
|
+
["heartsAdded", "hearts"],
|
|
1702
3343
|
["customObjectsAdded", "customObjects"]
|
|
1703
3344
|
];
|
|
1704
3345
|
var UPDATED = [
|
|
@@ -1709,6 +3350,14 @@ var UPDATED = [
|
|
|
1709
3350
|
["tablesUpdated", "tables"],
|
|
1710
3351
|
["imagesUpdated", "images"],
|
|
1711
3352
|
["timersUpdated", "timers"],
|
|
3353
|
+
["rectanglesUpdated", "rectangles"],
|
|
3354
|
+
["ellipsesUpdated", "ellipses"],
|
|
3355
|
+
["groupsUpdated", "groups"],
|
|
3356
|
+
["linesUpdated", "lines"],
|
|
3357
|
+
["arrowsUpdated", "arrows"],
|
|
3358
|
+
["polygonsUpdated", "polygons"],
|
|
3359
|
+
["starsUpdated", "stars"],
|
|
3360
|
+
["heartsUpdated", "hearts"],
|
|
1712
3361
|
["customObjectsUpdated", "customObjects"]
|
|
1713
3362
|
];
|
|
1714
3363
|
var REMOVED = [
|
|
@@ -1718,6 +3367,14 @@ var REMOVED = [
|
|
|
1718
3367
|
["tablesRemoved", "tables"],
|
|
1719
3368
|
["imagesRemoved", "images"],
|
|
1720
3369
|
["timersRemoved", "timers"],
|
|
3370
|
+
["rectanglesRemoved", "rectangles"],
|
|
3371
|
+
["ellipsesRemoved", "ellipses"],
|
|
3372
|
+
["groupsRemoved", "groups"],
|
|
3373
|
+
["linesRemoved", "lines"],
|
|
3374
|
+
["arrowsRemoved", "arrows"],
|
|
3375
|
+
["polygonsRemoved", "polygons"],
|
|
3376
|
+
["starsRemoved", "stars"],
|
|
3377
|
+
["heartsRemoved", "hearts"],
|
|
1721
3378
|
["customObjectsRemoved", "customObjects"]
|
|
1722
3379
|
];
|
|
1723
3380
|
function toWireObject(collection, object) {
|
|
@@ -1787,22 +3444,74 @@ function ribbonEdges(points, baseWidth, handDrawn = true) {
|
|
|
1787
3444
|
//#endregion
|
|
1788
3445
|
//#region src/core/document/spatialIndex.ts
|
|
1789
3446
|
var CELL = 8;
|
|
3447
|
+
/**
|
|
3448
|
+
* Non-stroke types have no separate "transformed" event the way strokes
|
|
3449
|
+
* do — any content replace (`setNote`/`setTable`/...) goes through the
|
|
3450
|
+
* type's single `*Updated` field regardless of whether it moved, so a
|
|
3451
|
+
* reindex on every update is unconditional (cheap arithmetic per object,
|
|
3452
|
+
* matching `objectBounds.ts`'s own "no caching needed" precedent for these
|
|
3453
|
+
* types — not worth distinguishing "moved" from "just retexted").
|
|
3454
|
+
*/
|
|
3455
|
+
var ADDED_FIELDS = [
|
|
3456
|
+
"added",
|
|
3457
|
+
"notesAdded",
|
|
3458
|
+
"textAdded",
|
|
3459
|
+
"tablesAdded",
|
|
3460
|
+
"imagesAdded",
|
|
3461
|
+
"timersAdded",
|
|
3462
|
+
"customObjectsAdded",
|
|
3463
|
+
"rectanglesAdded",
|
|
3464
|
+
"ellipsesAdded"
|
|
3465
|
+
];
|
|
3466
|
+
var REMOVED_FIELDS = [
|
|
3467
|
+
"removed",
|
|
3468
|
+
"notesRemoved",
|
|
3469
|
+
"textRemoved",
|
|
3470
|
+
"tablesRemoved",
|
|
3471
|
+
"imagesRemoved",
|
|
3472
|
+
"timersRemoved",
|
|
3473
|
+
"customObjectsRemoved",
|
|
3474
|
+
"rectanglesRemoved",
|
|
3475
|
+
"ellipsesRemoved"
|
|
3476
|
+
];
|
|
3477
|
+
var UPDATED_FIELDS = [
|
|
3478
|
+
"notesUpdated",
|
|
3479
|
+
"textUpdated",
|
|
3480
|
+
"tablesUpdated",
|
|
3481
|
+
"imagesUpdated",
|
|
3482
|
+
"timersUpdated",
|
|
3483
|
+
"customObjectsUpdated",
|
|
3484
|
+
"rectanglesUpdated",
|
|
3485
|
+
"ellipsesUpdated"
|
|
3486
|
+
];
|
|
1790
3487
|
var SpatialIndex = class {
|
|
1791
3488
|
constructor(doc) {
|
|
1792
3489
|
this.doc = doc;
|
|
1793
3490
|
this.cells = /* @__PURE__ */ new Map();
|
|
1794
|
-
this.
|
|
3491
|
+
this.objectCells = /* @__PURE__ */ new Map();
|
|
1795
3492
|
this.unsubscribe = doc.subscribe((change) => {
|
|
1796
|
-
for (const id of change
|
|
1797
|
-
for (const
|
|
3493
|
+
for (const field of REMOVED_FIELDS) for (const id of change[field]) this.remove(id);
|
|
3494
|
+
for (const field of ADDED_FIELDS) for (const object of change[field]) this.insert(object.id);
|
|
1798
3495
|
for (const stroke of change.transformed) {
|
|
1799
3496
|
this.remove(stroke.id);
|
|
1800
3497
|
this.insert(stroke.id);
|
|
1801
3498
|
}
|
|
3499
|
+
for (const field of UPDATED_FIELDS) for (const object of change[field]) {
|
|
3500
|
+
this.remove(object.id);
|
|
3501
|
+
this.insert(object.id);
|
|
3502
|
+
}
|
|
1802
3503
|
});
|
|
1803
3504
|
for (const stroke of doc.all()) this.insert(stroke.id);
|
|
1804
|
-
|
|
1805
|
-
|
|
3505
|
+
for (const note of doc.allNotes()) this.insert(note.id);
|
|
3506
|
+
for (const text of doc.allTexts()) this.insert(text.id);
|
|
3507
|
+
for (const table of doc.allTables()) this.insert(table.id);
|
|
3508
|
+
for (const image of doc.allImages()) this.insert(image.id);
|
|
3509
|
+
for (const timer of doc.allTimers()) this.insert(timer.id);
|
|
3510
|
+
for (const object of doc.allCustomObjects()) this.insert(object.id);
|
|
3511
|
+
for (const rect of doc.allRectangles()) this.insert(rect.id);
|
|
3512
|
+
for (const ellipse of doc.allEllipses()) this.insert(ellipse.id);
|
|
3513
|
+
}
|
|
3514
|
+
/** Ids of content objects (any type except groups) whose bbox may overlap the query rect. */
|
|
1806
3515
|
query(minX, minY, maxX, maxY) {
|
|
1807
3516
|
const result = /* @__PURE__ */ new Set();
|
|
1808
3517
|
for (const key of cellsOf({
|
|
@@ -1819,13 +3528,13 @@ var SpatialIndex = class {
|
|
|
1819
3528
|
dispose() {
|
|
1820
3529
|
this.unsubscribe();
|
|
1821
3530
|
this.cells.clear();
|
|
1822
|
-
this.
|
|
3531
|
+
this.objectCells.clear();
|
|
1823
3532
|
}
|
|
1824
3533
|
insert(id) {
|
|
1825
3534
|
const box = this.doc.bbox(id);
|
|
1826
3535
|
if (!box) return;
|
|
1827
3536
|
const keys = cellsOf(box);
|
|
1828
|
-
this.
|
|
3537
|
+
this.objectCells.set(id, keys);
|
|
1829
3538
|
for (const key of keys) {
|
|
1830
3539
|
let bucket = this.cells.get(key);
|
|
1831
3540
|
if (!bucket) {
|
|
@@ -1836,9 +3545,9 @@ var SpatialIndex = class {
|
|
|
1836
3545
|
}
|
|
1837
3546
|
}
|
|
1838
3547
|
remove(id) {
|
|
1839
|
-
const keys = this.
|
|
3548
|
+
const keys = this.objectCells.get(id);
|
|
1840
3549
|
if (!keys) return;
|
|
1841
|
-
this.
|
|
3550
|
+
this.objectCells.delete(id);
|
|
1842
3551
|
for (const key of keys) {
|
|
1843
3552
|
const bucket = this.cells.get(key);
|
|
1844
3553
|
if (bucket) {
|
|
@@ -1943,6 +3652,135 @@ function clamp(value, min, max) {
|
|
|
1943
3652
|
return Math.min(max, Math.max(min, value));
|
|
1944
3653
|
}
|
|
1945
3654
|
//#endregion
|
|
3655
|
+
//#region src/core/shapes/lineGeometry.ts
|
|
3656
|
+
var BARB_ANGLE = Math.PI * (25 / 180);
|
|
3657
|
+
/** Two barb segments (each `[fromPoint, tip]`), or `[]` for `head: "none"`. */
|
|
3658
|
+
function arrowHeadBarbs(start, end, head) {
|
|
3659
|
+
if (head === "none") return [];
|
|
3660
|
+
const len = Math.hypot(end.x - start.x, end.y - start.y);
|
|
3661
|
+
if (len < 1e-6) return [];
|
|
3662
|
+
const dirX = (end.x - start.x) / len;
|
|
3663
|
+
const dirY = (end.y - start.y) / len;
|
|
3664
|
+
const headLen = Math.min(4, Math.max(1.2, len * .25));
|
|
3665
|
+
const barb = (sign) => {
|
|
3666
|
+
const cos = Math.cos(BARB_ANGLE * sign);
|
|
3667
|
+
const sin = Math.sin(BARB_ANGLE * sign);
|
|
3668
|
+
return {
|
|
3669
|
+
x: end.x - headLen * (dirX * cos - dirY * sin),
|
|
3670
|
+
y: end.y - headLen * (dirX * sin + dirY * cos)
|
|
3671
|
+
};
|
|
3672
|
+
};
|
|
3673
|
+
return [[barb(1), end], [barb(-1), end]];
|
|
3674
|
+
}
|
|
3675
|
+
//#endregion
|
|
3676
|
+
//#region src/core/shapes/polygonGeometry.ts
|
|
3677
|
+
/**
|
|
3678
|
+
* The tuned per-`sides` starting angle that makes each regular-polygon kind
|
|
3679
|
+
* look right inscribed in its bounding box (a point-up triangle, a
|
|
3680
|
+
* flat-sided diamond, a point-up pentagon/hexagon, a flat-topped octagon) —
|
|
3681
|
+
* extracted unchanged from the legacy shape-stroke tool's own per-call
|
|
3682
|
+
* arguments, which were already tuned and shipped. Any `sides` count not
|
|
3683
|
+
* listed falls back to the common "point at top" case.
|
|
3684
|
+
*/
|
|
3685
|
+
function polygonStartAngle(sides) {
|
|
3686
|
+
switch (sides) {
|
|
3687
|
+
case 3: return Math.PI / 2;
|
|
3688
|
+
case 4: return 0;
|
|
3689
|
+
case 8: return Math.PI / 8;
|
|
3690
|
+
default: return Math.PI / 2;
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
3693
|
+
/**
|
|
3694
|
+
* Rescales/recenters an arbitrary point set so its own bounding box exactly
|
|
3695
|
+
* fills `[-width/2, width/2] × [-height/2, height/2]`, centered at the
|
|
3696
|
+
* origin. Needed because "inscribed on a radius-rx,ry ellipse" (the natural
|
|
3697
|
+
* way to place N evenly-spaced vertices) does *not* by itself guarantee a
|
|
3698
|
+
* full, centered fit: a shape only touches every edge of its box when it
|
|
3699
|
+
* has a vertex at each of the four cardinal angles (true for a square/
|
|
3700
|
+
* diamond/octagon-with-the-right-start-angle) — a triangle or pentagon's
|
|
3701
|
+
* *actual* bounding box is both smaller than and off-center from the
|
|
3702
|
+
* nominal rx,ry ellipse (verified: an upward triangle's own bounding box is
|
|
3703
|
+
* ~13% narrower than its circumradius, and vertically off-center by a
|
|
3704
|
+
* quarter of its own height). Every semantic shape's declared `width`/
|
|
3705
|
+
* `height` has to be the shape's *real* extent — `objectBounds.ts` and
|
|
3706
|
+
* rotate-about-center math both assume it — so every generator below routes
|
|
3707
|
+
* through this instead of leaving each one to reinvent (or skip) the fix.
|
|
3708
|
+
*/
|
|
3709
|
+
function fitToBox(points, width, height) {
|
|
3710
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
3711
|
+
for (const p of points) {
|
|
3712
|
+
if (p.x < minX) minX = p.x;
|
|
3713
|
+
if (p.x > maxX) maxX = p.x;
|
|
3714
|
+
if (p.y < minY) minY = p.y;
|
|
3715
|
+
if (p.y > maxY) maxY = p.y;
|
|
3716
|
+
}
|
|
3717
|
+
const rawW = maxX - minX || 1;
|
|
3718
|
+
const rawH = maxY - minY || 1;
|
|
3719
|
+
const cx = (minX + maxX) / 2;
|
|
3720
|
+
const cy = (minY + maxY) / 2;
|
|
3721
|
+
return points.map((p) => ({
|
|
3722
|
+
x: (p.x - cx) / rawW * width,
|
|
3723
|
+
y: (p.y - cy) / rawH * height
|
|
3724
|
+
}));
|
|
3725
|
+
}
|
|
3726
|
+
/**
|
|
3727
|
+
* A regular `sides`-gon inscribed in a `width`×`height` box, centered at
|
|
3728
|
+
* the origin and fit exactly to it via `fitToBox` — independently scaled
|
|
3729
|
+
* per axis (like an ellipse inscribed in a non-square box), not a true
|
|
3730
|
+
* regular polygon forced square. `sides: 4` with the diamond start angle
|
|
3731
|
+
* (0) produces the four cardinal points of the box — a diamond is exactly
|
|
3732
|
+
* this case, not a separate shape.
|
|
3733
|
+
*/
|
|
3734
|
+
function regularPolygonPoints(sides, width, height, startAngle) {
|
|
3735
|
+
const raw = [];
|
|
3736
|
+
for (let i = 0; i < sides; i++) {
|
|
3737
|
+
const theta = startAngle + i / sides * Math.PI * 2;
|
|
3738
|
+
raw.push({
|
|
3739
|
+
x: Math.cos(theta),
|
|
3740
|
+
y: Math.sin(theta)
|
|
3741
|
+
});
|
|
3742
|
+
}
|
|
3743
|
+
return fitToBox(raw, width, height);
|
|
3744
|
+
}
|
|
3745
|
+
/**
|
|
3746
|
+
* A `points`-pointed star inscribed in a `width`×`height` box, centered at
|
|
3747
|
+
* the origin and fit exactly to it via `fitToBox` — alternating outer/inner
|
|
3748
|
+
* vertices, `innerRatio` of the outer radius. Matches the legacy tool's own
|
|
3749
|
+
* tuned defaults (5 points, 0.42).
|
|
3750
|
+
*/
|
|
3751
|
+
function starPoints(points, innerRatio, width, height) {
|
|
3752
|
+
const total = points * 2;
|
|
3753
|
+
const raw = [];
|
|
3754
|
+
for (let i = 0; i < total; i++) {
|
|
3755
|
+
const isOuter = i % 2 === 0;
|
|
3756
|
+
const theta = -Math.PI / 2 + i / total * Math.PI * 2;
|
|
3757
|
+
const r = isOuter ? 1 : innerRatio;
|
|
3758
|
+
raw.push({
|
|
3759
|
+
x: Math.cos(theta) * r,
|
|
3760
|
+
y: Math.sin(theta) * r
|
|
3761
|
+
});
|
|
3762
|
+
}
|
|
3763
|
+
return fitToBox(raw, width, height);
|
|
3764
|
+
}
|
|
3765
|
+
var HEART_SEGMENTS = 64;
|
|
3766
|
+
/**
|
|
3767
|
+
* A heart inscribed in a `width`×`height` box, centered at the origin and
|
|
3768
|
+
* fit exactly to it via `fitToBox` — the standard parametric heart curve
|
|
3769
|
+
* (`x = 16 sin³t`, `y = 13 cos t − 5 cos 2t − 2 cos 3t − cos 4t`, matching
|
|
3770
|
+
* the legacy shape-stroke tool's own formula).
|
|
3771
|
+
*/
|
|
3772
|
+
function heartPoints(width, height) {
|
|
3773
|
+
const raw = [];
|
|
3774
|
+
for (let i = 0; i < HEART_SEGMENTS; i++) {
|
|
3775
|
+
const t = i / HEART_SEGMENTS * Math.PI * 2;
|
|
3776
|
+
raw.push({
|
|
3777
|
+
x: 16 * Math.sin(t) ** 3,
|
|
3778
|
+
y: -(13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t))
|
|
3779
|
+
});
|
|
3780
|
+
}
|
|
3781
|
+
return fitToBox(raw, width, height);
|
|
3782
|
+
}
|
|
3783
|
+
//#endregion
|
|
1946
3784
|
//#region src/persistence/serialization/svg.ts
|
|
1947
3785
|
/** Matches the key light used by the board shader and note shadows. */
|
|
1948
3786
|
var LIGHT = {
|
|
@@ -1980,15 +3818,29 @@ function documentToSVG(doc, now = 0, registry, backgroundColor = DEFAULT_BACKGRO
|
|
|
1980
3818
|
const images = doc.images ?? [];
|
|
1981
3819
|
const timers = doc.timers ?? [];
|
|
1982
3820
|
const customObjects = doc.customObjects ?? [];
|
|
1983
|
-
const
|
|
3821
|
+
const rectangles = doc.rectangles ?? [];
|
|
3822
|
+
const ellipses = doc.ellipses ?? [];
|
|
3823
|
+
const lines = doc.lines ?? [];
|
|
3824
|
+
const arrows = doc.arrows ?? [];
|
|
3825
|
+
const polygons = doc.polygons ?? [];
|
|
3826
|
+
const stars = doc.stars ?? [];
|
|
3827
|
+
const hearts = doc.hearts ?? [];
|
|
3828
|
+
const bounds = contentBounds(doc.strokes, notes, texts, tables, images, timers, customObjects, rectangles, ellipses, lines, arrows, polygons, stars, hearts);
|
|
1984
3829
|
const view = `${fmt(bounds.minX)} ${fmt(bounds.minY)} ${fmt(bounds.width)} ${fmt(bounds.height)}`;
|
|
1985
3830
|
const defs = notes.map(noteShadowFilter).join("\n");
|
|
1986
3831
|
const highlights = doc.strokes.filter((s) => s.tool === "highlighter");
|
|
1987
3832
|
const ink = doc.strokes.filter((s) => s.tool !== "highlighter");
|
|
1988
3833
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${view}">\n<defs>\n${defs}\n</defs>\n${[
|
|
1989
|
-
`<rect x="${fmt(bounds.minX)}" y="${fmt(bounds.minY)}" width="${fmt(bounds.width)}" height="${fmt(bounds.height)}" fill="${backgroundColor}"/>`,
|
|
3834
|
+
`<rect x="${fmt(bounds.minX)}" y="${fmt(bounds.minY)}" width="${fmt(bounds.width)}" height="${fmt(bounds.height)}" fill="${escapeXML(backgroundColor)}"/>`,
|
|
1990
3835
|
...images.map((image) => imageToElement(image, resolvedAssets)),
|
|
1991
3836
|
...tables.map(tableToGroup),
|
|
3837
|
+
...rectangles.map(rectangleToElement),
|
|
3838
|
+
...ellipses.map(ellipseToElement),
|
|
3839
|
+
...polygons.map(polygonToElement),
|
|
3840
|
+
...stars.map(starToElement),
|
|
3841
|
+
...hearts.map(heartToElement),
|
|
3842
|
+
...lines.map(lineToElement),
|
|
3843
|
+
...arrows.map(arrowToGroup),
|
|
1992
3844
|
...highlights.flatMap(strokeToPaths),
|
|
1993
3845
|
...ink.flatMap(strokeToPaths),
|
|
1994
3846
|
...texts.map(textToElement),
|
|
@@ -2007,7 +3859,7 @@ function customObjectToGroup(object, registry, resolvedAssets) {
|
|
|
2007
3859
|
const definition = registry?.objectType(object.type);
|
|
2008
3860
|
if (definition) try {
|
|
2009
3861
|
const scene = definition.describe(object, { selected: false });
|
|
2010
|
-
return `<g data-id="${object.id}">${sceneNodeToSVG(scene, object.transform, resolvedAssets)}</g>`;
|
|
3862
|
+
return `<g data-id="${escapeXML(object.id)}">${sceneNodeToSVG(scene, object.transform, resolvedAssets)}</g>`;
|
|
2011
3863
|
} catch {}
|
|
2012
3864
|
return fallbackToGroup(object);
|
|
2013
3865
|
}
|
|
@@ -2031,17 +3883,17 @@ function sceneNodeToSVG(node, parent, resolvedAssets) {
|
|
|
2031
3883
|
case "rect": {
|
|
2032
3884
|
const svgY = -node.y - node.height;
|
|
2033
3885
|
const rx = node.cornerRadius ? ` rx="${fmt(node.cornerRadius)}"` : "";
|
|
2034
|
-
const stroke = node.stroke ? ` stroke="${node.stroke}" stroke-width="${fmt(node.strokeWidth ?? .1)}"` : "";
|
|
2035
|
-
return `<rect x="${fmt(node.x)}" y="${fmt(svgY)}" width="${fmt(node.width)}" height="${fmt(node.height)}" fill="${node.fill ?? "none"}"${rx}${stroke}${opacityAttr}${transformAttr}/>`;
|
|
3886
|
+
const stroke = node.stroke ? ` stroke="${escapeXML(node.stroke)}" stroke-width="${fmt(node.strokeWidth ?? .1)}"` : "";
|
|
3887
|
+
return `<rect x="${fmt(node.x)}" y="${fmt(svgY)}" width="${fmt(node.width)}" height="${fmt(node.height)}" fill="${escapeXML(node.fill ?? "none")}"${rx}${stroke}${opacityAttr}${transformAttr}/>`;
|
|
2036
3888
|
}
|
|
2037
3889
|
case "text": {
|
|
2038
3890
|
const anchor = node.align === "center" ? ` text-anchor="middle"` : node.align === "end" ? ` text-anchor="end"` : "";
|
|
2039
3891
|
const tspans = node.text.split("\n").map((line, i) => `<tspan x="${fmt(node.x)}" y="${fmt(-node.y + node.fontSize * (.8 + 1.25 * i))}">${escapeXML(line)}</tspan>`).join("");
|
|
2040
|
-
return `<text font-family="Inter, 'Segoe UI', sans-serif" font-size="${fmt(node.fontSize)}" font-weight="500" fill="${node.color}"${anchor}${opacityAttr}${transformAttr}>${tspans}</text>`;
|
|
3892
|
+
return `<text font-family="Inter, 'Segoe UI', sans-serif" font-size="${fmt(node.fontSize)}" font-weight="500" fill="${escapeXML(node.color)}"${anchor}${opacityAttr}${transformAttr}>${tspans}</text>`;
|
|
2041
3893
|
}
|
|
2042
3894
|
case "ellipse": {
|
|
2043
|
-
const stroke = node.stroke ? ` stroke="${node.stroke}" stroke-width="${fmt(node.strokeWidth ?? .1)}"` : "";
|
|
2044
|
-
return `<ellipse cx="${fmt(node.cx)}" cy="${fmt(-node.cy)}" rx="${fmt(node.rx)}" ry="${fmt(node.ry)}" fill="${node.fill ?? "none"}"${stroke}${opacityAttr}${transformAttr}/>`;
|
|
3895
|
+
const stroke = node.stroke ? ` stroke="${escapeXML(node.stroke)}" stroke-width="${fmt(node.strokeWidth ?? .1)}"` : "";
|
|
3896
|
+
return `<ellipse cx="${fmt(node.cx)}" cy="${fmt(-node.cy)}" rx="${fmt(node.rx)}" ry="${fmt(node.ry)}" fill="${escapeXML(node.fill ?? "none")}"${stroke}${opacityAttr}${transformAttr}/>`;
|
|
2045
3897
|
}
|
|
2046
3898
|
case "image": {
|
|
2047
3899
|
const svgY = -node.y - node.height;
|
|
@@ -2072,7 +3924,7 @@ function fallbackToGroup(object) {
|
|
|
2072
3924
|
const label = object.fallback.label;
|
|
2073
3925
|
const labelEl = label ? `<text x="${fmt(b.x + Math.min(.3, b.width * .1))}" y="${fmt(svgY + Math.min(1.2, b.height * .4))}" font-family="Inter, 'Segoe UI', sans-serif" font-size="${fmt(Math.min(1.1, b.height * .3))}" fill="#6B7280">${escapeXML(label)}</text>` : "";
|
|
2074
3926
|
return [
|
|
2075
|
-
`<g data-id="${object.id}" data-fallback="true"${svgTransformAttr(object.transform)}>`,
|
|
3927
|
+
`<g data-id="${escapeXML(object.id)}" data-fallback="true"${svgTransformAttr(object.transform)}>`,
|
|
2076
3928
|
`<rect x="${fmt(b.x)}" y="${fmt(svgY)}" width="${fmt(b.width)}" height="${fmt(b.height)}" fill="#F3F4F6" stroke="#9CA3AF" stroke-width="0.15" stroke-dasharray="0.3 0.3"/>`,
|
|
2077
3929
|
labelEl,
|
|
2078
3930
|
`</g>`
|
|
@@ -2096,7 +3948,7 @@ function strokeToPaths(stroke) {
|
|
|
2096
3948
|
const d = `M ${left.join(" L ")} L ${right.join(" L ")} Z`;
|
|
2097
3949
|
const opacity = run.opacity * baseOpacity;
|
|
2098
3950
|
const opacityAttr = opacity < 1 ? ` fill-opacity="${fmt(opacity)}"` : "";
|
|
2099
|
-
paths.push(`<path data-id="${stroke.id}" d="${d}" fill="${stroke.color}"${opacityAttr}${transform}/>`);
|
|
3951
|
+
paths.push(`<path data-id="${escapeXML(stroke.id)}" d="${d}" fill="${escapeXML(stroke.color)}"${opacityAttr}${transform}/>`);
|
|
2100
3952
|
}
|
|
2101
3953
|
return paths;
|
|
2102
3954
|
}
|
|
@@ -2118,14 +3970,14 @@ function splitByOpacity(edges) {
|
|
|
2118
3970
|
function textToElement(block) {
|
|
2119
3971
|
const top = -block.y;
|
|
2120
3972
|
const tspans = block.text.split("\n").map((line, i) => `<tspan x="${fmt(block.x)}" y="${fmt(top + block.fontSize * (.8 + 1.25 * i))}">${escapeXML(line)}</tspan>`).join("");
|
|
2121
|
-
return `<text font-family="Inter, 'Segoe UI', sans-serif" font-size="${fmt(block.fontSize)}" font-weight="500" fill="${block.color}">${tspans}</text>`;
|
|
3973
|
+
return `<text font-family="Inter, 'Segoe UI', sans-serif" font-size="${fmt(block.fontSize)}" font-weight="500" fill="${escapeXML(block.color)}">${tspans}</text>`;
|
|
2122
3974
|
}
|
|
2123
3975
|
function noteShadowFilter(note) {
|
|
2124
3976
|
const dx = -LIGHT.x / LIGHT.z * note.zOffset;
|
|
2125
3977
|
const dy = LIGHT.y / LIGHT.z * note.zOffset;
|
|
2126
3978
|
const blur = (.35 + note.zOffset * .55) / 2;
|
|
2127
3979
|
const opacity = Math.max(.05, .26 - note.zOffset * .028);
|
|
2128
|
-
return `<filter id="sh-${note.id}" x="-50%" y="-50%" width="200%" height="200%"><feDropShadow dx="${fmt(dx)}" dy="${fmt(dy)}" stdDeviation="${fmt(blur)}" flood-opacity="${fmt(opacity)}"/></filter>`;
|
|
3980
|
+
return `<filter id="sh-${escapeXML(note.id)}" x="-50%" y="-50%" width="200%" height="200%"><feDropShadow dx="${fmt(dx)}" dy="${fmt(dy)}" stdDeviation="${fmt(blur)}" flood-opacity="${fmt(opacity)}"/></filter>`;
|
|
2129
3981
|
}
|
|
2130
3982
|
function noteToGroup(note) {
|
|
2131
3983
|
const left = note.x - note.size / 2;
|
|
@@ -2135,7 +3987,7 @@ function noteToGroup(note) {
|
|
|
2135
3987
|
const tspans = wrapText(note.text, note.size - pad * 2, fontSize).map((line, i) => `<tspan x="${fmt(left + pad)}" y="${fmt(top + pad + fontSize * (.8 + 1.25 * i))}">${escapeXML(line)}</tspan>`).join("");
|
|
2136
3988
|
return [
|
|
2137
3989
|
`<g>`,
|
|
2138
|
-
`<rect x="${fmt(left)}" y="${fmt(top)}" width="${fmt(note.size)}" height="${fmt(note.size)}" fill="${note.color}" filter="url(#sh-${note.id})"/>`,
|
|
3990
|
+
`<rect x="${fmt(left)}" y="${fmt(top)}" width="${fmt(note.size)}" height="${fmt(note.size)}" fill="${escapeXML(note.color)}" filter="url(#sh-${escapeXML(note.id)})"/>`,
|
|
2139
3991
|
`<text font-family="ui-rounded, 'Segoe UI', sans-serif" font-size="${fmt(fontSize)}" font-weight="500" fill="#37352F">${tspans}</text>`,
|
|
2140
3992
|
`</g>`
|
|
2141
3993
|
].join("");
|
|
@@ -2166,8 +4018,8 @@ function tableToGroup(table) {
|
|
|
2166
4018
|
const left = table.x;
|
|
2167
4019
|
const top = -table.y;
|
|
2168
4020
|
const headerH = table.rowHeights[0] ?? height / table.rows;
|
|
2169
|
-
const borderColor = table.color || "#D1D5DB";
|
|
2170
|
-
const bgColor = table.backgroundColor || "#FFFFFF";
|
|
4021
|
+
const borderColor = escapeXML(table.color || "#D1D5DB");
|
|
4022
|
+
const bgColor = escapeXML(table.backgroundColor || "#FFFFFF");
|
|
2171
4023
|
const elements = [];
|
|
2172
4024
|
elements.push(`<rect x="${fmt(left)}" y="${fmt(top)}" width="${fmt(width)}" height="${fmt(height)}" fill="${bgColor}" rx="0.5"/>`);
|
|
2173
4025
|
elements.push(`<rect x="${fmt(left)}" y="${fmt(top)}" width="${fmt(width)}" height="${fmt(headerH)}" fill="#F3F4F6" rx="0.5"/>`);
|
|
@@ -2195,14 +4047,14 @@ function tableToGroup(table) {
|
|
|
2195
4047
|
const padY = Math.min(.3, rowH * .12);
|
|
2196
4048
|
const fontSize = Math.min(1.25, rowH * .45);
|
|
2197
4049
|
const weight = r === 0 ? "bold" : "500";
|
|
2198
|
-
const color = r === 0 ? "#111827" : table.color ?? "#374151";
|
|
4050
|
+
const color = escapeXML(r === 0 ? "#111827" : table.color ?? "#374151");
|
|
2199
4051
|
elements.push(`<text x="${fmt(left + cellLeft + padX)}" y="${fmt(top + cellTop + padY + fontSize * .8)}" font-family="Inter, 'Segoe UI', sans-serif" font-size="${fmt(fontSize)}" font-weight="${weight}" fill="${color}">${escapeXML(text)}</text>`);
|
|
2200
4052
|
}
|
|
2201
4053
|
cellLeft += colW;
|
|
2202
4054
|
}
|
|
2203
4055
|
cellTop += rowH;
|
|
2204
4056
|
}
|
|
2205
|
-
return `<g data-id="${table.id}">\n${elements.join("\n")}\n</g>`;
|
|
4057
|
+
return `<g data-id="${escapeXML(table.id)}">\n${elements.join("\n")}\n</g>`;
|
|
2206
4058
|
}
|
|
2207
4059
|
function timerToGroup(timer, now) {
|
|
2208
4060
|
const r = timer.size / 2;
|
|
@@ -2210,7 +4062,7 @@ function timerToGroup(timer, now) {
|
|
|
2210
4062
|
const cy = -timer.y;
|
|
2211
4063
|
const label = formatTimer(timerRemaining(timer, now));
|
|
2212
4064
|
return [
|
|
2213
|
-
`<g data-id="${timer.id}">`,
|
|
4065
|
+
`<g data-id="${escapeXML(timer.id)}">`,
|
|
2214
4066
|
`<circle cx="${fmt(cx + .4)}" cy="${fmt(cy + .4)}" r="${fmt(r)}" fill="#000" fill-opacity="0.12"/>`,
|
|
2215
4067
|
`<circle cx="${fmt(cx)}" cy="${fmt(cy)}" r="${fmt(r)}" fill="#E11D48"/>`,
|
|
2216
4068
|
`<circle cx="${fmt(cx)}" cy="${fmt(cy)}" r="${fmt(r * .72)}" fill="#FAFAF9"/>`,
|
|
@@ -2223,12 +4075,96 @@ function imageToElement(image, resolvedAssets) {
|
|
|
2223
4075
|
const top = -image.y - image.height / 2;
|
|
2224
4076
|
if (image.ref) {
|
|
2225
4077
|
const dataUri = resolvedAssets?.get(image.ref);
|
|
2226
|
-
if (!dataUri) return `<g data-id="${image.id}" data-fallback="true">${imagePlaceholderElement(left, top, image.width, image.height, image.name)}</g>`;
|
|
2227
|
-
return `<image data-id="${image.id}" href="${escapeXML(dataUri)}" x="${fmt(left)}" y="${fmt(top)}" width="${fmt(image.width)}" height="${fmt(image.height)}" preserveAspectRatio="none"/>`;
|
|
4078
|
+
if (!dataUri) return `<g data-id="${escapeXML(image.id)}" data-fallback="true">${imagePlaceholderElement(left, top, image.width, image.height, image.name)}</g>`;
|
|
4079
|
+
return `<image data-id="${escapeXML(image.id)}" href="${escapeXML(dataUri)}" x="${fmt(left)}" y="${fmt(top)}" width="${fmt(image.width)}" height="${fmt(image.height)}" preserveAspectRatio="none"/>`;
|
|
2228
4080
|
}
|
|
2229
4081
|
return `<image href="${escapeXML(image.src)}" x="${fmt(left)}" y="${fmt(top)}" width="${fmt(image.width)}" height="${fmt(image.height)}" preserveAspectRatio="none"/>`;
|
|
2230
4082
|
}
|
|
2231
|
-
|
|
4083
|
+
/** (x, y) is the top edge (core/shapes/shapeObjects.ts) — same convention as TableBlock, so `svgY = -y` directly, no height offset. */
|
|
4084
|
+
/**
|
|
4085
|
+
* SVG `transform` attribute for a top-edge, Y-up shape's `rotation` (about
|
|
4086
|
+
* its own center, per `shapeObjects.ts`'s convention) — board-space
|
|
4087
|
+
* `rotationAbout` fed through the existing `svgTransformAttr` F·M·F
|
|
4088
|
+
* conversion, exactly how `sceneNodeToSVG`'s identical-convention `rect`
|
|
4089
|
+
* case already does it. Shared by every top-edge shape kind (Rectangle,
|
|
4090
|
+
* Ellipse, and Phase 4's Polygon/Star/Heart), not just these two.
|
|
4091
|
+
*/
|
|
4092
|
+
function shapeRotationAttr(x, y, width, height, rotation) {
|
|
4093
|
+
if (!rotation) return "";
|
|
4094
|
+
return svgTransformAttr(rotationAbout(rotation, x + width / 2, y - height / 2));
|
|
4095
|
+
}
|
|
4096
|
+
function opacityAttrOf(opacity) {
|
|
4097
|
+
return opacity !== void 0 && opacity < 1 ? ` opacity="${fmt(opacity)}"` : "";
|
|
4098
|
+
}
|
|
4099
|
+
function rectangleToElement(rect) {
|
|
4100
|
+
const svgY = -rect.y;
|
|
4101
|
+
const stroke = rect.stroke ? ` stroke="${escapeXML(rect.stroke)}" stroke-width="${fmt(rect.strokeWidth ?? .12)}"` : "";
|
|
4102
|
+
const rx = rect.cornerRadius ? ` rx="${fmt(rect.cornerRadius)}"` : "";
|
|
4103
|
+
const transform = shapeRotationAttr(rect.x, rect.y, rect.width, rect.height, rect.rotation);
|
|
4104
|
+
const opacity = opacityAttrOf(rect.opacity);
|
|
4105
|
+
return `<rect data-id="${escapeXML(rect.id)}" x="${fmt(rect.x)}" y="${fmt(svgY)}" width="${fmt(rect.width)}" height="${fmt(rect.height)}" fill="${escapeXML(rect.fill ?? "none")}"${rx}${stroke}${opacity}${transform}/>`;
|
|
4106
|
+
}
|
|
4107
|
+
function ellipseToElement(ellipse) {
|
|
4108
|
+
const cx = ellipse.x + ellipse.width / 2;
|
|
4109
|
+
const cy = -ellipse.y + ellipse.height / 2;
|
|
4110
|
+
const stroke = ellipse.stroke ? ` stroke="${escapeXML(ellipse.stroke)}" stroke-width="${fmt(ellipse.strokeWidth ?? .12)}"` : "";
|
|
4111
|
+
const transform = shapeRotationAttr(ellipse.x, ellipse.y, ellipse.width, ellipse.height, ellipse.rotation);
|
|
4112
|
+
const opacity = opacityAttrOf(ellipse.opacity);
|
|
4113
|
+
return `<ellipse data-id="${escapeXML(ellipse.id)}" cx="${fmt(cx)}" cy="${fmt(cy)}" rx="${fmt(ellipse.width / 2)}" ry="${fmt(ellipse.height / 2)}" fill="${escapeXML(ellipse.fill ?? "none")}"${stroke}${opacity}${transform}/>`;
|
|
4114
|
+
}
|
|
4115
|
+
/**
|
|
4116
|
+
* A regular N-gon inscribed in the same top-edge/Y-up bounding box
|
|
4117
|
+
* Rectangle uses — points are authored unrotated (absolute, y-flipped),
|
|
4118
|
+
* with `transform` handling rotation about the shape's own center exactly
|
|
4119
|
+
* like `rectangleToElement`/`ellipseToElement` already do, so this reuses
|
|
4120
|
+
* that same proven pattern rather than rotating points by hand.
|
|
4121
|
+
*/
|
|
4122
|
+
function polygonToElement(polygon) {
|
|
4123
|
+
const cx = polygon.x + polygon.width / 2;
|
|
4124
|
+
const cy = polygon.y - polygon.height / 2;
|
|
4125
|
+
const svgPoints = regularPolygonPoints(polygon.sides, polygon.width, polygon.height, polygonStartAngle(polygon.sides)).map((p) => `${fmt(cx + p.x)},${fmt(-(cy + p.y))}`).join(" ");
|
|
4126
|
+
const stroke = polygon.stroke ? ` stroke="${escapeXML(polygon.stroke)}" stroke-width="${fmt(polygon.strokeWidth ?? SHAPE_DEFAULT_STROKE_WIDTH)}"` : "";
|
|
4127
|
+
const transform = shapeRotationAttr(polygon.x, polygon.y, polygon.width, polygon.height, polygon.rotation);
|
|
4128
|
+
const opacity = opacityAttrOf(polygon.opacity);
|
|
4129
|
+
return `<polygon data-id="${escapeXML(polygon.id)}" points="${svgPoints}" fill="${escapeXML(polygon.fill ?? "none")}"${stroke}${opacity}${transform}/>`;
|
|
4130
|
+
}
|
|
4131
|
+
/** Same convention/pattern as `polygonToElement` — a `points`-vertex star with `innerRadiusRatio` (`polygonGeometry.ts`'s `starPoints`). */
|
|
4132
|
+
function starToElement(star) {
|
|
4133
|
+
const cx = star.x + star.width / 2;
|
|
4134
|
+
const cy = star.y - star.height / 2;
|
|
4135
|
+
const svgPoints = starPoints(star.points, star.innerRadiusRatio, star.width, star.height).map((p) => `${fmt(cx + p.x)},${fmt(-(cy + p.y))}`).join(" ");
|
|
4136
|
+
const stroke = star.stroke ? ` stroke="${escapeXML(star.stroke)}" stroke-width="${fmt(star.strokeWidth ?? SHAPE_DEFAULT_STROKE_WIDTH)}"` : "";
|
|
4137
|
+
const transform = shapeRotationAttr(star.x, star.y, star.width, star.height, star.rotation);
|
|
4138
|
+
const opacity = opacityAttrOf(star.opacity);
|
|
4139
|
+
return `<polygon data-id="${escapeXML(star.id)}" points="${svgPoints}" fill="${escapeXML(star.fill ?? "none")}"${stroke}${opacity}${transform}/>`;
|
|
4140
|
+
}
|
|
4141
|
+
/** 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). */
|
|
4142
|
+
function heartToElement(heart) {
|
|
4143
|
+
const cx = heart.x + heart.width / 2;
|
|
4144
|
+
const cy = heart.y - heart.height / 2;
|
|
4145
|
+
const svgPoints = heartPoints(heart.width, heart.height).map((p) => `${fmt(cx + p.x)},${fmt(-(cy + p.y))}`).join(" ");
|
|
4146
|
+
const stroke = heart.stroke ? ` stroke="${escapeXML(heart.stroke)}" stroke-width="${fmt(heart.strokeWidth ?? SHAPE_DEFAULT_STROKE_WIDTH)}"` : "";
|
|
4147
|
+
const transform = shapeRotationAttr(heart.x, heart.y, heart.width, heart.height, heart.rotation);
|
|
4148
|
+
const opacity = opacityAttrOf(heart.opacity);
|
|
4149
|
+
return `<polygon data-id="${escapeXML(heart.id)}" points="${svgPoints}" fill="${escapeXML(heart.fill ?? "none")}"${stroke}${opacity}${transform}/>`;
|
|
4150
|
+
}
|
|
4151
|
+
/** No rotation/box concept (see shapeObjects.ts's header) — the two endpoints are emitted directly, y-flipped like every other coordinate here. */
|
|
4152
|
+
function lineToElement(line) {
|
|
4153
|
+
const stroke = escapeXML(line.stroke ?? SHAPE_DEFAULT_STROKE);
|
|
4154
|
+
const width = line.strokeWidth ?? SHAPE_DEFAULT_STROKE_WIDTH;
|
|
4155
|
+
const opacity = opacityAttrOf(line.opacity);
|
|
4156
|
+
return `<line data-id="${escapeXML(line.id)}" x1="${fmt(line.start.x)}" y1="${fmt(-line.start.y)}" x2="${fmt(line.end.x)}" y2="${fmt(-line.end.y)}" stroke="${stroke}" stroke-width="${fmt(width)}"${opacity}/>`;
|
|
4157
|
+
}
|
|
4158
|
+
/** Shaft plus head barbs — same `arrowHeadBarbs` the canvas renderer uses, so the two never drift. */
|
|
4159
|
+
function arrowToGroup(arrow) {
|
|
4160
|
+
const stroke = escapeXML(arrow.stroke ?? SHAPE_DEFAULT_STROKE);
|
|
4161
|
+
const width = arrow.strokeWidth ?? SHAPE_DEFAULT_STROKE_WIDTH;
|
|
4162
|
+
const opacity = opacityAttrOf(arrow.opacity);
|
|
4163
|
+
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)}"/>`;
|
|
4164
|
+
const segments = [segment(arrow.start, arrow.end), ...arrowHeadBarbs(arrow.start, arrow.end, arrow.head).map(([a, b]) => segment(a, b))];
|
|
4165
|
+
return `<g data-id="${escapeXML(arrow.id)}"${opacity}>${segments.join("")}</g>`;
|
|
4166
|
+
}
|
|
4167
|
+
function contentBounds(strokes, notes, texts, tables = [], images = [], timers = [], customObjects = [], rectangles = [], ellipses = [], lines = [], arrows = [], polygons = [], stars = [], hearts = []) {
|
|
2232
4168
|
let minX = Infinity;
|
|
2233
4169
|
let minY = Infinity;
|
|
2234
4170
|
let maxX = -Infinity;
|
|
@@ -2290,6 +4226,34 @@ function contentBounds(strokes, notes, texts, tables = [], images = [], timers =
|
|
|
2290
4226
|
grow(p.x, -p.y);
|
|
2291
4227
|
}
|
|
2292
4228
|
}
|
|
4229
|
+
for (const rect of rectangles) {
|
|
4230
|
+
grow(rect.x, -rect.y);
|
|
4231
|
+
grow(rect.x + rect.width, -rect.y + rect.height);
|
|
4232
|
+
}
|
|
4233
|
+
for (const ellipse of ellipses) {
|
|
4234
|
+
grow(ellipse.x, -ellipse.y);
|
|
4235
|
+
grow(ellipse.x + ellipse.width, -ellipse.y + ellipse.height);
|
|
4236
|
+
}
|
|
4237
|
+
for (const line of lines) {
|
|
4238
|
+
grow(line.start.x, -line.start.y);
|
|
4239
|
+
grow(line.end.x, -line.end.y);
|
|
4240
|
+
}
|
|
4241
|
+
for (const arrow of arrows) {
|
|
4242
|
+
grow(arrow.start.x, -arrow.start.y);
|
|
4243
|
+
grow(arrow.end.x, -arrow.end.y);
|
|
4244
|
+
}
|
|
4245
|
+
for (const polygon of polygons) {
|
|
4246
|
+
grow(polygon.x, -polygon.y);
|
|
4247
|
+
grow(polygon.x + polygon.width, -polygon.y + polygon.height);
|
|
4248
|
+
}
|
|
4249
|
+
for (const star of stars) {
|
|
4250
|
+
grow(star.x, -star.y);
|
|
4251
|
+
grow(star.x + star.width, -star.y + star.height);
|
|
4252
|
+
}
|
|
4253
|
+
for (const heart of hearts) {
|
|
4254
|
+
grow(heart.x, -heart.y);
|
|
4255
|
+
grow(heart.x + heart.width, -heart.y + heart.height);
|
|
4256
|
+
}
|
|
2293
4257
|
if (minX > maxX) return {
|
|
2294
4258
|
minX: 0,
|
|
2295
4259
|
minY: 0,
|
|
@@ -2314,4 +4278,4 @@ function fmt(n) {
|
|
|
2314
4278
|
var SDK_PACKAGE_NAME = "@scrawl-board/board";
|
|
2315
4279
|
var SDK_DEVELOPMENT_VERSION = "0.0.0-development";
|
|
2316
4280
|
//#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 };
|
|
4281
|
+
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, SHAPE_STYLE_DEFAULTS, STAMPS, STAMP_SIZE, SUPPORTED_ASSET_MEDIA_TYPES, SpatialIndex, TABLE_DEFAULT_CELL_HEIGHT, TABLE_DEFAULT_CELL_WIDTH, TABLE_DEFAULT_FONT_SIZE, TEXT_DEFAULT_SIZE, TIMER_DEFAULT_DURATION_MS, TIMER_DEFAULT_SIZE, TIMER_PRESETS_MS, TransformCommand, TransformObjectsCommand, UpdateArrowCommand, UpdateEllipseCommand, UpdateGroupCommand, UpdateHeartCommand, UpdateImageCommand, UpdateLineCommand, UpdateNoteCommand, UpdatePolygonCommand, UpdateRectangleCommand, UpdateStarCommand, UpdateTableCommand, UpdateTextCommand, UpdateTimerCommand, apply, applyItemLock, assetRef, avgScale, canUnlockItem, changeToOps, clampAssetCacheBytes, cloneArrow, cloneCustomObject, cloneEllipse, cloneGroup, cloneHeart, cloneImage, cloneLine, cloneNote, clonePolygon, cloneRectangle, cloneStar, cloneStroke, cloneTable, cloneText, cloneTimer, documentId, documentSize, documentToSVG, 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 };
|