@fieldnotes/core 0.38.0 → 0.38.2

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,16 +5745,244 @@ 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
  }
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);
5790
+ }
5791
+ }
5792
+ this.deps.recorder.commit();
5793
+ }
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();
5803
+ }
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();
5813
+ }
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.ts
5982
+ var EMPTY_IDS = [];
5983
+ var ARROW_HIT_THRESHOLD = 10;
5984
+ function noop() {
5985
+ }
5719
5986
  var Viewport = class {
5720
5987
  constructor(container, options = {}) {
5721
5988
  this.container = container;
@@ -5758,9 +6025,15 @@ var Viewport = class {
5758
6025
  this.dropHandler = options.onDrop;
5759
6026
  this.history = new HistoryStack();
5760
6027
  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();
6028
+ this.selectionOps = new SelectionOps({
6029
+ store: this.store,
6030
+ recorder: this.historyRecorder,
6031
+ getSelectedIds: () => this.getSelectedIds(),
6032
+ requestRender: () => this.requestRender()
6033
+ });
6034
+ this.wrapper = createWrapper();
6035
+ this.canvasEl = createCanvas();
6036
+ this.domLayer = createDomLayer();
5764
6037
  this.wrapper.appendChild(this.canvasEl);
5765
6038
  this.wrapper.appendChild(this.domLayer);
5766
6039
  this.container.appendChild(this.wrapper);
@@ -5838,21 +6111,29 @@ var Viewport = class {
5838
6111
  this.contextMenu?.close();
5839
6112
  this.requestRender();
5840
6113
  });
6114
+ this.gridController = new GridController({
6115
+ store: this.store,
6116
+ recorder: this.historyRecorder,
6117
+ requestRender: () => this.requestRender(),
6118
+ getActiveLayerId: () => this.layerManager.activeLayerId,
6119
+ toolContext: this.toolContext,
6120
+ defaultGridSize: this._gridSize
6121
+ });
5841
6122
  this.unsubStore = [
5842
6123
  this.store.on("add", (el) => {
5843
- if (el.type === "grid") this.syncGridContext();
6124
+ if (el.type === "grid") this.gridController.syncContext();
5844
6125
  this.renderLoop.markLayerDirty(el.layerId);
5845
6126
  this.requestRender();
5846
6127
  }),
5847
6128
  this.store.on("remove", (el) => {
5848
- if (el.type === "grid") this.syncGridContext();
6129
+ if (el.type === "grid") this.gridController.syncContext();
5849
6130
  this.unbindArrowsFrom(el);
5850
6131
  this.domNodeManager.removeDomNode(el.id);
5851
6132
  this.renderLoop.markLayerDirty(el.layerId);
5852
6133
  this.requestRender();
5853
6134
  }),
5854
6135
  this.store.on("update", ({ previous, current }) => {
5855
- if (current.type === "grid") this.syncGridContext();
6136
+ if (current.type === "grid") this.gridController.syncContext();
5856
6137
  this.renderLoop.markLayerDirty(current.layerId);
5857
6138
  if (previous.layerId !== current.layerId) {
5858
6139
  this.renderLoop.markLayerDirty(previous.layerId);
@@ -5862,7 +6143,7 @@ var Viewport = class {
5862
6143
  this.store.on("clear", () => {
5863
6144
  this.domNodeManager.clearDomNodes();
5864
6145
  this.renderLoop.markAllLayersDirty();
5865
- this.syncGridContext();
6146
+ this.gridController.syncContext();
5866
6147
  this.requestRender();
5867
6148
  })
5868
6149
  ];
@@ -5877,7 +6158,7 @@ var Viewport = class {
5877
6158
  this.observeResize();
5878
6159
  this.syncCanvasSize();
5879
6160
  this.renderLoop.start();
5880
- this.syncGridContext();
6161
+ this.gridController.syncContext();
5881
6162
  }
5882
6163
  camera;
5883
6164
  store;
@@ -5896,6 +6177,7 @@ var Viewport = class {
5896
6177
  noteEditor;
5897
6178
  arrowLabelEditor;
5898
6179
  historyRecorder;
6180
+ selectionOps;
5899
6181
  toolContext;
5900
6182
  marginViewport;
5901
6183
  resizeObserver = null;
@@ -5907,7 +6189,7 @@ var Viewport = class {
5907
6189
  interactMode;
5908
6190
  onHtmlElementMount;
5909
6191
  dropHandler;
5910
- gridChangeListeners = /* @__PURE__ */ new Set();
6192
+ gridController;
5911
6193
  doubleTapDetector = new DoubleTapDetector();
5912
6194
  tapDownX = 0;
5913
6195
  tapDownY = 0;
@@ -6079,48 +6361,19 @@ var Viewport = class {
6079
6361
  this.requestRender();
6080
6362
  }
6081
6363
  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;
6364
+ return this.gridController.add(input);
6092
6365
  }
6093
6366
  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();
6367
+ this.gridController.update(updates);
6100
6368
  }
6101
6369
  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();
6370
+ this.gridController.remove();
6108
6371
  }
6109
6372
  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
- };
6373
+ return this.gridController.getInfo();
6118
6374
  }
6119
6375
  onGridChange(listener) {
6120
- this.gridChangeListeners.add(listener);
6121
- return () => {
6122
- this.gridChangeListeners.delete(listener);
6123
- };
6376
+ return this.gridController.onChange(listener);
6124
6377
  }
6125
6378
  getSelectTool() {
6126
6379
  return this.toolManager.getTool("select");
@@ -6161,154 +6414,25 @@ var Viewport = class {
6161
6414
  return tool ? tool.onSelectionChange(listener) : noop;
6162
6415
  }
6163
6416
  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;
6417
+ return this.selectionOps.getStyle();
6184
6418
  }
6185
6419
  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();
6420
+ this.selectionOps.applyStyle(style);
6198
6421
  }
6199
6422
  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();
6423
+ this.selectionOps.group();
6208
6424
  }
6209
6425
  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();
6426
+ this.selectionOps.ungroup();
6218
6427
  }
6219
6428
  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();
6429
+ this.selectionOps.toggleLock();
6232
6430
  }
6233
6431
  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();
6432
+ this.selectionOps.align(edge);
6270
6433
  }
6271
6434
  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;
6435
+ this.selectionOps.distribute(axis);
6312
6436
  }
6313
6437
  getRenderStats() {
6314
6438
  return this.renderLoop.getStats();
@@ -6517,43 +6641,6 @@ var Viewport = class {
6517
6641
  }
6518
6642
  }
6519
6643
  }
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
6644
  applyCameraTransform() {
6558
6645
  this.domLayer.style.transform = this.camera.toCSSTransform();
6559
6646
  }
