@fieldnotes/core 0.50.7 → 0.50.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -239,6 +239,17 @@ function sanitizeAttributes(el, tag) {
239
239
 
240
240
  // src/core/state-serializer.ts
241
241
  var CURRENT_VERSION = 2;
242
+ var ELEMENT_TYPES = [
243
+ "stroke",
244
+ "note",
245
+ "arrow",
246
+ "image",
247
+ "html",
248
+ "text",
249
+ "shape",
250
+ "grid",
251
+ "template"
252
+ ];
242
253
  function exportState(elements, camera, layers = [], activeLayerId) {
243
254
  const state = {
244
255
  version: CURRENT_VERSION,
@@ -264,37 +275,42 @@ function parseState(json) {
264
275
  return data;
265
276
  }
266
277
  function validateState(data) {
267
- if (!data || typeof data !== "object") {
278
+ if (!isRecord(data)) {
268
279
  throw new Error("Invalid state: expected an object");
269
280
  }
270
281
  const obj = data;
271
- if (typeof obj["version"] !== "number") {
282
+ if (!Number.isInteger(obj["version"]) || obj["version"] < 1) {
272
283
  throw new Error("Invalid state: missing or invalid version");
273
284
  }
274
- if (!obj["camera"] || typeof obj["camera"] !== "object") {
285
+ if (obj["version"] > CURRENT_VERSION) {
286
+ throw new Error(`Invalid state: unsupported version ${String(obj["version"])}`);
287
+ }
288
+ if (!isRecord(obj["camera"])) {
275
289
  throw new Error("Invalid state: missing camera");
276
290
  }
277
291
  const cam = obj["camera"];
278
- if (!cam["position"] || typeof cam["position"] !== "object") {
292
+ if (!isRecord(cam["position"])) {
279
293
  throw new Error("Invalid state: missing camera.position");
280
294
  }
281
- const pos = cam["position"];
282
- if (typeof pos["x"] !== "number" || typeof pos["y"] !== "number") {
283
- throw new Error("Invalid state: camera.position must have x and y numbers");
295
+ if (!isPoint(cam["position"])) {
296
+ throw new Error("Invalid state: camera.position must have finite x and y numbers");
284
297
  }
285
- if (typeof cam["zoom"] !== "number") {
286
- throw new Error("Invalid state: missing camera.zoom");
298
+ if (!isFiniteNumber(cam["zoom"]) || cam["zoom"] <= 0) {
299
+ throw new Error("Invalid state: camera.zoom must be a positive finite number");
287
300
  }
288
301
  if (!Array.isArray(obj["elements"])) {
289
302
  throw new Error("Invalid state: elements must be an array");
290
303
  }
291
- for (const el of obj["elements"]) {
292
- validateElement(el);
293
- migrateElement(el);
304
+ if (obj["layers"] !== void 0 && !Array.isArray(obj["layers"])) {
305
+ throw new Error("Invalid state: layers must be an array");
294
306
  }
295
- cleanBindings(obj["elements"]);
296
- const layers = obj["layers"];
297
- if (!Array.isArray(layers) || layers.length === 0) {
307
+ const elements = obj["elements"];
308
+ const hasLayers = Array.isArray(obj["layers"]) && obj["layers"].length > 0;
309
+ for (const el of elements) {
310
+ if (!isRecord(el)) throw new Error("Invalid element: expected an object");
311
+ migrateElement(el, !hasLayers);
312
+ }
313
+ if (!hasLayers) {
298
314
  obj["layers"] = [
299
315
  {
300
316
  id: "default-layer",
@@ -306,31 +322,73 @@ function validateState(data) {
306
322
  }
307
323
  ];
308
324
  }
325
+ const layers = obj["layers"];
326
+ const layerIds = /* @__PURE__ */ new Set();
327
+ for (const layer of layers) {
328
+ validateLayer(layer);
329
+ if (layerIds.has(layer.id)) throw new Error(`Invalid state: duplicate layer id "${layer.id}"`);
330
+ layerIds.add(layer.id);
331
+ }
332
+ const elementIds = /* @__PURE__ */ new Set();
333
+ for (const el of elements) {
334
+ validateElement(el);
335
+ if (elementIds.has(el.id)) throw new Error(`Invalid state: duplicate element id "${el.id}"`);
336
+ elementIds.add(el.id);
337
+ if (!layerIds.has(el.layerId)) {
338
+ throw new Error(`Invalid element "${el.id}": unknown layerId "${el.layerId}"`);
339
+ }
340
+ }
341
+ if (obj["activeLayerId"] !== void 0) {
342
+ if (typeof obj["activeLayerId"] !== "string" || !layerIds.has(obj["activeLayerId"])) {
343
+ throw new Error("Invalid state: activeLayerId must reference an existing layer");
344
+ }
345
+ }
346
+ cleanBindings(elements);
309
347
  }
310
- var VALID_TYPES = /* @__PURE__ */ new Set([
311
- "stroke",
312
- "note",
313
- "arrow",
314
- "image",
315
- "html",
316
- "text",
317
- "shape",
318
- "grid",
319
- "template"
320
- ]);
321
348
  function validateElement(el) {
322
- if (!el || typeof el !== "object") {
349
+ if (!isRecord(el)) {
323
350
  throw new Error("Invalid element: expected an object");
324
351
  }
325
- const obj = el;
326
- if (typeof obj["id"] !== "string") {
352
+ if (typeof el["id"] !== "string" || el["id"].length === 0) {
327
353
  throw new Error("Invalid element: missing id");
328
354
  }
329
- if (typeof obj["type"] !== "string" || !VALID_TYPES.has(obj["type"])) {
330
- throw new Error(`Invalid element: unknown type "${String(obj["type"])}"`);
355
+ if (!isEnum(el["type"], ELEMENT_TYPES)) {
356
+ throw new Error(`Invalid element: unknown type "${String(el["type"])}"`);
357
+ }
358
+ if (!isFiniteNumber(el["zIndex"])) {
359
+ throw new Error(`Invalid element "${el["id"]}": missing or invalid zIndex`);
331
360
  }
332
- if (typeof obj["zIndex"] !== "number") {
333
- throw new Error("Invalid element: missing zIndex");
361
+ if (!isPoint(el["position"]) || typeof el["locked"] !== "boolean" || typeof el["layerId"] !== "string" || !isOptional(el["groupId"], isString) || !isOptional(el["rotation"], isFiniteNumber)) {
362
+ throw new Error(`Invalid element "${el["id"]}": invalid base fields or geometry`);
363
+ }
364
+ const valid = validateTypeFields(el, el["type"]);
365
+ if (!valid) throw new Error(`Invalid element "${el["id"]}": malformed ${el["type"]} data`);
366
+ }
367
+ function validateTypeFields(el, type) {
368
+ switch (type) {
369
+ case "stroke":
370
+ return Array.isArray(el["points"]) && el["points"].every(isStrokePoint) && isString(el["color"]) && isFiniteNumber(el["width"]) && isFiniteNumber(el["opacity"]) && isOptionalEnum(el["blendMode"], ["multiply"]);
371
+ case "note":
372
+ return isSize(el["size"]) && isString(el["text"]) && isString(el["backgroundColor"]) && isString(el["textColor"]) && isOptional(el["fontSize"], isFiniteNumber);
373
+ case "arrow":
374
+ 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"]);
375
+ case "image":
376
+ return isSize(el["size"]) && isString(el["src"]);
377
+ case "html":
378
+ return isSize(el["size"]) && isOptional(el["domId"], isString) && isOptional(el["interactive"], isBoolean) && isOptional(el["htmlType"], isString) && isOptional(el["data"], isRecord);
379
+ case "text":
380
+ return isSize(el["size"]) && isString(el["text"]) && isFiniteNumber(el["fontSize"]) && isString(el["color"]) && isEnum(el["textAlign"], ["left", "center", "right"]);
381
+ case "shape":
382
+ return isEnum(el["shape"], ["rectangle", "ellipse", "line"]) && isSize(el["size"]) && isString(el["strokeColor"]) && isFiniteNumber(el["strokeWidth"]) && isString(el["fillColor"]) && isOptional(el["flip"], isBoolean);
383
+ case "grid":
384
+ return isEnum(el["gridType"], ["square", "hex"]) && isEnum(el["hexOrientation"], ["pointy", "flat"]) && isFiniteNumber(el["cellSize"]) && isString(el["strokeColor"]) && isFiniteNumber(el["strokeWidth"]) && isFiniteNumber(el["opacity"]);
385
+ case "template":
386
+ 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"]);
387
+ }
388
+ }
389
+ function validateLayer(layer) {
390
+ 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) {
391
+ throw new Error("Invalid state: malformed layer");
334
392
  }
335
393
  }
336
394
  function cleanBindings(elements) {
@@ -347,30 +405,63 @@ function cleanBindings(elements) {
347
405
  }
348
406
  }
349
407
  }
350
- function migrateElement(obj) {
351
- if (typeof obj["layerId"] !== "string") {
408
+ function migrateElement(obj, useDefaultLayer) {
409
+ if (obj["layerId"] === void 0 || useDefaultLayer && obj["layerId"] === "") {
352
410
  obj["layerId"] = "default-layer";
353
411
  }
354
- if (obj["type"] === "arrow" && typeof obj["bend"] !== "number") {
412
+ if (obj["type"] === "arrow" && obj["bend"] === void 0) {
355
413
  obj["bend"] = 0;
356
414
  }
357
415
  if (obj["type"] === "stroke" && Array.isArray(obj["points"])) {
358
416
  for (const pt of obj["points"]) {
359
- if (typeof pt["pressure"] !== "number") {
417
+ if (pt["pressure"] === void 0) {
360
418
  pt["pressure"] = 0.5;
361
419
  }
362
420
  }
363
421
  }
364
- if (obj["type"] === "shape" && typeof obj["shape"] !== "string") {
422
+ if (obj["type"] === "shape" && obj["shape"] === void 0) {
365
423
  obj["shape"] = "rectangle";
366
424
  }
367
- if (obj["type"] === "note" && typeof obj["textColor"] !== "string") {
425
+ if (obj["type"] === "note" && obj["textColor"] === void 0) {
368
426
  obj["textColor"] = "#000000";
369
427
  }
370
428
  if ((obj["type"] === "note" || obj["type"] === "text") && typeof obj["text"] === "string") {
371
429
  obj["text"] = sanitizeNoteHtml(obj["text"]);
372
430
  }
373
431
  }
432
+ function isRecord(value) {
433
+ return typeof value === "object" && value !== null && !Array.isArray(value);
434
+ }
435
+ function isFiniteNumber(value) {
436
+ return typeof value === "number" && Number.isFinite(value);
437
+ }
438
+ function isString(value) {
439
+ return typeof value === "string";
440
+ }
441
+ function isBoolean(value) {
442
+ return typeof value === "boolean";
443
+ }
444
+ function isOptional(value, validate) {
445
+ return value === void 0 || validate(value);
446
+ }
447
+ function isEnum(value, values) {
448
+ return typeof value === "string" && values.includes(value);
449
+ }
450
+ function isOptionalEnum(value, values) {
451
+ return value === void 0 || isEnum(value, values);
452
+ }
453
+ function isPoint(value) {
454
+ return isRecord(value) && isFiniteNumber(value["x"]) && isFiniteNumber(value["y"]);
455
+ }
456
+ function isSize(value) {
457
+ return isRecord(value) && isFiniteNumber(value["w"]) && isFiniteNumber(value["h"]);
458
+ }
459
+ function isStrokePoint(value) {
460
+ return isRecord(value) && isPoint(value) && isFiniteNumber(value["pressure"]);
461
+ }
462
+ function isBinding(value) {
463
+ return isRecord(value) && typeof value["elementId"] === "string";
464
+ }
374
465
 
375
466
  // src/core/storage/local-storage-adapter.ts
376
467
  var LocalStorageAdapter = class {
@@ -6502,7 +6593,7 @@ var RenderLoop = class {
6502
6593
  layerGroups = /* @__PURE__ */ new Map();
6503
6594
  gridCacheCanvas = null;
6504
6595
  gridCacheCtx = null;
6505
- lastGridRefs = [];
6596
+ lastGridRef = null;
6506
6597
  constructor(deps) {
6507
6598
  this.canvasEl = deps.canvasEl;
6508
6599
  this.camera = deps.camera;
@@ -6702,8 +6793,8 @@ var RenderLoop = class {
6702
6793
  }
6703
6794
  if (gridElements.length > 0) {
6704
6795
  const gridT0 = performance.now();
6705
- const gridsChanged = gridElements.length !== this.lastGridRefs.length || gridElements.some((grid, index) => grid !== this.lastGridRefs[index]);
6706
- const gridDirty = this.gridCacheDirty || gridsChanged;
6796
+ const gridRef = gridElements[0];
6797
+ const gridDirty = this.gridCacheDirty || gridRef !== this.lastGridRef;
6707
6798
  if (gridDirty) {
6708
6799
  this.ensureGridCache();
6709
6800
  if (this.gridCacheCtx && this.gridCacheCanvas) {
@@ -6731,7 +6822,7 @@ var RenderLoop = class {
6731
6822
  }
6732
6823
  }
6733
6824
  this.gridCacheDirty = false;
6734
- this.lastGridRefs = [...gridElements];
6825
+ this.lastGridRef = gridRef;
6735
6826
  }
6736
6827
  if (this.gridCacheCanvas) {
6737
6828
  const offset = this.marginViewport.compositeOffset(
@@ -6751,8 +6842,6 @@ var RenderLoop = class {
6751
6842
  }
6752
6843
  }
6753
6844
  gridMs = performance.now() - gridT0;
6754
- } else {
6755
- this.lastGridRefs = [];
6756
6845
  }
6757
6846
  const overlayT0 = performance.now();
6758
6847
  const activeTool = this.toolManager.activeTool;
@@ -10726,7 +10815,7 @@ var LaserTool = class {
10726
10815
  };
10727
10816
 
10728
10817
  // src/index.ts
10729
- var VERSION = "0.50.7";
10818
+ var VERSION = "0.50.8";
10730
10819
  // Annotate the CommonJS export names for ESM import in node:
10731
10820
  0 && (module.exports = {
10732
10821
  ArrowTool,