@fieldnotes/core 0.12.0 → 0.14.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.cjs CHANGED
@@ -29,6 +29,7 @@ __export(index_exports, {
29
29
  CreateLayerCommand: () => CreateLayerCommand,
30
30
  DEFAULT_FONT_SIZE_PRESETS: () => DEFAULT_FONT_SIZE_PRESETS,
31
31
  DEFAULT_NOTE_FONT_SIZE: () => DEFAULT_NOTE_FONT_SIZE,
32
+ DoubleTapDetector: () => DoubleTapDetector,
32
33
  ElementRenderer: () => ElementRenderer,
33
34
  ElementStore: () => ElementStore,
34
35
  EraserTool: () => EraserTool,
@@ -83,6 +84,7 @@ __export(index_exports, {
83
84
  getEdgeIntersection: () => getEdgeIntersection,
84
85
  getElementBounds: () => getElementBounds,
85
86
  getElementCenter: () => getElementCenter,
87
+ getElementsBoundingBox: () => getElementsBoundingBox,
86
88
  getHexCellsInCone: () => getHexCellsInCone,
87
89
  getHexCellsInLine: () => getHexCellsInLine,
88
90
  getHexCellsInRadius: () => getHexCellsInRadius,
@@ -392,8 +394,8 @@ function sanitizeAttributes(el, tag) {
392
394
 
393
395
  // src/core/state-serializer.ts
394
396
  var CURRENT_VERSION = 2;
395
- function exportState(elements, camera, layers = []) {
396
- return {
397
+ function exportState(elements, camera, layers = [], activeLayerId) {
398
+ const state = {
397
399
  version: CURRENT_VERSION,
398
400
  camera: {
399
401
  position: { ...camera.position },
@@ -408,6 +410,8 @@ function exportState(elements, camera, layers = []) {
408
410
  }),
409
411
  layers: layers.map((l) => ({ ...l }))
410
412
  };
413
+ if (activeLayerId) state.activeLayerId = activeLayerId;
414
+ return state;
411
415
  }
412
416
  function parseState(json) {
413
417
  const data = JSON.parse(json);
@@ -565,12 +569,14 @@ var AutoSave = class {
565
569
  this.key = options.key ?? DEFAULT_KEY;
566
570
  this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
567
571
  this.layerManager = options.layerManager;
572
+ this.onError = options.onError;
568
573
  }
569
574
  key;
570
575
  debounceMs;
571
576
  layerManager;
572
577
  timerId = null;
573
578
  unsubscribers = [];
579
+ onError;
574
580
  start() {
575
581
  const schedule = () => this.scheduleSave();
576
582
  this.unsubscribers = [
@@ -618,8 +624,9 @@ var AutoSave = class {
618
624
  const state = exportState(this.store.snapshot(), this.camera, layers);
619
625
  try {
620
626
  localStorage.setItem(this.key, JSON.stringify(state));
621
- } catch {
627
+ } catch (e) {
622
628
  console.warn("Auto-save failed: storage quota exceeded. State too large for localStorage.");
629
+ this.onError?.(e instanceof Error ? e : new Error(String(e)));
623
630
  }
624
631
  }
625
632
  };
@@ -688,6 +695,15 @@ var Camera = class {
688
695
  h: bottomRight.y - topLeft.y
689
696
  };
690
697
  }
698
+ fitToContent(boundingBox, canvasWidth, canvasHeight, padding = 40) {
699
+ if (boundingBox.w === 0 && boundingBox.h === 0) return;
700
+ const scaleX = canvasWidth / (boundingBox.w + 2 * padding);
701
+ const scaleY = canvasHeight / (boundingBox.h + 2 * padding);
702
+ this.z = Math.min(this.maxZoom, Math.max(this.minZoom, Math.min(scaleX, scaleY)));
703
+ this.x = (canvasWidth - boundingBox.w * this.z) / 2 - boundingBox.x * this.z;
704
+ this.y = (canvasHeight - boundingBox.h * this.z) / 2 - boundingBox.y * this.z;
705
+ this.notifyPanAndZoom();
706
+ }
691
707
  toCSSTransform() {
692
708
  return `translate3d(${this.x}px, ${this.y}px, 0) scale(${this.z})`;
693
709
  }
@@ -1005,7 +1021,9 @@ var InputHandler = class {
1005
1021
  this.camera.pan(dx, dy);
1006
1022
  return;
1007
1023
  }
1008
- if (this.isToolActive) {
1024
+ if (e.pointerType === "pen" && !this.activePointers.has(e.pointerId)) {
1025
+ this.dispatchToolHover(e);
1026
+ } else if (this.isToolActive) {
1009
1027
  this.dispatchToolMove(e);
1010
1028
  } else if (this.deferredDown) {
1011
1029
  const result = this.inputFilter.filterMove(e);
@@ -1251,6 +1269,48 @@ var InputHandler = class {
1251
1269
  }
1252
1270
  };
1253
1271
 
1272
+ // src/canvas/double-tap-detector.ts
1273
+ var DEFAULT_TIMEOUT = 300;
1274
+ var DEFAULT_MAX_DISTANCE = 20;
1275
+ var DoubleTapDetector = class {
1276
+ timeout;
1277
+ maxDistance;
1278
+ lastTapTime = 0;
1279
+ lastTapX = 0;
1280
+ lastTapY = 0;
1281
+ hasPendingTap = false;
1282
+ constructor(options) {
1283
+ this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
1284
+ this.maxDistance = options?.maxDistance ?? DEFAULT_MAX_DISTANCE;
1285
+ }
1286
+ feed(e) {
1287
+ const now = Date.now();
1288
+ const x = e.clientX;
1289
+ const y = e.clientY;
1290
+ if (this.hasPendingTap) {
1291
+ const elapsed = now - this.lastTapTime;
1292
+ const dx = x - this.lastTapX;
1293
+ const dy = y - this.lastTapY;
1294
+ const dist = Math.sqrt(dx * dx + dy * dy);
1295
+ if (elapsed <= this.timeout && dist <= this.maxDistance) {
1296
+ this.reset();
1297
+ return true;
1298
+ }
1299
+ }
1300
+ this.lastTapTime = now;
1301
+ this.lastTapX = x;
1302
+ this.lastTapY = y;
1303
+ this.hasPendingTap = true;
1304
+ return false;
1305
+ }
1306
+ reset() {
1307
+ this.hasPendingTap = false;
1308
+ this.lastTapTime = 0;
1309
+ this.lastTapX = 0;
1310
+ this.lastTapY = 0;
1311
+ }
1312
+ };
1313
+
1254
1314
  // src/elements/arrow-geometry.ts
1255
1315
  function getArrowControlPoint(from, to, bend) {
1256
1316
  const midX = (from.x + to.x) / 2;
@@ -2700,6 +2760,7 @@ function createHtmlElement(input) {
2700
2760
  size: input.size
2701
2761
  };
2702
2762
  if (input.domId) el.domId = input.domId;
2763
+ if (input.interactive) el.interactive = input.interactive;
2703
2764
  return el;
2704
2765
  }
2705
2766
  function createShape(input) {
@@ -2812,7 +2873,7 @@ function getActiveFormats() {
2812
2873
  }
2813
2874
 
2814
2875
  // src/elements/note-toolbar.ts
2815
- var TOOLBAR_HEIGHT = 32;
2876
+ var TOOLBAR_HEIGHT = 52;
2816
2877
  var TOOLBAR_GAP = 4;
2817
2878
  var FORMAT_BUTTONS = [
2818
2879
  { label: "B", format: "bold", command: "bold" },
@@ -2899,9 +2960,9 @@ var NoteToolbar = class {
2899
2960
  fontWeight: config.format === "bold" ? "bold" : "normal",
2900
2961
  fontStyle: config.format === "italic" ? "italic" : "normal",
2901
2962
  textDecoration: config.format === "underline" ? "underline" : config.format === "strikethrough" ? "line-through" : "none",
2902
- minWidth: "24px",
2903
- height: "24px",
2904
- lineHeight: "24px"
2963
+ minWidth: "44px",
2964
+ height: "44px",
2965
+ lineHeight: "44px"
2905
2966
  });
2906
2967
  btn.addEventListener("pointerdown", (e) => {
2907
2968
  e.preventDefault();
@@ -2919,7 +2980,7 @@ var NoteToolbar = class {
2919
2980
  cursor: "pointer",
2920
2981
  padding: "2px",
2921
2982
  fontSize: "12px",
2922
- height: "24px",
2983
+ height: "44px",
2923
2984
  marginLeft: "4px"
2924
2985
  });
2925
2986
  for (const preset of this.fontSizePresets) {
@@ -3917,10 +3978,13 @@ var DomNodeManager = class {
3917
3978
  wordWrap: "break-word"
3918
3979
  });
3919
3980
  node.innerHTML = element.text || "";
3920
- node.addEventListener("dblclick", (e) => {
3921
- e.stopPropagation();
3922
- const id = node.dataset["elementId"];
3923
- if (id) this.onEditRequest(id);
3981
+ const detector = new DoubleTapDetector();
3982
+ node.addEventListener("pointerup", (e) => {
3983
+ if (detector.feed(e)) {
3984
+ e.stopPropagation();
3985
+ const id = node.dataset["elementId"];
3986
+ if (id) this.onEditRequest(id);
3987
+ }
3924
3988
  });
3925
3989
  }
3926
3990
  if (!this.isEditingElement(element.id)) {
@@ -3933,15 +3997,19 @@ var DomNodeManager = class {
3933
3997
  node.style.fontSize = `${element.fontSize ?? DEFAULT_NOTE_FONT_SIZE}px`;
3934
3998
  }
3935
3999
  }
3936
- if (element.type === "html" && !node.dataset["initialized"]) {
3937
- const content = this.htmlContent.get(element.id);
3938
- if (content) {
3939
- node.dataset["initialized"] = "true";
3940
- Object.assign(node.style, {
3941
- overflow: "hidden",
3942
- pointerEvents: "none"
3943
- });
3944
- node.appendChild(content);
4000
+ if (element.type === "html") {
4001
+ if (!node.dataset["initialized"]) {
4002
+ const content = this.htmlContent.get(element.id);
4003
+ if (content) {
4004
+ node.dataset["initialized"] = "true";
4005
+ Object.assign(node.style, {
4006
+ overflow: "hidden",
4007
+ pointerEvents: element.interactive ? "auto" : "none"
4008
+ });
4009
+ node.appendChild(content);
4010
+ }
4011
+ } else {
4012
+ node.style.pointerEvents = element.interactive ? "auto" : "none";
3945
4013
  }
3946
4014
  }
3947
4015
  if (element.type === "text") {
@@ -3963,10 +4031,13 @@ var DomNodeManager = class {
3963
4031
  lineHeight: "1.4"
3964
4032
  });
3965
4033
  node.textContent = element.text || "";
3966
- node.addEventListener("dblclick", (e) => {
3967
- e.stopPropagation();
3968
- const id = node.dataset["elementId"];
3969
- if (id) this.onEditRequest(id);
4034
+ const detector = new DoubleTapDetector();
4035
+ node.addEventListener("pointerup", (e) => {
4036
+ if (detector.feed(e)) {
4037
+ e.stopPropagation();
4038
+ const id = node.dataset["elementId"];
4039
+ if (id) this.onEditRequest(id);
4040
+ }
3970
4041
  });
3971
4042
  }
3972
4043
  if (!this.isEditingElement(element.id)) {
@@ -4455,7 +4526,8 @@ var Viewport = class {
4455
4526
  this.toolContext.activeLayerId = this.layerManager.activeLayerId;
4456
4527
  this.requestRender();
4457
4528
  });
4458
- this.wrapper.addEventListener("dblclick", this.onDblClick);
4529
+ this.wrapper.addEventListener("pointerdown", this.onTapDown);
4530
+ this.wrapper.addEventListener("pointerup", this.onDoubleTap);
4459
4531
  this.wrapper.addEventListener("dragover", this.onDragOver);
4460
4532
  this.wrapper.addEventListener("drop", this.onDrop);
4461
4533
  this.observeResize();
@@ -4486,6 +4558,9 @@ var Viewport = class {
4486
4558
  domNodeManager;
4487
4559
  interactMode;
4488
4560
  gridChangeListeners = /* @__PURE__ */ new Set();
4561
+ doubleTapDetector = new DoubleTapDetector();
4562
+ tapDownX = 0;
4563
+ tapDownY = 0;
4489
4564
  get ctx() {
4490
4565
  return this.canvasEl.getContext("2d");
4491
4566
  }
@@ -4500,7 +4575,12 @@ var Viewport = class {
4500
4575
  this.renderLoop.requestRender();
4501
4576
  }
4502
4577
  exportState() {
4503
- return exportState(this.store.snapshot(), this.camera, this.layerManager.snapshot());
4578
+ return exportState(
4579
+ this.store.snapshot(),
4580
+ this.camera,
4581
+ this.layerManager.snapshot(),
4582
+ this.layerManager.activeLayerId
4583
+ );
4504
4584
  }
4505
4585
  exportJSON() {
4506
4586
  return JSON.stringify(this.exportState());
@@ -4516,6 +4596,9 @@ var Viewport = class {
4516
4596
  if (state.layers && state.layers.length > 0) {
4517
4597
  this.layerManager.loadSnapshot(state.layers);
4518
4598
  }
4599
+ if (state.activeLayerId) {
4600
+ this.layerManager.setActiveLayer(state.activeLayerId);
4601
+ }
4519
4602
  this.domNodeManager.reattachHtmlContent(this.store);
4520
4603
  this.history.clear();
4521
4604
  this.historyRecorder.resume();
@@ -4623,7 +4706,8 @@ var Viewport = class {
4623
4706
  this.interactMode.destroy();
4624
4707
  this.noteEditor.destroy(this.store);
4625
4708
  this.historyRecorder.destroy();
4626
- this.wrapper.removeEventListener("dblclick", this.onDblClick);
4709
+ this.wrapper.removeEventListener("pointerdown", this.onTapDown);
4710
+ this.wrapper.removeEventListener("pointerup", this.onDoubleTap);
4627
4711
  this.wrapper.removeEventListener("dragover", this.onDragOver);
4628
4712
  this.wrapper.removeEventListener("drop", this.onDrop);
4629
4713
  this.inputHandler.destroy();
@@ -4661,7 +4745,17 @@ var Viewport = class {
4661
4745
  }
4662
4746
  }
4663
4747
  }
4664
- onDblClick = (e) => {
4748
+ onTapDown = (e) => {
4749
+ this.tapDownX = e.clientX;
4750
+ this.tapDownY = e.clientY;
4751
+ };
4752
+ onDoubleTap = (e) => {
4753
+ const dx = e.clientX - this.tapDownX;
4754
+ const dy = e.clientY - this.tapDownY;
4755
+ const moved = Math.sqrt(dx * dx + dy * dy);
4756
+ if (moved > 10) return;
4757
+ if (!this.doubleTapDetector.feed(e)) return;
4758
+ if (typeof document.elementFromPoint !== "function") return;
4665
4759
  const el = document.elementFromPoint(e.clientX, e.clientY);
4666
4760
  const nodeEl = el?.closest("[data-element-id]");
4667
4761
  if (nodeEl) {
@@ -4824,6 +4918,26 @@ var Viewport = class {
4824
4918
  }
4825
4919
  };
4826
4920
 
4921
+ // src/elements/bounds.ts
4922
+ function getElementsBoundingBox(elements) {
4923
+ let minX = Infinity;
4924
+ let minY = Infinity;
4925
+ let maxX = -Infinity;
4926
+ let maxY = -Infinity;
4927
+ let found = false;
4928
+ for (const el of elements) {
4929
+ const b = getElementBounds(el);
4930
+ if (!b) continue;
4931
+ found = true;
4932
+ if (b.x < minX) minX = b.x;
4933
+ if (b.y < minY) minY = b.y;
4934
+ if (b.x + b.w > maxX) maxX = b.x + b.w;
4935
+ if (b.y + b.h > maxY) maxY = b.y + b.h;
4936
+ }
4937
+ if (!found) return null;
4938
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
4939
+ }
4940
+
4827
4941
  // src/tools/hand-tool.ts
4828
4942
  var HandTool = class {
4829
4943
  name = "hand";
@@ -6533,7 +6647,7 @@ var UpdateLayerCommand = class {
6533
6647
  };
6534
6648
 
6535
6649
  // src/index.ts
6536
- var VERSION = "0.12.0";
6650
+ var VERSION = "0.14.0";
6537
6651
  // Annotate the CommonJS export names for ESM import in node:
6538
6652
  0 && (module.exports = {
6539
6653
  AddElementCommand,
@@ -6545,6 +6659,7 @@ var VERSION = "0.12.0";
6545
6659
  CreateLayerCommand,
6546
6660
  DEFAULT_FONT_SIZE_PRESETS,
6547
6661
  DEFAULT_NOTE_FONT_SIZE,
6662
+ DoubleTapDetector,
6548
6663
  ElementRenderer,
6549
6664
  ElementStore,
6550
6665
  EraserTool,
@@ -6599,6 +6714,7 @@ var VERSION = "0.12.0";
6599
6714
  getEdgeIntersection,
6600
6715
  getElementBounds,
6601
6716
  getElementCenter,
6717
+ getElementsBoundingBox,
6602
6718
  getHexCellsInCone,
6603
6719
  getHexCellsInLine,
6604
6720
  getHexCellsInRadius,
package/dist/index.d.cts CHANGED
@@ -85,6 +85,7 @@ interface HtmlElement extends BaseElement {
85
85
  type: 'html';
86
86
  size: Size;
87
87
  domId?: string;
88
+ interactive?: boolean;
88
89
  }
89
90
  interface TextElement extends BaseElement {
90
91
  type: 'text';
@@ -146,11 +147,12 @@ interface CanvasState {
146
147
  };
147
148
  elements: CanvasElement[];
148
149
  layers?: Layer[];
150
+ activeLayerId?: string;
149
151
  }
150
152
  declare function exportState(elements: CanvasElement[], camera: {
151
153
  position: Point;
152
154
  zoom: number;
153
- }, layers?: Layer[]): CanvasState;
155
+ }, layers?: Layer[], activeLayerId?: string): CanvasState;
154
156
  declare function parseState(json: string): CanvasState;
155
157
 
156
158
  interface CameraOptions {
@@ -178,6 +180,7 @@ declare class Camera {
178
180
  screenToWorld(screen: Point): Point;
179
181
  worldToScreen(world: Point): Point;
180
182
  getVisibleRect(canvasWidth: number, canvasHeight: number): Bounds;
183
+ fitToContent(boundingBox: Bounds, canvasWidth: number, canvasHeight: number, padding?: number): void;
181
184
  toCSSTransform(): string;
182
185
  onChange(listener: (info: CameraChangeInfo) => void): () => void;
183
186
  private notifyPan;
@@ -296,6 +299,7 @@ interface AutoSaveOptions {
296
299
  key?: string;
297
300
  debounceMs?: number;
298
301
  layerManager?: LayerManager;
302
+ onError?: (error: Error) => void;
299
303
  }
300
304
  declare class AutoSave {
301
305
  private readonly store;
@@ -305,6 +309,7 @@ declare class AutoSave {
305
309
  private readonly layerManager?;
306
310
  private timerId;
307
311
  private unsubscribers;
312
+ private readonly onError?;
308
313
  constructor(store: ElementStore, camera: Camera, options?: AutoSaveOptions);
309
314
  start(): void;
310
315
  stop(): void;
@@ -481,6 +486,22 @@ declare class InputFilter {
481
486
  reset(): void;
482
487
  }
483
488
 
489
+ interface DoubleTapDetectorOptions {
490
+ timeout?: number;
491
+ maxDistance?: number;
492
+ }
493
+ declare class DoubleTapDetector {
494
+ private readonly timeout;
495
+ private readonly maxDistance;
496
+ private lastTapTime;
497
+ private lastTapX;
498
+ private lastTapY;
499
+ private hasPendingTap;
500
+ constructor(options?: DoubleTapDetectorOptions);
501
+ feed(e: PointerEvent): boolean;
502
+ reset(): void;
503
+ }
504
+
484
505
  interface FontSizePreset {
485
506
  label: string;
486
507
  size: number;
@@ -557,6 +578,9 @@ declare class Viewport {
557
578
  private readonly domNodeManager;
558
579
  private readonly interactMode;
559
580
  private readonly gridChangeListeners;
581
+ private readonly doubleTapDetector;
582
+ private tapDownX;
583
+ private tapDownY;
560
584
  constructor(container: HTMLElement, options?: ViewportOptions);
561
585
  get ctx(): CanvasRenderingContext2D | null;
562
586
  get snapToGrid(): boolean;
@@ -600,7 +624,8 @@ declare class Viewport {
600
624
  destroy(): void;
601
625
  private startEditingElement;
602
626
  private onTextEditStop;
603
- private onDblClick;
627
+ private onTapDown;
628
+ private onDoubleTap;
604
629
  private hitTestWorld;
605
630
  stopInteracting(): void;
606
631
  private onDragOver;
@@ -737,6 +762,7 @@ interface HtmlInput extends BaseDefaults {
737
762
  position: Point;
738
763
  size: Size;
739
764
  domId?: string;
765
+ interactive?: boolean;
740
766
  }
741
767
  interface TextInput extends BaseDefaults {
742
768
  position: Point;
@@ -803,6 +829,8 @@ declare function unbindArrow(arrow: ArrowElement, store: ElementStore): Partial<
803
829
  declare function getElementBounds(element: CanvasElement): Bounds | null;
804
830
  declare function boundsIntersect(a: Bounds, b: Bounds): boolean;
805
831
 
832
+ declare function getElementsBoundingBox(elements: CanvasElement[]): Bounds | null;
833
+
806
834
  declare function getHexDistance(a: Point, b: Point, cellSize: number, orientation: HexOrientation): number;
807
835
  declare function getHexCellsInRadius(center: Point, radiusCells: number, cellSize: number, orientation: HexOrientation): Point[];
808
836
  declare function getHexCellsInCone(center: Point, angle: number, radiusCells: number, cellSize: number, orientation: HexOrientation): Point[];
@@ -1153,6 +1181,6 @@ declare class UpdateLayerCommand implements Command {
1153
1181
  undo(_store: ElementStore): void;
1154
1182
  }
1155
1183
 
1156
- declare const VERSION = "0.12.0";
1184
+ declare const VERSION = "0.14.0";
1157
1185
 
1158
- export { type ActiveFormats, AddElementCommand, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, Background, type BackgroundOptions, type BackgroundPattern, BatchCommand, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, CreateLayerCommand, DEFAULT_FONT_SIZE_PRESETS, DEFAULT_NOTE_FONT_SIZE, ElementRenderer, ElementStore, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, EventBus, type ExportImageOptions, type FilterAction, type FilteredEvent, type FilteredUpEvent, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryRecorder, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, InputFilter, InputHandler, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, NoteEditor, type NoteEditorOptions, type NoteElement, NoteTool, type NoteToolOptions, NoteToolbar, PencilTool, type PencilToolOptions, type Point, type PointerState, Quadtree, RemoveElementCommand, RemoveLayerCommand, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type Size, type StrokeElement, type StrokePoint, type StyledRun, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, UpdateElementCommand, UpdateLayerCommand, VERSION, Viewport, type ViewportOptions, boundsIntersect, clearStaleBindings, createArrow, createGrid, createHtmlElement, createId, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportState, findBindTarget, findBoundArrows, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getEdgeIntersection, getElementBounds, getElementCenter, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isBindable, isNearBezier, parseState, sanitizeNoteHtml, setFontSize, smartSnap, snapPoint, snapToHexCenter, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline, unbindArrow, updateBoundArrow };
1186
+ export { type ActiveFormats, AddElementCommand, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, Background, type BackgroundOptions, type BackgroundPattern, BatchCommand, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, CreateLayerCommand, DEFAULT_FONT_SIZE_PRESETS, DEFAULT_NOTE_FONT_SIZE, DoubleTapDetector, type DoubleTapDetectorOptions, ElementRenderer, ElementStore, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, EventBus, type ExportImageOptions, type FilterAction, type FilteredEvent, type FilteredUpEvent, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryRecorder, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, InputFilter, InputHandler, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, NoteEditor, type NoteEditorOptions, type NoteElement, NoteTool, type NoteToolOptions, NoteToolbar, PencilTool, type PencilToolOptions, type Point, type PointerState, Quadtree, RemoveElementCommand, RemoveLayerCommand, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type Size, type StrokeElement, type StrokePoint, type StyledRun, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, UpdateElementCommand, UpdateLayerCommand, VERSION, Viewport, type ViewportOptions, boundsIntersect, clearStaleBindings, createArrow, createGrid, createHtmlElement, createId, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportState, findBindTarget, findBoundArrows, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getEdgeIntersection, getElementBounds, getElementCenter, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isBindable, isNearBezier, parseState, sanitizeNoteHtml, setFontSize, smartSnap, snapPoint, snapToHexCenter, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline, unbindArrow, updateBoundArrow };
package/dist/index.d.ts CHANGED
@@ -85,6 +85,7 @@ interface HtmlElement extends BaseElement {
85
85
  type: 'html';
86
86
  size: Size;
87
87
  domId?: string;
88
+ interactive?: boolean;
88
89
  }
89
90
  interface TextElement extends BaseElement {
90
91
  type: 'text';
@@ -146,11 +147,12 @@ interface CanvasState {
146
147
  };
147
148
  elements: CanvasElement[];
148
149
  layers?: Layer[];
150
+ activeLayerId?: string;
149
151
  }
150
152
  declare function exportState(elements: CanvasElement[], camera: {
151
153
  position: Point;
152
154
  zoom: number;
153
- }, layers?: Layer[]): CanvasState;
155
+ }, layers?: Layer[], activeLayerId?: string): CanvasState;
154
156
  declare function parseState(json: string): CanvasState;
155
157
 
156
158
  interface CameraOptions {
@@ -178,6 +180,7 @@ declare class Camera {
178
180
  screenToWorld(screen: Point): Point;
179
181
  worldToScreen(world: Point): Point;
180
182
  getVisibleRect(canvasWidth: number, canvasHeight: number): Bounds;
183
+ fitToContent(boundingBox: Bounds, canvasWidth: number, canvasHeight: number, padding?: number): void;
181
184
  toCSSTransform(): string;
182
185
  onChange(listener: (info: CameraChangeInfo) => void): () => void;
183
186
  private notifyPan;
@@ -296,6 +299,7 @@ interface AutoSaveOptions {
296
299
  key?: string;
297
300
  debounceMs?: number;
298
301
  layerManager?: LayerManager;
302
+ onError?: (error: Error) => void;
299
303
  }
300
304
  declare class AutoSave {
301
305
  private readonly store;
@@ -305,6 +309,7 @@ declare class AutoSave {
305
309
  private readonly layerManager?;
306
310
  private timerId;
307
311
  private unsubscribers;
312
+ private readonly onError?;
308
313
  constructor(store: ElementStore, camera: Camera, options?: AutoSaveOptions);
309
314
  start(): void;
310
315
  stop(): void;
@@ -481,6 +486,22 @@ declare class InputFilter {
481
486
  reset(): void;
482
487
  }
483
488
 
489
+ interface DoubleTapDetectorOptions {
490
+ timeout?: number;
491
+ maxDistance?: number;
492
+ }
493
+ declare class DoubleTapDetector {
494
+ private readonly timeout;
495
+ private readonly maxDistance;
496
+ private lastTapTime;
497
+ private lastTapX;
498
+ private lastTapY;
499
+ private hasPendingTap;
500
+ constructor(options?: DoubleTapDetectorOptions);
501
+ feed(e: PointerEvent): boolean;
502
+ reset(): void;
503
+ }
504
+
484
505
  interface FontSizePreset {
485
506
  label: string;
486
507
  size: number;
@@ -557,6 +578,9 @@ declare class Viewport {
557
578
  private readonly domNodeManager;
558
579
  private readonly interactMode;
559
580
  private readonly gridChangeListeners;
581
+ private readonly doubleTapDetector;
582
+ private tapDownX;
583
+ private tapDownY;
560
584
  constructor(container: HTMLElement, options?: ViewportOptions);
561
585
  get ctx(): CanvasRenderingContext2D | null;
562
586
  get snapToGrid(): boolean;
@@ -600,7 +624,8 @@ declare class Viewport {
600
624
  destroy(): void;
601
625
  private startEditingElement;
602
626
  private onTextEditStop;
603
- private onDblClick;
627
+ private onTapDown;
628
+ private onDoubleTap;
604
629
  private hitTestWorld;
605
630
  stopInteracting(): void;
606
631
  private onDragOver;
@@ -737,6 +762,7 @@ interface HtmlInput extends BaseDefaults {
737
762
  position: Point;
738
763
  size: Size;
739
764
  domId?: string;
765
+ interactive?: boolean;
740
766
  }
741
767
  interface TextInput extends BaseDefaults {
742
768
  position: Point;
@@ -803,6 +829,8 @@ declare function unbindArrow(arrow: ArrowElement, store: ElementStore): Partial<
803
829
  declare function getElementBounds(element: CanvasElement): Bounds | null;
804
830
  declare function boundsIntersect(a: Bounds, b: Bounds): boolean;
805
831
 
832
+ declare function getElementsBoundingBox(elements: CanvasElement[]): Bounds | null;
833
+
806
834
  declare function getHexDistance(a: Point, b: Point, cellSize: number, orientation: HexOrientation): number;
807
835
  declare function getHexCellsInRadius(center: Point, radiusCells: number, cellSize: number, orientation: HexOrientation): Point[];
808
836
  declare function getHexCellsInCone(center: Point, angle: number, radiusCells: number, cellSize: number, orientation: HexOrientation): Point[];
@@ -1153,6 +1181,6 @@ declare class UpdateLayerCommand implements Command {
1153
1181
  undo(_store: ElementStore): void;
1154
1182
  }
1155
1183
 
1156
- declare const VERSION = "0.12.0";
1184
+ declare const VERSION = "0.14.0";
1157
1185
 
1158
- export { type ActiveFormats, AddElementCommand, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, Background, type BackgroundOptions, type BackgroundPattern, BatchCommand, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, CreateLayerCommand, DEFAULT_FONT_SIZE_PRESETS, DEFAULT_NOTE_FONT_SIZE, ElementRenderer, ElementStore, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, EventBus, type ExportImageOptions, type FilterAction, type FilteredEvent, type FilteredUpEvent, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryRecorder, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, InputFilter, InputHandler, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, NoteEditor, type NoteEditorOptions, type NoteElement, NoteTool, type NoteToolOptions, NoteToolbar, PencilTool, type PencilToolOptions, type Point, type PointerState, Quadtree, RemoveElementCommand, RemoveLayerCommand, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type Size, type StrokeElement, type StrokePoint, type StyledRun, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, UpdateElementCommand, UpdateLayerCommand, VERSION, Viewport, type ViewportOptions, boundsIntersect, clearStaleBindings, createArrow, createGrid, createHtmlElement, createId, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportState, findBindTarget, findBoundArrows, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getEdgeIntersection, getElementBounds, getElementCenter, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isBindable, isNearBezier, parseState, sanitizeNoteHtml, setFontSize, smartSnap, snapPoint, snapToHexCenter, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline, unbindArrow, updateBoundArrow };
1186
+ export { type ActiveFormats, AddElementCommand, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, Background, type BackgroundOptions, type BackgroundPattern, BatchCommand, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, CreateLayerCommand, DEFAULT_FONT_SIZE_PRESETS, DEFAULT_NOTE_FONT_SIZE, DoubleTapDetector, type DoubleTapDetectorOptions, ElementRenderer, ElementStore, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, EventBus, type ExportImageOptions, type FilterAction, type FilteredEvent, type FilteredUpEvent, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryRecorder, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, InputFilter, InputHandler, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, NoteEditor, type NoteEditorOptions, type NoteElement, NoteTool, type NoteToolOptions, NoteToolbar, PencilTool, type PencilToolOptions, type Point, type PointerState, Quadtree, RemoveElementCommand, RemoveLayerCommand, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type Size, type StrokeElement, type StrokePoint, type StyledRun, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, UpdateElementCommand, UpdateLayerCommand, VERSION, Viewport, type ViewportOptions, boundsIntersect, clearStaleBindings, createArrow, createGrid, createHtmlElement, createId, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportState, findBindTarget, findBoundArrows, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getEdgeIntersection, getElementBounds, getElementCenter, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isBindable, isNearBezier, parseState, sanitizeNoteHtml, setFontSize, smartSnap, snapPoint, snapToHexCenter, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline, unbindArrow, updateBoundArrow };
package/dist/index.js CHANGED
@@ -285,8 +285,8 @@ function sanitizeAttributes(el, tag) {
285
285
 
286
286
  // src/core/state-serializer.ts
287
287
  var CURRENT_VERSION = 2;
288
- function exportState(elements, camera, layers = []) {
289
- return {
288
+ function exportState(elements, camera, layers = [], activeLayerId) {
289
+ const state = {
290
290
  version: CURRENT_VERSION,
291
291
  camera: {
292
292
  position: { ...camera.position },
@@ -301,6 +301,8 @@ function exportState(elements, camera, layers = []) {
301
301
  }),
302
302
  layers: layers.map((l) => ({ ...l }))
303
303
  };
304
+ if (activeLayerId) state.activeLayerId = activeLayerId;
305
+ return state;
304
306
  }
305
307
  function parseState(json) {
306
308
  const data = JSON.parse(json);
@@ -458,12 +460,14 @@ var AutoSave = class {
458
460
  this.key = options.key ?? DEFAULT_KEY;
459
461
  this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
460
462
  this.layerManager = options.layerManager;
463
+ this.onError = options.onError;
461
464
  }
462
465
  key;
463
466
  debounceMs;
464
467
  layerManager;
465
468
  timerId = null;
466
469
  unsubscribers = [];
470
+ onError;
467
471
  start() {
468
472
  const schedule = () => this.scheduleSave();
469
473
  this.unsubscribers = [
@@ -511,8 +515,9 @@ var AutoSave = class {
511
515
  const state = exportState(this.store.snapshot(), this.camera, layers);
512
516
  try {
513
517
  localStorage.setItem(this.key, JSON.stringify(state));
514
- } catch {
518
+ } catch (e) {
515
519
  console.warn("Auto-save failed: storage quota exceeded. State too large for localStorage.");
520
+ this.onError?.(e instanceof Error ? e : new Error(String(e)));
516
521
  }
517
522
  }
518
523
  };
@@ -581,6 +586,15 @@ var Camera = class {
581
586
  h: bottomRight.y - topLeft.y
582
587
  };
583
588
  }
589
+ fitToContent(boundingBox, canvasWidth, canvasHeight, padding = 40) {
590
+ if (boundingBox.w === 0 && boundingBox.h === 0) return;
591
+ const scaleX = canvasWidth / (boundingBox.w + 2 * padding);
592
+ const scaleY = canvasHeight / (boundingBox.h + 2 * padding);
593
+ this.z = Math.min(this.maxZoom, Math.max(this.minZoom, Math.min(scaleX, scaleY)));
594
+ this.x = (canvasWidth - boundingBox.w * this.z) / 2 - boundingBox.x * this.z;
595
+ this.y = (canvasHeight - boundingBox.h * this.z) / 2 - boundingBox.y * this.z;
596
+ this.notifyPanAndZoom();
597
+ }
584
598
  toCSSTransform() {
585
599
  return `translate3d(${this.x}px, ${this.y}px, 0) scale(${this.z})`;
586
600
  }
@@ -898,7 +912,9 @@ var InputHandler = class {
898
912
  this.camera.pan(dx, dy);
899
913
  return;
900
914
  }
901
- if (this.isToolActive) {
915
+ if (e.pointerType === "pen" && !this.activePointers.has(e.pointerId)) {
916
+ this.dispatchToolHover(e);
917
+ } else if (this.isToolActive) {
902
918
  this.dispatchToolMove(e);
903
919
  } else if (this.deferredDown) {
904
920
  const result = this.inputFilter.filterMove(e);
@@ -1144,6 +1160,48 @@ var InputHandler = class {
1144
1160
  }
1145
1161
  };
1146
1162
 
1163
+ // src/canvas/double-tap-detector.ts
1164
+ var DEFAULT_TIMEOUT = 300;
1165
+ var DEFAULT_MAX_DISTANCE = 20;
1166
+ var DoubleTapDetector = class {
1167
+ timeout;
1168
+ maxDistance;
1169
+ lastTapTime = 0;
1170
+ lastTapX = 0;
1171
+ lastTapY = 0;
1172
+ hasPendingTap = false;
1173
+ constructor(options) {
1174
+ this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
1175
+ this.maxDistance = options?.maxDistance ?? DEFAULT_MAX_DISTANCE;
1176
+ }
1177
+ feed(e) {
1178
+ const now = Date.now();
1179
+ const x = e.clientX;
1180
+ const y = e.clientY;
1181
+ if (this.hasPendingTap) {
1182
+ const elapsed = now - this.lastTapTime;
1183
+ const dx = x - this.lastTapX;
1184
+ const dy = y - this.lastTapY;
1185
+ const dist = Math.sqrt(dx * dx + dy * dy);
1186
+ if (elapsed <= this.timeout && dist <= this.maxDistance) {
1187
+ this.reset();
1188
+ return true;
1189
+ }
1190
+ }
1191
+ this.lastTapTime = now;
1192
+ this.lastTapX = x;
1193
+ this.lastTapY = y;
1194
+ this.hasPendingTap = true;
1195
+ return false;
1196
+ }
1197
+ reset() {
1198
+ this.hasPendingTap = false;
1199
+ this.lastTapTime = 0;
1200
+ this.lastTapX = 0;
1201
+ this.lastTapY = 0;
1202
+ }
1203
+ };
1204
+
1147
1205
  // src/elements/arrow-geometry.ts
1148
1206
  function getArrowControlPoint(from, to, bend) {
1149
1207
  const midX = (from.x + to.x) / 2;
@@ -2593,6 +2651,7 @@ function createHtmlElement(input) {
2593
2651
  size: input.size
2594
2652
  };
2595
2653
  if (input.domId) el.domId = input.domId;
2654
+ if (input.interactive) el.interactive = input.interactive;
2596
2655
  return el;
2597
2656
  }
2598
2657
  function createShape(input) {
@@ -2705,7 +2764,7 @@ function getActiveFormats() {
2705
2764
  }
2706
2765
 
2707
2766
  // src/elements/note-toolbar.ts
2708
- var TOOLBAR_HEIGHT = 32;
2767
+ var TOOLBAR_HEIGHT = 52;
2709
2768
  var TOOLBAR_GAP = 4;
2710
2769
  var FORMAT_BUTTONS = [
2711
2770
  { label: "B", format: "bold", command: "bold" },
@@ -2792,9 +2851,9 @@ var NoteToolbar = class {
2792
2851
  fontWeight: config.format === "bold" ? "bold" : "normal",
2793
2852
  fontStyle: config.format === "italic" ? "italic" : "normal",
2794
2853
  textDecoration: config.format === "underline" ? "underline" : config.format === "strikethrough" ? "line-through" : "none",
2795
- minWidth: "24px",
2796
- height: "24px",
2797
- lineHeight: "24px"
2854
+ minWidth: "44px",
2855
+ height: "44px",
2856
+ lineHeight: "44px"
2798
2857
  });
2799
2858
  btn.addEventListener("pointerdown", (e) => {
2800
2859
  e.preventDefault();
@@ -2812,7 +2871,7 @@ var NoteToolbar = class {
2812
2871
  cursor: "pointer",
2813
2872
  padding: "2px",
2814
2873
  fontSize: "12px",
2815
- height: "24px",
2874
+ height: "44px",
2816
2875
  marginLeft: "4px"
2817
2876
  });
2818
2877
  for (const preset of this.fontSizePresets) {
@@ -3810,10 +3869,13 @@ var DomNodeManager = class {
3810
3869
  wordWrap: "break-word"
3811
3870
  });
3812
3871
  node.innerHTML = element.text || "";
3813
- node.addEventListener("dblclick", (e) => {
3814
- e.stopPropagation();
3815
- const id = node.dataset["elementId"];
3816
- if (id) this.onEditRequest(id);
3872
+ const detector = new DoubleTapDetector();
3873
+ node.addEventListener("pointerup", (e) => {
3874
+ if (detector.feed(e)) {
3875
+ e.stopPropagation();
3876
+ const id = node.dataset["elementId"];
3877
+ if (id) this.onEditRequest(id);
3878
+ }
3817
3879
  });
3818
3880
  }
3819
3881
  if (!this.isEditingElement(element.id)) {
@@ -3826,15 +3888,19 @@ var DomNodeManager = class {
3826
3888
  node.style.fontSize = `${element.fontSize ?? DEFAULT_NOTE_FONT_SIZE}px`;
3827
3889
  }
3828
3890
  }
3829
- if (element.type === "html" && !node.dataset["initialized"]) {
3830
- const content = this.htmlContent.get(element.id);
3831
- if (content) {
3832
- node.dataset["initialized"] = "true";
3833
- Object.assign(node.style, {
3834
- overflow: "hidden",
3835
- pointerEvents: "none"
3836
- });
3837
- node.appendChild(content);
3891
+ if (element.type === "html") {
3892
+ if (!node.dataset["initialized"]) {
3893
+ const content = this.htmlContent.get(element.id);
3894
+ if (content) {
3895
+ node.dataset["initialized"] = "true";
3896
+ Object.assign(node.style, {
3897
+ overflow: "hidden",
3898
+ pointerEvents: element.interactive ? "auto" : "none"
3899
+ });
3900
+ node.appendChild(content);
3901
+ }
3902
+ } else {
3903
+ node.style.pointerEvents = element.interactive ? "auto" : "none";
3838
3904
  }
3839
3905
  }
3840
3906
  if (element.type === "text") {
@@ -3856,10 +3922,13 @@ var DomNodeManager = class {
3856
3922
  lineHeight: "1.4"
3857
3923
  });
3858
3924
  node.textContent = element.text || "";
3859
- node.addEventListener("dblclick", (e) => {
3860
- e.stopPropagation();
3861
- const id = node.dataset["elementId"];
3862
- if (id) this.onEditRequest(id);
3925
+ const detector = new DoubleTapDetector();
3926
+ node.addEventListener("pointerup", (e) => {
3927
+ if (detector.feed(e)) {
3928
+ e.stopPropagation();
3929
+ const id = node.dataset["elementId"];
3930
+ if (id) this.onEditRequest(id);
3931
+ }
3863
3932
  });
3864
3933
  }
3865
3934
  if (!this.isEditingElement(element.id)) {
@@ -4348,7 +4417,8 @@ var Viewport = class {
4348
4417
  this.toolContext.activeLayerId = this.layerManager.activeLayerId;
4349
4418
  this.requestRender();
4350
4419
  });
4351
- this.wrapper.addEventListener("dblclick", this.onDblClick);
4420
+ this.wrapper.addEventListener("pointerdown", this.onTapDown);
4421
+ this.wrapper.addEventListener("pointerup", this.onDoubleTap);
4352
4422
  this.wrapper.addEventListener("dragover", this.onDragOver);
4353
4423
  this.wrapper.addEventListener("drop", this.onDrop);
4354
4424
  this.observeResize();
@@ -4379,6 +4449,9 @@ var Viewport = class {
4379
4449
  domNodeManager;
4380
4450
  interactMode;
4381
4451
  gridChangeListeners = /* @__PURE__ */ new Set();
4452
+ doubleTapDetector = new DoubleTapDetector();
4453
+ tapDownX = 0;
4454
+ tapDownY = 0;
4382
4455
  get ctx() {
4383
4456
  return this.canvasEl.getContext("2d");
4384
4457
  }
@@ -4393,7 +4466,12 @@ var Viewport = class {
4393
4466
  this.renderLoop.requestRender();
4394
4467
  }
4395
4468
  exportState() {
4396
- return exportState(this.store.snapshot(), this.camera, this.layerManager.snapshot());
4469
+ return exportState(
4470
+ this.store.snapshot(),
4471
+ this.camera,
4472
+ this.layerManager.snapshot(),
4473
+ this.layerManager.activeLayerId
4474
+ );
4397
4475
  }
4398
4476
  exportJSON() {
4399
4477
  return JSON.stringify(this.exportState());
@@ -4409,6 +4487,9 @@ var Viewport = class {
4409
4487
  if (state.layers && state.layers.length > 0) {
4410
4488
  this.layerManager.loadSnapshot(state.layers);
4411
4489
  }
4490
+ if (state.activeLayerId) {
4491
+ this.layerManager.setActiveLayer(state.activeLayerId);
4492
+ }
4412
4493
  this.domNodeManager.reattachHtmlContent(this.store);
4413
4494
  this.history.clear();
4414
4495
  this.historyRecorder.resume();
@@ -4516,7 +4597,8 @@ var Viewport = class {
4516
4597
  this.interactMode.destroy();
4517
4598
  this.noteEditor.destroy(this.store);
4518
4599
  this.historyRecorder.destroy();
4519
- this.wrapper.removeEventListener("dblclick", this.onDblClick);
4600
+ this.wrapper.removeEventListener("pointerdown", this.onTapDown);
4601
+ this.wrapper.removeEventListener("pointerup", this.onDoubleTap);
4520
4602
  this.wrapper.removeEventListener("dragover", this.onDragOver);
4521
4603
  this.wrapper.removeEventListener("drop", this.onDrop);
4522
4604
  this.inputHandler.destroy();
@@ -4554,7 +4636,17 @@ var Viewport = class {
4554
4636
  }
4555
4637
  }
4556
4638
  }
4557
- onDblClick = (e) => {
4639
+ onTapDown = (e) => {
4640
+ this.tapDownX = e.clientX;
4641
+ this.tapDownY = e.clientY;
4642
+ };
4643
+ onDoubleTap = (e) => {
4644
+ const dx = e.clientX - this.tapDownX;
4645
+ const dy = e.clientY - this.tapDownY;
4646
+ const moved = Math.sqrt(dx * dx + dy * dy);
4647
+ if (moved > 10) return;
4648
+ if (!this.doubleTapDetector.feed(e)) return;
4649
+ if (typeof document.elementFromPoint !== "function") return;
4558
4650
  const el = document.elementFromPoint(e.clientX, e.clientY);
4559
4651
  const nodeEl = el?.closest("[data-element-id]");
4560
4652
  if (nodeEl) {
@@ -4717,6 +4809,26 @@ var Viewport = class {
4717
4809
  }
4718
4810
  };
4719
4811
 
4812
+ // src/elements/bounds.ts
4813
+ function getElementsBoundingBox(elements) {
4814
+ let minX = Infinity;
4815
+ let minY = Infinity;
4816
+ let maxX = -Infinity;
4817
+ let maxY = -Infinity;
4818
+ let found = false;
4819
+ for (const el of elements) {
4820
+ const b = getElementBounds(el);
4821
+ if (!b) continue;
4822
+ found = true;
4823
+ if (b.x < minX) minX = b.x;
4824
+ if (b.y < minY) minY = b.y;
4825
+ if (b.x + b.w > maxX) maxX = b.x + b.w;
4826
+ if (b.y + b.h > maxY) maxY = b.y + b.h;
4827
+ }
4828
+ if (!found) return null;
4829
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
4830
+ }
4831
+
4720
4832
  // src/tools/hand-tool.ts
4721
4833
  var HandTool = class {
4722
4834
  name = "hand";
@@ -6426,7 +6538,7 @@ var UpdateLayerCommand = class {
6426
6538
  };
6427
6539
 
6428
6540
  // src/index.ts
6429
- var VERSION = "0.12.0";
6541
+ var VERSION = "0.14.0";
6430
6542
  export {
6431
6543
  AddElementCommand,
6432
6544
  ArrowTool,
@@ -6437,6 +6549,7 @@ export {
6437
6549
  CreateLayerCommand,
6438
6550
  DEFAULT_FONT_SIZE_PRESETS,
6439
6551
  DEFAULT_NOTE_FONT_SIZE,
6552
+ DoubleTapDetector,
6440
6553
  ElementRenderer,
6441
6554
  ElementStore,
6442
6555
  EraserTool,
@@ -6491,6 +6604,7 @@ export {
6491
6604
  getEdgeIntersection,
6492
6605
  getElementBounds,
6493
6606
  getElementCenter,
6607
+ getElementsBoundingBox,
6494
6608
  getHexCellsInCone,
6495
6609
  getHexCellsInLine,
6496
6610
  getHexCellsInRadius,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fieldnotes/core",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Vanilla TypeScript infinite canvas engine",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",