@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.js CHANGED
@@ -3974,17 +3974,43 @@ var ContextMenu = class {
3974
3974
  }
3975
3975
  };
3976
3976
 
3977
- // src/elements/translate.ts
3978
- function translateElementPatch(el, dx, dy) {
3979
- const position = { x: el.position.x + dx, y: el.position.y + dy };
3980
- if (el.type === "arrow") {
3981
- return {
3982
- position,
3983
- from: { x: el.from.x + dx, y: el.from.y + dy },
3984
- to: { x: el.to.x + dx, y: el.to.y + dy }
3985
- };
3986
- }
3987
- return { position };
3977
+ // src/canvas/viewport-dom.ts
3978
+ function createWrapper() {
3979
+ const el = document.createElement("div");
3980
+ Object.assign(el.style, {
3981
+ position: "relative",
3982
+ width: "100%",
3983
+ height: "100%",
3984
+ overflow: "hidden",
3985
+ overscrollBehavior: "none",
3986
+ userSelect: "none",
3987
+ webkitUserSelect: "none"
3988
+ });
3989
+ return el;
3990
+ }
3991
+ function createCanvas() {
3992
+ const el = document.createElement("canvas");
3993
+ Object.assign(el.style, {
3994
+ position: "absolute",
3995
+ top: "0",
3996
+ left: "0",
3997
+ width: "100%",
3998
+ height: "100%"
3999
+ });
4000
+ return el;
4001
+ }
4002
+ function createDomLayer() {
4003
+ const el = document.createElement("div");
4004
+ Object.assign(el.style, {
4005
+ position: "absolute",
4006
+ top: "0",
4007
+ left: "0",
4008
+ width: "100%",
4009
+ height: "100%",
4010
+ pointerEvents: "none",
4011
+ transformOrigin: "0 0"
4012
+ });
4013
+ return el;
3988
4014
  }
3989
4015
 
3990
4016
  // src/elements/arrow-label-editor.ts
@@ -5608,6 +5634,19 @@ var MarginViewport = class {
5608
5634
  }
5609
5635
  };
5610
5636
 
5637
+ // src/elements/translate.ts
5638
+ function translateElementPatch(el, dx, dy) {
5639
+ const position = { x: el.position.x + dx, y: el.position.y + dy };
5640
+ if (el.type === "arrow") {
5641
+ return {
5642
+ position,
5643
+ from: { x: el.from.x + dx, y: el.from.y + dy },
5644
+ to: { x: el.to.x + dx, y: el.to.y + dy }
5645
+ };
5646
+ }
5647
+ return { position };
5648
+ }
5649
+
5611
5650
  // src/elements/element-style.ts
5612
5651
  function styleToPatch(element, style) {
5613
5652
  const { color, fillColor, strokeWidth, opacity, fontSize } = style;
@@ -5695,7 +5734,7 @@ function getElementStyle(element) {
5695
5734
  }
5696
5735
  }
5697
5736
 
