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