@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.cjs CHANGED
@@ -4055,17 +4055,43 @@ var ContextMenu = class {
4055
4055
  }
4056
4056
  };
4057
4057
 
4058
- // src/elements/translate.ts
4059
- function translateElementPatch(el, dx, dy) {
4060
- const position = { x: el.position.x + dx, y: el.position.y + dy };
4061
- if (el.type === "arrow") {
4062
- return {
4063
- position,
4064
- from: { x: el.from.x + dx, y: el.from.y + dy },
4065
- to: { x: el.to.x + dx, y: el.to.y + dy }
4066
- };
4067
- }
4068
- return { position };
4058
+ // src/canvas/viewport-dom.ts
4059
+ function createWrapper() {
4060
+ const el = document.createElement("div");
4061
+ Object.assign(el.style, {
4062
+ position: "relative",
4063
+ width: "100%",
4064
+ height: "100%",
4065
+ overflow: "hidden",
4066
+ overscrollBehavior: "none",
4067
+ userSelect: "none",
4068
+ webkitUserSelect: "none"
4069
+ });
4070
+ return el;
4071
+ }
4072
+ function createCanvas() {
4073
+ const el = document.createElement("canvas");
4074
+ Object.assign(el.style, {
4075
+ position: "absolute",
4076
+ top: "0",
4077
+ left: "0",
4078
+ width: "100%",
4079
+ height: "100%"
4080
+ });
4081
+ return el;
4082
+ }
4083
+ function createDomLayer() {
4084
+ const el = document.createElement("div");
4085
+ Object.assign(el.style, {
4086
+ position: "absolute",
4087
+ top: "0",
4088
+ left: "0",
4089
+ width: "100%",
4090
+ height: "100%",
4091
+ pointerEvents: "none",
4092
+ transformOrigin: "0 0"
4093
+ });
4094
+ return el;
4069
4095
  }
4070
4096
 
4071
4097
  // src/elements/arrow-label-editor.ts
@@ -5689,6 +5715,19 @@ var MarginViewport = class {
5689
5715
  }
5690
5716
  };
5691
5717
 
5718
+ // src/elements/translate.ts
5719
+ function translateElementPatch(el, dx, dy) {
5720
+ const position = { x: el.position.x + dx, y: el.position.y + dy };
5721
+ if (el.type === "arrow") {
5722
+ return {
5723
+ position,
5724
+ from: { x: el.from.x + dx, y: el.from.y + dy },
5725
+ to: { x: el.to.x + dx, y: el.to.y + dy }
5726
+ };
5727
+ }
5728
+ return { position };
5729
+ }
5730
+
5692
5731
  // src/elements/element-style.ts
5693
5732
  function styleToPatch(element, style) {
5694
5733
  const { color, fillColor, strokeWidth, opacity, fontSize } = style;
@@ -5776,7 +5815,7 @@ function getElementStyle(element) {
5776
5815
  }
5777
5816
  }
5778
5817
 
