@fieldnotes/core 0.50.7 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -447,15 +447,32 @@ declare class HistoryStack {
447
447
  private notifyChange;
448
448
  }
449
449
 
450
- interface ExportImageOptions {
450
+ interface ExportImageOptions extends ExportResourceOptions {
451
451
  scale?: number;
452
452
  padding?: number;
453
453
  background?: string;
454
454
  filter?: (element: CanvasElement) => boolean;
455
455
  }
456
+ type ExportAssetErrorReason = 'load' | 'timeout' | 'encode';
457
+ interface ExportAssetError {
458
+ elementId: string;
459
+ src: string;
460
+ reason: ExportAssetErrorReason;
461
+ cause?: unknown;
462
+ }
463
+ interface ExportResourceOptions {
464
+ /** Maximum wait for each image asset. Defaults to 10 seconds. */
465
+ imageTimeoutMs?: number;
466
+ /** Maximum width or height of any allocated export canvas. Defaults to 16,384. */
467
+ maxDimension?: number;
468
+ /** Maximum pixel count of any allocated export canvas. Defaults to 67,108,864. */
469
+ maxPixels?: number;
470
+ /** Called when an image cannot be loaded or embedded. The export continues. */
471
+ onAssetError?: (error: ExportAssetError) => void;
472
+ }
456
473
  declare function exportImage(store: ElementStore, options?: ExportImageOptions, layerManager?: LayerManager): Promise<Blob | null>;
457
474
 
458
- interface ExportSvgOptions {
475
+ interface ExportSvgOptions extends ExportResourceOptions {
459
476
  padding?: number;
460
477
  background?: string;
461
478
  filter?: (el: CanvasElement) => boolean;
@@ -1123,6 +1140,6 @@ declare class LaserTool implements Tool {
1123
1140
  private notifyOptionsChange;
1124
1141
  }
1125
1142
 
1126
- declare const VERSION = "0.50.7";
1143
+ declare const VERSION = "0.51.0";
1127
1144
 
1128
- export { type ActiveFormats, type AlignEdge, type ArrowElement, type ArrowStrokeStyle, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, type DistributeAxis, type ElementChangeMeta, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportImageOptions, type ExportSvgOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, IndexedDBAdapter, type IndexedDBAdapterOptions, LaserTool, type LaserToolOptions, type Layer, LayerManager, LocalStorageAdapter, MeasureTool, type MeasureToolOptions, type Measurement, MemoryAdapter, type NoteElement, NoteTool, type NoteToolOptions, PencilTool, type PencilToolOptions, type Point, type PointerState, type RenderStatsSnapshot, type RotateDirection, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StorageAdapter, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateRenderStyle, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, boundsIntersect, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportSvg, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInRectangle, getHexCellsInSquare, getHexDistance, isNearBezier, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
1145
+ export { type ActiveFormats, type AlignEdge, type ArrowElement, type ArrowStrokeStyle, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, type DistributeAxis, type ElementChangeMeta, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportAssetError, type ExportAssetErrorReason, type ExportImageOptions, type ExportResourceOptions, type ExportSvgOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, IndexedDBAdapter, type IndexedDBAdapterOptions, LaserTool, type LaserToolOptions, type Layer, LayerManager, LocalStorageAdapter, MeasureTool, type MeasureToolOptions, type Measurement, MemoryAdapter, type NoteElement, NoteTool, type NoteToolOptions, PencilTool, type PencilToolOptions, type Point, type PointerState, type RenderStatsSnapshot, type RotateDirection, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StorageAdapter, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateRenderStyle, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, boundsIntersect, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportSvg, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInRectangle, getHexCellsInSquare, getHexDistance, isNearBezier, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
package/dist/index.d.ts CHANGED
@@ -447,15 +447,32 @@ declare class HistoryStack {
447
447
  private notifyChange;
448
448
  }
449
449
 
450
- interface ExportImageOptions {
450
+ interface ExportImageOptions extends ExportResourceOptions {
451
451
  scale?: number;
452
452
  padding?: number;
453
453
  background?: string;
454
454
  filter?: (element: CanvasElement) => boolean;
455
455
  }
456
+ type ExportAssetErrorReason = 'load' | 'timeout' | 'encode';
457
+ interface ExportAssetError {
458
+ elementId: string;
459
+ src: string;
460
+ reason: ExportAssetErrorReason;
461
+ cause?: unknown;
462
+ }
463
+ interface ExportResourceOptions {
464
+ /** Maximum wait for each image asset. Defaults to 10 seconds. */
465
+ imageTimeoutMs?: number;
466
+ /** Maximum width or height of any allocated export canvas. Defaults to 16,384. */
467
+ maxDimension?: number;
468
+ /** Maximum pixel count of any allocated export canvas. Defaults to 67,108,864. */
469
+ maxPixels?: number;
470
+ /** Called when an image cannot be loaded or embedded. The export continues. */
471
+ onAssetError?: (error: ExportAssetError) => void;
472
+ }
456
473
  declare function exportImage(store: ElementStore, options?: ExportImageOptions, layerManager?: LayerManager): Promise<Blob | null>;
457
474
 
458
- interface ExportSvgOptions {
475
+ interface ExportSvgOptions extends ExportResourceOptions {
459
476
  padding?: number;
460
477
  background?: string;
461
478
  filter?: (el: CanvasElement) => boolean;
@@ -1123,6 +1140,6 @@ declare class LaserTool implements Tool {
1123
1140
  private notifyOptionsChange;
1124
1141
  }
1125
1142
 
1126
- declare const VERSION = "0.50.7";
1143
+ declare const VERSION = "0.51.0";
1127
1144
 
1128
- export { type ActiveFormats, type AlignEdge, type ArrowElement, type ArrowStrokeStyle, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, type DistributeAxis, type ElementChangeMeta, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportImageOptions, type ExportSvgOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, IndexedDBAdapter, type IndexedDBAdapterOptions, LaserTool, type LaserToolOptions, type Layer, LayerManager, LocalStorageAdapter, MeasureTool, type MeasureToolOptions, type Measurement, MemoryAdapter, type NoteElement, NoteTool, type NoteToolOptions, PencilTool, type PencilToolOptions, type Point, type PointerState, type RenderStatsSnapshot, type RotateDirection, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StorageAdapter, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateRenderStyle, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, boundsIntersect, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportSvg, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInRectangle, getHexCellsInSquare, getHexDistance, isNearBezier, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
1145
+ export { type ActiveFormats, type AlignEdge, type ArrowElement, type ArrowStrokeStyle, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, type DistributeAxis, type ElementChangeMeta, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportAssetError, type ExportAssetErrorReason, type ExportImageOptions, type ExportResourceOptions, type ExportSvgOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, IndexedDBAdapter, type IndexedDBAdapterOptions, LaserTool, type LaserToolOptions, type Layer, LayerManager, LocalStorageAdapter, MeasureTool, type MeasureToolOptions, type Measurement, MemoryAdapter, type NoteElement, NoteTool, type NoteToolOptions, PencilTool, type PencilToolOptions, type Point, type PointerState, type RenderStatsSnapshot, type RotateDirection, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StorageAdapter, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateRenderStyle, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, boundsIntersect, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportSvg, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInRectangle, getHexCellsInSquare, getHexDistance, isNearBezier, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
package/dist/index.js CHANGED
@@ -152,6 +152,17 @@ function sanitizeAttributes(el, tag) {
152
152
 
153
153
  // src/core/state-serializer.ts
154
154
  var CURRENT_VERSION = 2;
155
+ var ELEMENT_TYPES = [
156
+ "stroke",
157
+ "note",
158
+ "arrow",
159
+ "image",
160
+ "html",
161
+ "text",
162
+ "shape",
163
+ "grid",
164
+ "template"
165
+ ];
155
166
  function exportState(elements, camera, layers = [], activeLayerId) {
156
167
  const state = {
157
168
  version: CURRENT_VERSION,
@@ -177,37 +188,42 @@ function parseState(json) {
177
188
  return data;
178
189
  }
179
190
  function validateState(data) {
180
- if (!data || typeof data !== "object") {
191
+ if (!isRecord(data)) {
181
192
  throw new Error("Invalid state: expected an object");
182
193
  }
183
194
  const obj = data;
184
- if (typeof obj["version"] !== "number") {
195
+ if (!Number.isInteger(obj["version"]) || obj["version"] < 1) {
185
196
  throw new Error("Invalid state: missing or invalid version");
186
197
  }
187
- if (!obj["camera"] || typeof obj["camera"] !== "object") {
198
+ if (obj["version"] > CURRENT_VERSION) {
199
+ throw new Error(`Invalid state: unsupported version ${String(obj["version"])}`);
200
+ }
201
+ if (!isRecord(obj["camera"])) {
188
202
  throw new Error("Invalid state: missing camera");
189
203
  }
190
204
  const cam = obj["camera"];
191
- if (!cam["position"] || typeof cam["position"] !== "object") {
205
+ if (!isRecord(cam["position"])) {
192
206
  throw new Error("Invalid state: missing camera.position");
193
207
  }
194
- const pos = cam["position"];
195
- if (typeof pos["x"] !== "number" || typeof pos["y"] !== "number") {
196
- throw new Error("Invalid state: camera.position must have x and y numbers");
208
+ if (!isPoint(cam["position"])) {
209
+ throw new Error("Invalid state: camera.position must have finite x and y numbers");
197
210
  }
198
- if (typeof cam["zoom"] !== "number") {
199
- throw new Error("Invalid state: missing camera.zoom");
211
+ if (!isFiniteNumber(cam["zoom"]) || cam["zoom"] <= 0) {
212
+ throw new Error("Invalid state: camera.zoom must be a positive finite number");
200
213
  }
201
214
  if (!Array.isArray(obj["elements"])) {
202
215
  throw new Error("Invalid state: elements must be an array");
203
216
  }
204
- for (const el of obj["elements"]) {
205
- validateElement(el);
206
- migrateElement(el);
217
+ if (obj["layers"] !== void 0 && !Array.isArray(obj["layers"])) {
218
+ throw new Error("Invalid state: layers must be an array");
207
219
  }
208
- cleanBindings(obj["elements"]);
209
- const layers = obj["layers"];
210
- if (!Array.isArray(layers) || layers.length === 0) {
220
+ const elements = obj["elements"];
221
+ const hasLayers = Array.isArray(obj["layers"]) && obj["layers"].length > 0;
222
+ for (const el of elements) {
223
+ if (!isRecord(el)) throw new Error("Invalid element: expected an object");
224
+ migrateElement(el, !hasLayers);
225
+ }
226
+ if (!hasLayers) {
211
227
  obj["layers"] = [
212
228
  {
213
229
  id: "default-layer",
@@ -219,31 +235,73 @@ function validateState(data) {
219
235
  }
220
236
  ];
221
237
  }
238
+ const layers = obj["layers"];
239
+ const layerIds = /* @__PURE__ */ new Set();
240
+ for (const layer of layers) {
241
+ validateLayer(layer);
242
+ if (layerIds.has(layer.id)) throw new Error(`Invalid state: duplicate layer id "${layer.id}"`);
243
+ layerIds.add(layer.id);
244
+ }
245
+ const elementIds = /* @__PURE__ */ new Set();
246
+ for (const el of elements) {
247
+ validateElement(el);
248
+ if (elementIds.has(el.id)) throw new Error(`Invalid state: duplicate element id "${el.id}"`);
249
+ elementIds.add(el.id);
250
+ if (!layerIds.has(el.layerId)) {
251
+ throw new Error(`Invalid element "${el.id}": unknown layerId "${el.layerId}"`);
252
+ }
253
+ }
254
+ if (obj["activeLayerId"] !== void 0) {
255
+ if (typeof obj["activeLayerId"] !== "string" || !layerIds.has(obj["activeLayerId"])) {
256
+ throw new Error("Invalid state: activeLayerId must reference an existing layer");
257
+ }
258
+ }
259
+ cleanBindings(elements);
222
260
  }
223
- var VALID_TYPES = /* @__PURE__ */ new Set([
224
- "stroke",
225
- "note",
226
- "arrow",
227
- "image",
228
- "html",
229
- "text",
230
- "shape",
231
- "grid",
232
- "template"
233
- ]);
234
261
  function validateElement(el) {
235
- if (!el || typeof el !== "object") {
262
+ if (!isRecord(el)) {
236
263
  throw new Error("Invalid element: expected an object");
237
264
  }
238
- const obj = el;
239
- if (typeof obj["id"] !== "string") {
265
+ if (typeof el["id"] !== "string" || el["id"].length === 0) {
240
266
  throw new Error("Invalid element: missing id");
241
267
  }
242
- if (typeof obj["type"] !== "string" || !VALID_TYPES.has(obj["type"])) {
243
- throw new Error(`Invalid element: unknown type "${String(obj["type"])}"`);
268
+ if (!isEnum(el["type"], ELEMENT_TYPES)) {
269
+ throw new Error(`Invalid element: unknown type "${String(el["type"])}"`);
270
+ }
271
+ if (!isFiniteNumber(el["zIndex"])) {
272
+ throw new Error(`Invalid element "${el["id"]}": missing or invalid zIndex`);
273
+ }
274
+ if (!isPoint(el["position"]) || typeof el["locked"] !== "boolean" || typeof el["layerId"] !== "string" || !isOptional(el["groupId"], isString) || !isOptional(el["rotation"], isFiniteNumber)) {
275
+ throw new Error(`Invalid element "${el["id"]}": invalid base fields or geometry`);
276
+ }
277
+ const valid = validateTypeFields(el, el["type"]);
278
+ if (!valid) throw new Error(`Invalid element "${el["id"]}": malformed ${el["type"]} data`);
279
+ }
280
+ function validateTypeFields(el, type) {
281
+ switch (type) {
282
+ case "stroke":
283
+ return Array.isArray(el["points"]) && el["points"].every(isStrokePoint) && isString(el["color"]) && isFiniteNumber(el["width"]) && isFiniteNumber(el["opacity"]) && isOptionalEnum(el["blendMode"], ["multiply"]);
284
+ case "note":
285
+ return isSize(el["size"]) && isString(el["text"]) && isString(el["backgroundColor"]) && isString(el["textColor"]) && isOptional(el["fontSize"], isFiniteNumber);
286
+ case "arrow":
287
+ return isPoint(el["from"]) && isPoint(el["to"]) && isFiniteNumber(el["bend"]) && isString(el["color"]) && isFiniteNumber(el["width"]) && isOptional(el["fromBinding"], isBinding) && isOptional(el["toBinding"], isBinding) && isOptional(el["cachedControlPoint"], isPoint) && isOptional(el["label"], isString) && isOptionalEnum(el["strokeStyle"], ["solid", "dashed", "dotted"]);
288
+ case "image":
289
+ return isSize(el["size"]) && isString(el["src"]);
290
+ case "html":
291
+ return isSize(el["size"]) && isOptional(el["domId"], isString) && isOptional(el["interactive"], isBoolean) && isOptional(el["htmlType"], isString) && isOptional(el["data"], isRecord);
292
+ case "text":
293
+ return isSize(el["size"]) && isString(el["text"]) && isFiniteNumber(el["fontSize"]) && isString(el["color"]) && isEnum(el["textAlign"], ["left", "center", "right"]);
294
+ case "shape":
295
+ return isEnum(el["shape"], ["rectangle", "ellipse", "line"]) && isSize(el["size"]) && isString(el["strokeColor"]) && isFiniteNumber(el["strokeWidth"]) && isString(el["fillColor"]) && isOptional(el["flip"], isBoolean);
296
+ case "grid":
297
+ return isEnum(el["gridType"], ["square", "hex"]) && isEnum(el["hexOrientation"], ["pointy", "flat"]) && isFiniteNumber(el["cellSize"]) && isString(el["strokeColor"]) && isFiniteNumber(el["strokeWidth"]) && isFiniteNumber(el["opacity"]);
298
+ case "template":
299
+ return isEnum(el["templateShape"], ["circle", "cone", "line", "square", "rectangle"]) && isFiniteNumber(el["radius"]) && isFiniteNumber(el["angle"]) && isOptional(el["width"], isFiniteNumber) && isString(el["fillColor"]) && isString(el["strokeColor"]) && isFiniteNumber(el["strokeWidth"]) && isFiniteNumber(el["opacity"]) && isOptional(el["feetPerCell"], isFiniteNumber) && isOptional(el["radiusFeet"], isFiniteNumber) && isOptionalEnum(el["renderStyle"], ["cells", "geometric"]);
244
300
  }
245
- if (typeof obj["zIndex"] !== "number") {
246
- throw new Error("Invalid element: missing zIndex");
301
+ }
302
+ function validateLayer(layer) {
303
+ if (!isRecord(layer) || typeof layer["id"] !== "string" || layer["id"].length === 0 || typeof layer["name"] !== "string" || typeof layer["visible"] !== "boolean" || typeof layer["locked"] !== "boolean" || !isFiniteNumber(layer["order"]) || !isFiniteNumber(layer["opacity"]) || layer["opacity"] < 0 || layer["opacity"] > 1) {
304
+ throw new Error("Invalid state: malformed layer");
247
305
  }
248
306
  }
249
307
  function cleanBindings(elements) {
@@ -260,30 +318,63 @@ function cleanBindings(elements) {
260
318
  }
261
319
  }
262
320
  }
263
- function migrateElement(obj) {
264
- if (typeof obj["layerId"] !== "string") {
321
+ function migrateElement(obj, useDefaultLayer) {
322
+ if (obj["layerId"] === void 0 || useDefaultLayer && obj["layerId"] === "") {
265
323
  obj["layerId"] = "default-layer";
266
324
  }
267
- if (obj["type"] === "arrow" && typeof obj["bend"] !== "number") {
325
+ if (obj["type"] === "arrow" && obj["bend"] === void 0) {
268
326
  obj["bend"] = 0;
269
327
  }
270
328
  if (obj["type"] === "stroke" && Array.isArray(obj["points"])) {
271
329
  for (const pt of obj["points"]) {
272
- if (typeof pt["pressure"] !== "number") {
330
+ if (pt["pressure"] === void 0) {
273
331
  pt["pressure"] = 0.5;
274
332
  }
275
333
  }
276
334
  }
277
- if (obj["type"] === "shape" && typeof obj["shape"] !== "string") {
335
+ if (obj["type"] === "shape" && obj["shape"] === void 0) {
278
336
  obj["shape"] = "rectangle";
279
337
  }
280
- if (obj["type"] === "note" && typeof obj["textColor"] !== "string") {
338
+ if (obj["type"] === "note" && obj["textColor"] === void 0) {
281
339
  obj["textColor"] = "#000000";
282
340
  }
283
341
  if ((obj["type"] === "note" || obj["type"] === "text") && typeof obj["text"] === "string") {
284
342
  obj["text"] = sanitizeNoteHtml(obj["text"]);
285
343
  }
286
344
  }
345
+ function isRecord(value) {
346
+ return typeof value === "object" && value !== null && !Array.isArray(value);
347
+ }
348
+ function isFiniteNumber(value) {
349
+ return typeof value === "number" && Number.isFinite(value);
350
+ }
351
+ function isString(value) {
352
+ return typeof value === "string";
353
+ }
354
+ function isBoolean(value) {
355
+ return typeof value === "boolean";
356
+ }
357
+ function isOptional(value, validate) {
358
+ return value === void 0 || validate(value);
359
+ }
360
+ function isEnum(value, values) {
361
+ return typeof value === "string" && values.includes(value);
362
+ }
363
+ function isOptionalEnum(value, values) {
364
+ return value === void 0 || isEnum(value, values);
365
+ }
366
+ function isPoint(value) {
367
+ return isRecord(value) && isFiniteNumber(value["x"]) && isFiniteNumber(value["y"]);
368
+ }
369
+ function isSize(value) {
370
+ return isRecord(value) && isFiniteNumber(value["w"]) && isFiniteNumber(value["h"]);
371
+ }
372
+ function isStrokePoint(value) {
373
+ return isRecord(value) && isPoint(value) && isFiniteNumber(value["pressure"]);
374
+ }
375
+ function isBinding(value) {
376
+ return isRecord(value) && typeof value["elementId"] === "string";
377
+ }
287
378
 
288
379
  // src/core/storage/local-storage-adapter.ts
289
380
  var LocalStorageAdapter = class {
@@ -5265,6 +5356,9 @@ function renderTextOnCanvas(ctx, text) {
5265
5356
  }
5266
5357
 
5267
5358
  // src/canvas/export-image.ts
5359
+ var DEFAULT_IMAGE_TIMEOUT_MS = 1e4;
5360
+ var DEFAULT_MAX_DIMENSION = 16384;
5361
+ var DEFAULT_MAX_PIXELS = 67108864;
5268
5362
  var center = (b) => ({ x: b.x + b.w / 2, y: b.y + b.h / 2 });
5269
5363
  function getStrokeBounds(el) {
5270
5364
  if (el.type !== "stroke") return null;
@@ -5374,12 +5468,53 @@ function renderGridForBounds(ctx, grid, bounds) {
5374
5468
  );
5375
5469
  }
5376
5470
  }
5377
- function loadImages(elements) {
5471
+ function positiveOption(value, fallback, name) {
5472
+ const resolved = value ?? fallback;
5473
+ if (!Number.isFinite(resolved) || resolved <= 0) {
5474
+ throw new RangeError(`${name} must be a finite number greater than 0`);
5475
+ }
5476
+ return resolved;
5477
+ }
5478
+ function nonNegativeOption(value, fallback, name) {
5479
+ const resolved = value ?? fallback;
5480
+ if (!Number.isFinite(resolved) || resolved < 0) {
5481
+ throw new RangeError(`${name} must be a finite number greater than or equal to 0`);
5482
+ }
5483
+ return resolved;
5484
+ }
5485
+ function assertExportSize(width, height, options) {
5486
+ const maxDimension = positiveOption(options.maxDimension, DEFAULT_MAX_DIMENSION, "maxDimension");
5487
+ const maxPixels = positiveOption(options.maxPixels, DEFAULT_MAX_PIXELS, "maxPixels");
5488
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
5489
+ throw new RangeError("Export dimensions must be finite numbers greater than 0");
5490
+ }
5491
+ if (width > maxDimension || height > maxDimension) {
5492
+ throw new RangeError(
5493
+ `Export dimensions ${width}x${height} exceed the maximum dimension of ${maxDimension}`
5494
+ );
5495
+ }
5496
+ if (width * height > maxPixels) {
5497
+ throw new RangeError(
5498
+ `Export size ${width}x${height} exceeds the maximum of ${maxPixels} pixels`
5499
+ );
5500
+ }
5501
+ }
5502
+ function validateExportResourceOptions(options) {
5503
+ positiveOption(options.imageTimeoutMs, DEFAULT_IMAGE_TIMEOUT_MS, "imageTimeoutMs");
5504
+ positiveOption(options.maxDimension, DEFAULT_MAX_DIMENSION, "maxDimension");
5505
+ positiveOption(options.maxPixels, DEFAULT_MAX_PIXELS, "maxPixels");
5506
+ }
5507
+ function loadImages(elements, options = {}) {
5378
5508
  const imageElements = elements.filter(
5379
5509
  (el) => el.type === "image" && "src" in el
5380
5510
  );
5381
5511
  const cache3 = /* @__PURE__ */ new Map();
5382
5512
  if (imageElements.length === 0) return Promise.resolve(cache3);
5513
+ const timeoutMs = positiveOption(
5514
+ options.imageTimeoutMs,
5515
+ DEFAULT_IMAGE_TIMEOUT_MS,
5516
+ "imageTimeoutMs"
5517
+ );
5383
5518
  return new Promise((resolve) => {
5384
5519
  let remaining = imageElements.length;
5385
5520
  const done = () => {
@@ -5389,19 +5524,41 @@ function loadImages(elements) {
5389
5524
  for (const el of imageElements) {
5390
5525
  const img = new Image();
5391
5526
  img.crossOrigin = "anonymous";
5527
+ let settled = false;
5528
+ const timer = setTimeout(() => {
5529
+ if (settled) return;
5530
+ settled = true;
5531
+ img.onload = null;
5532
+ img.onerror = null;
5533
+ options.onAssetError?.({ elementId: el.id, src: el.src, reason: "timeout" });
5534
+ done();
5535
+ }, timeoutMs);
5536
+ const settle = () => {
5537
+ if (settled) return false;
5538
+ settled = true;
5539
+ clearTimeout(timer);
5540
+ img.onload = null;
5541
+ img.onerror = null;
5542
+ return true;
5543
+ };
5392
5544
  img.onload = () => {
5545
+ if (!settle()) return;
5393
5546
  cache3.set(el.id, img);
5394
5547
  done();
5395
5548
  };
5396
- img.onerror = done;
5397
- const sep = el.src.includes("?") ? "&" : "?";
5398
- img.src = `${el.src}${sep}_cors=1`;
5549
+ img.onerror = (cause) => {
5550
+ if (!settle()) return;
5551
+ options.onAssetError?.({ elementId: el.id, src: el.src, reason: "load", cause });
5552
+ done();
5553
+ };
5554
+ img.src = el.src;
5399
5555
  }
5400
5556
  });
5401
5557
  }
5402
5558
  async function exportImage(store, options = {}, layerManager) {
5403
- const scale = options.scale ?? 2;
5404
- const padding = options.padding ?? 0;
5559
+ const scale = positiveOption(options.scale, 2, "scale");
5560
+ const padding = nonNegativeOption(options.padding, 0, "padding");
5561
+ validateExportResourceOptions(options);
5405
5562
  const background = options.background ?? "#ffffff";
5406
5563
  const filter = options.filter;
5407
5564
  const allElements = store.getAll();
@@ -5411,10 +5568,13 @@ async function exportImage(store, options = {}, layerManager) {
5411
5568
  }
5412
5569
  const bounds = computeBounds(visibleElements, padding);
5413
5570
  if (!bounds) return null;
5414
- const imageCache = await loadImages(visibleElements);
5571
+ const width = Math.ceil(bounds.w * scale);
5572
+ const height = Math.ceil(bounds.h * scale);
5573
+ assertExportSize(width, height, options);
5574
+ const imageCache = await loadImages(visibleElements, options);
5415
5575
  const canvas = document.createElement("canvas");
5416
- canvas.width = Math.ceil(bounds.w * scale);
5417
- canvas.height = Math.ceil(bounds.h * scale);
5576
+ canvas.width = width;
5577
+ canvas.height = height;
5418
5578
  const ctx = canvas.getContext("2d");
5419
5579
  if (!ctx) return null;
5420
5580
  ctx.scale(scale, scale);
@@ -5597,14 +5757,17 @@ function emitImage(image, dataUri) {
5597
5757
  const { w, h } = image.size;
5598
5758
  return `<image href="${esc(href)}" x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" />`;
5599
5759
  }
5600
- function emitText(text, rasterScale) {
5760
+ function emitText(text, rasterScale, resourceOptions) {
5601
5761
  if (!text.text) return "";
5602
5762
  const { x, y } = text.position;
5603
5763
  const { w, h } = text.size;
5604
5764
  if (typeof document === "undefined") return "";
5765
+ const width = Math.max(1, Math.ceil(w * rasterScale));
5766
+ const height = Math.max(1, Math.ceil(h * rasterScale));
5767
+ assertExportSize(width, height, resourceOptions);
5605
5768
  const canvas = document.createElement("canvas");
5606
- canvas.width = Math.max(1, Math.ceil(w * rasterScale));
5607
- canvas.height = Math.max(1, Math.ceil(h * rasterScale));
5769
+ canvas.width = width;
5770
+ canvas.height = height;
5608
5771
  const ctx = canvas.getContext("2d");
5609
5772
  if (!ctx) return "";
5610
5773
  ctx.scale(rasterScale, rasterScale);
@@ -5619,13 +5782,16 @@ function emitText(text, rasterScale) {
5619
5782
  if (!dataUri || !dataUri.startsWith("data:")) return "";
5620
5783
  return `<image href="${esc(dataUri)}" x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" />`;
5621
5784
  }
5622
- function emitNote(note, rasterScale) {
5785
+ function emitNote(note, rasterScale, resourceOptions) {
5623
5786
  const { x, y } = note.position;
5624
5787
  const { w, h } = note.size;
5625
5788
  if (typeof document === "undefined") return emitNotePlaceholder(note);
5789
+ const width = Math.max(1, Math.ceil(w * rasterScale));
5790
+ const height = Math.max(1, Math.ceil(h * rasterScale));
5791
+ assertExportSize(width, height, resourceOptions);
5626
5792
  const canvas = document.createElement("canvas");
5627
- canvas.width = Math.max(1, Math.ceil(w * rasterScale));
5628
- canvas.height = Math.max(1, Math.ceil(h * rasterScale));
5793
+ canvas.width = width;
5794
+ canvas.height = height;
5629
5795
  const ctx = canvas.getContext("2d");
5630
5796
  if (!ctx) return emitNotePlaceholder(note);
5631
5797
  ctx.scale(rasterScale, rasterScale);
@@ -5786,8 +5952,9 @@ function emitHexTemplate(t, grid) {
5786
5952
  return `<path d="${d}" fill="${esc(t.fillColor)}" stroke="${esc(t.strokeColor)}" stroke-width="${n(t.strokeWidth)}" opacity="${n(t.opacity)}" />`;
5787
5953
  }
5788
5954
  async function exportSvg(store, options = {}, layerManager) {
5789
- const padding = options.padding ?? 0;
5790
- const rasterScale = options.rasterScale ?? 2;
5955
+ const padding = nonNegativeOption(options.padding, 0, "padding");
5956
+ const rasterScale = positiveOption(options.rasterScale, 2, "rasterScale");
5957
+ validateExportResourceOptions(options);
5791
5958
  const filter = options.filter;
5792
5959
  const allElements = store.getAll();
5793
5960
  let visibleElements = layerManager ? allElements.filter((el) => layerManager.isLayerVisible(el.layerId)) : allElements;
@@ -5796,11 +5963,12 @@ async function exportSvg(store, options = {}, layerManager) {
5796
5963
  if (!bounds) {
5797
5964
  return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
5798
5965
  }
5966
+ assertExportSize(Math.ceil(bounds.w), Math.ceil(bounds.h), options);
5799
5967
  const remoteImages = visibleElements.filter(
5800
5968
  (el) => el.type === "image" && !el.src.startsWith("data:")
5801
5969
  );
5802
- const imageCache = await loadImages(remoteImages);
5803
- const imageDataUris = encodeImages(visibleElements, imageCache, rasterScale);
5970
+ const imageCache = await loadImages(remoteImages, options);
5971
+ const imageDataUris = encodeImages(visibleElements, imageCache, rasterScale, options);
5804
5972
  const grids = visibleElements.filter((el) => el.type === "grid");
5805
5973
  const firstGrid = grids[0];
5806
5974
  let body = "";
@@ -5809,7 +5977,7 @@ async function exportSvg(store, options = {}, layerManager) {
5809
5977
  }
5810
5978
  const layerBodies = /* @__PURE__ */ new Map();
5811
5979
  for (const el of visibleElements) {
5812
- const emitted = emitElement(el, imageDataUris, rasterScale, firstGrid, store);
5980
+ const emitted = emitElement(el, imageDataUris, rasterScale, firstGrid, store, options);
5813
5981
  layerBodies.set(el.layerId, (layerBodies.get(el.layerId) ?? "") + emitted);
5814
5982
  }
5815
5983
  for (const [layerId, emitted] of layerBodies) {
@@ -5823,7 +5991,7 @@ async function exportSvg(store, options = {}, layerManager) {
5823
5991
  }
5824
5992
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${n(bounds.w)}" height="${n(bounds.h)}" viewBox="${n(bounds.x)} ${n(bounds.y)} ${n(bounds.w)} ${n(bounds.h)}">${body}</svg>`;
5825
5993
  }
5826
- function emitElement(el, imageDataUris, rasterScale, firstGrid, store) {
5994
+ function emitElement(el, imageDataUris, rasterScale, firstGrid, store, resourceOptions) {
5827
5995
  switch (el.type) {
5828
5996
  case "stroke":
5829
5997
  return withRotationSvg(el, emitStroke(el));
@@ -5834,9 +6002,9 @@ function emitElement(el, imageDataUris, rasterScale, firstGrid, store) {
5834
6002
  case "image":
5835
6003
  return withRotationSvg(el, emitImage(el, imageDataUris.get(el.id)));
5836
6004
  case "text":
5837
- return withRotationSvg(el, emitText(el, rasterScale));
6005
+ return withRotationSvg(el, emitText(el, rasterScale, resourceOptions));
5838
6006
  case "note":
5839
- return withRotationSvg(el, emitNote(el, rasterScale));
6007
+ return withRotationSvg(el, emitNote(el, rasterScale, resourceOptions));
5840
6008
  case "template":
5841
6009
  return emitTemplate(el, firstGrid);
5842
6010
  case "grid":
@@ -5847,7 +6015,7 @@ function emitElement(el, imageDataUris, rasterScale, firstGrid, store) {
5847
6015
  return "";
5848
6016
  }
5849
6017
  }
5850
- function encodeImages(elements, imageCache, rasterScale) {
6018
+ function encodeImages(elements, imageCache, rasterScale, resourceOptions) {
5851
6019
  const out = /* @__PURE__ */ new Map();
5852
6020
  for (const el of elements) {
5853
6021
  if (el.type !== "image") continue;
@@ -5857,16 +6025,24 @@ function encodeImages(elements, imageCache, rasterScale) {
5857
6025
  }
5858
6026
  const img = imageCache.get(el.id);
5859
6027
  if (!img || typeof document === "undefined") continue;
6028
+ const width = Math.max(1, Math.ceil(el.size.w * rasterScale));
6029
+ const height = Math.max(1, Math.ceil(el.size.h * rasterScale));
6030
+ assertExportSize(width, height, resourceOptions);
5860
6031
  const canvas = document.createElement("canvas");
5861
- canvas.width = Math.max(1, Math.ceil(el.size.w * rasterScale));
5862
- canvas.height = Math.max(1, Math.ceil(el.size.h * rasterScale));
6032
+ canvas.width = width;
6033
+ canvas.height = height;
5863
6034
  const ctx = canvas.getContext("2d");
5864
- if (!ctx) continue;
6035
+ if (!ctx) {
6036
+ resourceOptions.onAssetError?.({ elementId: el.id, src: el.src, reason: "encode" });
6037
+ continue;
6038
+ }
5865
6039
  try {
5866
6040
  ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
5867
6041
  const uri = canvas.toDataURL();
5868
6042
  if (uri.startsWith("data:")) out.set(el.id, uri);
5869
- } catch {
6043
+ else resourceOptions.onAssetError?.({ elementId: el.id, src: el.src, reason: "encode" });
6044
+ } catch (cause) {
6045
+ resourceOptions.onAssetError?.({ elementId: el.id, src: el.src, reason: "encode", cause });
5870
6046
  }
5871
6047
  }
5872
6048
  return out;
@@ -10639,7 +10815,7 @@ var LaserTool = class {
10639
10815
  };
10640
10816
 
10641
10817
  // src/index.ts
10642
- var VERSION = "0.50.7";
10818
+ var VERSION = "0.51.0";
10643
10819
  export {
10644
10820
  ArrowTool,
10645
10821
  AutoSave,