@fieldnotes/core 0.38.1 → 0.38.3

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
@@ -4055,17 +4055,43 @@ var ContextMenu = class {
4055
4055
  }
4056
4056
  };
4057
4057
 
4058
- // src/elements/translate.ts
4059
- function translateElementPatch(el, dx, dy) {
4060
- const position = { x: el.position.x + dx, y: el.position.y + dy };
4061
- if (el.type === "arrow") {
4062
- return {
4063
- position,
4064
- from: { x: el.from.x + dx, y: el.from.y + dy },
4065
- to: { x: el.to.x + dx, y: el.to.y + dy }
4066
- };
4067
- }
4068
- return { position };
4058
+ // src/canvas/viewport-dom.ts
4059
+ function createWrapper() {
4060
+ const el = document.createElement("div");
4061
+ Object.assign(el.style, {
4062
+ position: "relative",
4063
+ width: "100%",
4064
+ height: "100%",
4065
+ overflow: "hidden",
4066
+ overscrollBehavior: "none",
4067
+ userSelect: "none",
4068
+ webkitUserSelect: "none"
4069
+ });
4070
+ return el;
4071
+ }
4072
+ function createCanvas() {
4073
+ const el = document.createElement("canvas");
4074
+ Object.assign(el.style, {
4075
+ position: "absolute",
4076
+ top: "0",
4077
+ left: "0",
4078
+ width: "100%",
4079
+ height: "100%"
4080
+ });
4081
+ return el;
4082
+ }
4083
+ function createDomLayer() {
4084
+ const el = document.createElement("div");
4085
+ Object.assign(el.style, {
4086
+ position: "absolute",
4087
+ top: "0",
4088
+ left: "0",
4089
+ width: "100%",
4090
+ height: "100%",
4091
+ pointerEvents: "none",
4092
+ transformOrigin: "0 0"
4093
+ });
4094
+ return el;
4069
4095
  }
4070
4096
 
4071
4097
  // src/elements/arrow-label-editor.ts
@@ -5689,6 +5715,19 @@ var MarginViewport = class {
5689
5715
  }
5690
5716
  };
5691
5717
 
5718
+ // src/elements/translate.ts
5719
+ function translateElementPatch(el, dx, dy) {
5720
+ const position = { x: el.position.x + dx, y: el.position.y + dy };
5721
+ if (el.type === "arrow") {
5722
+ return {
5723
+ position,
5724
+ from: { x: el.from.x + dx, y: el.from.y + dy },
5725
+ to: { x: el.to.x + dx, y: el.to.y + dy }
5726
+ };
5727
+ }
5728
+ return { position };
5729
+ }
5730
+
5692
5731
  // src/elements/element-style.ts
5693
5732
  function styleToPatch(element, style) {
5694
5733
  const { color, fillColor, strokeWidth, opacity, fontSize } = style;
@@ -5776,7 +5815,7 @@ function getElementStyle(element) {
5776
5815
  }
5777
5816
  }
5778
5817
 
