@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/README.md +675 -675
- package/dist/index.cjs +857 -760
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -26
- package/dist/index.d.ts +5 -26
- package/dist/index.js +857 -760
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3974,17 +3974,43 @@ var ContextMenu = class {
|
|
|
3974
3974
|
}
|
|
3975
3975
|
};
|
|
3976
3976
|
|
|
3977
|
-
// src/
|
|
3978
|
-
function
|
|
3979
|
-
const
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
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/
|
|
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.
|
|
5762
|
-
|
|
5763
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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.
|
|
7421
|
-
|
|
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.
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
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 (
|
|
8040
|
+
if (hitTestLineHandles(world, ctx, this._selectedIds)) {
|
|
7507
8041
|
ctx.setCursor?.("grab");
|
|
7508
8042
|
return null;
|
|
7509
8043
|
}
|
|
7510
|
-
const templateResizeHit =
|
|
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 (
|
|
8049
|
+
if (hitTestRotateHandle(world, ctx, this._selectedIds)) {
|
|
7516
8050
|
ctx.setCursor?.("grab");
|
|
7517
8051
|
return null;
|
|
7518
8052
|
}
|
|
7519
|
-
const resizeHit =
|
|
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 =
|
|
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
|
-
|
|
7539
|
-
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
|
|
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
|
-
|
|
7659
|
-
|
|
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
|
-
|
|
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
|
|
7939
|
-
|
|
7940
|
-
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
|
|
7946
|
-
|
|
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.
|
|
8966
|
+
var VERSION = "0.38.2";
|
|
8870
8967
|
export {
|
|
8871
8968
|
ArrowTool,
|
|
8872
8969
|
AutoSave,
|