5779
- // src/canvas/viewport.ts
5818
+ // src/canvas/selection-ops.ts
5780
5819
  function unionBounds(list) {
5781
5820
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
5782
5821
  for (const b of list) {
@@ -5787,16 +5826,244 @@ function unionBounds(list) {
5787
5826
  }
5788
5827
  return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
5789
5828
  }
5790
- var EMPTY_IDS = [];
5791
- var ARROW_HIT_THRESHOLD = 10;
5792
- function noop() {
5793
- }
5794
5829
  function sharedValue(values) {
5795
5830
  const present = values.filter((v) => v !== void 0);
5796
5831
  if (present.length === 0) return void 0;
5797
5832
  const first = present[0];
5798
5833
  return present.every((v) => v === first) ? first : void 0;
5799
5834
  }
5835
+ var SelectionOps = class {
5836
+ constructor(deps) {
5837
+ this.deps = deps;
5838
+ }
5839
+ getStyle() {
5840
+ const ids = this.deps.getSelectedIds();
5841
+ if (ids.length === 0) return null;
5842
+ const styles = [];
5843
+ for (const id of ids) {
5844
+ const el = this.deps.store.getById(id);
5845
+ if (el) styles.push(getElementStyle(el));
5846
+ }
5847
+ if (styles.length === 0) return null;
5848
+ const result = {};
5849
+ const color = sharedValue(styles.map((s) => s.color));
5850
+ if (color !== void 0) result.color = color;
5851
+ const fillColor = sharedValue(styles.map((s) => s.fillColor));
5852
+ if (fillColor !== void 0) result.fillColor = fillColor;
5853
+ const strokeWidth = sharedValue(styles.map((s) => s.strokeWidth));
5854
+ if (strokeWidth !== void 0) result.strokeWidth = strokeWidth;
5855
+ const opacity = sharedValue(styles.map((s) => s.opacity));
5856
+ if (opacity !== void 0) result.opacity = opacity;
5857
+ const fontSize = sharedValue(styles.map((s) => s.fontSize));
5858
+ if (fontSize !== void 0) result.fontSize = fontSize;
5859
+ return result;
5860
+ }
5861
+ applyStyle(style) {
5862
+ const ids = this.deps.getSelectedIds();
5863
+ if (ids.length === 0) return;
5864
+ this.deps.recorder.begin();
5865
+ for (const id of ids) {
5866
+ const el = this.deps.store.getById(id);
5867
+ if (!el) continue;
5868
+ const patch = styleToPatch(el, style);
5869
+ if (Object.keys(patch).length > 0) {
5870
+ this.deps.store.update(id, patch);
5871
+ }
5872
+ }
5873
+ this.deps.recorder.commit();
5874
+ }
5875
+ group() {
5876
+ const ids = this.deps.getSelectedIds();
5877
+ if (ids.length < 2) return;
5878
+ const groupId = createId("group");
5879
+ this.deps.recorder.begin();
5880
+ for (const id of ids) {
5881
+ if (this.deps.store.getById(id)) this.deps.store.update(id, { groupId });
5882
+ }
5883
+ this.deps.recorder.commit();
5884
+ }
5885
+ ungroup() {
5886
+ const ids = this.deps.getSelectedIds();
5887
+ if (ids.length === 0) return;
5888
+ this.deps.recorder.begin();
5889
+ for (const id of ids) {
5890
+ const el = this.deps.store.getById(id);
5891
+ if (el && el.groupId !== void 0) this.deps.store.update(id, { groupId: void 0 });
5892
+ }
5893
+ this.deps.recorder.commit();
5894
+ }
5895
+ toggleLock() {
5896
+ const ids = this.deps.getSelectedIds();
5897
+ if (ids.length === 0) return;
5898
+ const anyUnlocked = ids.some((id) => {
5899
+ const el = this.deps.store.getById(id);
5900
+ return el ? !el.locked : false;
5901
+ });
5902
+ this.deps.recorder.begin();
5903
+ for (const id of ids) {
5904
+ const el = this.deps.store.getById(id);
5905
+ if (el && el.locked !== anyUnlocked) this.deps.store.update(id, { locked: anyUnlocked });
5906
+ }
5907
+ this.deps.recorder.commit();
5908
+ }
5909
+ align(edge) {
5910
+ const bounded = this.boundedSelection();
5911
+ if (bounded.length < 2) return;
5912
+ const B = unionBounds(bounded.map((e) => e.bounds));
5913
+ this.deps.recorder.begin();
5914
+ const moved = [];
5915
+ for (const { id, el, bounds: b } of bounded) {
5916
+ if (!this.isMovable(el)) continue;
5917
+ let dx = 0;
5918
+ let dy = 0;
5919
+ switch (edge) {
5920
+ case "left":
5921
+ dx = B.x - b.x;
5922
+ break;
5923
+ case "right":
5924
+ dx = B.x + B.w - (b.x + b.w);
5925
+ break;
5926
+ case "center-x":
5927
+ dx = B.x + B.w / 2 - (b.x + b.w / 2);
5928
+ break;
5929
+ case "top":
5930
+ dy = B.y - b.y;
5931
+ break;
5932
+ case "bottom":
5933
+ dy = B.y + B.h - (b.y + b.h);
5934
+ break;
5935
+ case "middle":
5936
+ dy = B.y + B.h / 2 - (b.y + b.h / 2);
5937
+ break;
5938
+ }
5939
+ if (dx === 0 && dy === 0) continue;
5940
+ this.deps.store.update(id, translateElementPatch(el, dx, dy));
5941
+ moved.push(id);
5942
+ }
5943
+ updateArrowsBoundToElements(moved, this.deps.store);
5944
+ this.deps.recorder.commit();
5945
+ this.deps.requestRender();
5946
+ }
5947
+ distribute(axis) {
5948
+ const bounded = this.boundedSelection();
5949
+ if (bounded.length < 3) return;
5950
+ const center2 = (b) => axis === "horizontal" ? b.x + b.w / 2 : b.y + b.h / 2;
5951
+ const sorted = [...bounded].sort((p, q) => center2(p.bounds) - center2(q.bounds));
5952
+ const first = sorted[0];
5953
+ const last = sorted[sorted.length - 1];
5954
+ if (!first || !last) return;
5955
+ const c0 = center2(first.bounds);
5956
+ const cN = center2(last.bounds);
5957
+ const n = sorted.length;
5958
+ this.deps.recorder.begin();
5959
+ const moved = [];
5960
+ for (let i = 1; i < n - 1; i++) {
5961
+ const item = sorted[i];
5962
+ if (!item || !this.isMovable(item.el)) continue;
5963
+ const target = c0 + i * (cN - c0) / (n - 1);
5964
+ const delta = target - center2(item.bounds);
5965
+ if (delta === 0) continue;
5966
+ const [dx, dy] = axis === "horizontal" ? [delta, 0] : [0, delta];
5967
+ this.deps.store.update(item.id, translateElementPatch(item.el, dx, dy));
5968
+ moved.push(item.id);
5969
+ }
5970
+ updateArrowsBoundToElements(moved, this.deps.store);
5971
+ this.deps.recorder.commit();
5972
+ this.deps.requestRender();
5973
+ }
5974
+ boundedSelection() {
5975
+ const out = [];
5976
+ for (const id of this.deps.getSelectedIds()) {
5977
+ const el = this.deps.store.getById(id);
5978
+ if (!el) continue;
5979
+ const bounds = getElementBounds(el);
5980
+ if (bounds) out.push({ id, el, bounds });
5981
+ }
5982
+ return out;
5983
+ }
5984
+ isMovable(el) {
5985
+ if (el.locked) return false;
5986
+ if (el.type === "arrow" && (el.fromBinding ?? el.toBinding)) return false;
5987
+ return true;
5988
+ }
5989
+ };
5990
+
5991
+ // src/canvas/grid-controller.ts
5992
+ var GridController = class {
5993
+ constructor(deps) {
5994
+ this.deps = deps;
5995
+ }
5996
+ listeners = /* @__PURE__ */ new Set();
5997
+ add(input) {
5998
+ const existing = this.deps.store.getElementsByType("grid")[0];
5999
+ this.deps.recorder.begin();
6000
+ if (existing) {
6001
+ this.deps.store.remove(existing.id);
6002
+ }
6003
+ const grid = createGrid({ ...input, layerId: this.deps.getActiveLayerId() });
6004
+ this.deps.store.add(grid);
6005
+ this.deps.recorder.commit();
6006
+ this.deps.requestRender();
6007
+ return grid.id;
6008
+ }
6009
+ update(updates) {
6010
+ const grid = this.deps.store.getElementsByType("grid")[0];
6011
+ if (!grid) return;
6012
+ this.deps.recorder.begin();
6013
+ this.deps.store.update(grid.id, updates);
6014
+ this.deps.recorder.commit();
6015
+ this.deps.requestRender();
6016
+ }
6017
+ remove() {
6018
+ const grid = this.deps.store.getElementsByType("grid")[0];
6019
+ if (!grid) return;
6020
+ this.deps.recorder.begin();
6021
+ this.deps.store.remove(grid.id);
6022
+ this.deps.recorder.commit();
6023
+ this.deps.requestRender();
6024
+ }
6025
+ getInfo() {
6026
+ const grid = this.deps.store.getElementsByType("grid")[0];
6027
+ if (!grid) return null;
6028
+ return {
6029
+ gridType: grid.gridType,
6030
+ hexOrientation: grid.hexOrientation,
6031
+ cellSize: grid.cellSize,
6032
+ cellRadius: grid.gridType === "hex" ? grid.cellSize : grid.cellSize / 2
6033
+ };
6034
+ }
6035
+ onChange(listener) {
6036
+ this.listeners.add(listener);
6037
+ return () => {
6038
+ this.listeners.delete(listener);
6039
+ };
6040
+ }
6041
+ syncContext() {
6042
+ const grid = this.deps.store.getElementsByType("grid")[0];
6043
+ if (grid) {
6044
+ this.deps.toolContext.gridSize = grid.cellSize;
6045
+ this.deps.toolContext.gridType = grid.gridType;
6046
+ this.deps.toolContext.hexOrientation = grid.hexOrientation;
6047
+ } else {
6048
+ this.deps.toolContext.gridSize = this.deps.defaultGridSize;
6049
+ this.deps.toolContext.gridType = void 0;
6050
+ this.deps.toolContext.hexOrientation = void 0;
6051
+ }
6052
+ this.notify();
6053
+ }
6054
+ notify() {
6055
+ const info = this.getInfo();
6056
+ for (const listener of this.listeners) {
6057
+ listener(info);
6058
+ }
6059
+ }
6060
+ };
6061
+
6062
+ // src/canvas/viewport.ts
6063
+ var EMPTY_IDS = [];
6064
+ var ARROW_HIT_THRESHOLD = 10;
6065
+ function noop() {
6066
+ }
5800
6067
  var Viewport = class {
5801
6068
  constructor(container, options = {}) {
5802
6069
  this.container = container;
@@ -5839,9 +6106,15 @@ var Viewport = class {
5839
6106
  this.dropHandler = options.onDrop;
5840
6107
  this.history = new HistoryStack();
5841
6108
  this.historyRecorder = new HistoryRecorder(this.store, this.history, this.layerManager);
5842
- this.wrapper = this.createWrapper();
5843
- this.canvasEl = this.createCanvas();
5844
- this.domLayer = this.createDomLayer();
6109
+ this.selectionOps = new SelectionOps({
6110
+ store: this.store,
6111
+ recorder: this.historyRecorder,
6112
+ getSelectedIds: () => this.getSelectedIds(),
6113
+ requestRender: () => this.requestRender()
6114
+ });
6115
+ this.wrapper = createWrapper();
6116
+ this.canvasEl = createCanvas();
6117
+ this.domLayer = createDomLayer();
5845
6118
  this.wrapper.appendChild(this.canvasEl);
5846
6119
  this.wrapper.appendChild(this.domLayer);
5847
6120
  this.container.appendChild(this.wrapper);
@@ -5919,21 +6192,29 @@ var Viewport = class {
5919
6192
  this.contextMenu?.close();
5920
6193
  this.requestRender();
5921
6194
  });
6195
+ this.gridController = new GridController({
6196
+ store: this.store,
6197
+ recorder: this.historyRecorder,
6198
+ requestRender: () => this.requestRender(),
6199
+ getActiveLayerId: () => this.layerManager.activeLayerId,
6200
+ toolContext: this.toolContext,
6201
+ defaultGridSize: this._gridSize
6202
+ });
5922
6203
  this.unsubStore = [
5923
6204
  this.store.on("add", (el) => {
5924
- if (el.type === "grid") this.syncGridContext();
6205
+ if (el.type === "grid") this.gridController.syncContext();
5925
6206
  this.renderLoop.markLayerDirty(el.layerId);
5926
6207
  this.requestRender();
5927
6208
  }),
5928
6209
  this.store.on("remove", (el) => {
5929
- if (el.type === "grid") this.syncGridContext();
6210
+ if (el.type === "grid") this.gridController.syncContext();
5930
6211
  this.unbindArrowsFrom(el);
5931
6212
  this.domNodeManager.removeDomNode(el.id);
5932
6213
  this.renderLoop.markLayerDirty(el.layerId);
5933
6214
  this.requestRender();
5934
6215
  }),
5935
6216
  this.store.on("update", ({ previous, current }) => {
5936
- if (current.type === "grid") this.syncGridContext();
6217
+ if (current.type === "grid") this.gridController.syncContext();
5937
6218
  this.renderLoop.markLayerDirty(current.layerId);
5938
6219
  if (previous.layerId !== current.layerId) {
5939
6220
  this.renderLoop.markLayerDirty(previous.layerId);
@@ -5943,7 +6224,7 @@ var Viewport = class {
5943
6224
  this.store.on("clear", () => {
5944
6225
  this.domNodeManager.clearDomNodes();
5945
6226
  this.renderLoop.markAllLayersDirty();
5946
- this.syncGridContext();
6227
+ this.gridController.syncContext();
5947
6228
  this.requestRender();
5948
6229
  })
5949
6230
  ];
@@ -5958,7 +6239,7 @@ var Viewport = class {
5958
6239
  this.observeResize();
5959
6240
  this.syncCanvasSize();
5960
6241
  this.renderLoop.start();
5961
- this.syncGridContext();
6242
+ this.gridController.syncContext();
5962
6243
  }
5963
6244
  camera;
5964
6245
  store;
@@ -5977,6 +6258,7 @@ var Viewport = class {
5977
6258
  noteEditor;
5978
6259
  arrowLabelEditor;
5979
6260
  historyRecorder;
6261
+ selectionOps;
5980
6262
  toolContext;
5981
6263
  marginViewport;
5982
6264
  resizeObserver = null;
@@ -5988,7 +6270,7 @@ var Viewport = class {
5988
6270
  interactMode;
5989
6271
  onHtmlElementMount;
5990
6272
  dropHandler;
5991
- gridChangeListeners = /* @__PURE__ */ new Set();
6273
+ gridController;
5992
6274
  doubleTapDetector = new DoubleTapDetector();
5993
6275
  tapDownX = 0;
5994
6276
  tapDownY = 0;
@@ -6160,48 +6442,19 @@ var Viewport = class {
6160
6442
  this.requestRender();
6161
6443
  }
6162
6444
  addGrid(input) {
6163
- const existing = this.store.getElementsByType("grid")[0];
6164
- this.historyRecorder.begin();
6165
- if (existing) {
6166
- this.store.remove(existing.id);
6167
- }
6168
- const grid = createGrid({ ...input, layerId: this.layerManager.activeLayerId });
6169
- this.store.add(grid);
6170
- this.historyRecorder.commit();
6171
- this.requestRender();
6172
- return grid.id;
6445
+ return this.gridController.add(input);
6173
6446
  }
6174
6447
  updateGrid(updates) {
6175
- const grid = this.store.getElementsByType("grid")[0];
6176
- if (!grid) return;
6177
- this.historyRecorder.begin();
6178
- this.store.update(grid.id, updates);
6179
- this.historyRecorder.commit();
6180
- this.requestRender();
6448
+ this.gridController.update(updates);
6181
6449
  }
6182
6450
  removeGrid() {
6183
- const grid = this.store.getElementsByType("grid")[0];
6184
- if (!grid) return;
6185
- this.historyRecorder.begin();
6186
- this.store.remove(grid.id);
6187
- this.historyRecorder.commit();
6188
- this.requestRender();
6451
+ this.gridController.remove();
6189
6452
  }
6190
6453
  getGridInfo() {
6191
- const grid = this.store.getElementsByType("grid")[0];
6192
- if (!grid) return null;
6193
- return {
6194
- gridType: grid.gridType,
6195
- hexOrientation: grid.hexOrientation,
6196
- cellSize: grid.cellSize,
6197
- cellRadius: grid.gridType === "hex" ? grid.cellSize : grid.cellSize / 2
6198
- };
6454
+ return this.gridController.getInfo();
6199
6455
  }
6200
6456
  onGridChange(listener) {
6201
- this.gridChangeListeners.add(listener);
6202
- return () => {
6203
- this.gridChangeListeners.delete(listener);
6204
- };
6457
+ return this.gridController.onChange(listener);
6205
6458
  }
6206
6459
  getSelectTool() {
6207
6460
  return this.toolManager.getTool("select");
@@ -6242,154 +6495,25 @@ var Viewport = class {
6242
6495
  return tool ? tool.onSelectionChange(listener) : noop;
6243
6496
  }
6244
6497
  getSelectionStyle() {
6245
- const ids = this.getSelectedIds();
6246
- if (ids.length === 0) return null;
6247
- const styles = [];
6248
- for (const id of ids) {
6249
- const el = this.store.getById(id);
6250
- if (el) styles.push(getElementStyle(el));
6251
- }
6252
- if (styles.length === 0) return null;
6253
- const result = {};
6254
- const color = sharedValue(styles.map((s) => s.color));
6255
- if (color !== void 0) result.color = color;
6256
- const fillColor = sharedValue(styles.map((s) => s.fillColor));
6257
- if (fillColor !== void 0) result.fillColor = fillColor;
6258
- const strokeWidth = sharedValue(styles.map((s) => s.strokeWidth));
6259
- if (strokeWidth !== void 0) result.strokeWidth = strokeWidth;
6260
- const opacity = sharedValue(styles.map((s) => s.opacity));
6261
- if (opacity !== void 0) result.opacity = opacity;
6262
- const fontSize = sharedValue(styles.map((s) => s.fontSize));
6263
- if (fontSize !== void 0) result.fontSize = fontSize;
6264
- return result;
6498
+ return this.selectionOps.getStyle();
6265
6499
  }
6266
6500
  applyStyleToSelection(style) {
6267
- const ids = this.getSelectedIds();
6268
- if (ids.length === 0) return;
6269
- this.historyRecorder.begin();
6270
- for (const id of ids) {
6271
- const el = this.store.getById(id);
6272
- if (!el) continue;
6273
- const patch = styleToPatch(el, style);
6274
- if (Object.keys(patch).length > 0) {
6275
- this.store.update(id, patch);
6276
- }
6277
- }
6278
- this.historyRecorder.commit();
6501
+ this.selectionOps.applyStyle(style);
6279
6502
  }
6280
6503
  groupSelection() {
6281
- const ids = this.getSelectedIds();
6282
- if (ids.length < 2) return;
6283
- const groupId = createId("group");
6284
- this.historyRecorder.begin();
6285
- for (const id of ids) {
6286
- if (this.store.getById(id)) this.store.update(id, { groupId });
6287
- }
6288
- this.historyRecorder.commit();
6504
+ this.selectionOps.group();
6289
6505
  }
6290
6506
  ungroupSelection() {
6291
- const ids = this.getSelectedIds();
6292
- if (ids.length === 0) return;
6293
- this.historyRecorder.begin();
6294
- for (const id of ids) {
6295
- const el = this.store.getById(id);
6296
- if (el && el.groupId !== void 0) this.store.update(id, { groupId: void 0 });
6297
- }
6298
- this.historyRecorder.commit();
6507
+ this.selectionOps.ungroup();
6299
6508
  }
6300
6509
  toggleLockSelection() {
6301
- const ids = this.getSelectedIds();
6302
- if (ids.length === 0) return;
6303
- const anyUnlocked = ids.some((id) => {
6304
- const el = this.store.getById(id);
6305
- return el ? !el.locked : false;
6306
- });
6307
- this.historyRecorder.begin();
6308
- for (const id of ids) {
6309
- const el = this.store.getById(id);
6310
- if (el && el.locked !== anyUnlocked) this.store.update(id, { locked: anyUnlocked });
6311
- }
6312
- this.historyRecorder.commit();
6510
+ this.selectionOps.toggleLock();
6313
6511
  }
6314
6512
  alignSelection(edge) {
6315
- const bounded = this.boundedSelection();
6316
- if (bounded.length < 2) return;
6317
- const B = unionBounds(bounded.map((e) => e.bounds));
6318
- this.historyRecorder.begin();
6319
- const moved = [];
6320
- for (const { id, el, bounds: b } of bounded) {
6321
- if (!this.isMovable(el)) continue;
6322
- let dx = 0;
6323
- let dy = 0;
6324
- switch (edge) {
6325
- case "left":
6326
- dx = B.x - b.x;
6327
- break;
6328
- case "right":
6329
- dx = B.x + B.w - (b.x + b.w);
6330
- break;
6331
- case "center-x":
6332
- dx = B.x + B.w / 2 - (b.x + b.w / 2);
6333
- break;
6334
- case "top":
6335
- dy = B.y - b.y;
6336
- break;
6337
- case "bottom":
6338
- dy = B.y + B.h - (b.y + b.h);
6339
- break;
6340
- case "middle":
6341
- dy = B.y + B.h / 2 - (b.y + b.h / 2);
6342
- break;
6343
- }
6344
- if (dx === 0 && dy === 0) continue;
6345
- this.store.update(id, translateElementPatch(el, dx, dy));
6346
- moved.push(id);
6347
- }
6348
- updateArrowsBoundToElements(moved, this.store);
6349
- this.historyRecorder.commit();
6350
- this.requestRender();
6513
+ this.selectionOps.align(edge);
6351
6514
  }
6352
6515
  distributeSelection(axis) {
6353
- const bounded = this.boundedSelection();
6354
- if (bounded.length < 3) return;
6355
- const center2 = (b) => axis === "horizontal" ? b.x + b.w / 2 : b.y + b.h / 2;
6356
- const sorted = [...bounded].sort((p, q) => center2(p.bounds) - center2(q.bounds));
6357
- const first = sorted[0];
6358
- const last = sorted[sorted.length - 1];
6359
- if (!first || !last) return;
6360
- const c0 = center2(first.bounds);
6361
- const cN = center2(last.bounds);
6362
- const n = sorted.length;
6363
- this.historyRecorder.begin();
6364
- const moved = [];
6365
- for (let i = 1; i < n - 1; i++) {
6366
- const item = sorted[i];
6367
- if (!item || !this.isMovable(item.el)) continue;
6368
- const target = c0 + i * (cN - c0) / (n - 1);
6369
- const delta = target - center2(item.bounds);
6370
- if (delta === 0) continue;
6371
- const [dx, dy] = axis === "horizontal" ? [delta, 0] : [0, delta];
6372
- this.store.update(item.id, translateElementPatch(item.el, dx, dy));
6373
- moved.push(item.id);
6374
- }
6375
- updateArrowsBoundToElements(moved, this.store);
6376
- this.historyRecorder.commit();
6377
- this.requestRender();
6378
- }
6379
- boundedSelection() {
6380
- const out = [];
6381
- for (const id of this.getSelectedIds()) {
6382
- const el = this.store.getById(id);
6383
- if (!el) continue;
6384
- const bounds = getElementBounds(el);
6385
- if (bounds) out.push({ id, el, bounds });
6386
- }
6387
- return out;
6388
- }
6389
- isMovable(el) {
6390
- if (el.locked) return false;
6391
- if (el.type === "arrow" && (el.fromBinding ?? el.toBinding)) return false;
6392
- return true;
6516
+ this.selectionOps.distribute(axis);
6393
6517
  }
6394
6518
  getRenderStats() {
6395
6519
  return this.renderLoop.getStats();
@@ -6598,43 +6722,6 @@ var Viewport = class {
6598
6722
  }
6599
6723
  }
6600
6724
  }
6601
- createWrapper() {
6602
- const el = document.createElement("div");
6603
- Object.assign(el.style, {
6604
- position: "relative",
6605
- width: "100%",
6606
- height: "100%",
6607
- overflow: "hidden",
6608
- overscrollBehavior: "none",
6609
- userSelect: "none",
6610
- webkitUserSelect: "none"
6611
- });
6612
- return el;
6613
- }
6614
- createCanvas() {
6615
- const el = document.createElement("canvas");
6616
- Object.assign(el.style, {
6617
- position: "absolute",
6618
- top: "0",
6619
- left: "0",
6620
- width: "100%",
6621
- height: "100%"
6622
- });
6623
- return el;
6624
- }
6625
- createDomLayer() {
6626
- const el = document.createElement("div");
6627
- Object.assign(el.style, {
6628
- position: "absolute",
6629
- top: "0",
6630
- left: "0",
6631
- width: "100%",
6632
- height: "100%",
6633
- pointerEvents: "none",
6634
- transformOrigin: "0 0"
6635
- });
6636
- return el;
6637
- }
6638
6725
  applyCameraTransform() {
6639
6726
  this.domLayer.style.transform = this.camera.toCSSTransform();
6640
6727
  }
@@ -6644,25 +6731,6 @@ var Viewport = class {
6644
6731
  this.renderLoop.setCanvasSize(rect.width * dpr, rect.height * dpr);
6645
6732
  this.requestRender();
6646
6733
  }
6647
- syncGridContext() {
6648
- const grid = this.store.getElementsByType("grid")[0];
6649
- if (grid) {
6650
- this.toolContext.gridSize = grid.cellSize;
6651
- this.toolContext.gridType = grid.gridType;
6652
- this.toolContext.hexOrientation = grid.hexOrientation;
6653
- } else {
6654
- this.toolContext.gridSize = this._gridSize;
6655
- this.toolContext.gridType = void 0;
6656
- this.toolContext.hexOrientation = void 0;
6657
- }
6658
- this.notifyGridChangeListeners();
6659
- }
6660
- notifyGridChangeListeners() {
6661
- const info = this.getGridInfo();
6662
- for (const listener of this.gridChangeListeners) {
6663
- listener(info);
6664
- }
6665
- }
6666
6734
  observeResize() {
6667
6735
  if (typeof ResizeObserver === "undefined") return;
6668
6736
  this.resizeObserver = new ResizeObserver(() => this.syncCanvasSize());
@@ -7195,14 +7263,11 @@ function computeSnapGuides(moving, targets, threshold) {
7195
7263
  return { dx: xSnap?.delta ?? 0, dy: ySnap?.delta ?? 0, guides };
7196
7264
  }
7197
7265
 
7198
- // src/tools/select-tool.ts
7266
+ // src/tools/select-overlay.ts
7199
7267
  var HANDLE_SIZE = 8;
7200
- var SNAP_PX = 6;
7201
7268
  var HANDLE_HIT_PADDING2 = 4;
7202
7269
  var SELECTION_PAD = 4;
7203
- var MIN_ELEMENT_SIZE = 20;
7204
7270
  var ROTATE_HANDLE_OFFSET = 24;
7205
- var ROTATE_SNAP = Math.PI / 12;
7206
7271
  var ROTATABLE_TYPES = /* @__PURE__ */ new Set(["note", "text", "image", "html", "shape", "stroke"]);
7207
7272
  var HANDLE_CURSORS = {
7208
7273
  nw: "nwse-resize",
@@ -7210,6 +7275,486 @@ var HANDLE_CURSORS = {
7210
7275
  ne: "nesw-resize",
7211
7276
  sw: "nesw-resize"
7212
7277
  };
7278
+ function getOverlayLayout(el, zoom) {
7279
+ const bounds = getElementBounds(el);
7280
+ if (!bounds) return null;
7281
+ const angle = el.rotation ?? 0;
7282
+ const pad = SELECTION_PAD / zoom;
7283
+ const center2 = { x: bounds.x + bounds.w / 2, y: bounds.y + bounds.h / 2 };
7284
+ const raw = [
7285
+ ["nw", { x: bounds.x - pad, y: bounds.y - pad }],
7286
+ ["ne", { x: bounds.x + bounds.w + pad, y: bounds.y - pad }],
7287
+ ["sw", { x: bounds.x - pad, y: bounds.y + bounds.h + pad }],
7288
+ ["se", { x: bounds.x + bounds.w + pad, y: bounds.y + bounds.h + pad }]
7289
+ ];
7290
+ const corners = raw.map(
7291
+ ([h, p]) => [h, rotatePoint(p, center2, angle)]
7292
+ );
7293
+ const topMid = { x: center2.x, y: bounds.y - pad - ROTATE_HANDLE_OFFSET / zoom };
7294
+ const rotateHandle = rotatePoint(topMid, center2, angle);
7295
+ return { center: center2, corners, rotateHandle, angle };
7296
+ }
7297
+ function getHandlePositions(bounds) {
7298
+ return [
7299
+ ["nw", { x: bounds.x, y: bounds.y }],
7300
+ ["ne", { x: bounds.x + bounds.w, y: bounds.y }],
7301
+ ["sw", { x: bounds.x, y: bounds.y + bounds.h }],
7302
+ ["se", { x: bounds.x + bounds.w, y: bounds.y + bounds.h }]
7303
+ ];
7304
+ }
7305
+ function topMidpoint(layout) {
7306
+ const nw = layout.corners.find(([h]) => h === "nw")?.[1] ?? { x: 0, y: 0 };
7307
+ const ne = layout.corners.find(([h]) => h === "ne")?.[1] ?? { x: 0, y: 0 };
7308
+ return { x: (nw.x + ne.x) / 2, y: (nw.y + ne.y) / 2 };
7309
+ }
7310
+ function drawLockBadge(ctx, at, zoom) {
7311
+ const r = 9 / zoom;
7312
+ ctx.save();
7313
+ ctx.setLineDash([]);
7314
+ ctx.beginPath();
7315
+ ctx.arc(at.x, at.y, r, 0, Math.PI * 2);
7316
+ ctx.fillStyle = "#ffffff";
7317
+ ctx.fill();
7318
+ ctx.strokeStyle = "#2196F3";
7319
+ ctx.lineWidth = 1.5 / zoom;
7320
+ ctx.stroke();
7321
+ const bw = 8 / zoom;
7322
+ const bh = 6 / zoom;
7323
+ ctx.fillStyle = "#2196F3";
7324
+ ctx.fillRect(at.x - bw / 2, at.y - bh / 2 + 1 / zoom, bw, bh);
7325
+ ctx.beginPath();
7326
+ ctx.arc(at.x, at.y - bh / 2 + 1 / zoom, 2.5 / zoom, Math.PI, 0);
7327
+ ctx.lineWidth = 1.4 / zoom;
7328
+ ctx.stroke();
7329
+ ctx.restore();
7330
+ }
7331
+ function renderMarquee(ctx, rect) {
7332
+ ctx.save();
7333
+ ctx.strokeStyle = "#2196F3";
7334
+ ctx.fillStyle = "rgba(33, 150, 243, 0.08)";
7335
+ ctx.lineWidth = 1;
7336
+ ctx.setLineDash([4, 4]);
7337
+ ctx.strokeRect(rect.x, rect.y, rect.w, rect.h);
7338
+ ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
7339
+ ctx.restore();
7340
+ }
7341
+ function renderBindingHighlights(ctx, arrow, zoom, store) {
7342
+ if (!arrow.fromBinding && !arrow.toBinding) return;
7343
+ const pad = SELECTION_PAD / zoom;
7344
+ ctx.save();
7345
+ ctx.strokeStyle = "#2196F3";
7346
+ ctx.lineWidth = 2 / zoom;
7347
+ ctx.setLineDash([]);
7348
+ const drawn = /* @__PURE__ */ new Set();
7349
+ for (const binding of [arrow.fromBinding, arrow.toBinding]) {
7350
+ if (!binding || drawn.has(binding.elementId)) continue;
7351
+ drawn.add(binding.elementId);
7352
+ const target = store.getById(binding.elementId);
7353
+ if (!target) continue;
7354
+ const bounds = getElementBounds(target);
7355
+ if (!bounds) continue;
7356
+ ctx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.w + pad * 2, bounds.h + pad * 2);
7357
+ }
7358
+ ctx.restore();
7359
+ }
7360
+ function renderSelectionBoxes(ctx, p) {
7361
+ if (p.selectedIds.length === 0) return;
7362
+ const zoom = p.zoom;
7363
+ const handleWorldSize = HANDLE_SIZE / zoom;
7364
+ ctx.save();
7365
+ ctx.strokeStyle = "#2196F3";
7366
+ ctx.lineWidth = 1.5 / zoom;
7367
+ ctx.setLineDash([4 / zoom, 4 / zoom]);
7368
+ for (const id of p.selectedIds) {
7369
+ const el = p.store.getById(id);
7370
+ if (!el) continue;
7371
+ if (el.type === "arrow") {
7372
+ renderArrowHandles(ctx, el, zoom);
7373
+ renderBindingHighlights(ctx, el, zoom, p.store);
7374
+ continue;
7375
+ }
7376
+ if (el.type === "shape" && el.shape === "line") {
7377
+ ctx.setLineDash([]);
7378
+ ctx.fillStyle = "#ffffff";
7379
+ const r = handleWorldSize / 2;
7380
+ for (const pt of lineEndpoints(el)) {
7381
+ ctx.beginPath();
7382
+ ctx.arc(pt.x, pt.y, r, 0, Math.PI * 2);
7383
+ ctx.fill();
7384
+ ctx.stroke();
7385
+ }
7386
+ ctx.setLineDash([4 / zoom, 4 / zoom]);
7387
+ continue;
7388
+ }
7389
+ const bounds = getElementBounds(el);
7390
+ if (!bounds) continue;
7391
+ const layout = getOverlayLayout(el, zoom);
7392
+ if (!layout) continue;
7393
+ const pad = SELECTION_PAD / zoom;
7394
+ if (layout.angle === 0) {
7395
+ ctx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.w + pad * 2, bounds.h + pad * 2);
7396
+ } else {
7397
+ const ordered = ["nw", "ne", "se", "sw"].map((h) => layout.corners.find(([c]) => c === h)?.[1]).filter((pp) => !!pp);
7398
+ const [p0, ...others] = ordered;
7399
+ if (p0) {
7400
+ ctx.beginPath();
7401
+ ctx.moveTo(p0.x, p0.y);
7402
+ for (const pp of others) ctx.lineTo(pp.x, pp.y);
7403
+ ctx.closePath();
7404
+ ctx.stroke();
7405
+ }
7406
+ }
7407
+ if (!el.locked) {
7408
+ if ("size" in el) {
7409
+ ctx.setLineDash([]);
7410
+ ctx.fillStyle = "#ffffff";
7411
+ const corners = layout.angle === 0 ? getHandlePositions(bounds) : layout.corners;
7412
+ for (const [, pos] of corners) {
7413
+ ctx.fillRect(
7414
+ pos.x - handleWorldSize / 2,
7415
+ pos.y - handleWorldSize / 2,
7416
+ handleWorldSize,
7417
+ handleWorldSize
7418
+ );
7419
+ ctx.strokeRect(
7420
+ pos.x - handleWorldSize / 2,
7421
+ pos.y - handleWorldSize / 2,
7422
+ handleWorldSize,
7423
+ handleWorldSize
7424
+ );
7425
+ }
7426
+ ctx.setLineDash([4 / zoom, 4 / zoom]);
7427
+ } else if (el.type === "template") {
7428
+ ctx.setLineDash([]);
7429
+ ctx.fillStyle = "#ffffff";
7430
+ const hx = bounds.x + bounds.w;
7431
+ const hy = bounds.y + bounds.h;
7432
+ ctx.fillRect(
7433
+ hx - handleWorldSize / 2,
7434
+ hy - handleWorldSize / 2,
7435
+ handleWorldSize,
7436
+ handleWorldSize
7437
+ );
7438
+ ctx.strokeRect(
7439
+ hx - handleWorldSize / 2,
7440
+ hy - handleWorldSize / 2,
7441
+ handleWorldSize,
7442
+ handleWorldSize
7443
+ );
7444
+ ctx.setLineDash([4 / zoom, 4 / zoom]);
7445
+ }
7446
+ if (p.selectedIds.length === 1 && ROTATABLE_TYPES.has(el.type)) {
7447
+ const stemStart = topMidpoint(layout);
7448
+ const stemEnd = layout.rotateHandle;
7449
+ ctx.beginPath();
7450
+ ctx.moveTo(stemStart.x, stemStart.y);
7451
+ ctx.lineTo(stemEnd.x, stemEnd.y);
7452
+ ctx.stroke();
7453
+ ctx.setLineDash([]);
7454
+ ctx.fillStyle = "#ffffff";
7455
+ ctx.beginPath();
7456
+ ctx.arc(stemEnd.x, stemEnd.y, handleWorldSize / 2, 0, Math.PI * 2);
7457
+ ctx.fill();
7458
+ ctx.stroke();
7459
+ ctx.setLineDash([4 / zoom, 4 / zoom]);
7460
+ }
7461
+ }
7462
+ if (el.locked) {
7463
+ const ne = layout.corners.find(([h]) => h === "ne")?.[1];
7464
+ if (ne) drawLockBadge(ctx, ne, zoom);
7465
+ }
7466
+ }
7467
+ ctx.restore();
7468
+ }
7469
+ function renderGuideLines(ctx, p) {
7470
+ const zoom = p.zoom;
7471
+ const rect = p.rect;
7472
+ ctx.save();
7473
+ ctx.strokeStyle = "#FF4081";
7474
+ ctx.lineWidth = 1 / zoom;
7475
+ ctx.setLineDash([]);
7476
+ for (const g of p.guides) {
7477
+ ctx.beginPath();
7478
+ if (g.axis === "x") {
7479
+ const y0 = rect ? rect.y : p.currentWorld.y - 1e5;
7480
+ const y1 = rect ? rect.y + rect.h : p.currentWorld.y + 1e5;
7481
+ ctx.moveTo(g.position, y0);
7482
+ ctx.lineTo(g.position, y1);
7483
+ } else {
7484
+ const x0 = rect ? rect.x : p.currentWorld.x - 1e5;
7485
+ const x1 = rect ? rect.x + rect.w : p.currentWorld.x + 1e5;
7486
+ ctx.moveTo(x0, g.position);
7487
+ ctx.lineTo(x1, g.position);
7488
+ }
7489
+ ctx.stroke();
7490
+ }
7491
+ ctx.restore();
7492
+ }
7493
+
7494
+ // src/tools/select-hit.ts
7495
+ function hitTest(world, ctx) {
7496
+ const r = 10;
7497
+ const candidates = ctx.store.queryRect({ x: world.x - r, y: world.y - r, w: r * 2, h: r * 2 }).reverse();
7498
+ for (const el of candidates) {
7499
+ if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
7500
+ if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
7501
+ if (el.type === "grid") continue;
7502
+ if (isInsideBounds(world, el)) return el;
7503
+ }
7504
+ return null;
7505
+ }
7506
+ function isInsideBounds(point, el) {
7507
+ if (el.type === "grid") return false;
7508
+ const angle = el.rotation ?? 0;
7509
+ if (angle !== 0) {
7510
+ const b = getElementBounds(el);
7511
+ if (b) {
7512
+ point = rotatePoint(point, { x: b.x + b.w / 2, y: b.y + b.h / 2 }, -angle);
7513
+ }
7514
+ }
7515
+ if (el.type === "shape" && el.shape === "line") {
7516
+ const [a, b] = lineEndpoints(el);
7517
+ const threshold = Math.max(el.strokeWidth / 2, 6);
7518
+ return distSqToSegment(point, a, b) <= threshold * threshold;
7519
+ }
7520
+ if ("size" in el) {
7521
+ const s = el.size;
7522
+ 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;
7523
+ }
7524
+ if (el.type === "stroke") {
7525
+ return hitTestStroke(el, point, 10);
7526
+ }
7527
+ if (el.type === "arrow") {
7528
+ return isNearBezier(point, el.from, el.to, el.bend, 10);
7529
+ }
7530
+ if (el.type === "template") {
7531
+ const bounds = getElementBounds(el);
7532
+ if (!bounds) return false;
7533
+ return point.x >= bounds.x && point.x <= bounds.x + bounds.w && point.y >= bounds.y && point.y <= bounds.y + bounds.h;
7534
+ }
7535
+ return false;
7536
+ }
7537
+ function hitTestResizeHandle(world, ctx, selectedIds) {
7538
+ if (selectedIds.length === 0) return null;
7539
+ const zoom = ctx.camera.zoom;
7540
+ const handleHalf = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
7541
+ for (const id of selectedIds) {
7542
+ const el = ctx.store.getById(id);
7543
+ if (!el || !("size" in el)) continue;
7544
+ if (el.locked) continue;
7545
+ if (el.type === "shape" && el.shape === "line") continue;
7546
+ const layout = getOverlayLayout(el, zoom);
7547
+ if (!layout) continue;
7548
+ for (const [handle, pos] of layout.corners) {
7549
+ if (Math.abs(world.x - pos.x) <= handleHalf && Math.abs(world.y - pos.y) <= handleHalf) {
7550
+ return { elementId: id, handle };
7551
+ }
7552
+ }
7553
+ }
7554
+ return null;
7555
+ }
7556
+ function hitTestRotateHandle(world, ctx, selectedIds) {
7557
+ if (selectedIds.length !== 1) return null;
7558
+ const id = selectedIds[0];
7559
+ if (!id) return null;
7560
+ const el = ctx.store.getById(id);
7561
+ if (!el || el.locked || !ROTATABLE_TYPES.has(el.type)) return null;
7562
+ const layout = getOverlayLayout(el, ctx.camera.zoom);
7563
+ if (!layout) return null;
7564
+ const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / ctx.camera.zoom;
7565
+ const dx = world.x - layout.rotateHandle.x;
7566
+ const dy = world.y - layout.rotateHandle.y;
7567
+ return dx * dx + dy * dy <= r * r ? { elementId: id } : null;
7568
+ }
7569
+ function hitTestLineHandles(world, ctx, selectedIds) {
7570
+ if (selectedIds.length === 0) return null;
7571
+ const zoom = ctx.camera.zoom;
7572
+ const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
7573
+ const r2 = r * r;
7574
+ for (const id of selectedIds) {
7575
+ const el = ctx.store.getById(id);
7576
+ if (!el || el.type !== "shape" || el.shape !== "line") continue;
7577
+ const [a, b] = lineEndpoints(el);
7578
+ if ((world.x - a.x) ** 2 + (world.y - a.y) ** 2 <= r2) return { elementId: id, fixed: b };
7579
+ if ((world.x - b.x) ** 2 + (world.y - b.y) ** 2 <= r2) return { elementId: id, fixed: a };
7580
+ }
7581
+ return null;
7582
+ }
7583
+ function hitTestTemplateResizeHandle(world, ctx, selectedIds) {
7584
+ if (selectedIds.length === 0) return null;
7585
+ const zoom = ctx.camera.zoom;
7586
+ const handleHalf = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
7587
+ for (const id of selectedIds) {
7588
+ const el = ctx.store.getById(id);
7589
+ if (!el || el.type !== "template") continue;
7590
+ const bounds = getElementBounds(el);
7591
+ if (!bounds) continue;
7592
+ const hx = bounds.x + bounds.w;
7593
+ const hy = bounds.y + bounds.h;
7594
+ if (Math.abs(world.x - hx) <= handleHalf && Math.abs(world.y - hy) <= handleHalf) {
7595
+ return id;
7596
+ }
7597
+ }
7598
+ return null;
7599
+ }
7600
+ function findElementsInRect(marquee, ctx) {
7601
+ const candidates = ctx.store.queryRect(marquee);
7602
+ const ids = [];
7603
+ for (const el of candidates) {
7604
+ if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
7605
+ if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
7606
+ if (el.type === "grid") continue;
7607
+ const bounds = getElementBounds(el);
7608
+ if (bounds && rectsOverlap(marquee, rotatedAABB(bounds, el.rotation ?? 0))) {
7609
+ ids.push(el.id);
7610
+ }
7611
+ }
7612
+ return ids;
7613
+ }
7614
+ function rectsOverlap(a, b) {
7615
+ 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;
7616
+ }
7617
+
7618
+ // src/tools/select-resize.ts
7619
+ var MIN_ELEMENT_SIZE = 20;
7620
+ function anchorOffset(handle, w, h) {
7621
+ switch (handle) {
7622
+ case "se":
7623
+ return { x: -w / 2, y: -h / 2 };
7624
+ case "sw":
7625
+ return { x: w / 2, y: -h / 2 };
7626
+ case "ne":
7627
+ return { x: -w / 2, y: h / 2 };
7628
+ case "nw":
7629
+ return { x: w / 2, y: h / 2 };
7630
+ default:
7631
+ return { x: 0, y: 0 };
7632
+ }
7633
+ }
7634
+ function computeResize(el, handle, world, lastWorld, aspectRatio, shiftKey) {
7635
+ const dx = world.x - lastWorld.x;
7636
+ const dy = world.y - lastWorld.y;
7637
+ let { x, y, w, h } = { x: el.position.x, y: el.position.y, w: el.size.w, h: el.size.h };
7638
+ switch (handle) {
7639
+ case "se":
7640
+ w += dx;
7641
+ h += dy;
7642
+ break;
7643
+ case "sw":
7644
+ x += dx;
7645
+ w -= dx;
7646
+ h += dy;
7647
+ break;
7648
+ case "ne":
7649
+ y += dy;
7650
+ w += dx;
7651
+ h -= dy;
7652
+ break;
7653
+ case "nw":
7654
+ x += dx;
7655
+ y += dy;
7656
+ w -= dx;
7657
+ h -= dy;
7658
+ break;
7659
+ }
7660
+ if (shiftKey && aspectRatio > 0) {
7661
+ const absDw = Math.abs(w - el.size.w);
7662
+ const absDh = Math.abs(h - el.size.h);
7663
+ if (absDw >= absDh) {
7664
+ h = w / aspectRatio;
7665
+ } else {
7666
+ w = h * aspectRatio;
7667
+ }
7668
+ if (handle === "nw" || handle === "sw") {
7669
+ x = el.position.x + el.size.w - w;
7670
+ }
7671
+ if (handle === "nw" || handle === "ne") {
7672
+ y = el.position.y + el.size.h - h;
7673
+ }
7674
+ }
7675
+ if (w < MIN_ELEMENT_SIZE) {
7676
+ if (handle === "nw" || handle === "sw") x = el.position.x + el.size.w - MIN_ELEMENT_SIZE;
7677
+ w = MIN_ELEMENT_SIZE;
7678
+ }
7679
+ if (h < MIN_ELEMENT_SIZE) {
7680
+ if (handle === "nw" || handle === "ne") y = el.position.y + el.size.h - MIN_ELEMENT_SIZE;
7681
+ h = MIN_ELEMENT_SIZE;
7682
+ }
7683
+ return { position: { x, y }, size: { w, h } };
7684
+ }
7685
+ function computeRotatedResize(el, handle, angle, world, lastWorld, aspectRatio, shiftKey) {
7686
+ const wdx = world.x - lastWorld.x;
7687
+ const wdy = world.y - lastWorld.y;
7688
+ const cosN = Math.cos(-angle);
7689
+ const sinN = Math.sin(-angle);
7690
+ const ldx = wdx * cosN - wdy * sinN;
7691
+ const ldy = wdx * sinN + wdy * cosN;
7692
+ let w = el.size.w;
7693
+ let h = el.size.h;
7694
+ switch (handle) {
7695
+ case "se":
7696
+ w += ldx;
7697
+ h += ldy;
7698
+ break;
7699
+ case "sw":
7700
+ w -= ldx;
7701
+ h += ldy;
7702
+ break;
7703
+ case "ne":
7704
+ w += ldx;
7705
+ h -= ldy;
7706
+ break;
7707
+ case "nw":
7708
+ w -= ldx;
7709
+ h -= ldy;
7710
+ break;
7711
+ }
7712
+ if (shiftKey && aspectRatio > 0) {
7713
+ const absDw = Math.abs(w - el.size.w);
7714
+ const absDh = Math.abs(h - el.size.h);
7715
+ if (absDw >= absDh) h = w / aspectRatio;
7716
+ else w = h * aspectRatio;
7717
+ }
7718
+ w = Math.max(w, MIN_ELEMENT_SIZE);
7719
+ h = Math.max(h, MIN_ELEMENT_SIZE);
7720
+ const oldCenter = { x: el.position.x + el.size.w / 2, y: el.position.y + el.size.h / 2 };
7721
+ const oldAnchorLocal = anchorOffset(handle, el.size.w, el.size.h);
7722
+ const anchorWorld = rotatePoint(
7723
+ { x: oldCenter.x + oldAnchorLocal.x, y: oldCenter.y + oldAnchorLocal.y },
7724
+ oldCenter,
7725
+ angle
7726
+ );
7727
+ const newAnchorLocal = anchorOffset(handle, w, h);
7728
+ const cos = Math.cos(angle);
7729
+ const sin = Math.sin(angle);
7730
+ const rotatedAnchor = {
7731
+ x: newAnchorLocal.x * cos - newAnchorLocal.y * sin,
7732
+ y: newAnchorLocal.x * sin + newAnchorLocal.y * cos
7733
+ };
7734
+ const newCenter = { x: anchorWorld.x - rotatedAnchor.x, y: anchorWorld.y - rotatedAnchor.y };
7735
+ const position = { x: newCenter.x - w / 2, y: newCenter.y - h / 2 };
7736
+ return { position, size: { w, h } };
7737
+ }
7738
+ function computeTemplateResize(el, world, opts) {
7739
+ const dx = world.x - el.position.x;
7740
+ const dy = world.y - el.position.y;
7741
+ let newRadius = Math.sqrt(dx * dx + dy * dy);
7742
+ if (opts.snapToGrid && opts.gridSize && opts.gridSize > 0) {
7743
+ const snapUnit = opts.gridType === "hex" ? Math.sqrt(3) * opts.gridSize : opts.gridSize;
7744
+ newRadius = Math.max(snapUnit, Math.round(newRadius / snapUnit) * snapUnit);
7745
+ }
7746
+ newRadius = Math.max(MIN_ELEMENT_SIZE, newRadius);
7747
+ const updates = { radius: newRadius };
7748
+ if (el.feetPerCell != null && opts.gridSize && opts.gridSize > 0) {
7749
+ const snapUnit = opts.gridType === "hex" ? Math.sqrt(3) * opts.gridSize : opts.gridSize;
7750
+ updates.radiusFeet = newRadius / snapUnit * el.feetPerCell;
7751
+ }
7752
+ return updates;
7753
+ }
7754
+
7755
+ // src/tools/select-tool.ts
7756
+ var SNAP_PX = 6;
7757
+ var ROTATE_SNAP = Math.PI / 12;
7213
7758
  var SelectTool = class {
7214
7759
  name = "select";
7215
7760
  _selectedIds = [];
@@ -7245,7 +7790,7 @@ var SelectTool = class {
7245
7790
  this.ctx?.requestRender();
7246
7791
  }
7247
7792
  selectAtPoint(world, ctx) {
7248
- const hit = this.hitTest(world, ctx);
7793
+ const hit = hitTest(world, ctx);
7249
7794
  if (!hit) {
7250
7795
  this.setSelectedIds([]);
7251
7796
  return;
@@ -7289,19 +7834,19 @@ var SelectTool = class {
7289
7834
  ctx.requestRender();
7290
7835
  return;
7291
7836
  }
7292
- const lineHit = this.hitTestLineHandles(world, ctx);
7837
+ const lineHit = hitTestLineHandles(world, ctx, this._selectedIds);
7293
7838
  if (lineHit) {
7294
7839
  this.mode = { type: "line-handle", elementId: lineHit.elementId, fixed: lineHit.fixed };
7295
7840
  ctx.requestRender();
7296
7841
  return;
7297
7842
  }
7298
- const templateResizeHit = this.hitTestTemplateResizeHandle(world, ctx);
7843
+ const templateResizeHit = hitTestTemplateResizeHandle(world, ctx, this._selectedIds);
7299
7844
  if (templateResizeHit) {
7300
7845
  this.mode = { type: "resizing-template", elementId: templateResizeHit };
7301
7846
  ctx.requestRender();
7302
7847
  return;
7303
7848
  }
7304
- const rotateHit = this.hitTestRotateHandle(world, ctx);
7849
+ const rotateHit = hitTestRotateHandle(world, ctx, this._selectedIds);
7305
7850
  if (rotateHit) {
7306
7851
  const el = ctx.store.getById(rotateHit.elementId);
7307
7852
  const layout = el ? this.getOverlayLayout(el, ctx.camera.zoom) : null;
@@ -7317,7 +7862,7 @@ var SelectTool = class {
7317
7862
  return;
7318
7863
  }
7319
7864
  }
7320
- const resizeHit = this.hitTestResizeHandle(world, ctx);
7865
+ const resizeHit = hitTestResizeHandle(world, ctx, this._selectedIds);
7321
7866
  if (resizeHit) {
7322
7867
  const el = ctx.store.getById(resizeHit.elementId);
7323
7868
  if (el && "size" in el) {
@@ -7333,7 +7878,7 @@ var SelectTool = class {
7333
7878
  }
7334
7879
  this.pendingSingleSelectId = null;
7335
7880
  this.hasDragged = false;
7336
- const hit = this.hitTest(world, ctx);
7881
+ const hit = hitTest(world, ctx);
7337
7882
  if (hit) {
7338
7883
  const all = ctx.store.getAll();
7339
7884
  const alreadySelected = this._selectedIds.includes(hit.id);
@@ -7471,7 +8016,7 @@ var SelectTool = class {
7471
8016
  if (this.mode.type === "marquee") {
7472
8017
  const rect = this.getMarqueeRect();
7473
8018
  if (rect) {
7474
- this.setSelectedIds(expandToGroups(this.findElementsInRect(rect, ctx), ctx.store.getAll()));
8019
+ this.setSelectedIds(expandToGroups(findElementsInRect(rect, ctx), ctx.store.getAll()));
7475
8020
  }
7476
8021
  ctx.requestRender();
7477
8022
  }
@@ -7498,8 +8043,16 @@ var SelectTool = class {
7498
8043
  this.setHovered(hoverId, ctx);
7499
8044
  }
7500
8045
  renderOverlay(canvasCtx) {
7501
- this.renderMarquee(canvasCtx);
7502
- this.renderSelectionBoxes(canvasCtx);
8046
+ if (this.mode.type === "marquee") {
8047
+ const rect = this.getMarqueeRect();
8048
+ if (rect) renderMarquee(canvasCtx, rect);
8049
+ }
8050
+ if (this.ctx)
8051
+ renderSelectionBoxes(canvasCtx, {
8052
+ selectedIds: this._selectedIds,
8053
+ store: this.ctx.store,
8054
+ zoom: this.ctx.camera.zoom
8055
+ });
7503
8056
  if (this.mode.type === "arrow-handle" && this.ctx) {
7504
8057
  const target = getArrowHandleDragTarget(
7505
8058
  this.mode.handle,
@@ -7533,32 +8086,13 @@ var SelectTool = class {
7533
8086
  }
7534
8087
  }
7535
8088
  }
7536
- this.renderGuideLines(canvasCtx);
7537
- }
7538
- renderGuideLines(canvasCtx) {
7539
- if (this.mode.type !== "dragging" || !this.ctx || this.activeGuides.length === 0) return;
7540
- const zoom = this.ctx.camera.zoom;
7541
- const rect = this.dragVisibleRect;
7542
- canvasCtx.save();
7543
- canvasCtx.strokeStyle = "#FF4081";
7544
- canvasCtx.lineWidth = 1 / zoom;
7545
- canvasCtx.setLineDash([]);
7546
- for (const g of this.activeGuides) {
7547
- canvasCtx.beginPath();
7548
- if (g.axis === "x") {
7549
- const y0 = rect ? rect.y : this.currentWorld.y - 1e5;
7550
- const y1 = rect ? rect.y + rect.h : this.currentWorld.y + 1e5;
7551
- canvasCtx.moveTo(g.position, y0);
7552
- canvasCtx.lineTo(g.position, y1);
7553
- } else {
7554
- const x0 = rect ? rect.x : this.currentWorld.x - 1e5;
7555
- const x1 = rect ? rect.x + rect.w : this.currentWorld.x + 1e5;
7556
- canvasCtx.moveTo(x0, g.position);
7557
- canvasCtx.lineTo(x1, g.position);
7558
- }
7559
- canvasCtx.stroke();
7560
- }
7561
- canvasCtx.restore();
8089
+ if (this.mode.type === "dragging" && this.ctx && this.activeGuides.length)
8090
+ renderGuideLines(canvasCtx, {
8091
+ guides: this.activeGuides,
8092
+ rect: this.dragVisibleRect,
8093
+ currentWorld: this.currentWorld,
8094
+ zoom: this.ctx.camera.zoom
8095
+ });
7562
8096
  }
7563
8097
  updateArrowsBoundTo(ids, ctx) {
7564
8098
  updateArrowsBoundToElements(ids, ctx.store);
@@ -7584,25 +8118,25 @@ var SelectTool = class {
7584
8118
  ctx.setCursor?.(getArrowHandleCursor(arrowHit.handle, false));
7585
8119
  return null;
7586
8120
  }
7587
- if (this.hitTestLineHandles(world, ctx)) {
8121
+ if (hitTestLineHandles(world, ctx, this._selectedIds)) {
7588
8122
  ctx.setCursor?.("grab");
7589
8123
  return null;
7590
8124
  }
7591
- const templateResizeHit = this.hitTestTemplateResizeHandle(world, ctx);
8125
+ const templateResizeHit = hitTestTemplateResizeHandle(world, ctx, this._selectedIds);
7592
8126
  if (templateResizeHit) {
7593
8127
  ctx.setCursor?.("nwse-resize");
7594
8128
  return null;
7595
8129
  }
7596
- if (this.hitTestRotateHandle(world, ctx)) {
8130
+ if (hitTestRotateHandle(world, ctx, this._selectedIds)) {
7597
8131
  ctx.setCursor?.("grab");
7598
8132
  return null;
7599
8133
  }
7600
- const resizeHit = this.hitTestResizeHandle(world, ctx);
8134
+ const resizeHit = hitTestResizeHandle(world, ctx, this._selectedIds);
7601
8135
  if (resizeHit) {
7602
8136
  ctx.setCursor?.(HANDLE_CURSORS[resizeHit.handle]);
7603
8137
  return null;
7604
8138
  }
7605
- const hit = this.hitTest(world, ctx);
8139
+ const hit = hitTest(world, ctx);
7606
8140
  ctx.setCursor?.(hit ? "move" : "default");
7607
8141
  return hit ? hit.id : null;
7608
8142
  }
@@ -7616,421 +8150,43 @@ var SelectTool = class {
7616
8150
  const el = ctx.store.getById(this.mode.elementId);
7617
8151
  if (!el || !("size" in el) || el.locked) return;
7618
8152
  const angle = el.rotation ?? 0;
7619
- if (angle !== 0) {
7620
- this.handleRotatedResize(world, el, angle, ctx, shiftKey);
7621
- return;
7622
- }
7623
- const { handle } = this.mode;
7624
- const dx = world.x - this.lastWorld.x;
7625
- const dy = world.y - this.lastWorld.y;
7626
- this.lastWorld = world;
7627
- let { x, y, w, h } = { x: el.position.x, y: el.position.y, w: el.size.w, h: el.size.h };
7628
- switch (handle) {
7629
- case "se":
7630
- w += dx;
7631
- h += dy;
7632
- break;
7633
- case "sw":
7634
- x += dx;
7635
- w -= dx;
7636
- h += dy;
7637
- break;
7638
- case "ne":
7639
- y += dy;
7640
- w += dx;
7641
- h -= dy;
7642
- break;
7643
- case "nw":
7644
- x += dx;
7645
- y += dy;
7646
- w -= dx;
7647
- h -= dy;
7648
- break;
7649
- }
7650
- if (shiftKey && this.resizeAspectRatio > 0) {
7651
- const absDw = Math.abs(w - el.size.w);
7652
- const absDh = Math.abs(h - el.size.h);
7653
- if (absDw >= absDh) {
7654
- h = w / this.resizeAspectRatio;
7655
- } else {
7656
- w = h * this.resizeAspectRatio;
7657
- }
7658
- if (handle === "nw" || handle === "sw") {
7659
- x = el.position.x + el.size.w - w;
7660
- }
7661
- if (handle === "nw" || handle === "ne") {
7662
- y = el.position.y + el.size.h - h;
7663
- }
7664
- }
7665
- if (w < MIN_ELEMENT_SIZE) {
7666
- if (handle === "nw" || handle === "sw") x = el.position.x + el.size.w - MIN_ELEMENT_SIZE;
7667
- w = MIN_ELEMENT_SIZE;
7668
- }
7669
- if (h < MIN_ELEMENT_SIZE) {
7670
- if (handle === "nw" || handle === "ne") y = el.position.y + el.size.h - MIN_ELEMENT_SIZE;
7671
- h = MIN_ELEMENT_SIZE;
7672
- }
7673
- ctx.store.update(this.mode.elementId, {
7674
- position: { x, y },
7675
- size: { w, h }
7676
- });
7677
- this.updateArrowsBoundTo([this.mode.elementId], ctx);
7678
- ctx.requestRender();
7679
- }
7680
- anchorOffset(handle, w, h) {
7681
- switch (handle) {
7682
- case "se":
7683
- return { x: -w / 2, y: -h / 2 };
7684
- case "sw":
7685
- return { x: w / 2, y: -h / 2 };
7686
- case "ne":
7687
- return { x: -w / 2, y: h / 2 };
7688
- case "nw":
7689
- return { x: w / 2, y: h / 2 };
7690
- default:
7691
- return { x: 0, y: 0 };
7692
- }
7693
- }
7694
- handleRotatedResize(world, el, angle, ctx, shiftKey) {
7695
- if (this.mode.type !== "resizing") return;
7696
- const { handle } = this.mode;
7697
- const wdx = world.x - this.lastWorld.x;
7698
- const wdy = world.y - this.lastWorld.y;
7699
- this.lastWorld = world;
7700
- const cosN = Math.cos(-angle);
7701
- const sinN = Math.sin(-angle);
7702
- const ldx = wdx * cosN - wdy * sinN;
7703
- const ldy = wdx * sinN + wdy * cosN;
7704
- let w = el.size.w;
7705
- let h = el.size.h;
7706
- switch (handle) {
7707
- case "se":
7708
- w += ldx;
7709
- h += ldy;
7710
- break;
7711
- case "sw":
7712
- w -= ldx;
7713
- h += ldy;
7714
- break;
7715
- case "ne":
7716
- w += ldx;
7717
- h -= ldy;
7718
- break;
7719
- case "nw":
7720
- w -= ldx;
7721
- h -= ldy;
7722
- break;
7723
- }
7724
- if (shiftKey && this.resizeAspectRatio > 0) {
7725
- const absDw = Math.abs(w - el.size.w);
7726
- const absDh = Math.abs(h - el.size.h);
7727
- if (absDw >= absDh) h = w / this.resizeAspectRatio;
7728
- else w = h * this.resizeAspectRatio;
7729
- }
7730
- w = Math.max(w, MIN_ELEMENT_SIZE);
7731
- h = Math.max(h, MIN_ELEMENT_SIZE);
7732
- const oldCenter = { x: el.position.x + el.size.w / 2, y: el.position.y + el.size.h / 2 };
7733
- const oldAnchorLocal = this.anchorOffset(handle, el.size.w, el.size.h);
7734
- const anchorWorld = rotatePoint(
7735
- { x: oldCenter.x + oldAnchorLocal.x, y: oldCenter.y + oldAnchorLocal.y },
7736
- oldCenter,
7737
- angle
8153
+ const patch = angle !== 0 ? computeRotatedResize(
8154
+ el,
8155
+ this.mode.handle,
8156
+ angle,
8157
+ world,
8158
+ this.lastWorld,
8159
+ this.resizeAspectRatio,
8160
+ shiftKey
8161
+ ) : computeResize(
8162
+ el,
8163
+ this.mode.handle,
8164
+ world,
8165
+ this.lastWorld,
8166
+ this.resizeAspectRatio,
8167
+ shiftKey
7738
8168
  );
7739
- const newAnchorLocal = this.anchorOffset(handle, w, h);
7740
- const cos = Math.cos(angle);
7741
- const sin = Math.sin(angle);
7742
- const rotatedAnchor = {
7743
- x: newAnchorLocal.x * cos - newAnchorLocal.y * sin,
7744
- y: newAnchorLocal.x * sin + newAnchorLocal.y * cos
7745
- };
7746
- const newCenter = { x: anchorWorld.x - rotatedAnchor.x, y: anchorWorld.y - rotatedAnchor.y };
7747
- const position = { x: newCenter.x - w / 2, y: newCenter.y - h / 2 };
7748
- ctx.store.update(this.mode.elementId, { position, size: { w, h } });
8169
+ this.lastWorld = world;
8170
+ ctx.store.update(this.mode.elementId, patch);
7749
8171
  this.updateArrowsBoundTo([this.mode.elementId], ctx);
7750
8172
  ctx.requestRender();
7751
8173
  }
7752
- hitTestResizeHandle(world, ctx) {
7753
- if (this._selectedIds.length === 0) return null;
7754
- const zoom = ctx.camera.zoom;
7755
- const handleHalf = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
7756
- for (const id of this._selectedIds) {
7757
- const el = ctx.store.getById(id);
7758
- if (!el || !("size" in el)) continue;
7759
- if (el.locked) continue;
7760
- if (el.type === "shape" && el.shape === "line") continue;
7761
- const layout = this.getOverlayLayout(el, zoom);
7762
- if (!layout) continue;
7763
- for (const [handle, pos] of layout.corners) {
7764
- if (Math.abs(world.x - pos.x) <= handleHalf && Math.abs(world.y - pos.y) <= handleHalf) {
7765
- return { elementId: id, handle };
7766
- }
7767
- }
7768
- }
7769
- return null;
7770
- }
7771
- hitTestRotateHandle(world, ctx) {
7772
- if (this._selectedIds.length !== 1) return null;
7773
- const id = this._selectedIds[0];
7774
- if (!id) return null;
7775
- const el = ctx.store.getById(id);
7776
- if (!el || el.locked || !ROTATABLE_TYPES.has(el.type)) return null;
7777
- const layout = this.getOverlayLayout(el, ctx.camera.zoom);
7778
- if (!layout) return null;
7779
- const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / ctx.camera.zoom;
7780
- const dx = world.x - layout.rotateHandle.x;
7781
- const dy = world.y - layout.rotateHandle.y;
7782
- return dx * dx + dy * dy <= r * r ? { elementId: id } : null;
7783
- }
7784
- hitTestLineHandles(world, ctx) {
7785
- if (this._selectedIds.length === 0) return null;
7786
- const zoom = ctx.camera.zoom;
7787
- const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
7788
- const r2 = r * r;
7789
- for (const id of this._selectedIds) {
7790
- const el = ctx.store.getById(id);
7791
- if (!el || el.type !== "shape" || el.shape !== "line") continue;
7792
- const [a, b] = lineEndpoints(el);
7793
- if ((world.x - a.x) ** 2 + (world.y - a.y) ** 2 <= r2) return { elementId: id, fixed: b };
7794
- if ((world.x - b.x) ** 2 + (world.y - b.y) ** 2 <= r2) return { elementId: id, fixed: a };
7795
- }
7796
- return null;
7797
- }
7798
- getHandlePositions(bounds) {
7799
- return [
7800
- ["nw", { x: bounds.x, y: bounds.y }],
7801
- ["ne", { x: bounds.x + bounds.w, y: bounds.y }],
7802
- ["sw", { x: bounds.x, y: bounds.y + bounds.h }],
7803
- ["se", { x: bounds.x + bounds.w, y: bounds.y + bounds.h }]
7804
- ];
7805
- }
7806
8174
  getOverlayLayout(el, zoom) {
7807
- const bounds = getElementBounds(el);
7808
- if (!bounds) return null;
7809
- const angle = el.rotation ?? 0;
7810
- const pad = SELECTION_PAD / zoom;
7811
- const center2 = { x: bounds.x + bounds.w / 2, y: bounds.y + bounds.h / 2 };
7812
- const raw = [
7813
- ["nw", { x: bounds.x - pad, y: bounds.y - pad }],
7814
- ["ne", { x: bounds.x + bounds.w + pad, y: bounds.y - pad }],
7815
- ["sw", { x: bounds.x - pad, y: bounds.y + bounds.h + pad }],
7816
- ["se", { x: bounds.x + bounds.w + pad, y: bounds.y + bounds.h + pad }]
7817
- ];
7818
- const corners = raw.map(
7819
- ([h, p]) => [h, rotatePoint(p, center2, angle)]
7820
- );
7821
- const topMid = { x: center2.x, y: bounds.y - pad - ROTATE_HANDLE_OFFSET / zoom };
7822
- const rotateHandle = rotatePoint(topMid, center2, angle);
7823
- return { center: center2, corners, rotateHandle, angle };
7824
- }
7825
- topMidpoint(layout) {
7826
- const nw = layout.corners.find(([h]) => h === "nw")?.[1] ?? { x: 0, y: 0 };
7827
- const ne = layout.corners.find(([h]) => h === "ne")?.[1] ?? { x: 0, y: 0 };
7828
- return { x: (nw.x + ne.x) / 2, y: (nw.y + ne.y) / 2 };
7829
- }
7830
- renderMarquee(canvasCtx) {
7831
- if (this.mode.type !== "marquee") return;
7832
- const rect = this.getMarqueeRect();
7833
- if (!rect) return;
7834
- canvasCtx.save();
7835
- canvasCtx.strokeStyle = "#2196F3";
7836
- canvasCtx.fillStyle = "rgba(33, 150, 243, 0.08)";
7837
- canvasCtx.lineWidth = 1;
7838
- canvasCtx.setLineDash([4, 4]);
7839
- canvasCtx.strokeRect(rect.x, rect.y, rect.w, rect.h);
7840
- canvasCtx.fillRect(rect.x, rect.y, rect.w, rect.h);
7841
- canvasCtx.restore();
7842
- }
7843
- renderSelectionBoxes(canvasCtx) {
7844
- if (this._selectedIds.length === 0 || !this.ctx) return;
7845
- const zoom = this.ctx.camera.zoom;
7846
- const handleWorldSize = HANDLE_SIZE / zoom;
7847
- canvasCtx.save();
7848
- canvasCtx.strokeStyle = "#2196F3";
7849
- canvasCtx.lineWidth = 1.5 / zoom;
7850
- canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7851
- for (const id of this._selectedIds) {
7852
- const el = this.ctx.store.getById(id);
7853
- if (!el) continue;
7854
- if (el.type === "arrow") {
7855
- renderArrowHandles(canvasCtx, el, zoom);
7856
- this.renderBindingHighlights(canvasCtx, el, zoom);
7857
- continue;
7858
- }
7859
- if (el.type === "shape" && el.shape === "line") {
7860
- canvasCtx.setLineDash([]);
7861
- canvasCtx.fillStyle = "#ffffff";
7862
- const r = handleWorldSize / 2;
7863
- for (const p of lineEndpoints(el)) {
7864
- canvasCtx.beginPath();
7865
- canvasCtx.arc(p.x, p.y, r, 0, Math.PI * 2);
7866
- canvasCtx.fill();
7867
- canvasCtx.stroke();
7868
- }
7869
- canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7870
- continue;
7871
- }
7872
- const bounds = getElementBounds(el);
7873
- if (!bounds) continue;
7874
- const layout = this.getOverlayLayout(el, zoom);
7875
- if (!layout) continue;
7876
- const pad = SELECTION_PAD / zoom;
7877
- if (layout.angle === 0) {
7878
- canvasCtx.strokeRect(
7879
- bounds.x - pad,
7880
- bounds.y - pad,
7881
- bounds.w + pad * 2,
7882
- bounds.h + pad * 2
7883
- );
7884
- } else {
7885
- const ordered = ["nw", "ne", "se", "sw"].map((h) => layout.corners.find(([c]) => c === h)?.[1]).filter((p) => !!p);
7886
- const [p0, ...others] = ordered;
7887
- if (p0) {
7888
- canvasCtx.beginPath();
7889
- canvasCtx.moveTo(p0.x, p0.y);
7890
- for (const p of others) canvasCtx.lineTo(p.x, p.y);
7891
- canvasCtx.closePath();
7892
- canvasCtx.stroke();
7893
- }
7894
- }
7895
- if (!el.locked) {
7896
- if ("size" in el) {
7897
- canvasCtx.setLineDash([]);
7898
- canvasCtx.fillStyle = "#ffffff";
7899
- const corners = layout.angle === 0 ? this.getHandlePositions(bounds) : layout.corners;
7900
- for (const [, pos] of corners) {
7901
- canvasCtx.fillRect(
7902
- pos.x - handleWorldSize / 2,
7903
- pos.y - handleWorldSize / 2,
7904
- handleWorldSize,
7905
- handleWorldSize
7906
- );
7907
- canvasCtx.strokeRect(
7908
- pos.x - handleWorldSize / 2,
7909
- pos.y - handleWorldSize / 2,
7910
- handleWorldSize,
7911
- handleWorldSize
7912
- );
7913
- }
7914
- canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7915
- } else if (el.type === "template") {
7916
- canvasCtx.setLineDash([]);
7917
- canvasCtx.fillStyle = "#ffffff";
7918
- const hx = bounds.x + bounds.w;
7919
- const hy = bounds.y + bounds.h;
7920
- canvasCtx.fillRect(
7921
- hx - handleWorldSize / 2,
7922
- hy - handleWorldSize / 2,
7923
- handleWorldSize,
7924
- handleWorldSize
7925
- );
7926
- canvasCtx.strokeRect(
7927
- hx - handleWorldSize / 2,
7928
- hy - handleWorldSize / 2,
7929
- handleWorldSize,
7930
- handleWorldSize
7931
- );
7932
- canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7933
- }
7934
- if (this._selectedIds.length === 1 && ROTATABLE_TYPES.has(el.type)) {
7935
- const stemStart = this.topMidpoint(layout);
7936
- const stemEnd = layout.rotateHandle;
7937
- canvasCtx.beginPath();
7938
- canvasCtx.moveTo(stemStart.x, stemStart.y);
7939
- canvasCtx.lineTo(stemEnd.x, stemEnd.y);
7940
- canvasCtx.stroke();
7941
- canvasCtx.setLineDash([]);
7942
- canvasCtx.fillStyle = "#ffffff";
7943
- canvasCtx.beginPath();
7944
- canvasCtx.arc(stemEnd.x, stemEnd.y, handleWorldSize / 2, 0, Math.PI * 2);
7945
- canvasCtx.fill();
7946
- canvasCtx.stroke();
7947
- canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7948
- }
7949
- }
7950
- if (el.locked) {
7951
- const ne = layout.corners.find(([h]) => h === "ne")?.[1];
7952
- if (ne) this.drawLockBadge(canvasCtx, ne, zoom);
7953
- }
7954
- }
7955
- canvasCtx.restore();
7956
- }
7957
- drawLockBadge(ctx, at, zoom) {
7958
- const r = 9 / zoom;
7959
- ctx.save();
7960
- ctx.setLineDash([]);
7961
- ctx.beginPath();
7962
- ctx.arc(at.x, at.y, r, 0, Math.PI * 2);
7963
- ctx.fillStyle = "#ffffff";
7964
- ctx.fill();
7965
- ctx.strokeStyle = "#2196F3";
7966
- ctx.lineWidth = 1.5 / zoom;
7967
- ctx.stroke();
7968
- const bw = 8 / zoom;
7969
- const bh = 6 / zoom;
7970
- ctx.fillStyle = "#2196F3";
7971
- ctx.fillRect(at.x - bw / 2, at.y - bh / 2 + 1 / zoom, bw, bh);
7972
- ctx.beginPath();
7973
- ctx.arc(at.x, at.y - bh / 2 + 1 / zoom, 2.5 / zoom, Math.PI, 0);
7974
- ctx.lineWidth = 1.4 / zoom;
7975
- ctx.stroke();
7976
- ctx.restore();
7977
- }
7978
- renderBindingHighlights(canvasCtx, arrow, zoom) {
7979
- if (!this.ctx) return;
7980
- if (!arrow.fromBinding && !arrow.toBinding) return;
7981
- const pad = SELECTION_PAD / zoom;
7982
- canvasCtx.save();
7983
- canvasCtx.strokeStyle = "#2196F3";
7984
- canvasCtx.lineWidth = 2 / zoom;
7985
- canvasCtx.setLineDash([]);
7986
- const drawn = /* @__PURE__ */ new Set();
7987
- for (const binding of [arrow.fromBinding, arrow.toBinding]) {
7988
- if (!binding || drawn.has(binding.elementId)) continue;
7989
- drawn.add(binding.elementId);
7990
- const target = this.ctx.store.getById(binding.elementId);
7991
- if (!target) continue;
7992
- const bounds = getElementBounds(target);
7993
- if (!bounds) continue;
7994
- canvasCtx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.w + pad * 2, bounds.h + pad * 2);
7995
- }
7996
- canvasCtx.restore();
7997
- }
7998
- hitTestTemplateResizeHandle(world, ctx) {
7999
- if (this._selectedIds.length === 0) return null;
8000
- const zoom = ctx.camera.zoom;
8001
- const handleHalf = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
8002
- for (const id of this._selectedIds) {
8003
- const el = ctx.store.getById(id);
8004
- if (!el || el.type !== "template") continue;
8005
- const bounds = getElementBounds(el);
8006
- if (!bounds) continue;
8007
- const hx = bounds.x + bounds.w;
8008
- const hy = bounds.y + bounds.h;
8009
- if (Math.abs(world.x - hx) <= handleHalf && Math.abs(world.y - hy) <= handleHalf) {
8010
- return id;
8011
- }
8012
- }
8013
- return null;
8175
+ return getOverlayLayout(el, zoom);
8014
8176
  }
8015
8177
  handleTemplateResize(world, ctx) {
8016
8178
  if (this.mode.type !== "resizing-template") return;
8017
8179
  const el = ctx.store.getById(this.mode.elementId);
8018
8180
  if (!el || el.type !== "template" || el.locked) return;
8019
- const dx = world.x - el.position.x;
8020
- const dy = world.y - el.position.y;
8021
- let newRadius = Math.sqrt(dx * dx + dy * dy);
8022
- if (ctx.snapToGrid && ctx.gridSize && ctx.gridSize > 0) {
8023
- const snapUnit = ctx.gridType === "hex" ? Math.sqrt(3) * ctx.gridSize : ctx.gridSize;
8024
- newRadius = Math.max(snapUnit, Math.round(newRadius / snapUnit) * snapUnit);
8025
- }
8026
- newRadius = Math.max(MIN_ELEMENT_SIZE, newRadius);
8027
- const updates = { radius: newRadius };
8028
- if (el.feetPerCell != null && ctx.gridSize && ctx.gridSize > 0) {
8029
- const snapUnit = ctx.gridType === "hex" ? Math.sqrt(3) * ctx.gridSize : ctx.gridSize;
8030
- updates.radiusFeet = newRadius / snapUnit * el.feetPerCell;
8031
- }
8032
- ctx.store.update(this.mode.elementId, updates);
8033
- ctx.requestRender();
8181
+ const patch = computeTemplateResize(el, world, {
8182
+ snapToGrid: ctx.snapToGrid,
8183
+ gridSize: ctx.gridSize,
8184
+ gridType: ctx.gridType
8185
+ });
8186
+ if (patch) {
8187
+ ctx.store.update(this.mode.elementId, patch);
8188
+ ctx.requestRender();
8189
+ }
8034
8190
  }
8035
8191
  getMarqueeRect() {
8036
8192
  if (this.mode.type !== "marquee") return null;
@@ -8043,65 +8199,6 @@ var SelectTool = class {
8043
8199
  if (w === 0 && h === 0) return null;
8044
8200
  return { x, y, w, h };
8045
8201
  }
8046
- findElementsInRect(marquee, ctx) {
8047
- const candidates = ctx.store.queryRect(marquee);
8048
- const ids = [];
8049
- for (const el of candidates) {
8050
- if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
8051
- if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
8052
- if (el.type === "grid") continue;
8053
- const bounds = getElementBounds(el);
8054
- if (bounds && this.rectsOverlap(marquee, rotatedAABB(bounds, el.rotation ?? 0))) {
8055
- ids.push(el.id);
8056
- }
8057
- }
8058
- return ids;
8059
- }
8060
- rectsOverlap(a, b) {
8061
- 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;
8062
- }
8063
- hitTest(world, ctx) {
8064
- const r = 10;
8065
- const candidates = ctx.store.queryRect({ x: world.x - r, y: world.y - r, w: r * 2, h: r * 2 }).reverse();
8066
- for (const el of candidates) {
8067
- if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
8068
- if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
8069
- if (el.type === "grid") continue;
8070
- if (this.isInsideBounds(world, el)) return el;
8071
- }
8072
- return null;
8073
- }
8074
- isInsideBounds(point, el) {
8075
- if (el.type === "grid") return false;
8076
- const angle = el.rotation ?? 0;
8077
- if (angle !== 0) {
8078
- const b = getElementBounds(el);
8079
- if (b) {
8080
- point = rotatePoint(point, { x: b.x + b.w / 2, y: b.y + b.h / 2 }, -angle);
8081
- }
8082
- }
8083
- if (el.type === "shape" && el.shape === "line") {
8084
- const [a, b] = lineEndpoints(el);
8085
- const threshold = Math.max(el.strokeWidth / 2, 6);
8086
- return distSqToSegment(point, a, b) <= threshold * threshold;
8087
- }
8088
- if ("size" in el) {
8089
- const s = el.size;
8090
- 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;
8091
- }
8092
- if (el.type === "stroke") {
8093
- return hitTestStroke(el, point, 10);
8094
- }
8095
- if (el.type === "arrow") {
8096
- return isNearBezier(point, el.from, el.to, el.bend, 10);
8097
- }
8098
- if (el.type === "template") {
8099
- const bounds = getElementBounds(el);
8100
- if (!bounds) return false;
8101
- return point.x >= bounds.x && point.x <= bounds.x + bounds.w && point.y >= bounds.y && point.y <= bounds.y + bounds.h;
8102
- }
8103
- return false;
8104
- }
8105
8202
  };
8106
8203
 
8107
8204
  // src/tools/arrow-tool.ts
@@ -8947,7 +9044,7 @@ var TemplateTool = class {
8947
9044
  };
8948
9045
 
8949
9046
  // src/index.ts
8950
- var VERSION = "0.38.0";
9047
+ var VERSION = "0.38.2";
8951
9048
  // Annotate the CommonJS export names for ESM import in node:
8952
9049
  0 && (module.exports = {
8953
9050
  ArrowTool,