@@ -6563,25 +6650,6 @@ var Viewport = class {
6563
6650
  this.renderLoop.setCanvasSize(rect.width * dpr, rect.height * dpr);
6564
6651
  this.requestRender();
6565
6652
  }
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
6653
  observeResize() {
6586
6654
  if (typeof ResizeObserver === "undefined") return;
6587
6655
  this.resizeObserver = new ResizeObserver(() => this.syncCanvasSize());
@@ -7114,14 +7182,11 @@ function computeSnapGuides(moving, targets, threshold) {
7114
7182
  return { dx: xSnap?.delta ?? 0, dy: ySnap?.delta ?? 0, guides };
7115
7183
  }
7116
7184
 
7117
- // src/tools/select-tool.ts
7185
+ // src/tools/select-overlay.ts
7118
7186
  var HANDLE_SIZE = 8;
7119
- var SNAP_PX = 6;
7120
7187
  var HANDLE_HIT_PADDING2 = 4;
7121
7188
  var SELECTION_PAD = 4;
7122
- var MIN_ELEMENT_SIZE = 20;
7123
7189
  var ROTATE_HANDLE_OFFSET = 24;
7124
- var ROTATE_SNAP = Math.PI / 12;
7125
7190
  var ROTATABLE_TYPES = /* @__PURE__ */ new Set(["note", "text", "image", "html", "shape", "stroke"]);
7126
7191
  var HANDLE_CURSORS = {
7127
7192
  nw: "nwse-resize",
@@ -7129,6 +7194,486 @@ var HANDLE_CURSORS = {
7129
7194
  ne: "nesw-resize",
7130
7195
  sw: "nesw-resize"
7131
7196
  };
7197
+ function getOverlayLayout(el, zoom) {
7198
+ const bounds = getElementBounds(el);
7199
+ if (!bounds) return null;
7200
+ const angle = el.rotation ?? 0;
7201
+ const pad = SELECTION_PAD / zoom;
7202
+ const center2 = { x: bounds.x + bounds.w / 2, y: bounds.y + bounds.h / 2 };
7203
+ const raw = [
7204
+ ["nw", { x: bounds.x - pad, y: bounds.y - pad }],
7205
+ ["ne", { x: bounds.x + bounds.w + pad, y: bounds.y - pad }],
7206
+ ["sw", { x: bounds.x - pad, y: bounds.y + bounds.h + pad }],
7207
+ ["se", { x: bounds.x + bounds.w + pad, y: bounds.y + bounds.h + pad }]
7208
+ ];
7209
+ const corners = raw.map(
7210
+ ([h, p]) => [h, rotatePoint(p, center2, angle)]
7211
+ );
7212
+ const topMid = { x: center2.x, y: bounds.y - pad - ROTATE_HANDLE_OFFSET / zoom };
7213
+ const rotateHandle = rotatePoint(topMid, center2, angle);
7214
+ return { center: center2, corners, rotateHandle, angle };
7215
+ }
7216
+ function getHandlePositions(bounds) {
7217
+ return [
7218
+ ["nw", { x: bounds.x, y: bounds.y }],
7219
+ ["ne", { x: bounds.x + bounds.w, y: bounds.y }],
7220
+ ["sw", { x: bounds.x, y: bounds.y + bounds.h }],
7221
+ ["se", { x: bounds.x + bounds.w, y: bounds.y + bounds.h }]
7222
+ ];
7223
+ }
7224
+ function topMidpoint(layout) {
7225
+ const nw = layout.corners.find(([h]) => h === "nw")?.[1] ?? { x: 0, y: 0 };
7226
+ const ne = layout.corners.find(([h]) => h === "ne")?.[1] ?? { x: 0, y: 0 };
7227
+ return { x: (nw.x + ne.x) / 2, y: (nw.y + ne.y) / 2 };
7228
+ }
7229
+ function drawLockBadge(ctx, at, zoom) {
7230
+ const r = 9 / zoom;
7231
+ ctx.save();
7232
+ ctx.setLineDash([]);
7233
+ ctx.beginPath();
7234
+ ctx.arc(at.x, at.y, r, 0, Math.PI * 2);
7235
+ ctx.fillStyle = "#ffffff";
7236
+ ctx.fill();
7237
+ ctx.strokeStyle = "#2196F3";
7238
+ ctx.lineWidth = 1.5 / zoom;
7239
+ ctx.stroke();
7240
+ const bw = 8 / zoom;
7241
+ const bh = 6 / zoom;
7242
+ ctx.fillStyle = "#2196F3";
7243
+ ctx.fillRect(at.x - bw / 2, at.y - bh / 2 + 1 / zoom, bw, bh);
7244
+ ctx.beginPath();
7245
+ ctx.arc(at.x, at.y - bh / 2 + 1 / zoom, 2.5 / zoom, Math.PI, 0);
7246
+ ctx.lineWidth = 1.4 / zoom;
7247
+ ctx.stroke();
7248
+ ctx.restore();
7249
+ }
7250
+ function renderMarquee(ctx, rect) {
7251
+ ctx.save();
7252
+ ctx.strokeStyle = "#2196F3";
7253
+ ctx.fillStyle = "rgba(33, 150, 243, 0.08)";
7254
+ ctx.lineWidth = 1;
7255
+ ctx.setLineDash([4, 4]);
7256
+ ctx.strokeRect(rect.x, rect.y, rect.w, rect.h);
7257
+ ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
7258
+ ctx.restore();
7259
+ }
7260
+ function renderBindingHighlights(ctx, arrow, zoom, store) {
7261
+ if (!arrow.fromBinding && !arrow.toBinding) return;
7262
+ const pad = SELECTION_PAD / zoom;
7263
+ ctx.save();
7264
+ ctx.strokeStyle = "#2196F3";
7265
+ ctx.lineWidth = 2 / zoom;
7266
+ ctx.setLineDash([]);
7267
+ const drawn = /* @__PURE__ */ new Set();
7268
+ for (const binding of [arrow.fromBinding, arrow.toBinding]) {
7269
+ if (!binding || drawn.has(binding.elementId)) continue;
7270
+ drawn.add(binding.elementId);
7271
+ const target = store.getById(binding.elementId);
7272
+ if (!target) continue;
7273
+ const bounds = getElementBounds(target);
7274
+ if (!bounds) continue;
7275
+ ctx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.w + pad * 2, bounds.h + pad * 2);
7276
+ }
7277
+ ctx.restore();
7278
+ }
7279
+ function renderSelectionBoxes(ctx, p) {
7280
+ if (p.selectedIds.length === 0) return;
7281
+ const zoom = p.zoom;
7282
+ const handleWorldSize = HANDLE_SIZE / zoom;
7283
+ ctx.save();
7284
+ ctx.strokeStyle = "#2196F3";
7285
+ ctx.lineWidth = 1.5 / zoom;
7286
+ ctx.setLineDash([4 / zoom, 4 / zoom]);
7287
+ for (const id of p.selectedIds) {
7288
+ const el = p.store.getById(id);
7289
+ if (!el) continue;
7290
+ if (el.type === "arrow") {
7291
+ renderArrowHandles(ctx, el, zoom);
7292
+ renderBindingHighlights(ctx, el, zoom, p.store);
7293
+ continue;
7294
+ }
7295
+ if (el.type === "shape" && el.shape === "line") {
7296
+ ctx.setLineDash([]);
7297
+ ctx.fillStyle = "#ffffff";
7298
+ const r = handleWorldSize / 2;
7299
+ for (const pt of lineEndpoints(el)) {
7300
+ ctx.beginPath();
7301
+ ctx.arc(pt.x, pt.y, r, 0, Math.PI * 2);
7302
+ ctx.fill();
7303
+ ctx.stroke();
7304
+ }
7305
+ ctx.setLineDash([4 / zoom, 4 / zoom]);
7306
+ continue;
7307
+ }
7308
+ const bounds = getElementBounds(el);
7309
+ if (!bounds) continue;
7310
+ const layout = getOverlayLayout(el, zoom);
7311
+ if (!layout) continue;
7312
+ const pad = SELECTION_PAD / zoom;
7313
+ if (layout.angle === 0) {
7314
+ ctx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.w + pad * 2, bounds.h + pad * 2);
7315
+ } else {
7316
+ const ordered = ["nw", "ne", "se", "sw"].map((h) => layout.corners.find(([c]) => c === h)?.[1]).filter((pp) => !!pp);
7317
+ const [p0, ...others] = ordered;
7318
+ if (p0) {
7319
+ ctx.beginPath();
7320
+ ctx.moveTo(p0.x, p0.y);
7321
+ for (const pp of others) ctx.lineTo(pp.x, pp.y);
7322
+ ctx.closePath();
7323
+ ctx.stroke();
7324
+ }
7325
+ }
7326
+ if (!el.locked) {
7327
+ if ("size" in el) {
7328
+ ctx.setLineDash([]);
7329
+ ctx.fillStyle = "#ffffff";
7330
+ const corners = layout.angle === 0 ? getHandlePositions(bounds) : layout.corners;
7331
+ for (const [, pos] of corners) {
7332
+ ctx.fillRect(
7333
+ pos.x - handleWorldSize / 2,
7334
+ pos.y - handleWorldSize / 2,
7335
+ handleWorldSize,
7336
+ handleWorldSize
7337
+ );
7338
+ ctx.strokeRect(
7339
+ pos.x - handleWorldSize / 2,
7340
+ pos.y - handleWorldSize / 2,
7341
+ handleWorldSize,
7342
+ handleWorldSize
7343
+ );
7344
+ }
7345
+ ctx.setLineDash([4 / zoom, 4 / zoom]);
7346
+ } else if (el.type === "template") {
7347
+ ctx.setLineDash([]);
7348
+ ctx.fillStyle = "#ffffff";
7349
+ const hx = bounds.x + bounds.w;
7350
+ const hy = bounds.y + bounds.h;
7351
+ ctx.fillRect(
7352
+ hx - handleWorldSize / 2,
7353
+ hy - handleWorldSize / 2,
7354
+ handleWorldSize,
7355
+ handleWorldSize
7356
+ );
7357
+ ctx.strokeRect(
7358
+ hx - handleWorldSize / 2,
7359
+ hy - handleWorldSize / 2,
7360
+ handleWorldSize,
7361
+ handleWorldSize
7362
+ );
7363
+ ctx.setLineDash([4 / zoom, 4 / zoom]);
7364
+ }
7365
+ if (p.selectedIds.length === 1 && ROTATABLE_TYPES.has(el.type)) {
7366
+ const stemStart = topMidpoint(layout);
7367
+ const stemEnd = layout.rotateHandle;
7368
+ ctx.beginPath();
7369
+ ctx.moveTo(stemStart.x, stemStart.y);
7370
+ ctx.lineTo(stemEnd.x, stemEnd.y);
7371
+ ctx.stroke();
7372
+ ctx.setLineDash([]);
7373
+ ctx.fillStyle = "#ffffff";
7374
+ ctx.beginPath();
7375
+ ctx.arc(stemEnd.x, stemEnd.y, handleWorldSize / 2, 0, Math.PI * 2);
7376
+ ctx.fill();
7377
+ ctx.stroke();
7378
+ ctx.setLineDash([4 / zoom, 4 / zoom]);
7379
+ }
7380
+ }
7381
+ if (el.locked) {
7382
+ const ne = layout.corners.find(([h]) => h === "ne")?.[1];
7383
+ if (ne) drawLockBadge(ctx, ne, zoom);
7384
+ }
7385
+ }
7386
+ ctx.restore();
7387
+ }
7388
+ function renderGuideLines(ctx, p) {
7389
+ const zoom = p.zoom;
7390
+ const rect = p.rect;
7391
+ ctx.save();
7392
+ ctx.strokeStyle = "#FF4081";
7393
+ ctx.lineWidth = 1 / zoom;
7394
+ ctx.setLineDash([]);
7395
+ for (const g of p.guides) {
7396
+ ctx.beginPath();
7397
+ if (g.axis === "x") {
7398
+ const y0 = rect ? rect.y : p.currentWorld.y - 1e5;
7399
+ const y1 = rect ? rect.y + rect.h : p.currentWorld.y + 1e5;
7400
+ ctx.moveTo(g.position, y0);
7401
+ ctx.lineTo(g.position, y1);
7402
+ } else {
7403
+ const x0 = rect ? rect.x : p.currentWorld.x - 1e5;
7404
+ const x1 = rect ? rect.x + rect.w : p.currentWorld.x + 1e5;
7405
+ ctx.moveTo(x0, g.position);
7406
+ ctx.lineTo(x1, g.position);
7407
+ }
7408
+ ctx.stroke();
7409
+ }
7410
+ ctx.restore();
7411
+ }
7412
+
7413
+ // src/tools/select-hit.ts
7414
+ function hitTest(world, ctx) {
7415
+ const r = 10;
7416
+ const candidates = ctx.store.queryRect({ x: world.x - r, y: world.y - r, w: r * 2, h: r * 2 }).reverse();
7417
+ for (const el of candidates) {
7418
+ if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
7419
+ if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
7420
+ if (el.type === "grid") continue;
7421
+ if (isInsideBounds(world, el)) return el;
7422
+ }
7423
+ return null;
7424
+ }
7425
+ function isInsideBounds(point, el) {
7426
+ if (el.type === "grid") return false;
7427
+ const angle = el.rotation ?? 0;
7428
+ if (angle !== 0) {
7429
+ const b = getElementBounds(el);
7430
+ if (b) {
7431
+ point = rotatePoint(point, { x: b.x + b.w / 2, y: b.y + b.h / 2 }, -angle);
7432
+ }
7433
+ }
7434
+ if (el.type === "shape" && el.shape === "line") {
7435
+ const [a, b] = lineEndpoints(el);
7436
+ const threshold = Math.max(el.strokeWidth / 2, 6);
7437
+ return distSqToSegment(point, a, b) <= threshold * threshold;
7438
+ }
7439
+ if ("size" in el) {
7440
+ const s = el.size;
7441
+ return point.x >= el.position.x && point.x <= el.position.x + s.w && point.y >= el.position.y && point.y <= el.position.y + s.h;
7442
+ }
7443
+ if (el.type === "stroke") {
7444
+ return hitTestStroke(el, point, 10);
7445
+ }
7446
+ if (el.type === "arrow") {
7447
+ return isNearBezier(point, el.from, el.to, el.bend, 10);
7448
+ }
7449
+ if (el.type === "template") {
7450
+ const bounds = getElementBounds(el);
7451
+ if (!bounds) return false;
7452
+ return point.x >= bounds.x && point.x <= bounds.x + bounds.w && point.y >= bounds.y && point.y <= bounds.y + bounds.h;
7453
+ }
7454
+ return false;
7455
+ }
7456
+ function hitTestResizeHandle(world, ctx, selectedIds) {
7457
+ if (selectedIds.length === 0) return null;
7458
+ const zoom = ctx.camera.zoom;
7459
+ const handleHalf = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
7460
+ for (const id of selectedIds) {
7461
+ const el = ctx.store.getById(id);
7462
+ if (!el || !("size" in el)) continue;
7463
+ if (el.locked) continue;
7464
+ if (el.type === "shape" && el.shape === "line") continue;
7465
+ const layout = getOverlayLayout(el, zoom);
7466
+ if (!layout) continue;
7467
+ for (const [handle, pos] of layout.corners) {
7468
+ if (Math.abs(world.x - pos.x) <= handleHalf && Math.abs(world.y - pos.y) <= handleHalf) {
7469
+ return { elementId: id, handle };
7470
+ }
7471
+ }
7472
+ }
7473
+ return null;
7474
+ }
7475
+ function hitTestRotateHandle(world, ctx, selectedIds) {
7476
+ if (selectedIds.length !== 1) return null;
7477
+ const id = selectedIds[0];
7478
+ if (!id) return null;
7479
+ const el = ctx.store.getById(id);
7480
+ if (!el || el.locked || !ROTATABLE_TYPES.has(el.type)) return null;
7481
+ const layout = getOverlayLayout(el, ctx.camera.zoom);
7482
+ if (!layout) return null;
7483
+ const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / ctx.camera.zoom;
7484
+ const dx = world.x - layout.rotateHandle.x;
7485
+ const dy = world.y - layout.rotateHandle.y;
7486
+ return dx * dx + dy * dy <= r * r ? { elementId: id } : null;
7487
+ }
7488
+ function hitTestLineHandles(world, ctx, selectedIds) {
7489
+ if (selectedIds.length === 0) return null;
7490
+ const zoom = ctx.camera.zoom;
7491
+ const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
7492
+ const r2 = r * r;
7493
+ for (const id of selectedIds) {
7494
+ const el = ctx.store.getById(id);
7495
+ if (!el || el.type !== "shape" || el.shape !== "line") continue;
7496
+ const [a, b] = lineEndpoints(el);
7497
+ if ((world.x - a.x) ** 2 + (world.y - a.y) ** 2 <= r2) return { elementId: id, fixed: b };
7498
+ if ((world.x - b.x) ** 2 + (world.y - b.y) ** 2 <= r2) return { elementId: id, fixed: a };
7499
+ }
7500
+ return null;
7501
+ }
7502
+ function hitTestTemplateResizeHandle(world, ctx, selectedIds) {
7503
+ if (selectedIds.length === 0) return null;
7504
+ const zoom = ctx.camera.zoom;
7505
+ const handleHalf = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
7506
+ for (const id of selectedIds) {
7507
+ const el = ctx.store.getById(id);
7508
+ if (!el || el.type !== "template") continue;
7509
+ const bounds = getElementBounds(el);
7510
+ if (!bounds) continue;
7511
+ const hx = bounds.x + bounds.w;
7512
+ const hy = bounds.y + bounds.h;
7513
+ if (Math.abs(world.x - hx) <= handleHalf && Math.abs(world.y - hy) <= handleHalf) {
7514
+ return id;
7515
+ }
7516
+ }
7517
+ return null;
7518
+ }
7519
+ function findElementsInRect(marquee, ctx) {
7520
+ const candidates = ctx.store.queryRect(marquee);
7521
+ const ids = [];
7522
+ for (const el of candidates) {
7523
+ if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
7524
+ if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
7525
+ if (el.type === "grid") continue;
7526
+ const bounds = getElementBounds(el);
7527
+ if (bounds && rectsOverlap(marquee, rotatedAABB(bounds, el.rotation ?? 0))) {
7528
+ ids.push(el.id);
7529
+ }
7530
+ }
7531
+ return ids;
7532
+ }
7533
+ function rectsOverlap(a, b) {
7534
+ return a.x <= b.x + b.w && a.x + a.w >= b.x && a.y <= b.y + b.h && a.y + a.h >= b.y;
7535
+ }
7536
+
7537
+ // src/tools/select-resize.ts
7538
+ var MIN_ELEMENT_SIZE = 20;
7539
+ function anchorOffset(handle, w, h) {
7540
+ switch (handle) {
7541
+ case "se":
7542
+ return { x: -w / 2, y: -h / 2 };
7543
+ case "sw":
7544
+ return { x: w / 2, y: -h / 2 };
7545
+ case "ne":
7546
+ return { x: -w / 2, y: h / 2 };
7547
+ case "nw":
7548
+ return { x: w / 2, y: h / 2 };
7549
+ default:
7550
+ return { x: 0, y: 0 };
7551
+ }
7552
+ }
7553
+ function computeResize(el, handle, world, lastWorld, aspectRatio, shiftKey) {
7554
+ const dx = world.x - lastWorld.x;
7555
+ const dy = world.y - lastWorld.y;
7556
+ let { x, y, w, h } = { x: el.position.x, y: el.position.y, w: el.size.w, h: el.size.h };
7557
+ switch (handle) {
7558
+ case "se":
7559
+ w += dx;
7560
+ h += dy;
7561
+ break;
7562
+ case "sw":
7563
+ x += dx;
7564
+ w -= dx;
7565
+ h += dy;
7566
+ break;
7567
+ case "ne":
7568
+ y += dy;
7569
+ w += dx;
7570
+ h -= dy;
7571
+ break;
7572
+ case "nw":
7573
+ x += dx;
7574
+ y += dy;
7575
+ w -= dx;
7576
+ h -= dy;
7577
+ break;
7578
+ }
7579
+ if (shiftKey && aspectRatio > 0) {
7580
+ const absDw = Math.abs(w - el.size.w);
7581
+ const absDh = Math.abs(h - el.size.h);
7582
+ if (absDw >= absDh) {
7583
+ h = w / aspectRatio;
7584
+ } else {
7585
+ w = h * aspectRatio;
7586
+ }
7587
+ if (handle === "nw" || handle === "sw") {
7588
+ x = el.position.x + el.size.w - w;
7589
+ }
7590
+ if (handle === "nw" || handle === "ne") {
7591
+ y = el.position.y + el.size.h - h;
7592
+ }
7593
+ }
7594
+ if (w < MIN_ELEMENT_SIZE) {
7595
+ if (handle === "nw" || handle === "sw") x = el.position.x + el.size.w - MIN_ELEMENT_SIZE;
7596
+ w = MIN_ELEMENT_SIZE;
7597
+ }
7598
+ if (h < MIN_ELEMENT_SIZE) {
7599
+ if (handle === "nw" || handle === "ne") y = el.position.y + el.size.h - MIN_ELEMENT_SIZE;
7600
+ h = MIN_ELEMENT_SIZE;
7601
+ }
7602
+ return { position: { x, y }, size: { w, h } };
7603
+ }
7604
+ function computeRotatedResize(el, handle, angle, world, lastWorld, aspectRatio, shiftKey) {
7605
+ const wdx = world.x - lastWorld.x;
7606
+ const wdy = world.y - lastWorld.y;
7607
+ const cosN = Math.cos(-angle);
7608
+ const sinN = Math.sin(-angle);
7609
+ const ldx = wdx * cosN - wdy * sinN;
7610
+ const ldy = wdx * sinN + wdy * cosN;
7611
+ let w = el.size.w;
7612
+ let h = el.size.h;
7613
+ switch (handle) {
7614
+ case "se":
7615
+ w += ldx;
7616
+ h += ldy;
7617
+ break;
7618
+ case "sw":
7619
+ w -= ldx;
7620
+ h += ldy;
7621
+ break;
7622
+ case "ne":
7623
+ w += ldx;
7624
+ h -= ldy;
7625
+ break;
7626
+ case "nw":
7627
+ w -= ldx;
7628
+ h -= ldy;
7629
+ break;
7630
+ }
7631
+ if (shiftKey && aspectRatio > 0) {
7632
+ const absDw = Math.abs(w - el.size.w);
7633
+ const absDh = Math.abs(h - el.size.h);
7634
+ if (absDw >= absDh) h = w / aspectRatio;
7635
+ else w = h * aspectRatio;
7636
+ }
7637
+ w = Math.max(w, MIN_ELEMENT_SIZE);
7638
+ h = Math.max(h, MIN_ELEMENT_SIZE);
7639
+ const oldCenter = { x: el.position.x + el.size.w / 2, y: el.position.y + el.size.h / 2 };
7640
+ const oldAnchorLocal = anchorOffset(handle, el.size.w, el.size.h);
7641
+ const anchorWorld = rotatePoint(
7642
+ { x: oldCenter.x + oldAnchorLocal.x, y: oldCenter.y + oldAnchorLocal.y },
7643
+ oldCenter,
7644
+ angle
7645
+ );
7646
+ const newAnchorLocal = anchorOffset(handle, w, h);
7647
+ const cos = Math.cos(angle);
7648
+ const sin = Math.sin(angle);
7649
+ const rotatedAnchor = {
7650
+ x: newAnchorLocal.x * cos - newAnchorLocal.y * sin,
7651
+ y: newAnchorLocal.x * sin + newAnchorLocal.y * cos
7652
+ };
7653
+ const newCenter = { x: anchorWorld.x - rotatedAnchor.x, y: anchorWorld.y - rotatedAnchor.y };
7654
+ const position = { x: newCenter.x - w / 2, y: newCenter.y - h / 2 };
7655
+ return { position, size: { w, h } };
7656
+ }
7657
+ function computeTemplateResize(el, world, opts) {
7658
+ const dx = world.x - el.position.x;
7659
+ const dy = world.y - el.position.y;
7660
+ let newRadius = Math.sqrt(dx * dx + dy * dy);
7661
+ if (opts.snapToGrid && opts.gridSize && opts.gridSize > 0) {
7662
+ const snapUnit = opts.gridType === "hex" ? Math.sqrt(3) * opts.gridSize : opts.gridSize;
7663
+ newRadius = Math.max(snapUnit, Math.round(newRadius / snapUnit) * snapUnit);
7664
+ }
7665
+ newRadius = Math.max(MIN_ELEMENT_SIZE, newRadius);
7666
+ const updates = { radius: newRadius };
7667
+ if (el.feetPerCell != null && opts.gridSize && opts.gridSize > 0) {
7668
+ const snapUnit = opts.gridType === "hex" ? Math.sqrt(3) * opts.gridSize : opts.gridSize;
7669
+ updates.radiusFeet = newRadius / snapUnit * el.feetPerCell;
7670
+ }
7671
+ return updates;
7672
+ }
7673
+
7674
+ // src/tools/select-tool.ts
7675
+ var SNAP_PX = 6;
7676
+ var ROTATE_SNAP = Math.PI / 12;
7132
7677
  var SelectTool = class {
7133
7678
  name = "select";
7134
7679
  _selectedIds = [];
@@ -7164,7 +7709,7 @@ var SelectTool = class {
7164
7709
  this.ctx?.requestRender();
7165
7710
  }
7166
7711
  selectAtPoint(world, ctx) {
7167
- const hit = this.hitTest(world, ctx);
7712
+ const hit = hitTest(world, ctx);
7168
7713
  if (!hit) {
7169
7714
  this.setSelectedIds([]);
7170
7715
  return;
@@ -7208,19 +7753,19 @@ var SelectTool = class {
7208
7753
  ctx.requestRender();
7209
7754
  return;
7210
7755
  }
7211
- const lineHit = this.hitTestLineHandles(world, ctx);
7756
+ const lineHit = hitTestLineHandles(world, ctx, this._selectedIds);
7212
7757
  if (lineHit) {
7213
7758
  this.mode = { type: "line-handle", elementId: lineHit.elementId, fixed: lineHit.fixed };
7214
7759
  ctx.requestRender();
7215
7760
  return;
7216
7761
  }
7217
- const templateResizeHit = this.hitTestTemplateResizeHandle(world, ctx);
7762
+ const templateResizeHit = hitTestTemplateResizeHandle(world, ctx, this._selectedIds);
7218
7763
  if (templateResizeHit) {
7219
7764
  this.mode = { type: "resizing-template", elementId: templateResizeHit };
7220
7765
  ctx.requestRender();
7221
7766
  return;
7222
7767
  }
7223
- const rotateHit = this.hitTestRotateHandle(world, ctx);
7768
+ const rotateHit = hitTestRotateHandle(world, ctx, this._selectedIds);
7224
7769
  if (rotateHit) {
7225
7770
  const el = ctx.store.getById(rotateHit.elementId);
7226
7771
  const layout = el ? this.getOverlayLayout(el, ctx.camera.zoom) : null;
@@ -7236,7 +7781,7 @@ var SelectTool = class {
7236
7781
  return;
7237
7782
  }
7238
7783
  }
7239
- const resizeHit = this.hitTestResizeHandle(world, ctx);
7784
+ const resizeHit = hitTestResizeHandle(world, ctx, this._selectedIds);
7240
7785
  if (resizeHit) {
7241
7786
  const el = ctx.store.getById(resizeHit.elementId);
7242
7787
  if (el && "size" in el) {
@@ -7252,7 +7797,7 @@ var SelectTool = class {
7252
7797
  }
7253
7798
  this.pendingSingleSelectId = null;
7254
7799
  this.hasDragged = false;
7255
- const hit = this.hitTest(world, ctx);
7800
+ const hit = hitTest(world, ctx);
7256
7801
  if (hit) {
7257
7802
  const all = ctx.store.getAll();
7258
7803
  const alreadySelected = this._selectedIds.includes(hit.id);
@@ -7390,7 +7935,7 @@ var SelectTool = class {
7390
7935
  if (this.mode.type === "marquee") {
7391
7936
  const rect = this.getMarqueeRect();
7392
7937
  if (rect) {
7393
- this.setSelectedIds(expandToGroups(this.findElementsInRect(rect, ctx), ctx.store.getAll()));
7938
+ this.setSelectedIds(expandToGroups(findElementsInRect(rect, ctx), ctx.store.getAll()));
7394
7939
  }
7395
7940
  ctx.requestRender();
7396
7941
  }
@@ -7417,8 +7962,16 @@ var SelectTool = class {
7417
7962
  this.setHovered(hoverId, ctx);
7418
7963
  }
7419
7964
  renderOverlay(canvasCtx) {
7420
- this.renderMarquee(canvasCtx);
7421
- this.renderSelectionBoxes(canvasCtx);
7965
+ if (this.mode.type === "marquee") {
7966
+ const rect = this.getMarqueeRect();
7967
+ if (rect) renderMarquee(canvasCtx, rect);
7968
+ }
7969
+ if (this.ctx)
7970
+ renderSelectionBoxes(canvasCtx, {
7971
+ selectedIds: this._selectedIds,
7972
+ store: this.ctx.store,
7973
+ zoom: this.ctx.camera.zoom
7974
+ });
7422
7975
  if (this.mode.type === "arrow-handle" && this.ctx) {
7423
7976
  const target = getArrowHandleDragTarget(
7424
7977
  this.mode.handle,
@@ -7452,32 +8005,13 @@ var SelectTool = class {
7452
8005
  }
7453
8006
  }
7454
8007
  }
7455
- this.renderGuideLines(canvasCtx);
7456
- }
7457
- renderGuideLines(canvasCtx) {
7458
- if (this.mode.type !== "dragging" || !this.ctx || this.activeGuides.length === 0) return;
7459
- const zoom = this.ctx.camera.zoom;
7460
- const rect = this.dragVisibleRect;
7461
- canvasCtx.save();
7462
- canvasCtx.strokeStyle = "#FF4081";
7463
- canvasCtx.lineWidth = 1 / zoom;
7464
- canvasCtx.setLineDash([]);
7465
- for (const g of this.activeGuides) {
7466
- canvasCtx.beginPath();
7467
- if (g.axis === "x") {
7468
- const y0 = rect ? rect.y : this.currentWorld.y - 1e5;
7469
- const y1 = rect ? rect.y + rect.h : this.currentWorld.y + 1e5;
7470
- canvasCtx.moveTo(g.position, y0);
7471
- canvasCtx.lineTo(g.position, y1);
7472
- } else {
7473
- const x0 = rect ? rect.x : this.currentWorld.x - 1e5;
7474
- const x1 = rect ? rect.x + rect.w : this.currentWorld.x + 1e5;
7475
- canvasCtx.moveTo(x0, g.position);
7476
- canvasCtx.lineTo(x1, g.position);
7477
- }
7478
- canvasCtx.stroke();
7479
- }
7480
- canvasCtx.restore();
8008
+ if (this.mode.type === "dragging" && this.ctx && this.activeGuides.length)
8009
+ renderGuideLines(canvasCtx, {
8010
+ guides: this.activeGuides,
8011
+ rect: this.dragVisibleRect,
8012
+ currentWorld: this.currentWorld,
8013
+ zoom: this.ctx.camera.zoom
8014
+ });
7481
8015
  }
7482
8016
  updateArrowsBoundTo(ids, ctx) {
7483
8017
  updateArrowsBoundToElements(ids, ctx.store);
@@ -7503,25 +8037,25 @@ var SelectTool = class {
7503
8037
  ctx.setCursor?.(getArrowHandleCursor(arrowHit.handle, false));
7504
8038
  return null;
7505
8039
  }
7506
- if (this.hitTestLineHandles(world, ctx)) {
8040
+ if (hitTestLineHandles(world, ctx, this._selectedIds)) {
7507
8041
  ctx.setCursor?.("grab");
7508
8042
  return null;
7509
8043
  }
7510
- const templateResizeHit = this.hitTestTemplateResizeHandle(world, ctx);
8044
+ const templateResizeHit = hitTestTemplateResizeHandle(world, ctx, this._selectedIds);
7511
8045
  if (templateResizeHit) {
7512
8046
  ctx.setCursor?.("nwse-resize");
7513
8047
  return null;
7514
8048
  }
7515
- if (this.hitTestRotateHandle(world, ctx)) {
8049
+ if (hitTestRotateHandle(world, ctx, this._selectedIds)) {
7516
8050
  ctx.setCursor?.("grab");
7517
8051
  return null;
7518
8052
  }
7519
- const resizeHit = this.hitTestResizeHandle(world, ctx);
8053
+ const resizeHit = hitTestResizeHandle(world, ctx, this._selectedIds);
7520
8054
  if (resizeHit) {
7521
8055
  ctx.setCursor?.(HANDLE_CURSORS[resizeHit.handle]);
7522
8056
  return null;
7523
8057
  }
7524
- const hit = this.hitTest(world, ctx);
8058
+ const hit = hitTest(world, ctx);
7525
8059
  ctx.setCursor?.(hit ? "move" : "default");
7526
8060
  return hit ? hit.id : null;
7527
8061
  }
@@ -7535,421 +8069,43 @@ var SelectTool = class {
7535
8069
  const el = ctx.store.getById(this.mode.elementId);
7536
8070
  if (!el || !("size" in el) || el.locked) return;
7537
8071
  const angle = el.rotation ?? 0;
7538
- if (angle !== 0) {
7539
- this.handleRotatedResize(world, el, angle, ctx, shiftKey);
7540
- return;
7541
- }
7542
- const { handle } = this.mode;
7543
- const dx = world.x - this.lastWorld.x;
7544
- const dy = world.y - this.lastWorld.y;
7545
- this.lastWorld = world;
7546
- let { x, y, w, h } = { x: el.position.x, y: el.position.y, w: el.size.w, h: el.size.h };
7547
- switch (handle) {
7548
- case "se":
7549
- w += dx;
7550
- h += dy;
7551
- break;
7552
- case "sw":
7553
- x += dx;
7554
- w -= dx;
7555
- h += dy;
7556
- break;
7557
- case "ne":
7558
- y += dy;
7559
- w += dx;
7560
- h -= dy;
7561
- break;
7562
- case "nw":
7563
- x += dx;
7564
- y += dy;
7565
- w -= dx;
7566
- h -= dy;
7567
- break;
7568
- }
7569
- if (shiftKey && this.resizeAspectRatio > 0) {
7570
- const absDw = Math.abs(w - el.size.w);
7571
- const absDh = Math.abs(h - el.size.h);
7572
- if (absDw >= absDh) {
7573
- h = w / this.resizeAspectRatio;
7574
- } else {
7575
- w = h * this.resizeAspectRatio;
7576
- }
7577
- if (handle === "nw" || handle === "sw") {
7578
- x = el.position.x + el.size.w - w;
7579
- }
7580
- if (handle === "nw" || handle === "ne") {
7581
- y = el.position.y + el.size.h - h;
7582
- }
7583
- }
7584
- if (w < MIN_ELEMENT_SIZE) {
7585
- if (handle === "nw" || handle === "sw") x = el.position.x + el.size.w - MIN_ELEMENT_SIZE;
7586
- w = MIN_ELEMENT_SIZE;
7587
- }
7588
- if (h < MIN_ELEMENT_SIZE) {
7589
- if (handle === "nw" || handle === "ne") y = el.position.y + el.size.h - MIN_ELEMENT_SIZE;
7590
- h = MIN_ELEMENT_SIZE;
7591
- }
7592
- ctx.store.update(this.mode.elementId, {
7593
- position: { x, y },
7594
- size: { w, h }
7595
- });
7596
- this.updateArrowsBoundTo([this.mode.elementId], ctx);
7597
- ctx.requestRender();
7598
- }
7599
- anchorOffset(handle, w, h) {
7600
- switch (handle) {
7601
- case "se":
7602
- return { x: -w / 2, y: -h / 2 };
7603
- case "sw":
7604
- return { x: w / 2, y: -h / 2 };
7605
- case "ne":
7606
- return { x: -w / 2, y: h / 2 };
7607
- case "nw":
7608
- return { x: w / 2, y: h / 2 };
7609
- default:
7610
- return { x: 0, y: 0 };
7611
- }
7612
- }
7613
- handleRotatedResize(world, el, angle, ctx, shiftKey) {
7614
- if (this.mode.type !== "resizing") return;
7615
- const { handle } = this.mode;
7616
- const wdx = world.x - this.lastWorld.x;
7617
- const wdy = world.y - this.lastWorld.y;
7618
- this.lastWorld = world;
7619
- const cosN = Math.cos(-angle);
7620
- const sinN = Math.sin(-angle);
7621
- const ldx = wdx * cosN - wdy * sinN;
7622
- const ldy = wdx * sinN + wdy * cosN;
7623
- let w = el.size.w;
7624
- let h = el.size.h;
7625
- switch (handle) {
7626
- case "se":
7627
- w += ldx;
7628
- h += ldy;
7629
- break;
7630
- case "sw":
7631
- w -= ldx;
7632
- h += ldy;
7633
- break;
7634
- case "ne":
7635
- w += ldx;
7636
- h -= ldy;
7637
- break;
7638
- case "nw":
7639
- w -= ldx;
7640
- h -= ldy;
7641
- break;
7642
- }
7643
- if (shiftKey && this.resizeAspectRatio > 0) {
7644
- const absDw = Math.abs(w - el.size.w);
7645
- const absDh = Math.abs(h - el.size.h);
7646
- if (absDw >= absDh) h = w / this.resizeAspectRatio;
7647
- else w = h * this.resizeAspectRatio;
7648
- }
7649
- w = Math.max(w, MIN_ELEMENT_SIZE);
7650
- h = Math.max(h, MIN_ELEMENT_SIZE);
7651
- const oldCenter = { x: el.position.x + el.size.w / 2, y: el.position.y + el.size.h / 2 };
7652
- const oldAnchorLocal = this.anchorOffset(handle, el.size.w, el.size.h);
7653
- const anchorWorld = rotatePoint(
7654
- { x: oldCenter.x + oldAnchorLocal.x, y: oldCenter.y + oldAnchorLocal.y },
7655
- oldCenter,
7656
- angle
8072
+ const patch = angle !== 0 ? computeRotatedResize(
8073
+ el,
8074
+ this.mode.handle,
8075
+ angle,
8076
+ world,
8077
+ this.lastWorld,
8078
+ this.resizeAspectRatio,
8079
+ shiftKey
8080
+ ) : computeResize(
8081
+ el,
8082
+ this.mode.handle,
8083
+ world,
8084
+ this.lastWorld,
8085
+ this.resizeAspectRatio,
8086
+ shiftKey
7657
8087
  );
7658
- const newAnchorLocal = this.anchorOffset(handle, w, h);
7659
- const cos = Math.cos(angle);
7660
- const sin = Math.sin(angle);
7661
- const rotatedAnchor = {
7662
- x: newAnchorLocal.x * cos - newAnchorLocal.y * sin,
7663
- y: newAnchorLocal.x * sin + newAnchorLocal.y * cos
7664
- };
7665
- const newCenter = { x: anchorWorld.x - rotatedAnchor.x, y: anchorWorld.y - rotatedAnchor.y };
7666
- const position = { x: newCenter.x - w / 2, y: newCenter.y - h / 2 };
7667
- ctx.store.update(this.mode.elementId, { position, size: { w, h } });
8088
+ this.lastWorld = world;
8089
+ ctx.store.update(this.mode.elementId, patch);
7668
8090
  this.updateArrowsBoundTo([this.mode.elementId], ctx);
7669
8091
  ctx.requestRender();
7670
8092
  }
7671
- hitTestResizeHandle(world, ctx) {
7672
- if (this._selectedIds.length === 0) return null;
7673
- const zoom = ctx.camera.zoom;
7674
- const handleHalf = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
7675
- for (const id of this._selectedIds) {
7676
- const el = ctx.store.getById(id);
7677
- if (!el || !("size" in el)) continue;
7678
- if (el.locked) continue;
7679
- if (el.type === "shape" && el.shape === "line") continue;
7680
- const layout = this.getOverlayLayout(el, zoom);
7681
- if (!layout) continue;
7682
- for (const [handle, pos] of layout.corners) {
7683
- if (Math.abs(world.x - pos.x) <= handleHalf && Math.abs(world.y - pos.y) <= handleHalf) {
7684
- return { elementId: id, handle };
7685
- }
7686
- }
7687
- }
7688
- return null;
7689
- }
7690
- hitTestRotateHandle(world, ctx) {
7691
- if (this._selectedIds.length !== 1) return null;
7692
- const id = this._selectedIds[0];
7693
- if (!id) return null;
7694
- const el = ctx.store.getById(id);
7695
- if (!el || el.locked || !ROTATABLE_TYPES.has(el.type)) return null;
7696
- const layout = this.getOverlayLayout(el, ctx.camera.zoom);
7697
- if (!layout) return null;
7698
- const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / ctx.camera.zoom;
7699
- const dx = world.x - layout.rotateHandle.x;
7700
- const dy = world.y - layout.rotateHandle.y;
7701
- return dx * dx + dy * dy <= r * r ? { elementId: id } : null;
7702
- }
7703
- hitTestLineHandles(world, ctx) {
7704
- if (this._selectedIds.length === 0) return null;
7705
- const zoom = ctx.camera.zoom;
7706
- const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
7707
- const r2 = r * r;
7708
- for (const id of this._selectedIds) {
7709
- const el = ctx.store.getById(id);
7710
- if (!el || el.type !== "shape" || el.shape !== "line") continue;
7711
- const [a, b] = lineEndpoints(el);
7712
- if ((world.x - a.x) ** 2 + (world.y - a.y) ** 2 <= r2) return { elementId: id, fixed: b };
7713
- if ((world.x - b.x) ** 2 + (world.y - b.y) ** 2 <= r2) return { elementId: id, fixed: a };
7714
- }
7715
- return null;
7716
- }
7717
- getHandlePositions(bounds) {
7718
- return [
7719
- ["nw", { x: bounds.x, y: bounds.y }],
7720
- ["ne", { x: bounds.x + bounds.w, y: bounds.y }],
7721
- ["sw", { x: bounds.x, y: bounds.y + bounds.h }],
7722
- ["se", { x: bounds.x + bounds.w, y: bounds.y + bounds.h }]
7723
- ];
7724
- }
7725
8093
  getOverlayLayout(el, zoom) {
7726
- const bounds = getElementBounds(el);
7727
- if (!bounds) return null;
7728
- const angle = el.rotation ?? 0;
7729
- const pad = SELECTION_PAD / zoom;
7730
- const center2 = { x: bounds.x + bounds.w / 2, y: bounds.y + bounds.h / 2 };
7731
- const raw = [
7732
- ["nw", { x: bounds.x - pad, y: bounds.y - pad }],
7733
- ["ne", { x: bounds.x + bounds.w + pad, y: bounds.y - pad }],
7734
- ["sw", { x: bounds.x - pad, y: bounds.y + bounds.h + pad }],
7735
- ["se", { x: bounds.x + bounds.w + pad, y: bounds.y + bounds.h + pad }]
7736
- ];
7737
- const corners = raw.map(
7738
- ([h, p]) => [h, rotatePoint(p, center2, angle)]
7739
- );
7740
- const topMid = { x: center2.x, y: bounds.y - pad - ROTATE_HANDLE_OFFSET / zoom };
7741
- const rotateHandle = rotatePoint(topMid, center2, angle);
7742
- return { center: center2, corners, rotateHandle, angle };
7743
- }
7744
- topMidpoint(layout) {
7745
- const nw = layout.corners.find(([h]) => h === "nw")?.[1] ?? { x: 0, y: 0 };
7746
- const ne = layout.corners.find(([h]) => h === "ne")?.[1] ?? { x: 0, y: 0 };
7747
- return { x: (nw.x + ne.x) / 2, y: (nw.y + ne.y) / 2 };
7748
- }
7749
- renderMarquee(canvasCtx) {
7750
- if (this.mode.type !== "marquee") return;
7751
- const rect = this.getMarqueeRect();
7752
- if (!rect) return;
7753
- canvasCtx.save();
7754
- canvasCtx.strokeStyle = "#2196F3";
7755
- canvasCtx.fillStyle = "rgba(33, 150, 243, 0.08)";
7756
- canvasCtx.lineWidth = 1;
7757
- canvasCtx.setLineDash([4, 4]);
7758
- canvasCtx.strokeRect(rect.x, rect.y, rect.w, rect.h);
7759
- canvasCtx.fillRect(rect.x, rect.y, rect.w, rect.h);
7760
- canvasCtx.restore();
7761
- }
7762
- renderSelectionBoxes(canvasCtx) {
7763
- if (this._selectedIds.length === 0 || !this.ctx) return;
7764
- const zoom = this.ctx.camera.zoom;
7765
- const handleWorldSize = HANDLE_SIZE / zoom;
7766
- canvasCtx.save();
7767
- canvasCtx.strokeStyle = "#2196F3";
7768
- canvasCtx.lineWidth = 1.5 / zoom;
7769
- canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7770
- for (const id of this._selectedIds) {
7771
- const el = this.ctx.store.getById(id);
7772
- if (!el) continue;
7773
- if (el.type === "arrow") {
7774
- renderArrowHandles(canvasCtx, el, zoom);
7775
- this.renderBindingHighlights(canvasCtx, el, zoom);
7776
- continue;
7777
- }
7778
- if (el.type === "shape" && el.shape === "line") {
7779
- canvasCtx.setLineDash([]);
7780
- canvasCtx.fillStyle = "#ffffff";
7781
- const r = handleWorldSize / 2;
7782
- for (const p of lineEndpoints(el)) {
7783
- canvasCtx.beginPath();
7784
- canvasCtx.arc(p.x, p.y, r, 0, Math.PI * 2);
7785
- canvasCtx.fill();
7786
- canvasCtx.stroke();
7787
- }
7788
- canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7789
- continue;
7790
- }
7791
- const bounds = getElementBounds(el);
7792
- if (!bounds) continue;
7793
- const layout = this.getOverlayLayout(el, zoom);
7794
- if (!layout) continue;
7795
- const pad = SELECTION_PAD / zoom;
7796
- if (layout.angle === 0) {
7797
- canvasCtx.strokeRect(
7798
- bounds.x - pad,
7799
- bounds.y - pad,
7800
- bounds.w + pad * 2,
7801
- bounds.h + pad * 2
7802
- );
7803
- } else {
7804
- const ordered = ["nw", "ne", "se", "sw"].map((h) => layout.corners.find(([c]) => c === h)?.[1]).filter((p) => !!p);
7805
- const [p0, ...others] = ordered;
7806
- if (p0) {
7807
- canvasCtx.beginPath();
7808
- canvasCtx.moveTo(p0.x, p0.y);
7809
- for (const p of others) canvasCtx.lineTo(p.x, p.y);
7810
- canvasCtx.closePath();
7811
- canvasCtx.stroke();
7812
- }
7813
- }
7814
- if (!el.locked) {
7815
- if ("size" in el) {
7816
- canvasCtx.setLineDash([]);
7817
- canvasCtx.fillStyle = "#ffffff";
7818
- const corners = layout.angle === 0 ? this.getHandlePositions(bounds) : layout.corners;
7819
- for (const [, pos] of corners) {
7820
- canvasCtx.fillRect(
7821
- pos.x - handleWorldSize / 2,
7822
- pos.y - handleWorldSize / 2,
7823
- handleWorldSize,
7824
- handleWorldSize
7825
- );
7826
- canvasCtx.strokeRect(
7827
- pos.x - handleWorldSize / 2,
7828
- pos.y - handleWorldSize / 2,
7829
- handleWorldSize,
7830
- handleWorldSize
7831
- );
7832
- }
7833
- canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7834
- } else if (el.type === "template") {
7835
- canvasCtx.setLineDash([]);
7836
- canvasCtx.fillStyle = "#ffffff";
7837
- const hx = bounds.x + bounds.w;
7838
- const hy = bounds.y + bounds.h;
7839
- canvasCtx.fillRect(
7840
- hx - handleWorldSize / 2,
7841
- hy - handleWorldSize / 2,
7842
- handleWorldSize,
7843
- handleWorldSize
7844
- );
7845
- canvasCtx.strokeRect(
7846
- hx - handleWorldSize / 2,
7847
- hy - handleWorldSize / 2,
7848
- handleWorldSize,
7849
- handleWorldSize
7850
- );
7851
- canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7852
- }
7853
- if (this._selectedIds.length === 1 && ROTATABLE_TYPES.has(el.type)) {
7854
- const stemStart = this.topMidpoint(layout);
7855
- const stemEnd = layout.rotateHandle;
7856
- canvasCtx.beginPath();
7857
- canvasCtx.moveTo(stemStart.x, stemStart.y);
7858
- canvasCtx.lineTo(stemEnd.x, stemEnd.y);
7859
- canvasCtx.stroke();
7860
- canvasCtx.setLineDash([]);
7861
- canvasCtx.fillStyle = "#ffffff";
7862
- canvasCtx.beginPath();
7863
- canvasCtx.arc(stemEnd.x, stemEnd.y, handleWorldSize / 2, 0, Math.PI * 2);
7864
- canvasCtx.fill();
7865
- canvasCtx.stroke();
7866
- canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7867
- }
7868
- }
7869
- if (el.locked) {
7870
- const ne = layout.corners.find(([h]) => h === "ne")?.[1];
7871
- if (ne) this.drawLockBadge(canvasCtx, ne, zoom);
7872
- }
7873
- }
7874
- canvasCtx.restore();
7875
- }
7876
- drawLockBadge(ctx, at, zoom) {
7877
- const r = 9 / zoom;
7878
- ctx.save();
7879
- ctx.setLineDash([]);
7880
- ctx.beginPath();
7881
- ctx.arc(at.x, at.y, r, 0, Math.PI * 2);
7882
- ctx.fillStyle = "#ffffff";
7883
- ctx.fill();
7884
- ctx.strokeStyle = "#2196F3";
7885
- ctx.lineWidth = 1.5 / zoom;
7886
- ctx.stroke();
7887
- const bw = 8 / zoom;
7888
- const bh = 6 / zoom;
7889
- ctx.fillStyle = "#2196F3";
7890
- ctx.fillRect(at.x - bw / 2, at.y - bh / 2 + 1 / zoom, bw, bh);
7891
- ctx.beginPath();
7892
- ctx.arc(at.x, at.y - bh / 2 + 1 / zoom, 2.5 / zoom, Math.PI, 0);
7893
- ctx.lineWidth = 1.4 / zoom;
7894
- ctx.stroke();
7895
- ctx.restore();
7896
- }
7897
- renderBindingHighlights(canvasCtx, arrow, zoom) {
7898
- if (!this.ctx) return;
7899
- if (!arrow.fromBinding && !arrow.toBinding) return;
7900
- const pad = SELECTION_PAD / zoom;
7901
- canvasCtx.save();
7902
- canvasCtx.strokeStyle = "#2196F3";
7903
- canvasCtx.lineWidth = 2 / zoom;
7904
- canvasCtx.setLineDash([]);
7905
- const drawn = /* @__PURE__ */ new Set();
7906
- for (const binding of [arrow.fromBinding, arrow.toBinding]) {
7907
- if (!binding || drawn.has(binding.elementId)) continue;
7908
- drawn.add(binding.elementId);
7909
- const target = this.ctx.store.getById(binding.elementId);
7910
- if (!target) continue;
7911
- const bounds = getElementBounds(target);
7912
- if (!bounds) continue;
7913
- canvasCtx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.w + pad * 2, bounds.h + pad * 2);
7914
- }
7915
- canvasCtx.restore();
7916
- }
7917
- hitTestTemplateResizeHandle(world, ctx) {
7918
- if (this._selectedIds.length === 0) return null;
7919
- const zoom = ctx.camera.zoom;
7920
- const handleHalf = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
7921
- for (const id of this._selectedIds) {
7922
- const el = ctx.store.getById(id);
7923
- if (!el || el.type !== "template") continue;
7924
- const bounds = getElementBounds(el);
7925
- if (!bounds) continue;
7926
- const hx = bounds.x + bounds.w;
7927
- const hy = bounds.y + bounds.h;
7928
- if (Math.abs(world.x - hx) <= handleHalf && Math.abs(world.y - hy) <= handleHalf) {
7929
- return id;
7930
- }
7931
- }
7932
- return null;
8094
+ return getOverlayLayout(el, zoom);
7933
8095
  }
7934
8096
  handleTemplateResize(world, ctx) {
7935
8097
  if (this.mode.type !== "resizing-template") return;
7936
8098
  const el = ctx.store.getById(this.mode.elementId);
7937
8099
  if (!el || el.type !== "template" || el.locked) return;
7938
- const dx = world.x - el.position.x;
7939
- const dy = world.y - el.position.y;
7940
- let newRadius = Math.sqrt(dx * dx + dy * dy);
7941
- if (ctx.snapToGrid && ctx.gridSize && ctx.gridSize > 0) {
7942
- const snapUnit = ctx.gridType === "hex" ? Math.sqrt(3) * ctx.gridSize : ctx.gridSize;
7943
- newRadius = Math.max(snapUnit, Math.round(newRadius / snapUnit) * snapUnit);
7944
- }
7945
- newRadius = Math.max(MIN_ELEMENT_SIZE, newRadius);
7946
- const updates = { radius: newRadius };
7947
- if (el.feetPerCell != null && ctx.gridSize && ctx.gridSize > 0) {
7948
- const snapUnit = ctx.gridType === "hex" ? Math.sqrt(3) * ctx.gridSize : ctx.gridSize;
7949
- updates.radiusFeet = newRadius / snapUnit * el.feetPerCell;
7950
- }
7951
- ctx.store.update(this.mode.elementId, updates);
7952
- ctx.requestRender();
8100
+ const patch = computeTemplateResize(el, world, {
8101
+ snapToGrid: ctx.snapToGrid,
8102
+ gridSize: ctx.gridSize,
8103
+ gridType: ctx.gridType
8104
+ });
8105
+ if (patch) {
8106
+ ctx.store.update(this.mode.elementId, patch);
8107
+ ctx.requestRender();
8108
+ }
7953
8109
  }
7954
8110
  getMarqueeRect() {
7955
8111
  if (this.mode.type !== "marquee") return null;
@@ -7962,65 +8118,6 @@ var SelectTool = class {
7962
8118
  if (w === 0 && h === 0) return null;
7963
8119
  return { x, y, w, h };
7964
8120
  }
7965
- findElementsInRect(marquee, ctx) {
7966
- const candidates = ctx.store.queryRect(marquee);
7967
- const ids = [];
7968
- for (const el of candidates) {
7969
- if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
7970
- if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
7971
- if (el.type === "grid") continue;
7972
- const bounds = getElementBounds(el);
7973
- if (bounds && this.rectsOverlap(marquee, rotatedAABB(bounds, el.rotation ?? 0))) {
7974
- ids.push(el.id);
7975
- }
7976
- }
7977
- return ids;
7978
- }
7979
- rectsOverlap(a, b) {
7980
- return a.x <= b.x + b.w && a.x + a.w >= b.x && a.y <= b.y + b.h && a.y + a.h >= b.y;
7981
- }
7982
- hitTest(world, ctx) {
7983
- const r = 10;
7984
- const candidates = ctx.store.queryRect({ x: world.x - r, y: world.y - r, w: r * 2, h: r * 2 }).reverse();
7985
- for (const el of candidates) {
7986
- if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
7987
- if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
7988
- if (el.type === "grid") continue;
7989
- if (this.isInsideBounds(world, el)) return el;
7990
- }
7991
- return null;
7992
- }
7993
- isInsideBounds(point, el) {
7994
- if (el.type === "grid") return false;
7995
- const angle = el.rotation ?? 0;
7996
- if (angle !== 0) {
7997
- const b = getElementBounds(el);
7998
- if (b) {
7999
- point = rotatePoint(point, { x: b.x + b.w / 2, y: b.y + b.h / 2 }, -angle);
8000
- }
8001
- }
8002
- if (el.type === "shape" && el.shape === "line") {
8003
- const [a, b] = lineEndpoints(el);
8004
- const threshold = Math.max(el.strokeWidth / 2, 6);
8005
- return distSqToSegment(point, a, b) <= threshold * threshold;
8006
- }
8007
- if ("size" in el) {
8008
- const s = el.size;
8009
- return point.x >= el.position.x && point.x <= el.position.x + s.w && point.y >= el.position.y && point.y <= el.position.y + s.h;
8010
- }
8011
- if (el.type === "stroke") {
8012
- return hitTestStroke(el, point, 10);
8013
- }
8014
- if (el.type === "arrow") {
8015
- return isNearBezier(point, el.from, el.to, el.bend, 10);
8016
- }
8017
- if (el.type === "template") {
8018
- const bounds = getElementBounds(el);
8019
- if (!bounds) return false;
8020
- return point.x >= bounds.x && point.x <= bounds.x + bounds.w && point.y >= bounds.y && point.y <= bounds.y + bounds.h;
8021
- }
8022
- return false;
8023
- }
8024
8121
  };
8025
8122
 
8026
8123
  // src/tools/arrow-tool.ts
@@ -8866,7 +8963,7 @@ var TemplateTool = class {
8866
8963
  };
8867
8964
 
8868
8965
  // src/index.ts
8869
- var VERSION = "0.38.0";
8966
+ var VERSION = "0.38.2";
8870
8967
  export {
8871
8968
  ArrowTool,
8872
8969
  AutoSave,