5779
- // src/canvas/viewport.ts
5818
+ // src/canvas/selection-ops.ts
5780
5819
  function unionBounds(list) {
5781
5820
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
5782
5821
  for (const b of list) {
@@ -5787,221 +5826,627 @@ function unionBounds(list) {
5787
5826
  }
5788
5827
  return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
5789
5828
  }
5790
- var EMPTY_IDS = [];
5791
- var ARROW_HIT_THRESHOLD = 10;
5792
- function noop() {
5793
- }
5794
5829
  function sharedValue(values) {
5795
5830
  const present = values.filter((v) => v !== void 0);
5796
5831
  if (present.length === 0) return void 0;
5797
5832
  const first = present[0];
5798
5833
  return present.every((v) => v === first) ? first : void 0;
5799
5834
  }
5800
- var Viewport = class {
5801
- constructor(container, options = {}) {
5802
- this.container = container;
5803
- this.camera = new Camera(options.camera);
5804
- this.background = new Background(options.background);
5805
- this._gridSize = options.background?.spacing ?? 24;
5806
- this.store = new ElementStore();
5807
- this.layerManager = new LayerManager(this.store);
5808
- this.toolManager = new ToolManager();
5809
- this.renderer = new ElementRenderer();
5810
- this.renderer.setStore(this.store);
5811
- this.renderer.setCamera(this.camera);
5812
- this.renderer.setOnImageLoad(() => {
5813
- this.renderLoop.markAllLayersDirty();
5814
- this.requestRender();
5815
- });
5816
- this.renderer.setOnImageError((src, cause) => {
5817
- const elementIds = [];
5818
- for (const el of this.store.getAll()) {
5819
- if (el.type === "image" && el.src === src) elementIds.push(el.id);
5820
- }
5821
- if (options.onImageError) {
5822
- options.onImageError({ src, elementIds, cause });
5823
- } else {
5824
- console.warn(`[fieldnotes] image failed to load: ${src}`);
5835
+ var SelectionOps = class {
5836
+ constructor(deps) {
5837
+ this.deps = deps;
5838
+ }
5839
+ getStyle() {
5840
+ const ids = this.deps.getSelectedIds();
5841
+ if (ids.length === 0) return null;
5842
+ const styles = [];
5843
+ for (const id of ids) {
5844
+ const el = this.deps.store.getById(id);
5845
+ if (el) styles.push(getElementStyle(el));
5846
+ }
5847
+ if (styles.length === 0) return null;
5848
+ const result = {};
5849
+ const color = sharedValue(styles.map((s) => s.color));
5850
+ if (color !== void 0) result.color = color;
5851
+ const fillColor = sharedValue(styles.map((s) => s.fillColor));
5852
+ if (fillColor !== void 0) result.fillColor = fillColor;
5853
+ const strokeWidth = sharedValue(styles.map((s) => s.strokeWidth));
5854
+ if (strokeWidth !== void 0) result.strokeWidth = strokeWidth;
5855
+ const opacity = sharedValue(styles.map((s) => s.opacity));
5856
+ if (opacity !== void 0) result.opacity = opacity;
5857
+ const fontSize = sharedValue(styles.map((s) => s.fontSize));
5858
+ if (fontSize !== void 0) result.fontSize = fontSize;
5859
+ return result;
5860
+ }
5861
+ applyStyle(style) {
5862
+ const ids = this.deps.getSelectedIds();
5863
+ if (ids.length === 0) return;
5864
+ this.deps.recorder.begin();
5865
+ for (const id of ids) {
5866
+ const el = this.deps.store.getById(id);
5867
+ if (!el) continue;
5868
+ const patch = styleToPatch(el, style);
5869
+ if (Object.keys(patch).length > 0) {
5870
+ this.deps.store.update(id, patch);
5825
5871
  }
5826
- });
5827
- this.noteEditor = new NoteEditor({
5828
- fontSizePresets: options.fontSizePresets,
5829
- toolbar: options.toolbar,
5830
- placeholder: options.placeholder
5831
- });
5832
- this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
5833
- this.arrowLabelEditor = new ArrowLabelEditor();
5834
- this.noteEditor.setHistoryHooks(
5835
- () => this.historyRecorder.begin(),
5836
- () => this.historyRecorder.commit()
5837
- );
5838
- this.onHtmlElementMount = options.onHtmlElementMount;
5839
- this.dropHandler = options.onDrop;
5840
- this.history = new HistoryStack();
5841
- this.historyRecorder = new HistoryRecorder(this.store, this.history, this.layerManager);
5842
- this.wrapper = this.createWrapper();
5843
- this.canvasEl = this.createCanvas();
5844
- this.domLayer = this.createDomLayer();
5845
- this.wrapper.appendChild(this.canvasEl);
5846
- this.wrapper.appendChild(this.domLayer);
5847
- this.container.appendChild(this.wrapper);
5848
- this.toolContext = {
5849
- camera: this.camera,
5850
- store: this.store,
5851
- requestRender: () => this.requestRender(),
5852
- switchTool: (name) => this.toolManager.setTool(name, this.toolContext),
5853
- editElement: (id) => this.startEditingElement(id),
5854
- fitNoteHeight: (id) => this.fitNoteHeight(id),
5855
- setCursor: (cursor) => {
5856
- this.wrapper.style.cursor = cursor;
5857
- },
5858
- snapToGrid: false,
5859
- gridSize: this._gridSize,
5860
- activeLayerId: this.layerManager.activeLayerId,
5861
- isLayerVisible: (id) => this.layerManager.isLayerVisible(id),
5862
- isLayerLocked: (id) => this.layerManager.isLayerLocked(id),
5863
- smartGuides: false,
5864
- getVisibleRect: () => this.camera.getVisibleRect(this.canvasEl.clientWidth, this.canvasEl.clientHeight)
5865
- };
5866
- this.inputHandler = new InputHandler(this.wrapper, this.camera, {
5867
- toolManager: this.toolManager,
5868
- toolContext: this.toolContext,
5869
- historyRecorder: this.historyRecorder,
5870
- historyStack: this.history,
5871
- fitToContent: () => this.fitToContent(),
5872
- group: () => this.groupSelection(),
5873
- ungroup: () => this.ungroupSelection(),
5874
- toggleLock: () => this.toggleLockSelection(),
5875
- openContextMenu: (screenPos, world) => {
5876
- this.getSelectTool()?.selectAtPoint(world, this.toolContext);
5877
- this.openContextMenu(screenPos);
5878
- },
5879
- shortcuts: options.shortcuts
5880
- });
5881
- if (options.contextMenu !== false) {
5882
- this.contextMenu = new ContextMenu({
5883
- onCommand: (action) => this.runAction(action),
5884
- onClose: noop
5885
- });
5886
5872
  }
5887
- this.unsubToolChange = this.toolManager.onChange(() => this.contextMenu?.close());
5888
- this.domNodeManager = new DomNodeManager({
5889
- domLayer: this.domLayer,
5890
- onEditRequest: (id) => this.startEditingElement(id),
5891
- isEditingElement: (id) => this.noteEditor.isEditing && this.noteEditor.editingElementId === id,
5892
- getVersion: (id) => this.store.getVersion(id)
5893
- });
5894
- this.interactMode = new InteractMode({
5895
- getNode: (id) => this.domNodeManager.getNode(id)
5896
- });
5897
- this.marginViewport = new MarginViewport(options.panBufferMargin ?? 256);
5898
- this.marginViewport.setViewport(
5899
- this.canvasEl.clientWidth || 800,
5900
- this.canvasEl.clientHeight || 600,
5901
- typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1
5902
- );
5903
- const layerCache = new LayerCache(this.marginViewport);
5904
- this.renderLoop = new RenderLoop({
5905
- canvasEl: this.canvasEl,
5906
- camera: this.camera,
5907
- background: this.background,
5908
- store: this.store,
5909
- renderer: this.renderer,
5910
- toolManager: this.toolManager,
5911
- layerManager: this.layerManager,
5912
- domNodeManager: this.domNodeManager,
5913
- layerCache,
5914
- marginViewport: this.marginViewport
5915
- });
5916
- this.unsubCamera = this.camera.onChange(() => {
5917
- this.applyCameraTransform();
5918
- this.noteEditor.updateToolbarPosition();
5919
- this.contextMenu?.close();
5920
- this.requestRender();
5921
- });
5922
- this.unsubStore = [
5923
- this.store.on("add", (el) => {
5924
- if (el.type === "grid") this.syncGridContext();
5925
- this.renderLoop.markLayerDirty(el.layerId);
5926
- this.requestRender();
5927
- }),
5928
- this.store.on("remove", (el) => {
5929
- if (el.type === "grid") this.syncGridContext();
5930
- this.unbindArrowsFrom(el);
5931
- this.domNodeManager.removeDomNode(el.id);
5932
- this.renderLoop.markLayerDirty(el.layerId);
5933
- this.requestRender();
5934
- }),
5935
- this.store.on("update", ({ previous, current }) => {
5936
- if (current.type === "grid") this.syncGridContext();
5937
- this.renderLoop.markLayerDirty(current.layerId);
5938
- if (previous.layerId !== current.layerId) {
5939
- this.renderLoop.markLayerDirty(previous.layerId);
5940
- }
5941
- this.requestRender();
5942
- }),
5943
- this.store.on("clear", () => {
5944
- this.domNodeManager.clearDomNodes();
5945
- this.renderLoop.markAllLayersDirty();
5946
- this.syncGridContext();
5947
- this.requestRender();
5948
- })
5949
- ];
5950
- this.layerManager.on("change", () => {
5951
- this.toolContext.activeLayerId = this.layerManager.activeLayerId;
5952
- this.requestRender();
5953
- });
5954
- this.wrapper.addEventListener("pointerdown", this.onTapDown);
5955
- this.wrapper.addEventListener("pointerup", this.onDoubleTap);
5956
- this.wrapper.addEventListener("dragover", this.onDragOver);
5957
- this.wrapper.addEventListener("drop", this.onDrop);
5958
- this.observeResize();
5959
- this.syncCanvasSize();
5960
- this.renderLoop.start();
5961
- this.syncGridContext();
5873
+ this.deps.recorder.commit();
5962
5874
  }
5963
- camera;
5964
- store;
5965
- layerManager;
5966
- toolManager;
5967
- history;
5968
- domLayer;
5969
- canvasEl;
5970
- wrapper;
5971
- unsubCamera;
5972
- unsubToolChange;
5973
- unsubStore;
5974
- inputHandler;
5975
- background;
5976
- renderer;
5977
- noteEditor;
5978
- arrowLabelEditor;
5979
- historyRecorder;
5980
- toolContext;
5981
- marginViewport;
5982
- resizeObserver = null;
5983
- _snapToGrid = false;
5984
- _smartGuides = false;
5985
- _gridSize;
5986
- renderLoop;
5987
- domNodeManager;
5988
- interactMode;
5989
- onHtmlElementMount;
5990
- dropHandler;
5991
- gridChangeListeners = /* @__PURE__ */ new Set();
5992
- doubleTapDetector = new DoubleTapDetector();
5993
- tapDownX = 0;
5994
- tapDownY = 0;
5995
- contextMenu = null;
5996
- get ctx() {
5997
- return this.canvasEl.getContext("2d");
5875
+ group() {
5876
+ const ids = this.deps.getSelectedIds();
5877
+ if (ids.length < 2) return;
5878
+ const groupId = createId("group");
5879
+ this.deps.recorder.begin();
5880
+ for (const id of ids) {
5881
+ if (this.deps.store.getById(id)) this.deps.store.update(id, { groupId });
5882
+ }
5883
+ this.deps.recorder.commit();
5998
5884
  }
5999
- get snapToGrid() {
6000
- return this._snapToGrid;
5885
+ ungroup() {
5886
+ const ids = this.deps.getSelectedIds();
5887
+ if (ids.length === 0) return;
5888
+ this.deps.recorder.begin();
5889
+ for (const id of ids) {
5890
+ const el = this.deps.store.getById(id);
5891
+ if (el && el.groupId !== void 0) this.deps.store.update(id, { groupId: void 0 });
5892
+ }
5893
+ this.deps.recorder.commit();
6001
5894
  }
6002
- setSnapToGrid(enabled) {
6003
- this._snapToGrid = enabled;
6004
- this.toolContext.snapToGrid = enabled;
5895
+ toggleLock() {
5896
+ const ids = this.deps.getSelectedIds();
5897
+ if (ids.length === 0) return;
5898
+ const anyUnlocked = ids.some((id) => {
5899
+ const el = this.deps.store.getById(id);
5900
+ return el ? !el.locked : false;
5901
+ });
5902
+ this.deps.recorder.begin();
5903
+ for (const id of ids) {
5904
+ const el = this.deps.store.getById(id);
5905
+ if (el && el.locked !== anyUnlocked) this.deps.store.update(id, { locked: anyUnlocked });
5906
+ }
5907
+ this.deps.recorder.commit();
5908
+ }
5909
+ align(edge) {
5910
+ const bounded = this.boundedSelection();
5911
+ if (bounded.length < 2) return;
5912
+ const B = unionBounds(bounded.map((e) => e.bounds));
5913
+ this.deps.recorder.begin();
5914
+ const moved = [];
5915
+ for (const { id, el, bounds: b } of bounded) {
5916
+ if (!this.isMovable(el)) continue;
5917
+ let dx = 0;
5918
+ let dy = 0;
5919
+ switch (edge) {
5920
+ case "left":
5921
+ dx = B.x - b.x;
5922
+ break;
5923
+ case "right":
5924
+ dx = B.x + B.w - (b.x + b.w);
5925
+ break;
5926
+ case "center-x":
5927
+ dx = B.x + B.w / 2 - (b.x + b.w / 2);
5928
+ break;
5929
+ case "top":
5930
+ dy = B.y - b.y;
5931
+ break;
5932
+ case "bottom":
5933
+ dy = B.y + B.h - (b.y + b.h);
5934
+ break;
5935
+ case "middle":
5936
+ dy = B.y + B.h / 2 - (b.y + b.h / 2);
5937
+ break;
5938
+ }
5939
+ if (dx === 0 && dy === 0) continue;
5940
+ this.deps.store.update(id, translateElementPatch(el, dx, dy));
5941
+ moved.push(id);
5942
+ }
5943
+ updateArrowsBoundToElements(moved, this.deps.store);
5944
+ this.deps.recorder.commit();
5945
+ this.deps.requestRender();
5946
+ }
5947
+ distribute(axis) {
5948
+ const bounded = this.boundedSelection();
5949
+ if (bounded.length < 3) return;
5950
+ const center2 = (b) => axis === "horizontal" ? b.x + b.w / 2 : b.y + b.h / 2;
5951
+ const sorted = [...bounded].sort((p, q) => center2(p.bounds) - center2(q.bounds));
5952
+ const first = sorted[0];
5953
+ const last = sorted[sorted.length - 1];
5954
+ if (!first || !last) return;
5955
+ const c0 = center2(first.bounds);
5956
+ const cN = center2(last.bounds);
5957
+ const n = sorted.length;
5958
+ this.deps.recorder.begin();
5959
+ const moved = [];
5960
+ for (let i = 1; i < n - 1; i++) {
5961
+ const item = sorted[i];
5962
+ if (!item || !this.isMovable(item.el)) continue;
5963
+ const target = c0 + i * (cN - c0) / (n - 1);
5964
+ const delta = target - center2(item.bounds);
5965
+ if (delta === 0) continue;
5966
+ const [dx, dy] = axis === "horizontal" ? [delta, 0] : [0, delta];
5967
+ this.deps.store.update(item.id, translateElementPatch(item.el, dx, dy));
5968
+ moved.push(item.id);
5969
+ }
5970
+ updateArrowsBoundToElements(moved, this.deps.store);
5971
+ this.deps.recorder.commit();
5972
+ this.deps.requestRender();
5973
+ }
5974
+ boundedSelection() {
5975
+ const out = [];
5976
+ for (const id of this.deps.getSelectedIds()) {
5977
+ const el = this.deps.store.getById(id);
5978
+ if (!el) continue;
5979
+ const bounds = getElementBounds(el);
5980
+ if (bounds) out.push({ id, el, bounds });
5981
+ }
5982
+ return out;
5983
+ }
5984
+ isMovable(el) {
5985
+ if (el.locked) return false;
5986
+ if (el.type === "arrow" && (el.fromBinding ?? el.toBinding)) return false;
5987
+ return true;
5988
+ }
5989
+ };
5990
+
5991
+ // src/canvas/grid-controller.ts
5992
+ var GridController = class {
5993
+ constructor(deps) {
5994
+ this.deps = deps;
5995
+ }
5996
+ listeners = /* @__PURE__ */ new Set();
5997
+ add(input) {
5998
+ const existing = this.deps.store.getElementsByType("grid")[0];
5999
+ this.deps.recorder.begin();
6000
+ if (existing) {
6001
+ this.deps.store.remove(existing.id);
6002
+ }
6003
+ const grid = createGrid({ ...input, layerId: this.deps.getActiveLayerId() });
6004
+ this.deps.store.add(grid);
6005
+ this.deps.recorder.commit();
6006
+ this.deps.requestRender();
6007
+ return grid.id;
6008
+ }
6009
+ update(updates) {
6010
+ const grid = this.deps.store.getElementsByType("grid")[0];
6011
+ if (!grid) return;
6012
+ this.deps.recorder.begin();
6013
+ this.deps.store.update(grid.id, updates);
6014
+ this.deps.recorder.commit();
6015
+ this.deps.requestRender();
6016
+ }
6017
+ remove() {
6018
+ const grid = this.deps.store.getElementsByType("grid")[0];
6019
+ if (!grid) return;
6020
+ this.deps.recorder.begin();
6021
+ this.deps.store.remove(grid.id);
6022
+ this.deps.recorder.commit();
6023
+ this.deps.requestRender();
6024
+ }
6025
+ getInfo() {
6026
+ const grid = this.deps.store.getElementsByType("grid")[0];
6027
+ if (!grid) return null;
6028
+ return {
6029
+ gridType: grid.gridType,
6030
+ hexOrientation: grid.hexOrientation,
6031
+ cellSize: grid.cellSize,
6032
+ cellRadius: grid.gridType === "hex" ? grid.cellSize : grid.cellSize / 2
6033
+ };
6034
+ }
6035
+ onChange(listener) {
6036
+ this.listeners.add(listener);
6037
+ return () => {
6038
+ this.listeners.delete(listener);
6039
+ };
6040
+ }
6041
+ syncContext() {
6042
+ const grid = this.deps.store.getElementsByType("grid")[0];
6043
+ if (grid) {
6044
+ this.deps.toolContext.gridSize = grid.cellSize;
6045
+ this.deps.toolContext.gridType = grid.gridType;
6046
+ this.deps.toolContext.hexOrientation = grid.hexOrientation;
6047
+ } else {
6048
+ this.deps.toolContext.gridSize = this.deps.defaultGridSize;
6049
+ this.deps.toolContext.gridType = void 0;
6050
+ this.deps.toolContext.hexOrientation = void 0;
6051
+ }
6052
+ this.notify();
6053
+ }
6054
+ notify() {
6055
+ const info = this.getInfo();
6056
+ for (const listener of this.listeners) {
6057
+ listener(info);
6058
+ }
6059
+ }
6060
+ };
6061
+
6062
+ // src/canvas/viewport-interactions.ts
6063
+ var ARROW_HIT_THRESHOLD = 10;
6064
+ var ViewportInteractions = class {
6065
+ constructor(deps) {
6066
+ this.deps = deps;
6067
+ }
6068
+ doubleTapDetector = new DoubleTapDetector();
6069
+ tapDownX = 0;
6070
+ tapDownY = 0;
6071
+ startEditingElement(id) {
6072
+ const element = this.deps.store.getById(id);
6073
+ if (!element || element.type !== "note" && element.type !== "text") return;
6074
+ this.deps.renderLoop.flush();
6075
+ const node = this.deps.domNodeManager.getNode(id);
6076
+ if (node) {
6077
+ this.deps.noteEditor.startEditing(node, id, this.deps.store);
6078
+ }
6079
+ }
6080
+ fitNoteHeight(elementId) {
6081
+ const element = this.deps.store.getById(elementId);
6082
+ if (!element || element.type !== "note") return;
6083
+ if (isNoteContentEmpty(element.text)) return;
6084
+ const node = this.deps.domNodeManager.getNode(elementId);
6085
+ if (!node) return;
6086
+ const measured = node.scrollHeight;
6087
+ if (measured > element.size.h) {
6088
+ this.deps.store.update(elementId, { size: { w: element.size.w, h: measured } });
6089
+ }
6090
+ }
6091
+ onTextEditStop(elementId) {
6092
+ const element = this.deps.store.getById(elementId);
6093
+ if (!element) return;
6094
+ if (element.type === "note") {
6095
+ if (isNoteContentEmpty(element.text)) {
6096
+ this.deps.store.remove(elementId);
6097
+ return;
6098
+ }
6099
+ this.fitNoteHeight(elementId);
6100
+ return;
6101
+ }
6102
+ if (element.type !== "text") return;
6103
+ if (!element.text || element.text.trim() === "") {
6104
+ this.deps.store.remove(elementId);
6105
+ return;
6106
+ }
6107
+ const node = this.deps.domNodeManager.getNode(elementId);
6108
+ if (node && "size" in element) {
6109
+ const measured = node.scrollHeight;
6110
+ if (measured !== element.size.h) {
6111
+ this.deps.store.update(elementId, { size: { w: element.size.w, h: measured } });
6112
+ }
6113
+ }
6114
+ }
6115
+ onTapDown = (e) => {
6116
+ this.tapDownX = e.clientX;
6117
+ this.tapDownY = e.clientY;
6118
+ };
6119
+ onDoubleTap = (e) => {
6120
+ const dx = e.clientX - this.tapDownX;
6121
+ const dy = e.clientY - this.tapDownY;
6122
+ const moved = Math.sqrt(dx * dx + dy * dy);
6123
+ if (moved > 10) return;
6124
+ if (!this.doubleTapDetector.feed(e)) return;
6125
+ if (typeof document.elementFromPoint !== "function") return;
6126
+ const el = document.elementFromPoint(e.clientX, e.clientY);
6127
+ const nodeEl = el?.closest("[data-element-id]");
6128
+ if (nodeEl) {
6129
+ const elementId = nodeEl.dataset["elementId"];
6130
+ if (elementId) {
6131
+ const element = this.deps.store.getById(elementId);
6132
+ if (element?.type === "note" || element?.type === "text") {
6133
+ this.startEditingElement(elementId);
6134
+ return;
6135
+ }
6136
+ }
6137
+ }
6138
+ const rect = this.deps.wrapper.getBoundingClientRect();
6139
+ const screen = { x: e.clientX - rect.left, y: e.clientY - rect.top };
6140
+ const world = this.deps.camera.screenToWorld(screen);
6141
+ const hit = this.hitTestWorld(world);
6142
+ if (hit?.type === "html") {
6143
+ this.deps.interactMode.startInteracting(hit.id);
6144
+ return;
6145
+ }
6146
+ const arrow = this.findArrowAt(world);
6147
+ if (arrow) {
6148
+ this.startArrowLabelEdit(arrow);
6149
+ }
6150
+ };
6151
+ findArrowAt(world) {
6152
+ const candidates = this.deps.store.queryPoint(world).reverse();
6153
+ for (const el of candidates) {
6154
+ if (el.type === "arrow" && isNearBezier(world, el.from, el.to, el.bend, ARROW_HIT_THRESHOLD)) {
6155
+ return el;
6156
+ }
6157
+ }
6158
+ return void 0;
6159
+ }
6160
+ startArrowLabelEdit(arrow) {
6161
+ this.deps.arrowLabelEditor.startEditing({
6162
+ arrow,
6163
+ layer: this.deps.domLayer,
6164
+ store: this.deps.store,
6165
+ recorder: this.deps.recorder,
6166
+ onDone: () => {
6167
+ this.deps.renderer.setLabelEditingId(null);
6168
+ this.deps.requestRender();
6169
+ }
6170
+ });
6171
+ this.deps.renderer.setLabelEditingId(arrow.id);
6172
+ }
6173
+ hitTestWorld(world) {
6174
+ const candidates = this.deps.store.queryPoint(world).reverse();
6175
+ for (const el of candidates) {
6176
+ if (!("size" in el)) continue;
6177
+ const { x, y } = el.position;
6178
+ const { w, h } = el.size;
6179
+ if (world.x >= x && world.x <= x + w && world.y >= y && world.y <= y + h) {
6180
+ return el;
6181
+ }
6182
+ }
6183
+ return null;
6184
+ }
6185
+ onDragOver = (e) => {
6186
+ e.preventDefault();
6187
+ };
6188
+ onDrop = (e) => {
6189
+ e.preventDefault();
6190
+ const rect = this.deps.wrapper.getBoundingClientRect();
6191
+ const screenPos = { x: e.clientX - rect.left, y: e.clientY - rect.top };
6192
+ const worldPos = this.deps.camera.screenToWorld(screenPos);
6193
+ if (this.deps.dropHandler) {
6194
+ this.deps.dropHandler(e, worldPos);
6195
+ return;
6196
+ }
6197
+ const files = e.dataTransfer?.files;
6198
+ if (!files) return;
6199
+ for (const file of files) {
6200
+ if (!file.type.startsWith("image/")) continue;
6201
+ const reader = new FileReader();
6202
+ reader.onload = () => {
6203
+ const src = reader.result;
6204
+ if (typeof src !== "string") return;
6205
+ this.deps.addImage(src, worldPos);
6206
+ };
6207
+ reader.readAsDataURL(file);
6208
+ }
6209
+ };
6210
+ };
6211
+
6212
+ // src/canvas/viewport.ts
6213
+ var EMPTY_IDS = [];
6214
+ function noop() {
6215
+ }
6216
+ var Viewport = class {
6217
+ constructor(container, options = {}) {
6218
+ this.container = container;
6219
+ this.camera = new Camera(options.camera);
6220
+ this.background = new Background(options.background);
6221
+ this._gridSize = options.background?.spacing ?? 24;
6222
+ this.store = new ElementStore();
6223
+ this.layerManager = new LayerManager(this.store);
6224
+ this.toolManager = new ToolManager();
6225
+ this.renderer = new ElementRenderer();
6226
+ this.renderer.setStore(this.store);
6227
+ this.renderer.setCamera(this.camera);
6228
+ this.renderer.setOnImageLoad(() => {
6229
+ this.renderLoop.markAllLayersDirty();
6230
+ this.requestRender();
6231
+ });
6232
+ this.renderer.setOnImageError((src, cause) => {
6233
+ const elementIds = [];
6234
+ for (const el of this.store.getAll()) {
6235
+ if (el.type === "image" && el.src === src) elementIds.push(el.id);
6236
+ }
6237
+ if (options.onImageError) {
6238
+ options.onImageError({ src, elementIds, cause });
6239
+ } else {
6240
+ console.warn(`[fieldnotes] image failed to load: ${src}`);
6241
+ }
6242
+ });
6243
+ this.noteEditor = new NoteEditor({
6244
+ fontSizePresets: options.fontSizePresets,
6245
+ toolbar: options.toolbar,
6246
+ placeholder: options.placeholder
6247
+ });
6248
+ this.noteEditor.setOnStop((id) => this.interactions.onTextEditStop(id));
6249
+ this.arrowLabelEditor = new ArrowLabelEditor();
6250
+ this.noteEditor.setHistoryHooks(
6251
+ () => this.historyRecorder.begin(),
6252
+ () => this.historyRecorder.commit()
6253
+ );
6254
+ this.onHtmlElementMount = options.onHtmlElementMount;
6255
+ this.dropHandler = options.onDrop;
6256
+ this.history = new HistoryStack();
6257
+ this.historyRecorder = new HistoryRecorder(this.store, this.history, this.layerManager);
6258
+ this.selectionOps = new SelectionOps({
6259
+ store: this.store,
6260
+ recorder: this.historyRecorder,
6261
+ getSelectedIds: () => this.getSelectedIds(),
6262
+ requestRender: () => this.requestRender()
6263
+ });
6264
+ this.wrapper = createWrapper();
6265
+ this.canvasEl = createCanvas();
6266
+ this.domLayer = createDomLayer();
6267
+ this.wrapper.appendChild(this.canvasEl);
6268
+ this.wrapper.appendChild(this.domLayer);
6269
+ this.container.appendChild(this.wrapper);
6270
+ this.toolContext = {
6271
+ camera: this.camera,
6272
+ store: this.store,
6273
+ requestRender: () => this.requestRender(),
6274
+ switchTool: (name) => this.toolManager.setTool(name, this.toolContext),
6275
+ editElement: (id) => this.interactions.startEditingElement(id),
6276
+ fitNoteHeight: (id) => this.interactions.fitNoteHeight(id),
6277
+ setCursor: (cursor) => {
6278
+ this.wrapper.style.cursor = cursor;
6279
+ },
6280
+ snapToGrid: false,
6281
+ gridSize: this._gridSize,
6282
+ activeLayerId: this.layerManager.activeLayerId,
6283
+ isLayerVisible: (id) => this.layerManager.isLayerVisible(id),
6284
+ isLayerLocked: (id) => this.layerManager.isLayerLocked(id),
6285
+ smartGuides: false,
6286
+ getVisibleRect: () => this.camera.getVisibleRect(this.canvasEl.clientWidth, this.canvasEl.clientHeight)
6287
+ };
6288
+ this.inputHandler = new InputHandler(this.wrapper, this.camera, {
6289
+ toolManager: this.toolManager,
6290
+ toolContext: this.toolContext,
6291
+ historyRecorder: this.historyRecorder,
6292
+ historyStack: this.history,
6293
+ fitToContent: () => this.fitToContent(),
6294
+ group: () => this.groupSelection(),
6295
+ ungroup: () => this.ungroupSelection(),
6296
+ toggleLock: () => this.toggleLockSelection(),
6297
+ openContextMenu: (screenPos, world) => {
6298
+ this.getSelectTool()?.selectAtPoint(world, this.toolContext);
6299
+ this.openContextMenu(screenPos);
6300
+ },
6301
+ shortcuts: options.shortcuts
6302
+ });
6303
+ if (options.contextMenu !== false) {
6304
+ this.contextMenu = new ContextMenu({
6305
+ onCommand: (action) => this.runAction(action),
6306
+ onClose: noop
6307
+ });
6308
+ }
6309
+ this.unsubToolChange = this.toolManager.onChange(() => this.contextMenu?.close());
6310
+ this.domNodeManager = new DomNodeManager({
6311
+ domLayer: this.domLayer,
6312
+ onEditRequest: (id) => this.interactions.startEditingElement(id),
6313
+ isEditingElement: (id) => this.noteEditor.isEditing && this.noteEditor.editingElementId === id,
6314
+ getVersion: (id) => this.store.getVersion(id)
6315
+ });
6316
+ this.interactMode = new InteractMode({
6317
+ getNode: (id) => this.domNodeManager.getNode(id)
6318
+ });
6319
+ this.marginViewport = new MarginViewport(options.panBufferMargin ?? 256);
6320
+ this.marginViewport.setViewport(
6321
+ this.canvasEl.clientWidth || 800,
6322
+ this.canvasEl.clientHeight || 600,
6323
+ typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1
6324
+ );
6325
+ const layerCache = new LayerCache(this.marginViewport);
6326
+ this.renderLoop = new RenderLoop({
6327
+ canvasEl: this.canvasEl,
6328
+ camera: this.camera,
6329
+ background: this.background,
6330
+ store: this.store,
6331
+ renderer: this.renderer,
6332
+ toolManager: this.toolManager,
6333
+ layerManager: this.layerManager,
6334
+ domNodeManager: this.domNodeManager,
6335
+ layerCache,
6336
+ marginViewport: this.marginViewport
6337
+ });
6338
+ this.unsubCamera = this.camera.onChange(() => {
6339
+ this.applyCameraTransform();
6340
+ this.noteEditor.updateToolbarPosition();
6341
+ this.contextMenu?.close();
6342
+ this.requestRender();
6343
+ });
6344
+ this.gridController = new GridController({
6345
+ store: this.store,
6346
+ recorder: this.historyRecorder,
6347
+ requestRender: () => this.requestRender(),
6348
+ getActiveLayerId: () => this.layerManager.activeLayerId,
6349
+ toolContext: this.toolContext,
6350
+ defaultGridSize: this._gridSize
6351
+ });
6352
+ this.unsubStore = [
6353
+ this.store.on("add", (el) => {
6354
+ if (el.type === "grid") this.gridController.syncContext();
6355
+ this.renderLoop.markLayerDirty(el.layerId);
6356
+ this.requestRender();
6357
+ }),
6358
+ this.store.on("remove", (el) => {
6359
+ if (el.type === "grid") this.gridController.syncContext();
6360
+ this.unbindArrowsFrom(el);
6361
+ this.domNodeManager.removeDomNode(el.id);
6362
+ this.renderLoop.markLayerDirty(el.layerId);
6363
+ this.requestRender();
6364
+ }),
6365
+ this.store.on("update", ({ previous, current }) => {
6366
+ if (current.type === "grid") this.gridController.syncContext();
6367
+ this.renderLoop.markLayerDirty(current.layerId);
6368
+ if (previous.layerId !== current.layerId) {
6369
+ this.renderLoop.markLayerDirty(previous.layerId);
6370
+ }
6371
+ this.requestRender();
6372
+ }),
6373
+ this.store.on("clear", () => {
6374
+ this.domNodeManager.clearDomNodes();
6375
+ this.renderLoop.markAllLayersDirty();
6376
+ this.gridController.syncContext();
6377
+ this.requestRender();
6378
+ })
6379
+ ];
6380
+ this.layerManager.on("change", () => {
6381
+ this.toolContext.activeLayerId = this.layerManager.activeLayerId;
6382
+ this.requestRender();
6383
+ });
6384
+ this.interactions = new ViewportInteractions({
6385
+ store: this.store,
6386
+ camera: this.camera,
6387
+ wrapper: this.wrapper,
6388
+ domLayer: this.domLayer,
6389
+ renderLoop: this.renderLoop,
6390
+ domNodeManager: this.domNodeManager,
6391
+ noteEditor: this.noteEditor,
6392
+ arrowLabelEditor: this.arrowLabelEditor,
6393
+ interactMode: this.interactMode,
6394
+ renderer: this.renderer,
6395
+ recorder: this.historyRecorder,
6396
+ requestRender: () => this.requestRender(),
6397
+ addImage: (src, position) => this.addImage(src, position),
6398
+ dropHandler: this.dropHandler
6399
+ });
6400
+ this.wrapper.addEventListener("pointerdown", this.interactions.onTapDown);
6401
+ this.wrapper.addEventListener("pointerup", this.interactions.onDoubleTap);
6402
+ this.wrapper.addEventListener("dragover", this.interactions.onDragOver);
6403
+ this.wrapper.addEventListener("drop", this.interactions.onDrop);
6404
+ this.observeResize();
6405
+ this.syncCanvasSize();
6406
+ this.renderLoop.start();
6407
+ this.gridController.syncContext();
6408
+ }
6409
+ camera;
6410
+ store;
6411
+ layerManager;
6412
+ toolManager;
6413
+ history;
6414
+ domLayer;
6415
+ canvasEl;
6416
+ wrapper;
6417
+ unsubCamera;
6418
+ unsubToolChange;
6419
+ unsubStore;
6420
+ inputHandler;
6421
+ background;
6422
+ renderer;
6423
+ noteEditor;
6424
+ arrowLabelEditor;
6425
+ historyRecorder;
6426
+ selectionOps;
6427
+ toolContext;
6428
+ marginViewport;
6429
+ resizeObserver = null;
6430
+ _snapToGrid = false;
6431
+ _smartGuides = false;
6432
+ _gridSize;
6433
+ renderLoop;
6434
+ domNodeManager;
6435
+ interactMode;
6436
+ onHtmlElementMount;
6437
+ dropHandler;
6438
+ gridController;
6439
+ interactions;
6440
+ contextMenu = null;
6441
+ get ctx() {
6442
+ return this.canvasEl.getContext("2d");
6443
+ }
6444
+ get snapToGrid() {
6445
+ return this._snapToGrid;
6446
+ }
6447
+ setSnapToGrid(enabled) {
6448
+ this._snapToGrid = enabled;
6449
+ this.toolContext.snapToGrid = enabled;
6005
6450
  }
6006
6451
  get smartGuides() {
6007
6452
  return this._smartGuides;
@@ -6160,48 +6605,19 @@ var Viewport = class {
6160
6605
  this.requestRender();
6161
6606
  }
6162
6607
  addGrid(input) {
6163
- const existing = this.store.getElementsByType("grid")[0];
6164
- this.historyRecorder.begin();
6165
- if (existing) {
6166
- this.store.remove(existing.id);
6167
- }
6168
- const grid = createGrid({ ...input, layerId: this.layerManager.activeLayerId });
6169
- this.store.add(grid);
6170
- this.historyRecorder.commit();
6171
- this.requestRender();
6172
- return grid.id;
6608
+ return this.gridController.add(input);
6173
6609
  }
6174
6610
  updateGrid(updates) {
6175
- const grid = this.store.getElementsByType("grid")[0];
6176
- if (!grid) return;
6177
- this.historyRecorder.begin();
6178
- this.store.update(grid.id, updates);
6179
- this.historyRecorder.commit();
6180
- this.requestRender();
6611
+ this.gridController.update(updates);
6181
6612
  }
6182
6613
  removeGrid() {
6183
- const grid = this.store.getElementsByType("grid")[0];
6184
- if (!grid) return;
6185
- this.historyRecorder.begin();
6186
- this.store.remove(grid.id);
6187
- this.historyRecorder.commit();
6188
- this.requestRender();
6614
+ this.gridController.remove();
6189
6615
  }
6190
6616
  getGridInfo() {
6191
- const grid = this.store.getElementsByType("grid")[0];
6192
- if (!grid) return null;
6193
- return {
6194
- gridType: grid.gridType,
6195
- hexOrientation: grid.hexOrientation,
6196
- cellSize: grid.cellSize,
6197
- cellRadius: grid.gridType === "hex" ? grid.cellSize : grid.cellSize / 2
6198
- };
6617
+ return this.gridController.getInfo();
6199
6618
  }
6200
- onGridChange(listener) {
6201
- this.gridChangeListeners.add(listener);
6202
- return () => {
6203
- this.gridChangeListeners.delete(listener);
6204
- };
6619
+ onGridChange(listener) {
6620
+ return this.gridController.onChange(listener);
6205
6621
  }
6206
6622
  getSelectTool() {
6207
6623
  return this.toolManager.getTool("select");
@@ -6242,154 +6658,25 @@ var Viewport = class {
6242
6658
  return tool ? tool.onSelectionChange(listener) : noop;
6243
6659
  }
6244
6660
  getSelectionStyle() {
6245
- const ids = this.getSelectedIds();
6246
- if (ids.length === 0) return null;
6247
- const styles = [];
6248
- for (const id of ids) {
6249
- const el = this.store.getById(id);
6250
- if (el) styles.push(getElementStyle(el));
6251
- }
6252
- if (styles.length === 0) return null;
6253
- const result = {};
6254
- const color = sharedValue(styles.map((s) => s.color));
6255
- if (color !== void 0) result.color = color;
6256
- const fillColor = sharedValue(styles.map((s) => s.fillColor));
6257
- if (fillColor !== void 0) result.fillColor = fillColor;
6258
- const strokeWidth = sharedValue(styles.map((s) => s.strokeWidth));
6259
- if (strokeWidth !== void 0) result.strokeWidth = strokeWidth;
6260
- const opacity = sharedValue(styles.map((s) => s.opacity));
6261
- if (opacity !== void 0) result.opacity = opacity;
6262
- const fontSize = sharedValue(styles.map((s) => s.fontSize));
6263
- if (fontSize !== void 0) result.fontSize = fontSize;
6264
- return result;
6661
+ return this.selectionOps.getStyle();
6265
6662
  }
6266
6663
  applyStyleToSelection(style) {
6267
- const ids = this.getSelectedIds();
6268
- if (ids.length === 0) return;
6269
- this.historyRecorder.begin();
6270
- for (const id of ids) {
6271
- const el = this.store.getById(id);
6272
- if (!el) continue;
6273
- const patch = styleToPatch(el, style);
6274
- if (Object.keys(patch).length > 0) {
6275
- this.store.update(id, patch);
6276
- }
6277
- }
6278
- this.historyRecorder.commit();
6664
+ this.selectionOps.applyStyle(style);
6279
6665
  }
6280
6666
  groupSelection() {
6281
- const ids = this.getSelectedIds();
6282
- if (ids.length < 2) return;
6283
- const groupId = createId("group");
6284
- this.historyRecorder.begin();
6285
- for (const id of ids) {
6286
- if (this.store.getById(id)) this.store.update(id, { groupId });
6287
- }
6288
- this.historyRecorder.commit();
6667
+ this.selectionOps.group();
6289
6668
  }
6290
6669
  ungroupSelection() {
6291
- const ids = this.getSelectedIds();
6292
- if (ids.length === 0) return;
6293
- this.historyRecorder.begin();
6294
- for (const id of ids) {
6295
- const el = this.store.getById(id);
6296
- if (el && el.groupId !== void 0) this.store.update(id, { groupId: void 0 });
6297
- }
6298
- this.historyRecorder.commit();
6670
+ this.selectionOps.ungroup();
6299
6671
  }
6300
6672
  toggleLockSelection() {
6301
- const ids = this.getSelectedIds();
6302
- if (ids.length === 0) return;
6303
- const anyUnlocked = ids.some((id) => {
6304
- const el = this.store.getById(id);
6305
- return el ? !el.locked : false;
6306
- });
6307
- this.historyRecorder.begin();
6308
- for (const id of ids) {
6309
- const el = this.store.getById(id);
6310
- if (el && el.locked !== anyUnlocked) this.store.update(id, { locked: anyUnlocked });
6311
- }
6312
- this.historyRecorder.commit();
6673
+ this.selectionOps.toggleLock();
6313
6674
  }
6314
6675
  alignSelection(edge) {
6315
- const bounded = this.boundedSelection();
6316
- if (bounded.length < 2) return;
6317
- const B = unionBounds(bounded.map((e) => e.bounds));
6318
- this.historyRecorder.begin();
6319
- const moved = [];
6320
- for (const { id, el, bounds: b } of bounded) {
6321
- if (!this.isMovable(el)) continue;
6322
- let dx = 0;
6323
- let dy = 0;
6324
- switch (edge) {
6325
- case "left":
6326
- dx = B.x - b.x;
6327
- break;
6328
- case "right":
6329
- dx = B.x + B.w - (b.x + b.w);
6330
- break;
6331
- case "center-x":
6332
- dx = B.x + B.w / 2 - (b.x + b.w / 2);
6333
- break;
6334
- case "top":
6335
- dy = B.y - b.y;
6336
- break;
6337
- case "bottom":
6338
- dy = B.y + B.h - (b.y + b.h);
6339
- break;
6340
- case "middle":
6341
- dy = B.y + B.h / 2 - (b.y + b.h / 2);
6342
- break;
6343
- }
6344
- if (dx === 0 && dy === 0) continue;
6345
- this.store.update(id, translateElementPatch(el, dx, dy));
6346
- moved.push(id);
6347
- }
6348
- updateArrowsBoundToElements(moved, this.store);
6349
- this.historyRecorder.commit();
6350
- this.requestRender();
6676
+ this.selectionOps.align(edge);
6351
6677
  }
6352
6678
  distributeSelection(axis) {
6353
- const bounded = this.boundedSelection();
6354
- if (bounded.length < 3) return;
6355
- const center2 = (b) => axis === "horizontal" ? b.x + b.w / 2 : b.y + b.h / 2;
6356
- const sorted = [...bounded].sort((p, q) => center2(p.bounds) - center2(q.bounds));
6357
- const first = sorted[0];
6358
- const last = sorted[sorted.length - 1];
6359
- if (!first || !last) return;
6360
- const c0 = center2(first.bounds);
6361
- const cN = center2(last.bounds);
6362
- const n = sorted.length;
6363
- this.historyRecorder.begin();
6364
- const moved = [];
6365
- for (let i = 1; i < n - 1; i++) {
6366
- const item = sorted[i];
6367
- if (!item || !this.isMovable(item.el)) continue;
6368
- const target = c0 + i * (cN - c0) / (n - 1);
6369
- const delta = target - center2(item.bounds);
6370
- if (delta === 0) continue;
6371
- const [dx, dy] = axis === "horizontal" ? [delta, 0] : [0, delta];
6372
- this.store.update(item.id, translateElementPatch(item.el, dx, dy));
6373
- moved.push(item.id);
6374
- }
6375
- updateArrowsBoundToElements(moved, this.store);
6376
- this.historyRecorder.commit();
6377
- this.requestRender();
6378
- }
6379
- boundedSelection() {
6380
- const out = [];
6381
- for (const id of this.getSelectedIds()) {
6382
- const el = this.store.getById(id);
6383
- if (!el) continue;
6384
- const bounds = getElementBounds(el);
6385
- if (bounds) out.push({ id, el, bounds });
6386
- }
6387
- return out;
6388
- }
6389
- isMovable(el) {
6390
- if (el.locked) return false;
6391
- if (el.type === "arrow" && (el.fromBinding ?? el.toBinding)) return false;
6392
- return true;
6679
+ this.selectionOps.distribute(axis);
6393
6680
  }
6394
6681
  getRenderStats() {
6395
6682
  return this.renderLoop.getStats();
@@ -6410,10 +6697,10 @@ var Viewport = class {
6410
6697
  this.arrowLabelEditor.cancel();
6411
6698
  this.historyRecorder.destroy();
6412
6699
  this.contextMenu?.dispose();
6413
- this.wrapper.removeEventListener("pointerdown", this.onTapDown);
6414
- this.wrapper.removeEventListener("pointerup", this.onDoubleTap);
6415
- this.wrapper.removeEventListener("dragover", this.onDragOver);
6416
- this.wrapper.removeEventListener("drop", this.onDrop);
6700
+ this.wrapper.removeEventListener("pointerdown", this.interactions.onTapDown);
6701
+ this.wrapper.removeEventListener("pointerup", this.interactions.onDoubleTap);
6702
+ this.wrapper.removeEventListener("dragover", this.interactions.onDragOver);
6703
+ this.wrapper.removeEventListener("drop", this.interactions.onDrop);
6417
6704
  this.inputHandler.destroy();
6418
6705
  this.unsubCamera();
6419
6706
  this.unsubToolChange();
@@ -6422,148 +6709,9 @@ var Viewport = class {
6422
6709
  this.resizeObserver = null;
6423
6710
  this.wrapper.remove();
6424
6711
  }
6425
- startEditingElement(id) {
6426
- const element = this.store.getById(id);
6427
- if (!element || element.type !== "note" && element.type !== "text") return;
6428
- this.renderLoop.flush();
6429
- const node = this.domNodeManager.getNode(id);
6430
- if (node) {
6431
- this.noteEditor.startEditing(node, id, this.store);
6432
- }
6433
- }
6434
- fitNoteHeight(elementId) {
6435
- const element = this.store.getById(elementId);
6436
- if (!element || element.type !== "note") return;
6437
- if (isNoteContentEmpty(element.text)) return;
6438
- const node = this.domNodeManager.getNode(elementId);
6439
- if (!node) return;
6440
- const measured = node.scrollHeight;
6441
- if (measured > element.size.h) {
6442
- this.store.update(elementId, { size: { w: element.size.w, h: measured } });
6443
- }
6444
- }
6445
- onTextEditStop(elementId) {
6446
- const element = this.store.getById(elementId);
6447
- if (!element) return;
6448
- if (element.type === "note") {
6449
- if (isNoteContentEmpty(element.text)) {
6450
- this.store.remove(elementId);
6451
- return;
6452
- }
6453
- this.fitNoteHeight(elementId);
6454
- return;
6455
- }
6456
- if (element.type !== "text") return;
6457
- if (!element.text || element.text.trim() === "") {
6458
- this.store.remove(elementId);
6459
- return;
6460
- }
6461
- const node = this.domNodeManager.getNode(elementId);
6462
- if (node && "size" in element) {
6463
- const measured = node.scrollHeight;
6464
- if (measured !== element.size.h) {
6465
- this.store.update(elementId, { size: { w: element.size.w, h: measured } });
6466
- }
6467
- }
6468
- }
6469
- onTapDown = (e) => {
6470
- this.tapDownX = e.clientX;
6471
- this.tapDownY = e.clientY;
6472
- };
6473
- onDoubleTap = (e) => {
6474
- const dx = e.clientX - this.tapDownX;
6475
- const dy = e.clientY - this.tapDownY;
6476
- const moved = Math.sqrt(dx * dx + dy * dy);
6477
- if (moved > 10) return;
6478
- if (!this.doubleTapDetector.feed(e)) return;
6479
- if (typeof document.elementFromPoint !== "function") return;
6480
- const el = document.elementFromPoint(e.clientX, e.clientY);
6481
- const nodeEl = el?.closest("[data-element-id]");
6482
- if (nodeEl) {
6483
- const elementId = nodeEl.dataset["elementId"];
6484
- if (elementId) {
6485
- const element = this.store.getById(elementId);
6486
- if (element?.type === "note" || element?.type === "text") {
6487
- this.startEditingElement(elementId);
6488
- return;
6489
- }
6490
- }
6491
- }
6492
- const rect = this.wrapper.getBoundingClientRect();
6493
- const screen = { x: e.clientX - rect.left, y: e.clientY - rect.top };
6494
- const world = this.camera.screenToWorld(screen);
6495
- const hit = this.hitTestWorld(world);
6496
- if (hit?.type === "html") {
6497
- this.interactMode.startInteracting(hit.id);
6498
- return;
6499
- }
6500
- const arrow = this.findArrowAt(world);
6501
- if (arrow) {
6502
- this.startArrowLabelEdit(arrow);
6503
- }
6504
- };
6505
- findArrowAt(world) {
6506
- const candidates = this.store.queryPoint(world).reverse();
6507
- for (const el of candidates) {
6508
- if (el.type === "arrow" && isNearBezier(world, el.from, el.to, el.bend, ARROW_HIT_THRESHOLD)) {
6509
- return el;
6510
- }
6511
- }
6512
- return void 0;
6513
- }
6514
- startArrowLabelEdit(arrow) {
6515
- this.arrowLabelEditor.startEditing({
6516
- arrow,
6517
- layer: this.domLayer,
6518
- store: this.store,
6519
- recorder: this.historyRecorder,
6520
- onDone: () => {
6521
- this.renderer.setLabelEditingId(null);
6522
- this.requestRender();
6523
- }
6524
- });
6525
- this.renderer.setLabelEditingId(arrow.id);
6526
- }
6527
- hitTestWorld(world) {
6528
- const candidates = this.store.queryPoint(world).reverse();
6529
- for (const el of candidates) {
6530
- if (!("size" in el)) continue;
6531
- const { x, y } = el.position;
6532
- const { w, h } = el.size;
6533
- if (world.x >= x && world.x <= x + w && world.y >= y && world.y <= y + h) {
6534
- return el;
6535
- }
6536
- }
6537
- return null;
6538
- }
6539
6712
  stopInteracting() {
6540
6713
  this.interactMode.stopInteracting();
6541
6714
  }
6542
- onDragOver = (e) => {
6543
- e.preventDefault();
6544
- };
6545
- onDrop = (e) => {
6546
- e.preventDefault();
6547
- const rect = this.wrapper.getBoundingClientRect();
6548
- const screenPos = { x: e.clientX - rect.left, y: e.clientY - rect.top };
6549
- const worldPos = this.camera.screenToWorld(screenPos);
6550
- if (this.dropHandler) {
6551
- this.dropHandler(e, worldPos);
6552
- return;
6553
- }
6554
- const files = e.dataTransfer?.files;
6555
- if (!files) return;
6556
- for (const file of files) {
6557
- if (!file.type.startsWith("image/")) continue;
6558
- const reader = new FileReader();
6559
- reader.onload = () => {
6560
- const src = reader.result;
6561
- if (typeof src !== "string") return;
6562
- this.addImage(src, worldPos);
6563
- };
6564
- reader.readAsDataURL(file);
6565
- }
6566
- };
6567
6715
  unbindArrowsFrom(removedElement) {
6568
6716
  const boundArrows = findBoundArrows(removedElement.id, this.store);
6569
6717
  const bounds = getElementBounds(removedElement);
@@ -6598,43 +6746,6 @@ var Viewport = class {
6598
6746
  }
6599
6747
  }
6600
6748
  }
6601
- createWrapper() {
6602
- const el = document.createElement("div");
6603
- Object.assign(el.style, {
6604
- position: "relative",
6605
- width: "100%",
6606
- height: "100%",
6607
- overflow: "hidden",
6608
- overscrollBehavior: "none",
6609
- userSelect: "none",
6610
- webkitUserSelect: "none"
6611
- });
6612
- return el;
6613
- }
6614
- createCanvas() {
6615
- const el = document.createElement("canvas");
6616
- Object.assign(el.style, {
6617
- position: "absolute",
6618
- top: "0",
6619
- left: "0",
6620
- width: "100%",
6621
- height: "100%"
6622
- });
6623
- return el;
6624
- }
6625
- createDomLayer() {
6626
- const el = document.createElement("div");
6627
- Object.assign(el.style, {
6628
- position: "absolute",
6629
- top: "0",
6630
- left: "0",
6631
- width: "100%",
6632
- height: "100%",
6633
- pointerEvents: "none",
6634
- transformOrigin: "0 0"
6635
- });
6636
- return el;
6637
- }
6638
6749
  applyCameraTransform() {
6639
6750
  this.domLayer.style.transform = this.camera.toCSSTransform();
6640
6751
  }
@@ -6644,25 +6755,6 @@ var Viewport = class {
6644
6755
  this.renderLoop.setCanvasSize(rect.width * dpr, rect.height * dpr);
6645
6756
  this.requestRender();
6646
6757
  }
6647
- syncGridContext() {
6648
- const grid = this.store.getElementsByType("grid")[0];
6649
- if (grid) {
6650
- this.toolContext.gridSize = grid.cellSize;
6651
- this.toolContext.gridType = grid.gridType;
6652
- this.toolContext.hexOrientation = grid.hexOrientation;
6653
- } else {
6654
- this.toolContext.gridSize = this._gridSize;
6655
- this.toolContext.gridType = void 0;
6656
- this.toolContext.hexOrientation = void 0;
6657
- }
6658
- this.notifyGridChangeListeners();
6659
- }
6660
- notifyGridChangeListeners() {
6661
- const info = this.getGridInfo();
6662
- for (const listener of this.gridChangeListeners) {
6663
- listener(info);
6664
- }
6665
- }
6666
6758
  observeResize() {
6667
6759
  if (typeof ResizeObserver === "undefined") return;
6668
6760
  this.resizeObserver = new ResizeObserver(() => this.syncCanvasSize());
@@ -8976,7 +9068,7 @@ var TemplateTool = class {
8976
9068
  };
8977
9069
 
8978
9070
  // src/index.ts
8979
- var VERSION = "0.38.0";
9071
+ var VERSION = "0.38.3";
8980
9072
  // Annotate the CommonJS export names for ESM import in node:
8981
9073
  0 && (module.exports = {
8982
9074
  ArrowTool,