5698
- // src/canvas/viewport.ts
5737
+ // src/canvas/selection-ops.ts
5699
5738
  function unionBounds(list) {
5700
5739
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
5701
5740
  for (const b of list) {
@@ -5706,221 +5745,627 @@ function unionBounds(list) {
5706
5745
  }
5707
5746
  return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
5708
5747
  }
5709
- var EMPTY_IDS = [];
5710
- var ARROW_HIT_THRESHOLD = 10;
5711
- function noop() {
5712
- }
5713
5748
  function sharedValue(values) {
5714
5749
  const present = values.filter((v) => v !== void 0);
5715
5750
  if (present.length === 0) return void 0;
5716
5751
  const first = present[0];
5717
5752
  return present.every((v) => v === first) ? first : void 0;
5718
5753
  }
5719
- var Viewport = class {
5720
- constructor(container, options = {}) {
5721
- this.container = container;
5722
- this.camera = new Camera(options.camera);
5723
- this.background = new Background(options.background);
5724
- this._gridSize = options.background?.spacing ?? 24;
5725
- this.store = new ElementStore();
5726
- this.layerManager = new LayerManager(this.store);
5727
- this.toolManager = new ToolManager();
5728
- this.renderer = new ElementRenderer();
5729
- this.renderer.setStore(this.store);
5730
- this.renderer.setCamera(this.camera);
5731
- this.renderer.setOnImageLoad(() => {
5732
- this.renderLoop.markAllLayersDirty();
5733
- this.requestRender();
5734
- });
5735
- this.renderer.setOnImageError((src, cause) => {
5736
- const elementIds = [];
5737
- for (const el of this.store.getAll()) {
5738
- if (el.type === "image" && el.src === src) elementIds.push(el.id);
5739
- }
5740
- if (options.onImageError) {
5741
- options.onImageError({ src, elementIds, cause });
5742
- } else {
5743
- console.warn(`[fieldnotes] image failed to load: ${src}`);
5754
+ var SelectionOps = class {
5755
+ constructor(deps) {
5756
+ this.deps = deps;
5757
+ }
5758
+ getStyle() {
5759
+ const ids = this.deps.getSelectedIds();
5760
+ if (ids.length === 0) return null;
5761
+ const styles = [];
5762
+ for (const id of ids) {
5763
+ const el = this.deps.store.getById(id);
5764
+ if (el) styles.push(getElementStyle(el));
5765
+ }
5766
+ if (styles.length === 0) return null;
5767
+ const result = {};
5768
+ const color = sharedValue(styles.map((s) => s.color));
5769
+ if (color !== void 0) result.color = color;
5770
+ const fillColor = sharedValue(styles.map((s) => s.fillColor));
5771
+ if (fillColor !== void 0) result.fillColor = fillColor;
5772
+ const strokeWidth = sharedValue(styles.map((s) => s.strokeWidth));
5773
+ if (strokeWidth !== void 0) result.strokeWidth = strokeWidth;
5774
+ const opacity = sharedValue(styles.map((s) => s.opacity));
5775
+ if (opacity !== void 0) result.opacity = opacity;
5776
+ const fontSize = sharedValue(styles.map((s) => s.fontSize));
5777
+ if (fontSize !== void 0) result.fontSize = fontSize;
5778
+ return result;
5779
+ }
5780
+ applyStyle(style) {
5781
+ const ids = this.deps.getSelectedIds();
5782
+ if (ids.length === 0) return;
5783
+ this.deps.recorder.begin();
5784
+ for (const id of ids) {
5785
+ const el = this.deps.store.getById(id);
5786
+ if (!el) continue;
5787
+ const patch = styleToPatch(el, style);
5788
+ if (Object.keys(patch).length > 0) {
5789
+ this.deps.store.update(id, patch);
5744
5790
  }
5745
- });
5746
- this.noteEditor = new NoteEditor({
5747
- fontSizePresets: options.fontSizePresets,
5748
- toolbar: options.toolbar,
5749
- placeholder: options.placeholder
5750
- });
5751
- this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
5752
- this.arrowLabelEditor = new ArrowLabelEditor();
5753
- this.noteEditor.setHistoryHooks(
5754
- () => this.historyRecorder.begin(),
5755
- () => this.historyRecorder.commit()
5756
- );
5757
- this.onHtmlElementMount = options.onHtmlElementMount;
5758
- this.dropHandler = options.onDrop;
5759
- this.history = new HistoryStack();
5760
- this.historyRecorder = new HistoryRecorder(this.store, this.history, this.layerManager);
5761
- this.wrapper = this.createWrapper();
5762
- this.canvasEl = this.createCanvas();
5763
- this.domLayer = this.createDomLayer();
5764
- this.wrapper.appendChild(this.canvasEl);
5765
- this.wrapper.appendChild(this.domLayer);
5766
- this.container.appendChild(this.wrapper);
5767
- this.toolContext = {
5768
- camera: this.camera,
5769
- store: this.store,
5770
- requestRender: () => this.requestRender(),
5771
- switchTool: (name) => this.toolManager.setTool(name, this.toolContext),
5772
- editElement: (id) => this.startEditingElement(id),
5773
- fitNoteHeight: (id) => this.fitNoteHeight(id),
5774
- setCursor: (cursor) => {
5775
- this.wrapper.style.cursor = cursor;
5776
- },
5777
- snapToGrid: false,
5778
- gridSize: this._gridSize,
5779
- activeLayerId: this.layerManager.activeLayerId,
5780
- isLayerVisible: (id) => this.layerManager.isLayerVisible(id),
5781
- isLayerLocked: (id) => this.layerManager.isLayerLocked(id),
5782
- smartGuides: false,
5783
- getVisibleRect: () => this.camera.getVisibleRect(this.canvasEl.clientWidth, this.canvasEl.clientHeight)
5784
- };
5785
- this.inputHandler = new InputHandler(this.wrapper, this.camera, {
5786
- toolManager: this.toolManager,
5787
- toolContext: this.toolContext,
5788
- historyRecorder: this.historyRecorder,
5789
- historyStack: this.history,
5790
- fitToContent: () => this.fitToContent(),
5791
- group: () => this.groupSelection(),
5792
- ungroup: () => this.ungroupSelection(),
5793
- toggleLock: () => this.toggleLockSelection(),
5794
- openContextMenu: (screenPos, world) => {
5795
- this.getSelectTool()?.selectAtPoint(world, this.toolContext);
5796
- this.openContextMenu(screenPos);
5797
- },
5798
- shortcuts: options.shortcuts
5799
- });
5800
- if (options.contextMenu !== false) {
5801
- this.contextMenu = new ContextMenu({
5802
- onCommand: (action) => this.runAction(action),
5803
- onClose: noop
5804
- });
5805
5791
  }
5806
- this.unsubToolChange = this.toolManager.onChange(() => this.contextMenu?.close());
5807
- this.domNodeManager = new DomNodeManager({
5808
- domLayer: this.domLayer,
5809
- onEditRequest: (id) => this.startEditingElement(id),
5810
- isEditingElement: (id) => this.noteEditor.isEditing && this.noteEditor.editingElementId === id,
5811
- getVersion: (id) => this.store.getVersion(id)
5812
- });
5813
- this.interactMode = new InteractMode({
5814
- getNode: (id) => this.domNodeManager.getNode(id)
5815
- });
5816
- this.marginViewport = new MarginViewport(options.panBufferMargin ?? 256);
5817
- this.marginViewport.setViewport(
5818
- this.canvasEl.clientWidth || 800,
5819
- this.canvasEl.clientHeight || 600,
5820
- typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1
5821
- );
5822
- const layerCache = new LayerCache(this.marginViewport);
5823
- this.renderLoop = new RenderLoop({
5824
- canvasEl: this.canvasEl,
5825
- camera: this.camera,
5826
- background: this.background,
5827
- store: this.store,
5828
- renderer: this.renderer,
5829
- toolManager: this.toolManager,
5830
- layerManager: this.layerManager,
5831
- domNodeManager: this.domNodeManager,
5832
- layerCache,
5833
- marginViewport: this.marginViewport
5834
- });
5835
- this.unsubCamera = this.camera.onChange(() => {
5836
- this.applyCameraTransform();
5837
- this.noteEditor.updateToolbarPosition();
5838
- this.contextMenu?.close();
5839
- this.requestRender();
5840
- });
5841
- this.unsubStore = [
5842
- this.store.on("add", (el) => {
5843
- if (el.type === "grid") this.syncGridContext();
5844
- this.renderLoop.markLayerDirty(el.layerId);
5845
- this.requestRender();
5846
- }),
5847
- this.store.on("remove", (el) => {
5848
- if (el.type === "grid") this.syncGridContext();
5849
- this.unbindArrowsFrom(el);
5850
- this.domNodeManager.removeDomNode(el.id);
5851
- this.renderLoop.markLayerDirty(el.layerId);
5852
- this.requestRender();
5853
- }),
5854
- this.store.on("update", ({ previous, current }) => {
5855
- if (current.type === "grid") this.syncGridContext();
5856
- this.renderLoop.markLayerDirty(current.layerId);
5857
- if (previous.layerId !== current.layerId) {
5858
- this.renderLoop.markLayerDirty(previous.layerId);
5859
- }
5860
- this.requestRender();
5861
- }),
5862
- this.store.on("clear", () => {
5863
- this.domNodeManager.clearDomNodes();
5864
- this.renderLoop.markAllLayersDirty();
5865
- this.syncGridContext();
5866
- this.requestRender();
5867
- })
5868
- ];
5869
- this.layerManager.on("change", () => {
5870
- this.toolContext.activeLayerId = this.layerManager.activeLayerId;
5871
- this.requestRender();
5872
- });
5873
- this.wrapper.addEventListener("pointerdown", this.onTapDown);
5874
- this.wrapper.addEventListener("pointerup", this.onDoubleTap);
5875
- this.wrapper.addEventListener("dragover", this.onDragOver);
5876
- this.wrapper.addEventListener("drop", this.onDrop);
5877
- this.observeResize();
5878
- this.syncCanvasSize();
5879
- this.renderLoop.start();
5880
- this.syncGridContext();
5792
+ this.deps.recorder.commit();
5881
5793
  }
5882
- camera;
5883
- store;
5884
- layerManager;
5885
- toolManager;
5886
- history;
5887
- domLayer;
5888
- canvasEl;
5889
- wrapper;
5890
- unsubCamera;
5891
- unsubToolChange;
5892
- unsubStore;
5893
- inputHandler;
5894
- background;
5895
- renderer;
5896
- noteEditor;
5897
- arrowLabelEditor;
5898
- historyRecorder;
5899
- toolContext;
5900
- marginViewport;
5901
- resizeObserver = null;
5902
- _snapToGrid = false;
5903
- _smartGuides = false;
5904
- _gridSize;
5905
- renderLoop;
5906
- domNodeManager;
5907
- interactMode;
5908
- onHtmlElementMount;
5909
- dropHandler;
5910
- gridChangeListeners = /* @__PURE__ */ new Set();
5911
- doubleTapDetector = new DoubleTapDetector();
5912
- tapDownX = 0;
5913
- tapDownY = 0;
5914
- contextMenu = null;
5915
- get ctx() {
5916
- return this.canvasEl.getContext("2d");
5794
+ group() {
5795
+ const ids = this.deps.getSelectedIds();
5796
+ if (ids.length < 2) return;
5797
+ const groupId = createId("group");
5798
+ this.deps.recorder.begin();
5799
+ for (const id of ids) {
5800
+ if (this.deps.store.getById(id)) this.deps.store.update(id, { groupId });
5801
+ }
5802
+ this.deps.recorder.commit();
5917
5803
  }
5918
- get snapToGrid() {
5919
- return this._snapToGrid;
5804
+ ungroup() {
5805
+ const ids = this.deps.getSelectedIds();
5806
+ if (ids.length === 0) return;
5807
+ this.deps.recorder.begin();
5808
+ for (const id of ids) {
5809
+ const el = this.deps.store.getById(id);
5810
+ if (el && el.groupId !== void 0) this.deps.store.update(id, { groupId: void 0 });
5811
+ }
5812
+ this.deps.recorder.commit();
5920
5813
  }
5921
- setSnapToGrid(enabled) {
5922
- this._snapToGrid = enabled;
5923
- this.toolContext.snapToGrid = enabled;
5814
+ toggleLock() {
5815
+ const ids = this.deps.getSelectedIds();
5816
+ if (ids.length === 0) return;
5817
+ const anyUnlocked = ids.some((id) => {
5818
+ const el = this.deps.store.getById(id);
5819
+ return el ? !el.locked : false;
5820
+ });
5821
+ this.deps.recorder.begin();
5822
+ for (const id of ids) {
5823
+ const el = this.deps.store.getById(id);
5824
+ if (el && el.locked !== anyUnlocked) this.deps.store.update(id, { locked: anyUnlocked });
5825
+ }
5826
+ this.deps.recorder.commit();
5827
+ }
5828
+ align(edge) {
5829
+ const bounded = this.boundedSelection();
5830
+ if (bounded.length < 2) return;
5831
+ const B = unionBounds(bounded.map((e) => e.bounds));
5832
+ this.deps.recorder.begin();
5833
+ const moved = [];
5834
+ for (const { id, el, bounds: b } of bounded) {
5835
+ if (!this.isMovable(el)) continue;
5836
+ let dx = 0;
5837
+ let dy = 0;
5838
+ switch (edge) {
5839
+ case "left":
5840
+ dx = B.x - b.x;
5841
+ break;
5842
+ case "right":
5843
+ dx = B.x + B.w - (b.x + b.w);
5844
+ break;
5845
+ case "center-x":
5846
+ dx = B.x + B.w / 2 - (b.x + b.w / 2);
5847
+ break;
5848
+ case "top":
5849
+ dy = B.y - b.y;
5850
+ break;
5851
+ case "bottom":
5852
+ dy = B.y + B.h - (b.y + b.h);
5853
+ break;
5854
+ case "middle":
5855
+ dy = B.y + B.h / 2 - (b.y + b.h / 2);
5856
+ break;
5857
+ }
5858
+ if (dx === 0 && dy === 0) continue;
5859
+ this.deps.store.update(id, translateElementPatch(el, dx, dy));
5860
+ moved.push(id);
5861
+ }
5862
+ updateArrowsBoundToElements(moved, this.deps.store);
5863
+ this.deps.recorder.commit();
5864
+ this.deps.requestRender();
5865
+ }
5866
+ distribute(axis) {
5867
+ const bounded = this.boundedSelection();
5868
+ if (bounded.length < 3) return;
5869
+ const center2 = (b) => axis === "horizontal" ? b.x + b.w / 2 : b.y + b.h / 2;
5870
+ const sorted = [...bounded].sort((p, q) => center2(p.bounds) - center2(q.bounds));
5871
+ const first = sorted[0];
5872
+ const last = sorted[sorted.length - 1];
5873
+ if (!first || !last) return;
5874
+ const c0 = center2(first.bounds);
5875
+ const cN = center2(last.bounds);
5876
+ const n = sorted.length;
5877
+ this.deps.recorder.begin();
5878
+ const moved = [];
5879
+ for (let i = 1; i < n - 1; i++) {
5880
+ const item = sorted[i];
5881
+ if (!item || !this.isMovable(item.el)) continue;
5882
+ const target = c0 + i * (cN - c0) / (n - 1);
5883
+ const delta = target - center2(item.bounds);
5884
+ if (delta === 0) continue;
5885
+ const [dx, dy] = axis === "horizontal" ? [delta, 0] : [0, delta];
5886
+ this.deps.store.update(item.id, translateElementPatch(item.el, dx, dy));
5887
+ moved.push(item.id);
5888
+ }
5889
+ updateArrowsBoundToElements(moved, this.deps.store);
5890
+ this.deps.recorder.commit();
5891
+ this.deps.requestRender();
5892
+ }
5893
+ boundedSelection() {
5894
+ const out = [];
5895
+ for (const id of this.deps.getSelectedIds()) {
5896
+ const el = this.deps.store.getById(id);
5897
+ if (!el) continue;
5898
+ const bounds = getElementBounds(el);
5899
+ if (bounds) out.push({ id, el, bounds });
5900
+ }
5901
+ return out;
5902
+ }
5903
+ isMovable(el) {
5904
+ if (el.locked) return false;
5905
+ if (el.type === "arrow" && (el.fromBinding ?? el.toBinding)) return false;
5906
+ return true;
5907
+ }
5908
+ };
5909
+
5910
+ // src/canvas/grid-controller.ts
5911
+ var GridController = class {
5912
+ constructor(deps) {
5913
+ this.deps = deps;
5914
+ }
5915
+ listeners = /* @__PURE__ */ new Set();
5916
+ add(input) {
5917
+ const existing = this.deps.store.getElementsByType("grid")[0];
5918
+ this.deps.recorder.begin();
5919
+ if (existing) {
5920
+ this.deps.store.remove(existing.id);
5921
+ }
5922
+ const grid = createGrid({ ...input, layerId: this.deps.getActiveLayerId() });
5923
+ this.deps.store.add(grid);
5924
+ this.deps.recorder.commit();
5925
+ this.deps.requestRender();
5926
+ return grid.id;
5927
+ }
5928
+ update(updates) {
5929
+ const grid = this.deps.store.getElementsByType("grid")[0];
5930
+ if (!grid) return;
5931
+ this.deps.recorder.begin();
5932
+ this.deps.store.update(grid.id, updates);
5933
+ this.deps.recorder.commit();
5934
+ this.deps.requestRender();
5935
+ }
5936
+ remove() {
5937
+ const grid = this.deps.store.getElementsByType("grid")[0];
5938
+ if (!grid) return;
5939
+ this.deps.recorder.begin();
5940
+ this.deps.store.remove(grid.id);
5941
+ this.deps.recorder.commit();
5942
+ this.deps.requestRender();
5943
+ }
5944
+ getInfo() {
5945
+ const grid = this.deps.store.getElementsByType("grid")[0];
5946
+ if (!grid) return null;
5947
+ return {
5948
+ gridType: grid.gridType,
5949
+ hexOrientation: grid.hexOrientation,
5950
+ cellSize: grid.cellSize,
5951
+ cellRadius: grid.gridType === "hex" ? grid.cellSize : grid.cellSize / 2
5952
+ };
5953
+ }
5954
+ onChange(listener) {
5955
+ this.listeners.add(listener);
5956
+ return () => {
5957
+ this.listeners.delete(listener);
5958
+ };
5959
+ }
5960
+ syncContext() {
5961
+ const grid = this.deps.store.getElementsByType("grid")[0];
5962
+ if (grid) {
5963
+ this.deps.toolContext.gridSize = grid.cellSize;
5964
+ this.deps.toolContext.gridType = grid.gridType;
5965
+ this.deps.toolContext.hexOrientation = grid.hexOrientation;
5966
+ } else {
5967
+ this.deps.toolContext.gridSize = this.deps.defaultGridSize;
5968
+ this.deps.toolContext.gridType = void 0;
5969
+ this.deps.toolContext.hexOrientation = void 0;
5970
+ }
5971
+ this.notify();
5972
+ }
5973
+ notify() {
5974
+ const info = this.getInfo();
5975
+ for (const listener of this.listeners) {
5976
+ listener(info);
5977
+ }
5978
+ }
5979
+ };
5980
+
5981
+ // src/canvas/viewport-interactions.ts
5982
+ var ARROW_HIT_THRESHOLD = 10;
5983
+ var ViewportInteractions = class {
5984
+ constructor(deps) {
5985
+ this.deps = deps;
5986
+ }
5987
+ doubleTapDetector = new DoubleTapDetector();
5988
+ tapDownX = 0;
5989
+ tapDownY = 0;
5990
+ startEditingElement(id) {
5991
+ const element = this.deps.store.getById(id);
5992
+ if (!element || element.type !== "note" && element.type !== "text") return;
5993
+ this.deps.renderLoop.flush();
5994
+ const node = this.deps.domNodeManager.getNode(id);
5995
+ if (node) {
5996
+ this.deps.noteEditor.startEditing(node, id, this.deps.store);
5997
+ }
5998
+ }
5999
+ fitNoteHeight(elementId) {
6000
+ const element = this.deps.store.getById(elementId);
6001
+ if (!element || element.type !== "note") return;
6002
+ if (isNoteContentEmpty(element.text)) return;
6003
+ const node = this.deps.domNodeManager.getNode(elementId);
6004
+ if (!node) return;
6005
+ const measured = node.scrollHeight;
6006
+ if (measured > element.size.h) {
6007
+ this.deps.store.update(elementId, { size: { w: element.size.w, h: measured } });
6008
+ }
6009
+ }
6010
+ onTextEditStop(elementId) {
6011
+ const element = this.deps.store.getById(elementId);
6012
+ if (!element) return;
6013
+ if (element.type === "note") {
6014
+ if (isNoteContentEmpty(element.text)) {
6015
+ this.deps.store.remove(elementId);
6016
+ return;
6017
+ }
6018
+ this.fitNoteHeight(elementId);
6019
+ return;
6020
+ }
6021
+ if (element.type !== "text") return;
6022
+ if (!element.text || element.text.trim() === "") {
6023
+ this.deps.store.remove(elementId);
6024
+ return;
6025
+ }
6026
+ const node = this.deps.domNodeManager.getNode(elementId);
6027
+ if (node && "size" in element) {
6028
+ const measured = node.scrollHeight;
6029
+ if (measured !== element.size.h) {
6030
+ this.deps.store.update(elementId, { size: { w: element.size.w, h: measured } });
6031
+ }
6032
+ }
6033
+ }
6034
+ onTapDown = (e) => {
6035
+ this.tapDownX = e.clientX;
6036
+ this.tapDownY = e.clientY;
6037
+ };
6038
+ onDoubleTap = (e) => {
6039
+ const dx = e.clientX - this.tapDownX;
6040
+ const dy = e.clientY - this.tapDownY;
6041
+ const moved = Math.sqrt(dx * dx + dy * dy);
6042
+ if (moved > 10) return;
6043
+ if (!this.doubleTapDetector.feed(e)) return;
6044
+ if (typeof document.elementFromPoint !== "function") return;
6045
+ const el = document.elementFromPoint(e.clientX, e.clientY);
6046
+ const nodeEl = el?.closest("[data-element-id]");
6047
+ if (nodeEl) {
6048
+ const elementId = nodeEl.dataset["elementId"];
6049
+ if (elementId) {
6050
+ const element = this.deps.store.getById(elementId);
6051
+ if (element?.type === "note" || element?.type === "text") {
6052
+ this.startEditingElement(elementId);
6053
+ return;
6054
+ }
6055
+ }
6056
+ }
6057
+ const rect = this.deps.wrapper.getBoundingClientRect();
6058
+ const screen = { x: e.clientX - rect.left, y: e.clientY - rect.top };
6059
+ const world = this.deps.camera.screenToWorld(screen);
6060
+ const hit = this.hitTestWorld(world);
6061
+ if (hit?.type === "html") {
6062
+ this.deps.interactMode.startInteracting(hit.id);
6063
+ return;
6064
+ }
6065
+ const arrow = this.findArrowAt(world);
6066
+ if (arrow) {
6067
+ this.startArrowLabelEdit(arrow);
6068
+ }
6069
+ };
6070
+ findArrowAt(world) {
6071
+ const candidates = this.deps.store.queryPoint(world).reverse();
6072
+ for (const el of candidates) {
6073
+ if (el.type === "arrow" && isNearBezier(world, el.from, el.to, el.bend, ARROW_HIT_THRESHOLD)) {
6074
+ return el;
6075
+ }
6076
+ }
6077
+ return void 0;
6078
+ }
6079
+ startArrowLabelEdit(arrow) {
6080
+ this.deps.arrowLabelEditor.startEditing({
6081
+ arrow,
6082
+ layer: this.deps.domLayer,
6083
+ store: this.deps.store,
6084
+ recorder: this.deps.recorder,
6085
+ onDone: () => {
6086
+ this.deps.renderer.setLabelEditingId(null);
6087
+ this.deps.requestRender();
6088
+ }
6089
+ });
6090
+ this.deps.renderer.setLabelEditingId(arrow.id);
6091
+ }
6092
+ hitTestWorld(world) {
6093
+ const candidates = this.deps.store.queryPoint(world).reverse();
6094
+ for (const el of candidates) {
6095
+ if (!("size" in el)) continue;
6096
+ const { x, y } = el.position;
6097
+ const { w, h } = el.size;
6098
+ if (world.x >= x && world.x <= x + w && world.y >= y && world.y <= y + h) {
6099
+ return el;
6100
+ }
6101
+ }
6102
+ return null;
6103
+ }
6104
+ onDragOver = (e) => {
6105
+ e.preventDefault();
6106
+ };
6107
+ onDrop = (e) => {
6108
+ e.preventDefault();
6109
+ const rect = this.deps.wrapper.getBoundingClientRect();
6110
+ const screenPos = { x: e.clientX - rect.left, y: e.clientY - rect.top };
6111
+ const worldPos = this.deps.camera.screenToWorld(screenPos);
6112
+ if (this.deps.dropHandler) {
6113
+ this.deps.dropHandler(e, worldPos);
6114
+ return;
6115
+ }
6116
+ const files = e.dataTransfer?.files;
6117
+ if (!files) return;
6118
+ for (const file of files) {
6119
+ if (!file.type.startsWith("image/")) continue;
6120
+ const reader = new FileReader();
6121
+ reader.onload = () => {
6122
+ const src = reader.result;
6123
+ if (typeof src !== "string") return;
6124
+ this.deps.addImage(src, worldPos);
6125
+ };
6126
+ reader.readAsDataURL(file);
6127
+ }
6128
+ };
6129
+ };
6130
+
6131
+ // src/canvas/viewport.ts
6132
+ var EMPTY_IDS = [];
6133
+ function noop() {
6134
+ }
6135
+ var Viewport = class {
6136
+ constructor(container, options = {}) {
6137
+ this.container = container;
6138
+ this.camera = new Camera(options.camera);
6139
+ this.background = new Background(options.background);
6140
+ this._gridSize = options.background?.spacing ?? 24;
6141
+ this.store = new ElementStore();
6142
+ this.layerManager = new LayerManager(this.store);
6143
+ this.toolManager = new ToolManager();
6144
+ this.renderer = new ElementRenderer();
6145
+ this.renderer.setStore(this.store);
6146
+ this.renderer.setCamera(this.camera);
6147
+ this.renderer.setOnImageLoad(() => {
6148
+ this.renderLoop.markAllLayersDirty();
6149
+ this.requestRender();
6150
+ });
6151
+ this.renderer.setOnImageError((src, cause) => {
6152
+ const elementIds = [];
6153
+ for (const el of this.store.getAll()) {
6154
+ if (el.type === "image" && el.src === src) elementIds.push(el.id);
6155
+ }
6156
+ if (options.onImageError) {
6157
+ options.onImageError({ src, elementIds, cause });
6158
+ } else {
6159
+ console.warn(`[fieldnotes] image failed to load: ${src}`);
6160
+ }
6161
+ });
6162
+ this.noteEditor = new NoteEditor({
6163
+ fontSizePresets: options.fontSizePresets,
6164
+ toolbar: options.toolbar,
6165
+ placeholder: options.placeholder
6166
+ });
6167
+ this.noteEditor.setOnStop((id) => this.interactions.onTextEditStop(id));
6168
+ this.arrowLabelEditor = new ArrowLabelEditor();
6169
+ this.noteEditor.setHistoryHooks(
6170
+ () => this.historyRecorder.begin(),
6171
+ () => this.historyRecorder.commit()
6172
+ );
6173
+ this.onHtmlElementMount = options.onHtmlElementMount;
6174
+ this.dropHandler = options.onDrop;
6175
+ this.history = new HistoryStack();
6176
+ this.historyRecorder = new HistoryRecorder(this.store, this.history, this.layerManager);
6177
+ this.selectionOps = new SelectionOps({
6178
+ store: this.store,
6179
+ recorder: this.historyRecorder,
6180
+ getSelectedIds: () => this.getSelectedIds(),
6181
+ requestRender: () => this.requestRender()
6182
+ });
6183
+ this.wrapper = createWrapper();
6184
+ this.canvasEl = createCanvas();
6185
+ this.domLayer = createDomLayer();
6186
+ this.wrapper.appendChild(this.canvasEl);
6187
+ this.wrapper.appendChild(this.domLayer);
6188
+ this.container.appendChild(this.wrapper);
6189
+ this.toolContext = {
6190
+ camera: this.camera,
6191
+ store: this.store,
6192
+ requestRender: () => this.requestRender(),
6193
+ switchTool: (name) => this.toolManager.setTool(name, this.toolContext),
6194
+ editElement: (id) => this.interactions.startEditingElement(id),
6195
+ fitNoteHeight: (id) => this.interactions.fitNoteHeight(id),
6196
+ setCursor: (cursor) => {
6197
+ this.wrapper.style.cursor = cursor;
6198
+ },
6199
+ snapToGrid: false,
6200
+ gridSize: this._gridSize,
6201
+ activeLayerId: this.layerManager.activeLayerId,
6202
+ isLayerVisible: (id) => this.layerManager.isLayerVisible(id),
6203
+ isLayerLocked: (id) => this.layerManager.isLayerLocked(id),
6204
+ smartGuides: false,
6205
+ getVisibleRect: () => this.camera.getVisibleRect(this.canvasEl.clientWidth, this.canvasEl.clientHeight)
6206
+ };
6207
+ this.inputHandler = new InputHandler(this.wrapper, this.camera, {
6208
+ toolManager: this.toolManager,
6209
+ toolContext: this.toolContext,
6210
+ historyRecorder: this.historyRecorder,
6211
+ historyStack: this.history,
6212
+ fitToContent: () => this.fitToContent(),
6213
+ group: () => this.groupSelection(),
6214
+ ungroup: () => this.ungroupSelection(),
6215
+ toggleLock: () => this.toggleLockSelection(),
6216
+ openContextMenu: (screenPos, world) => {
6217
+ this.getSelectTool()?.selectAtPoint(world, this.toolContext);
6218
+ this.openContextMenu(screenPos);
6219
+ },
6220
+ shortcuts: options.shortcuts
6221
+ });
6222
+ if (options.contextMenu !== false) {
6223
+ this.contextMenu = new ContextMenu({
6224
+ onCommand: (action) => this.runAction(action),
6225
+ onClose: noop
6226
+ });
6227
+ }
6228
+ this.unsubToolChange = this.toolManager.onChange(() => this.contextMenu?.close());
6229
+ this.domNodeManager = new DomNodeManager({
6230
+ domLayer: this.domLayer,
6231
+ onEditRequest: (id) => this.interactions.startEditingElement(id),
6232
+ isEditingElement: (id) => this.noteEditor.isEditing && this.noteEditor.editingElementId === id,
6233
+ getVersion: (id) => this.store.getVersion(id)
6234
+ });
6235
+ this.interactMode = new InteractMode({
6236
+ getNode: (id) => this.domNodeManager.getNode(id)
6237
+ });
6238
+ this.marginViewport = new MarginViewport(options.panBufferMargin ?? 256);
6239
+ this.marginViewport.setViewport(
6240
+ this.canvasEl.clientWidth || 800,
6241
+ this.canvasEl.clientHeight || 600,
6242
+ typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1
6243
+ );
6244
+ const layerCache = new LayerCache(this.marginViewport);
6245
+ this.renderLoop = new RenderLoop({
6246
+ canvasEl: this.canvasEl,
6247
+ camera: this.camera,
6248
+ background: this.background,
6249
+ store: this.store,
6250
+ renderer: this.renderer,
6251
+ toolManager: this.toolManager,
6252
+ layerManager: this.layerManager,
6253
+ domNodeManager: this.domNodeManager,
6254
+ layerCache,
6255
+ marginViewport: this.marginViewport
6256
+ });
6257
+ this.unsubCamera = this.camera.onChange(() => {
6258
+ this.applyCameraTransform();
6259
+ this.noteEditor.updateToolbarPosition();
6260
+ this.contextMenu?.close();
6261
+ this.requestRender();
6262
+ });
6263
+ this.gridController = new GridController({
6264
+ store: this.store,
6265
+ recorder: this.historyRecorder,
6266
+ requestRender: () => this.requestRender(),
6267
+ getActiveLayerId: () => this.layerManager.activeLayerId,
6268
+ toolContext: this.toolContext,
6269
+ defaultGridSize: this._gridSize
6270
+ });
6271
+ this.unsubStore = [
6272
+ this.store.on("add", (el) => {
6273
+ if (el.type === "grid") this.gridController.syncContext();
6274
+ this.renderLoop.markLayerDirty(el.layerId);
6275
+ this.requestRender();
6276
+ }),
6277
+ this.store.on("remove", (el) => {
6278
+ if (el.type === "grid") this.gridController.syncContext();
6279
+ this.unbindArrowsFrom(el);
6280
+ this.domNodeManager.removeDomNode(el.id);
6281
+ this.renderLoop.markLayerDirty(el.layerId);
6282
+ this.requestRender();
6283
+ }),
6284
+ this.store.on("update", ({ previous, current }) => {
6285
+ if (current.type === "grid") this.gridController.syncContext();
6286
+ this.renderLoop.markLayerDirty(current.layerId);
6287
+ if (previous.layerId !== current.layerId) {
6288
+ this.renderLoop.markLayerDirty(previous.layerId);
6289
+ }
6290
+ this.requestRender();
6291
+ }),
6292
+ this.store.on("clear", () => {
6293
+ this.domNodeManager.clearDomNodes();
6294
+ this.renderLoop.markAllLayersDirty();
6295
+ this.gridController.syncContext();
6296
+ this.requestRender();
6297
+ })
6298
+ ];
6299
+ this.layerManager.on("change", () => {
6300
+ this.toolContext.activeLayerId = this.layerManager.activeLayerId;
6301
+ this.requestRender();
6302
+ });
6303
+ this.interactions = new ViewportInteractions({
6304
+ store: this.store,
6305
+ camera: this.camera,
6306
+ wrapper: this.wrapper,
6307
+ domLayer: this.domLayer,
6308
+ renderLoop: this.renderLoop,
6309
+ domNodeManager: this.domNodeManager,
6310
+ noteEditor: this.noteEditor,
6311
+ arrowLabelEditor: this.arrowLabelEditor,
6312
+ interactMode: this.interactMode,
6313
+ renderer: this.renderer,
6314
+ recorder: this.historyRecorder,
6315
+ requestRender: () => this.requestRender(),
6316
+ addImage: (src, position) => this.addImage(src, position),
6317
+ dropHandler: this.dropHandler
6318
+ });
6319
+ this.wrapper.addEventListener("pointerdown", this.interactions.onTapDown);
6320
+ this.wrapper.addEventListener("pointerup", this.interactions.onDoubleTap);
6321
+ this.wrapper.addEventListener("dragover", this.interactions.onDragOver);
6322
+ this.wrapper.addEventListener("drop", this.interactions.onDrop);
6323
+ this.observeResize();
6324
+ this.syncCanvasSize();
6325
+ this.renderLoop.start();
6326
+ this.gridController.syncContext();
6327
+ }
6328
+ camera;
6329
+ store;
6330
+ layerManager;
6331
+ toolManager;
6332
+ history;
6333
+ domLayer;
6334
+ canvasEl;
6335
+ wrapper;
6336
+ unsubCamera;
6337
+ unsubToolChange;
6338
+ unsubStore;
6339
+ inputHandler;
6340
+ background;
6341
+ renderer;
6342
+ noteEditor;
6343
+ arrowLabelEditor;
6344
+ historyRecorder;
6345
+ selectionOps;
6346
+ toolContext;
6347
+ marginViewport;
6348
+ resizeObserver = null;
6349
+ _snapToGrid = false;
6350
+ _smartGuides = false;
6351
+ _gridSize;
6352
+ renderLoop;
6353
+ domNodeManager;
6354
+ interactMode;
6355
+ onHtmlElementMount;
6356
+ dropHandler;
6357
+ gridController;
6358
+ interactions;
6359
+ contextMenu = null;
6360
+ get ctx() {
6361
+ return this.canvasEl.getContext("2d");
6362
+ }
6363
+ get snapToGrid() {
6364
+ return this._snapToGrid;
6365
+ }
6366
+ setSnapToGrid(enabled) {
6367
+ this._snapToGrid = enabled;
6368
+ this.toolContext.snapToGrid = enabled;
5924
6369
  }
5925
6370
  get smartGuides() {
5926
6371
  return this._smartGuides;
@@ -6079,48 +6524,19 @@ var Viewport = class {
6079
6524
  this.requestRender();
6080
6525
  }
6081
6526
  addGrid(input) {
6082
- const existing = this.store.getElementsByType("grid")[0];
6083
- this.historyRecorder.begin();
6084
- if (existing) {
6085
- this.store.remove(existing.id);
6086
- }
6087
- const grid = createGrid({ ...input, layerId: this.layerManager.activeLayerId });
6088
- this.store.add(grid);
6089
- this.historyRecorder.commit();
6090
- this.requestRender();
6091
- return grid.id;
6527
+ return this.gridController.add(input);
6092
6528
  }
6093
6529
  updateGrid(updates) {
6094
- const grid = this.store.getElementsByType("grid")[0];
6095
- if (!grid) return;
6096
- this.historyRecorder.begin();
6097
- this.store.update(grid.id, updates);
6098
- this.historyRecorder.commit();
6099
- this.requestRender();
6530
+ this.gridController.update(updates);
6100
6531
  }
6101
6532
  removeGrid() {
6102
- const grid = this.store.getElementsByType("grid")[0];
6103
- if (!grid) return;
6104
- this.historyRecorder.begin();
6105
- this.store.remove(grid.id);
6106
- this.historyRecorder.commit();
6107
- this.requestRender();
6533
+ this.gridController.remove();
6108
6534
  }
6109
6535
  getGridInfo() {
6110
- const grid = this.store.getElementsByType("grid")[0];
6111
- if (!grid) return null;
6112
- return {
6113
- gridType: grid.gridType,
6114
- hexOrientation: grid.hexOrientation,
6115
- cellSize: grid.cellSize,
6116
- cellRadius: grid.gridType === "hex" ? grid.cellSize : grid.cellSize / 2
6117
- };
6536
+ return this.gridController.getInfo();
6118
6537
  }
6119
- onGridChange(listener) {
6120
- this.gridChangeListeners.add(listener);
6121
- return () => {
6122
- this.gridChangeListeners.delete(listener);
6123
- };
6538
+ onGridChange(listener) {
6539
+ return this.gridController.onChange(listener);
6124
6540
  }
6125
6541
  getSelectTool() {
6126
6542
  return this.toolManager.getTool("select");
@@ -6161,154 +6577,25 @@ var Viewport = class {
6161
6577
  return tool ? tool.onSelectionChange(listener) : noop;
6162
6578
  }
6163
6579
  getSelectionStyle() {
6164
- const ids = this.getSelectedIds();
6165
- if (ids.length === 0) return null;
6166
- const styles = [];
6167
- for (const id of ids) {
6168
- const el = this.store.getById(id);
6169
- if (el) styles.push(getElementStyle(el));
6170
- }
6171
- if (styles.length === 0) return null;
6172
- const result = {};
6173
- const color = sharedValue(styles.map((s) => s.color));
6174
- if (color !== void 0) result.color = color;
6175
- const fillColor = sharedValue(styles.map((s) => s.fillColor));
6176
- if (fillColor !== void 0) result.fillColor = fillColor;
6177
- const strokeWidth = sharedValue(styles.map((s) => s.strokeWidth));
6178
- if (strokeWidth !== void 0) result.strokeWidth = strokeWidth;
6179
- const opacity = sharedValue(styles.map((s) => s.opacity));
6180
- if (opacity !== void 0) result.opacity = opacity;
6181
- const fontSize = sharedValue(styles.map((s) => s.fontSize));
6182
- if (fontSize !== void 0) result.fontSize = fontSize;
6183
- return result;
6580
+ return this.selectionOps.getStyle();
6184
6581
  }
6185
6582
  applyStyleToSelection(style) {
6186
- const ids = this.getSelectedIds();
6187
- if (ids.length === 0) return;
6188
- this.historyRecorder.begin();
6189
- for (const id of ids) {
6190
- const el = this.store.getById(id);
6191
- if (!el) continue;
6192
- const patch = styleToPatch(el, style);
6193
- if (Object.keys(patch).length > 0) {
6194
- this.store.update(id, patch);
6195
- }
6196
- }
6197
- this.historyRecorder.commit();
6583
+ this.selectionOps.applyStyle(style);
6198
6584
  }
6199
6585
  groupSelection() {
6200
- const ids = this.getSelectedIds();
6201
- if (ids.length < 2) return;
6202
- const groupId = createId("group");
6203
- this.historyRecorder.begin();
6204
- for (const id of ids) {
6205
- if (this.store.getById(id)) this.store.update(id, { groupId });
6206
- }
6207
- this.historyRecorder.commit();
6586
+ this.selectionOps.group();
6208
6587
  }
6209
6588
  ungroupSelection() {
6210
- const ids = this.getSelectedIds();
6211
- if (ids.length === 0) return;
6212
- this.historyRecorder.begin();
6213
- for (const id of ids) {
6214
- const el = this.store.getById(id);
6215
- if (el && el.groupId !== void 0) this.store.update(id, { groupId: void 0 });
6216
- }
6217
- this.historyRecorder.commit();
6589
+ this.selectionOps.ungroup();
6218
6590
  }
6219
6591
  toggleLockSelection() {
6220
- const ids = this.getSelectedIds();
6221
- if (ids.length === 0) return;
6222
- const anyUnlocked = ids.some((id) => {
6223
- const el = this.store.getById(id);
6224
- return el ? !el.locked : false;
6225
- });
6226
- this.historyRecorder.begin();
6227
- for (const id of ids) {
6228
- const el = this.store.getById(id);
6229
- if (el && el.locked !== anyUnlocked) this.store.update(id, { locked: anyUnlocked });
6230
- }
6231
- this.historyRecorder.commit();
6592
+ this.selectionOps.toggleLock();
6232
6593
  }
6233
6594
  alignSelection(edge) {
6234
- const bounded = this.boundedSelection();
6235
- if (bounded.length < 2) return;
6236
- const B = unionBounds(bounded.map((e) => e.bounds));
6237
- this.historyRecorder.begin();
6238
- const moved = [];
6239
- for (const { id, el, bounds: b } of bounded) {
6240
- if (!this.isMovable(el)) continue;
6241
- let dx = 0;
6242
- let dy = 0;
6243
- switch (edge) {
6244
- case "left":
6245
- dx = B.x - b.x;
6246
- break;
6247
- case "right":
6248
- dx = B.x + B.w - (b.x + b.w);
6249
- break;
6250
- case "center-x":
6251
- dx = B.x + B.w / 2 - (b.x + b.w / 2);
6252
- break;
6253
- case "top":
6254
- dy = B.y - b.y;
6255
- break;
6256
- case "bottom":
6257
- dy = B.y + B.h - (b.y + b.h);
6258
- break;
6259
- case "middle":
6260
- dy = B.y + B.h / 2 - (b.y + b.h / 2);
6261
- break;
6262
- }
6263
- if (dx === 0 && dy === 0) continue;
6264
- this.store.update(id, translateElementPatch(el, dx, dy));
6265
- moved.push(id);
6266
- }
6267
- updateArrowsBoundToElements(moved, this.store);
6268
- this.historyRecorder.commit();
6269
- this.requestRender();
6595
+ this.selectionOps.align(edge);
6270
6596
  }
6271
6597
  distributeSelection(axis) {
6272
- const bounded = this.boundedSelection();
6273
- if (bounded.length < 3) return;
6274
- const center2 = (b) => axis === "horizontal" ? b.x + b.w / 2 : b.y + b.h / 2;
6275
- const sorted = [...bounded].sort((p, q) => center2(p.bounds) - center2(q.bounds));
6276
- const first = sorted[0];
6277
- const last = sorted[sorted.length - 1];
6278
- if (!first || !last) return;
6279
- const c0 = center2(first.bounds);
6280
- const cN = center2(last.bounds);
6281
- const n = sorted.length;
6282
- this.historyRecorder.begin();
6283
- const moved = [];
6284
- for (let i = 1; i < n - 1; i++) {
6285
- const item = sorted[i];
6286
- if (!item || !this.isMovable(item.el)) continue;
6287
- const target = c0 + i * (cN - c0) / (n - 1);
6288
- const delta = target - center2(item.bounds);
6289
- if (delta === 0) continue;
6290
- const [dx, dy] = axis === "horizontal" ? [delta, 0] : [0, delta];
6291
- this.store.update(item.id, translateElementPatch(item.el, dx, dy));
6292
- moved.push(item.id);
6293
- }
6294
- updateArrowsBoundToElements(moved, this.store);
6295
- this.historyRecorder.commit();
6296
- this.requestRender();
6297
- }
6298
- boundedSelection() {
6299
- const out = [];
6300
- for (const id of this.getSelectedIds()) {
6301
- const el = this.store.getById(id);
6302
- if (!el) continue;
6303
- const bounds = getElementBounds(el);
6304
- if (bounds) out.push({ id, el, bounds });
6305
- }
6306
- return out;
6307
- }
6308
- isMovable(el) {
6309
- if (el.locked) return false;
6310
- if (el.type === "arrow" && (el.fromBinding ?? el.toBinding)) return false;
6311
- return true;
6598
+ this.selectionOps.distribute(axis);
6312
6599
  }
6313
6600
  getRenderStats() {
6314
6601
  return this.renderLoop.getStats();
@@ -6329,10 +6616,10 @@ var Viewport = class {
6329
6616
  this.arrowLabelEditor.cancel();
6330
6617
  this.historyRecorder.destroy();
6331
6618
  this.contextMenu?.dispose();
6332
- this.wrapper.removeEventListener("pointerdown", this.onTapDown);
6333
- this.wrapper.removeEventListener("pointerup", this.onDoubleTap);
6334
- this.wrapper.removeEventListener("dragover", this.onDragOver);
6335
- this.wrapper.removeEventListener("drop", this.onDrop);
6619
+ this.wrapper.removeEventListener("pointerdown", this.interactions.onTapDown);
6620
+ this.wrapper.removeEventListener("pointerup", this.interactions.onDoubleTap);
6621
+ this.wrapper.removeEventListener("dragover", this.interactions.onDragOver);
6622
+ this.wrapper.removeEventListener("drop", this.interactions.onDrop);
6336
6623
  this.inputHandler.destroy();
6337
6624
  this.unsubCamera();
6338
6625
  this.unsubToolChange();
@@ -6341,148 +6628,9 @@ var Viewport = class {
6341
6628
  this.resizeObserver = null;
6342
6629
  this.wrapper.remove();
6343
6630
  }
6344
- startEditingElement(id) {
6345
- const element = this.store.getById(id);
6346
- if (!element || element.type !== "note" && element.type !== "text") return;
6347
- this.renderLoop.flush();
6348
- const node = this.domNodeManager.getNode(id);
6349
- if (node) {
6350
- this.noteEditor.startEditing(node, id, this.store);
6351
- }
6352
- }
6353
- fitNoteHeight(elementId) {
6354
- const element = this.store.getById(elementId);
6355
- if (!element || element.type !== "note") return;
6356
- if (isNoteContentEmpty(element.text)) return;
6357
- const node = this.domNodeManager.getNode(elementId);
6358
- if (!node) return;
6359
- const measured = node.scrollHeight;
6360
- if (measured > element.size.h) {
6361
- this.store.update(elementId, { size: { w: element.size.w, h: measured } });
6362
- }
6363
- }
6364
- onTextEditStop(elementId) {
6365
- const element = this.store.getById(elementId);
6366
- if (!element) return;
6367
- if (element.type === "note") {
6368
- if (isNoteContentEmpty(element.text)) {
6369
- this.store.remove(elementId);
6370
- return;
6371
- }
6372
- this.fitNoteHeight(elementId);
6373
- return;
6374
- }
6375
- if (element.type !== "text") return;
6376
- if (!element.text || element.text.trim() === "") {
6377
- this.store.remove(elementId);
6378
- return;
6379
- }
6380
- const node = this.domNodeManager.getNode(elementId);
6381
- if (node && "size" in element) {
6382
- const measured = node.scrollHeight;
6383
- if (measured !== element.size.h) {
6384
- this.store.update(elementId, { size: { w: element.size.w, h: measured } });
6385
- }
6386
- }
6387
- }
6388
- onTapDown = (e) => {
6389
- this.tapDownX = e.clientX;
6390
- this.tapDownY = e.clientY;
6391
- };
6392
- onDoubleTap = (e) => {
6393
- const dx = e.clientX - this.tapDownX;
6394
- const dy = e.clientY - this.tapDownY;
6395
- const moved = Math.sqrt(dx * dx + dy * dy);
6396
- if (moved > 10) return;
6397
- if (!this.doubleTapDetector.feed(e)) return;
6398
- if (typeof document.elementFromPoint !== "function") return;
6399
- const el = document.elementFromPoint(e.clientX, e.clientY);
6400
- const nodeEl = el?.closest("[data-element-id]");
6401
- if (nodeEl) {
6402
- const elementId = nodeEl.dataset["elementId"];
6403
- if (elementId) {
6404
- const element = this.store.getById(elementId);
6405
- if (element?.type === "note" || element?.type === "text") {
6406
- this.startEditingElement(elementId);
6407
- return;
6408
- }
6409
- }
6410
- }
6411
- const rect = this.wrapper.getBoundingClientRect();
6412
- const screen = { x: e.clientX - rect.left, y: e.clientY - rect.top };
6413
- const world = this.camera.screenToWorld(screen);
6414
- const hit = this.hitTestWorld(world);
6415
- if (hit?.type === "html") {
6416
- this.interactMode.startInteracting(hit.id);
6417
- return;
6418
- }
6419
- const arrow = this.findArrowAt(world);
6420
- if (arrow) {
6421
- this.startArrowLabelEdit(arrow);
6422
- }
6423
- };
6424
- findArrowAt(world) {
6425
- const candidates = this.store.queryPoint(world).reverse();
6426
- for (const el of candidates) {
6427
- if (el.type === "arrow" && isNearBezier(world, el.from, el.to, el.bend, ARROW_HIT_THRESHOLD)) {
6428
- return el;
6429
- }
6430
- }
6431
- return void 0;
6432
- }
6433
- startArrowLabelEdit(arrow) {
6434
- this.arrowLabelEditor.startEditing({
6435
- arrow,
6436
- layer: this.domLayer,
6437
- store: this.store,
6438
- recorder: this.historyRecorder,
6439
- onDone: () => {
6440
- this.renderer.setLabelEditingId(null);
6441
- this.requestRender();
6442
- }
6443
- });
6444
- this.renderer.setLabelEditingId(arrow.id);
6445
- }
6446
- hitTestWorld(world) {
6447
- const candidates = this.store.queryPoint(world).reverse();
6448
- for (const el of candidates) {
6449
- if (!("size" in el)) continue;
6450
- const { x, y } = el.position;
6451
- const { w, h } = el.size;
6452
- if (world.x >= x && world.x <= x + w && world.y >= y && world.y <= y + h) {
6453
- return el;
6454
- }
6455
- }
6456
- return null;
6457
- }
6458
6631
  stopInteracting() {
6459
6632
  this.interactMode.stopInteracting();
6460
6633
  }
6461
- onDragOver = (e) => {
6462
- e.preventDefault();
6463
- };
6464
- onDrop = (e) => {
6465
- e.preventDefault();
6466
- const rect = this.wrapper.getBoundingClientRect();
6467
- const screenPos = { x: e.clientX - rect.left, y: e.clientY - rect.top };
6468
- const worldPos = this.camera.screenToWorld(screenPos);
6469
- if (this.dropHandler) {
6470
- this.dropHandler(e, worldPos);
6471
- return;
6472
- }
6473
- const files = e.dataTransfer?.files;
6474
- if (!files) return;
6475
- for (const file of files) {
6476
- if (!file.type.startsWith("image/")) continue;
6477
- const reader = new FileReader();
6478
- reader.onload = () => {
6479
- const src = reader.result;
6480
- if (typeof src !== "string") return;
6481
- this.addImage(src, worldPos);
6482
- };
6483
- reader.readAsDataURL(file);
6484
- }
6485
- };
6486
6634
  unbindArrowsFrom(removedElement) {
6487
6635
  const boundArrows = findBoundArrows(removedElement.id, this.store);
6488
6636
  const bounds = getElementBounds(removedElement);
@@ -6517,43 +6665,6 @@ var Viewport = class {
6517
6665
  }
6518
6666
  }
6519
6667
  }
6520
- createWrapper() {
6521
- const el = document.createElement("div");
6522
- Object.assign(el.style, {
6523
- position: "relative",
6524
- width: "100%",
6525
- height: "100%",
6526
- overflow: "hidden",
6527
- overscrollBehavior: "none",
6528
- userSelect: "none",
6529
- webkitUserSelect: "none"
6530
- });
6531
- return el;
6532
- }
6533
- createCanvas() {
6534
- const el = document.createElement("canvas");
6535
- Object.assign(el.style, {
6536
- position: "absolute",
6537
- top: "0",
6538
- left: "0",
6539
- width: "100%",
6540
- height: "100%"
6541
- });
6542
- return el;
6543
- }
6544
- createDomLayer() {
6545
- const el = document.createElement("div");
6546
- Object.assign(el.style, {
6547
- position: "absolute",
6548
- top: "0",
6549
- left: "0",
6550
- width: "100%",
6551
- height: "100%",
6552
- pointerEvents: "none",
6553
- transformOrigin: "0 0"
6554
- });
6555
- return el;
6556
- }
6557
6668
  applyCameraTransform() {
6558
6669
  this.domLayer.style.transform = this.camera.toCSSTransform();
6559
6670
  }
@@ -6563,25 +6674,6 @@ var Viewport = class {
6563
6674
  this.renderLoop.setCanvasSize(rect.width * dpr, rect.height * dpr);
6564
6675
  this.requestRender();
6565
6676
  }
6566
- syncGridContext() {
6567
- const grid = this.store.getElementsByType("grid")[0];
6568
- if (grid) {
6569
- this.toolContext.gridSize = grid.cellSize;
6570
- this.toolContext.gridType = grid.gridType;
6571
- this.toolContext.hexOrientation = grid.hexOrientation;
6572
- } else {
6573
- this.toolContext.gridSize = this._gridSize;
6574
- this.toolContext.gridType = void 0;
6575
- this.toolContext.hexOrientation = void 0;
6576
- }
6577
- this.notifyGridChangeListeners();
6578
- }
6579
- notifyGridChangeListeners() {
6580
- const info = this.getGridInfo();
6581
- for (const listener of this.gridChangeListeners) {
6582
- listener(info);
6583
- }
6584
- }
6585
6677
  observeResize() {
6586
6678
  if (typeof ResizeObserver === "undefined") return;
6587
6679
  this.resizeObserver = new ResizeObserver(() => this.syncCanvasSize());
@@ -8895,7 +8987,7 @@ var TemplateTool = class {
8895
8987
  };
8896
8988
 
8897
8989
  // src/index.ts
8898
- var VERSION = "0.38.0";
8990
+ var VERSION = "0.38.3";
8899
8991
  export {
8900
8992
  ArrowTool,
8901
8993
  AutoSave,