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