@fieldnotes/core 0.8.11 → 0.9.0
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 +63 -9
- package/dist/index.cjs +973 -13
- package/dist/index.d.cts +183 -65
- package/dist/index.d.ts +183 -65
- package/dist/index.js +962 -13
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -37,6 +37,7 @@ __export(index_exports, {
|
|
|
37
37
|
ImageTool: () => ImageTool,
|
|
38
38
|
InputHandler: () => InputHandler,
|
|
39
39
|
LayerManager: () => LayerManager,
|
|
40
|
+
MeasureTool: () => MeasureTool,
|
|
40
41
|
NoteEditor: () => NoteEditor,
|
|
41
42
|
NoteTool: () => NoteTool,
|
|
42
43
|
PencilTool: () => PencilTool,
|
|
@@ -45,6 +46,7 @@ __export(index_exports, {
|
|
|
45
46
|
RemoveLayerCommand: () => RemoveLayerCommand,
|
|
46
47
|
SelectTool: () => SelectTool,
|
|
47
48
|
ShapeTool: () => ShapeTool,
|
|
49
|
+
TemplateTool: () => TemplateTool,
|
|
48
50
|
TextTool: () => TextTool,
|
|
49
51
|
ToolManager: () => ToolManager,
|
|
50
52
|
UpdateElementCommand: () => UpdateElementCommand,
|
|
@@ -61,7 +63,9 @@ __export(index_exports, {
|
|
|
61
63
|
createNote: () => createNote,
|
|
62
64
|
createShape: () => createShape,
|
|
63
65
|
createStroke: () => createStroke,
|
|
66
|
+
createTemplate: () => createTemplate,
|
|
64
67
|
createText: () => createText,
|
|
68
|
+
drawHexPath: () => drawHexPath,
|
|
65
69
|
exportImage: () => exportImage,
|
|
66
70
|
exportState: () => exportState,
|
|
67
71
|
findBindTarget: () => findBindTarget,
|
|
@@ -74,10 +78,17 @@ __export(index_exports, {
|
|
|
74
78
|
getEdgeIntersection: () => getEdgeIntersection,
|
|
75
79
|
getElementBounds: () => getElementBounds,
|
|
76
80
|
getElementCenter: () => getElementCenter,
|
|
81
|
+
getHexCellsInCone: () => getHexCellsInCone,
|
|
82
|
+
getHexCellsInLine: () => getHexCellsInLine,
|
|
83
|
+
getHexCellsInRadius: () => getHexCellsInRadius,
|
|
84
|
+
getHexCellsInSquare: () => getHexCellsInSquare,
|
|
85
|
+
getHexDistance: () => getHexDistance,
|
|
77
86
|
isBindable: () => isBindable,
|
|
78
87
|
isNearBezier: () => isNearBezier,
|
|
79
88
|
parseState: () => parseState,
|
|
89
|
+
smartSnap: () => smartSnap,
|
|
80
90
|
snapPoint: () => snapPoint,
|
|
91
|
+
snapToHexCenter: () => snapToHexCenter,
|
|
81
92
|
unbindArrow: () => unbindArrow,
|
|
82
93
|
updateBoundArrow: () => updateBoundArrow
|
|
83
94
|
});
|
|
@@ -375,6 +386,30 @@ function snapPoint(point, gridSize) {
|
|
|
375
386
|
y: Math.round(point.y / gridSize) * gridSize || 0
|
|
376
387
|
};
|
|
377
388
|
}
|
|
389
|
+
function snapToHexCenter(point, cellSize, orientation) {
|
|
390
|
+
if (orientation === "pointy") {
|
|
391
|
+
const hexW = Math.sqrt(3) * cellSize;
|
|
392
|
+
const rowH = 1.5 * cellSize;
|
|
393
|
+
const row = Math.round(point.y / rowH);
|
|
394
|
+
const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
|
|
395
|
+
const col = Math.round((point.x - offsetX) / hexW);
|
|
396
|
+
return { x: col * hexW + offsetX || 0, y: row * rowH || 0 };
|
|
397
|
+
} else {
|
|
398
|
+
const hexH = Math.sqrt(3) * cellSize;
|
|
399
|
+
const colW = 1.5 * cellSize;
|
|
400
|
+
const col = Math.round(point.x / colW);
|
|
401
|
+
const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
|
|
402
|
+
const row = Math.round((point.y - offsetY) / hexH);
|
|
403
|
+
return { x: col * colW || 0, y: row * hexH + offsetY || 0 };
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function smartSnap(point, ctx) {
|
|
407
|
+
if (!ctx.snapToGrid || !ctx.gridSize) return point;
|
|
408
|
+
if (ctx.gridType === "hex" && ctx.hexOrientation) {
|
|
409
|
+
return snapToHexCenter(point, ctx.gridSize, ctx.hexOrientation);
|
|
410
|
+
}
|
|
411
|
+
return snapPoint(point, ctx.gridSize);
|
|
412
|
+
}
|
|
378
413
|
|
|
379
414
|
// src/core/auto-save.ts
|
|
380
415
|
var DEFAULT_KEY = "fieldnotes-autosave";
|
|
@@ -1027,6 +1062,9 @@ function getElementBounds(element) {
|
|
|
1027
1062
|
if (element.type === "arrow") {
|
|
1028
1063
|
return getArrowBoundsAnalytical(element.from, element.to, element.bend);
|
|
1029
1064
|
}
|
|
1065
|
+
if (element.type === "template") {
|
|
1066
|
+
return getTemplateBounds(element);
|
|
1067
|
+
}
|
|
1030
1068
|
return null;
|
|
1031
1069
|
}
|
|
1032
1070
|
function getArrowBoundsAnalytical(from, to, bend) {
|
|
@@ -1067,6 +1105,62 @@ function getArrowBoundsAnalytical(from, to, bend) {
|
|
|
1067
1105
|
}
|
|
1068
1106
|
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
1069
1107
|
}
|
|
1108
|
+
function getTemplateBounds(el) {
|
|
1109
|
+
const { x: cx, y: cy } = el.position;
|
|
1110
|
+
const r = el.radius;
|
|
1111
|
+
switch (el.templateShape) {
|
|
1112
|
+
case "circle":
|
|
1113
|
+
return { x: cx - r, y: cy - r, w: 2 * r, h: 2 * r };
|
|
1114
|
+
case "square":
|
|
1115
|
+
return { x: cx - r / 2, y: cy - r / 2, w: r, h: r };
|
|
1116
|
+
case "cone": {
|
|
1117
|
+
const halfAngle = Math.atan(0.5);
|
|
1118
|
+
const tipX = cx;
|
|
1119
|
+
const tipY = cy;
|
|
1120
|
+
const leftX = cx + r * Math.cos(el.angle - halfAngle);
|
|
1121
|
+
const leftY = cy + r * Math.sin(el.angle - halfAngle);
|
|
1122
|
+
const rightX = cx + r * Math.cos(el.angle + halfAngle);
|
|
1123
|
+
const rightY = cy + r * Math.sin(el.angle + halfAngle);
|
|
1124
|
+
const farX = cx + r * Math.cos(el.angle);
|
|
1125
|
+
const farY = cy + r * Math.sin(el.angle);
|
|
1126
|
+
const xs = [tipX, leftX, rightX, farX];
|
|
1127
|
+
const ys = [tipY, leftY, rightY, farY];
|
|
1128
|
+
let minX = Infinity;
|
|
1129
|
+
let minY = Infinity;
|
|
1130
|
+
let maxX = -Infinity;
|
|
1131
|
+
let maxY = -Infinity;
|
|
1132
|
+
for (let i = 0; i < xs.length; i++) {
|
|
1133
|
+
const px = xs[i];
|
|
1134
|
+
const py = ys[i];
|
|
1135
|
+
if (px !== void 0 && px < minX) minX = px;
|
|
1136
|
+
if (px !== void 0 && px > maxX) maxX = px;
|
|
1137
|
+
if (py !== void 0 && py < minY) minY = py;
|
|
1138
|
+
if (py !== void 0 && py > maxY) maxY = py;
|
|
1139
|
+
}
|
|
1140
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
1141
|
+
}
|
|
1142
|
+
case "line": {
|
|
1143
|
+
const halfW = r / 12;
|
|
1144
|
+
const cos = Math.cos(el.angle);
|
|
1145
|
+
const sin = Math.sin(el.angle);
|
|
1146
|
+
const perpX = -sin * halfW;
|
|
1147
|
+
const perpY = cos * halfW;
|
|
1148
|
+
const x0 = cx + perpX;
|
|
1149
|
+
const y0 = cy + perpY;
|
|
1150
|
+
const x1 = cx + r * cos + perpX;
|
|
1151
|
+
const y1 = cy + r * sin + perpY;
|
|
1152
|
+
const x2 = cx + r * cos - perpX;
|
|
1153
|
+
const y2 = cy + r * sin - perpY;
|
|
1154
|
+
const x3 = cx - perpX;
|
|
1155
|
+
const y3 = cy - perpY;
|
|
1156
|
+
const minX = Math.min(x0, x1, x2, x3);
|
|
1157
|
+
const minY = Math.min(y0, y1, y2, y3);
|
|
1158
|
+
const maxX = Math.max(x0, x1, x2, x3);
|
|
1159
|
+
const maxY = Math.max(y0, y1, y2, y3);
|
|
1160
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1070
1164
|
function boundsIntersect(a, b) {
|
|
1071
1165
|
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;
|
|
1072
1166
|
}
|
|
@@ -1587,6 +1681,190 @@ function renderHexGridTiled(ctx, bounds, cellSize, tile) {
|
|
|
1587
1681
|
}
|
|
1588
1682
|
}
|
|
1589
1683
|
|
|
1684
|
+
// src/elements/hex-fill.ts
|
|
1685
|
+
function offsetToCube(col, row, orientation) {
|
|
1686
|
+
if (orientation === "pointy") {
|
|
1687
|
+
return { q: col - (row - (row & 1)) / 2, r: row };
|
|
1688
|
+
}
|
|
1689
|
+
return { q: col, r: row - (col - (col & 1)) / 2 };
|
|
1690
|
+
}
|
|
1691
|
+
function cubeToOffset(q, r, orientation) {
|
|
1692
|
+
if (orientation === "pointy") {
|
|
1693
|
+
return { col: q + (r - (r & 1)) / 2, row: r };
|
|
1694
|
+
}
|
|
1695
|
+
return { col: q, row: r + (q - (q & 1)) / 2 };
|
|
1696
|
+
}
|
|
1697
|
+
function offsetToPixel(col, row, cellSize, orientation) {
|
|
1698
|
+
if (orientation === "pointy") {
|
|
1699
|
+
const hexW = Math.sqrt(3) * cellSize;
|
|
1700
|
+
const rowH = 1.5 * cellSize;
|
|
1701
|
+
const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
|
|
1702
|
+
return { x: col * hexW + offsetX, y: row * rowH };
|
|
1703
|
+
}
|
|
1704
|
+
const hexH = Math.sqrt(3) * cellSize;
|
|
1705
|
+
const colW = 1.5 * cellSize;
|
|
1706
|
+
const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
|
|
1707
|
+
return { x: col * colW, y: row * hexH + offsetY };
|
|
1708
|
+
}
|
|
1709
|
+
function pixelToOffset(x, y, cellSize, orientation) {
|
|
1710
|
+
if (orientation === "pointy") {
|
|
1711
|
+
const hexW = Math.sqrt(3) * cellSize;
|
|
1712
|
+
const rowH = 1.5 * cellSize;
|
|
1713
|
+
const row = Math.round(y / rowH);
|
|
1714
|
+
const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
|
|
1715
|
+
return { col: Math.round((x - offsetX) / hexW), row };
|
|
1716
|
+
}
|
|
1717
|
+
const hexH = Math.sqrt(3) * cellSize;
|
|
1718
|
+
const colW = 1.5 * cellSize;
|
|
1719
|
+
const col = Math.round(x / colW);
|
|
1720
|
+
const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
|
|
1721
|
+
return { col, row: Math.round((y - offsetY) / hexH) };
|
|
1722
|
+
}
|
|
1723
|
+
function enumerateHexRing(centerQ, centerR, n, orientation, cellSize) {
|
|
1724
|
+
const cells = [];
|
|
1725
|
+
for (let dq = -n; dq <= n; dq++) {
|
|
1726
|
+
const rMin = Math.max(-n, -dq - n);
|
|
1727
|
+
const rMax = Math.min(n, -dq + n);
|
|
1728
|
+
for (let dr = rMin; dr <= rMax; dr++) {
|
|
1729
|
+
const absQ = centerQ + dq;
|
|
1730
|
+
const absR = centerR + dr;
|
|
1731
|
+
const off = cubeToOffset(absQ, absR, orientation);
|
|
1732
|
+
cells.push(offsetToPixel(off.col, off.row, cellSize, orientation));
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
return cells;
|
|
1736
|
+
}
|
|
1737
|
+
function getHexDistance(a, b, cellSize, orientation) {
|
|
1738
|
+
const offA = pixelToOffset(a.x, a.y, cellSize, orientation);
|
|
1739
|
+
const offB = pixelToOffset(b.x, b.y, cellSize, orientation);
|
|
1740
|
+
const cubeA = offsetToCube(offA.col, offA.row, orientation);
|
|
1741
|
+
const cubeB = offsetToCube(offB.col, offB.row, orientation);
|
|
1742
|
+
const dq = cubeA.q - cubeB.q;
|
|
1743
|
+
const dr = cubeA.r - cubeB.r;
|
|
1744
|
+
const ds = -dq - dr;
|
|
1745
|
+
return Math.max(Math.abs(dq), Math.abs(dr), Math.abs(ds));
|
|
1746
|
+
}
|
|
1747
|
+
function getHexCellsInRadius(center, radiusCells, cellSize, orientation) {
|
|
1748
|
+
const n = Math.round(radiusCells);
|
|
1749
|
+
const off = pixelToOffset(center.x, center.y, cellSize, orientation);
|
|
1750
|
+
const cube = offsetToCube(off.col, off.row, orientation);
|
|
1751
|
+
if (n <= 0) {
|
|
1752
|
+
return [offsetToPixel(off.col, off.row, cellSize, orientation)];
|
|
1753
|
+
}
|
|
1754
|
+
return enumerateHexRing(cube.q, cube.r, n, orientation, cellSize);
|
|
1755
|
+
}
|
|
1756
|
+
function getHexCellsInCone(center, angle, radiusCells, cellSize, orientation) {
|
|
1757
|
+
const n = Math.round(radiusCells);
|
|
1758
|
+
const off = pixelToOffset(center.x, center.y, cellSize, orientation);
|
|
1759
|
+
const cube = offsetToCube(off.col, off.row, orientation);
|
|
1760
|
+
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
1761
|
+
if (n <= 0) return [centerPixel];
|
|
1762
|
+
const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
1763
|
+
const step = Math.PI / 3;
|
|
1764
|
+
const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
|
|
1765
|
+
const halfAngle = Math.PI / 6 + 1e-6;
|
|
1766
|
+
const cells = [centerPixel];
|
|
1767
|
+
for (let dq = -n; dq <= n; dq++) {
|
|
1768
|
+
const rMin = Math.max(-n, -dq - n);
|
|
1769
|
+
const rMax = Math.min(n, -dq + n);
|
|
1770
|
+
for (let dr = rMin; dr <= rMax; dr++) {
|
|
1771
|
+
if (dq === 0 && dr === 0) continue;
|
|
1772
|
+
const absQ = cube.q + dq;
|
|
1773
|
+
const absR = cube.r + dr;
|
|
1774
|
+
const pixel = offsetToPixel(
|
|
1775
|
+
cubeToOffset(absQ, absR, orientation).col,
|
|
1776
|
+
cubeToOffset(absQ, absR, orientation).row,
|
|
1777
|
+
cellSize,
|
|
1778
|
+
orientation
|
|
1779
|
+
);
|
|
1780
|
+
const dx = pixel.x - centerPixel.x;
|
|
1781
|
+
const dy = pixel.y - centerPixel.y;
|
|
1782
|
+
let diff = Math.atan2(dy, dx) - snappedAngle;
|
|
1783
|
+
if (diff > Math.PI) diff -= 2 * Math.PI;
|
|
1784
|
+
if (diff < -Math.PI) diff += 2 * Math.PI;
|
|
1785
|
+
if (Math.abs(diff) <= halfAngle) {
|
|
1786
|
+
cells.push(pixel);
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
return cells;
|
|
1791
|
+
}
|
|
1792
|
+
function getHexCellsInLine(center, angle, radiusCells, cellSize, orientation) {
|
|
1793
|
+
const n = Math.round(radiusCells);
|
|
1794
|
+
const off = pixelToOffset(center.x, center.y, cellSize, orientation);
|
|
1795
|
+
const cube = offsetToCube(off.col, off.row, orientation);
|
|
1796
|
+
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
1797
|
+
if (n <= 0) return [centerPixel];
|
|
1798
|
+
const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
1799
|
+
const step = Math.PI / 3;
|
|
1800
|
+
const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
|
|
1801
|
+
const cos = Math.cos(snappedAngle);
|
|
1802
|
+
const sin = Math.sin(snappedAngle);
|
|
1803
|
+
const snapUnit = Math.sqrt(3) * cellSize;
|
|
1804
|
+
const lineLength = n * snapUnit;
|
|
1805
|
+
const halfWidth = snapUnit * 0.5 + 1e-6;
|
|
1806
|
+
const cells = [];
|
|
1807
|
+
for (let dq = -n; dq <= n; dq++) {
|
|
1808
|
+
const rMin = Math.max(-n, -dq - n);
|
|
1809
|
+
const rMax = Math.min(n, -dq + n);
|
|
1810
|
+
for (let dr = rMin; dr <= rMax; dr++) {
|
|
1811
|
+
const absQ = cube.q + dq;
|
|
1812
|
+
const absR = cube.r + dr;
|
|
1813
|
+
const pixel = offsetToPixel(
|
|
1814
|
+
cubeToOffset(absQ, absR, orientation).col,
|
|
1815
|
+
cubeToOffset(absQ, absR, orientation).row,
|
|
1816
|
+
cellSize,
|
|
1817
|
+
orientation
|
|
1818
|
+
);
|
|
1819
|
+
const dx = pixel.x - centerPixel.x;
|
|
1820
|
+
const dy = pixel.y - centerPixel.y;
|
|
1821
|
+
const along = dx * cos + dy * sin;
|
|
1822
|
+
const perp = Math.abs(-dx * sin + dy * cos);
|
|
1823
|
+
if (along >= -snapUnit * 0.1 && along <= lineLength + snapUnit * 0.1 && perp <= halfWidth) {
|
|
1824
|
+
cells.push(pixel);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
return cells;
|
|
1829
|
+
}
|
|
1830
|
+
function getHexCellsInSquare(center, radiusCells, cellSize, orientation) {
|
|
1831
|
+
const n = Math.round(radiusCells);
|
|
1832
|
+
const off = pixelToOffset(center.x, center.y, cellSize, orientation);
|
|
1833
|
+
const cube = offsetToCube(off.col, off.row, orientation);
|
|
1834
|
+
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
1835
|
+
if (n <= 0) return [centerPixel];
|
|
1836
|
+
const snapUnit = Math.sqrt(3) * cellSize;
|
|
1837
|
+
const halfSide = n * snapUnit / 2;
|
|
1838
|
+
const cells = [];
|
|
1839
|
+
for (let dq = -n; dq <= n; dq++) {
|
|
1840
|
+
const rMin = Math.max(-n, -dq - n);
|
|
1841
|
+
const rMax = Math.min(n, -dq + n);
|
|
1842
|
+
for (let dr = rMin; dr <= rMax; dr++) {
|
|
1843
|
+
const absQ = cube.q + dq;
|
|
1844
|
+
const absR = cube.r + dr;
|
|
1845
|
+
const pixel = offsetToPixel(
|
|
1846
|
+
cubeToOffset(absQ, absR, orientation).col,
|
|
1847
|
+
cubeToOffset(absQ, absR, orientation).row,
|
|
1848
|
+
cellSize,
|
|
1849
|
+
orientation
|
|
1850
|
+
);
|
|
1851
|
+
if (Math.abs(pixel.x - centerPixel.x) <= halfSide && Math.abs(pixel.y - centerPixel.y) <= halfSide) {
|
|
1852
|
+
cells.push(pixel);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
return cells;
|
|
1857
|
+
}
|
|
1858
|
+
function drawHexPath(ctx, cx, cy, cellSize, orientation) {
|
|
1859
|
+
const angleOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
1860
|
+
ctx.moveTo(cx + cellSize * Math.cos(angleOffset), cy + cellSize * Math.sin(angleOffset));
|
|
1861
|
+
for (let i = 1; i < 6; i++) {
|
|
1862
|
+
const a = angleOffset + Math.PI / 3 * i;
|
|
1863
|
+
ctx.lineTo(cx + cellSize * Math.cos(a), cy + cellSize * Math.sin(a));
|
|
1864
|
+
}
|
|
1865
|
+
ctx.closePath();
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1590
1868
|
// src/elements/element-renderer.ts
|
|
1591
1869
|
var DOM_ELEMENT_TYPES = /* @__PURE__ */ new Set(["note", "html", "text"]);
|
|
1592
1870
|
var ARROWHEAD_LENGTH = 12;
|
|
@@ -1631,6 +1909,9 @@ var ElementRenderer = class {
|
|
|
1631
1909
|
case "grid":
|
|
1632
1910
|
this.renderGrid(ctx, element);
|
|
1633
1911
|
break;
|
|
1912
|
+
case "template":
|
|
1913
|
+
this.renderTemplate(ctx, element);
|
|
1914
|
+
break;
|
|
1634
1915
|
}
|
|
1635
1916
|
}
|
|
1636
1917
|
renderStroke(ctx, stroke) {
|
|
@@ -1818,6 +2099,147 @@ var ElementRenderer = class {
|
|
|
1818
2099
|
);
|
|
1819
2100
|
}
|
|
1820
2101
|
}
|
|
2102
|
+
renderTemplate(ctx, template) {
|
|
2103
|
+
const grid = this.store?.getElementsByType("grid")[0];
|
|
2104
|
+
if (grid && grid.gridType === "hex") {
|
|
2105
|
+
this.renderHexTemplate(ctx, template, grid.cellSize, grid.hexOrientation);
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
this.renderGeometricTemplate(ctx, template);
|
|
2109
|
+
}
|
|
2110
|
+
renderGeometricTemplate(ctx, template) {
|
|
2111
|
+
const { x: cx, y: cy } = template.position;
|
|
2112
|
+
const r = template.radius;
|
|
2113
|
+
ctx.save();
|
|
2114
|
+
ctx.globalAlpha = template.opacity;
|
|
2115
|
+
ctx.fillStyle = template.fillColor;
|
|
2116
|
+
ctx.strokeStyle = template.strokeColor;
|
|
2117
|
+
ctx.lineWidth = template.strokeWidth;
|
|
2118
|
+
switch (template.templateShape) {
|
|
2119
|
+
case "circle":
|
|
2120
|
+
ctx.beginPath();
|
|
2121
|
+
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
2122
|
+
ctx.fill();
|
|
2123
|
+
ctx.stroke();
|
|
2124
|
+
if (template.radiusFeet != null && template.radiusFeet > 0) {
|
|
2125
|
+
this.renderRadiusMarker(ctx, cx, cy, r, template.radiusFeet);
|
|
2126
|
+
}
|
|
2127
|
+
break;
|
|
2128
|
+
case "square":
|
|
2129
|
+
ctx.fillRect(cx - r / 2, cy - r / 2, r, r);
|
|
2130
|
+
ctx.strokeRect(cx - r / 2, cy - r / 2, r, r);
|
|
2131
|
+
break;
|
|
2132
|
+
case "cone": {
|
|
2133
|
+
const halfAngle = Math.atan(0.5);
|
|
2134
|
+
ctx.beginPath();
|
|
2135
|
+
ctx.moveTo(cx, cy);
|
|
2136
|
+
ctx.arc(cx, cy, r, template.angle - halfAngle, template.angle + halfAngle);
|
|
2137
|
+
ctx.closePath();
|
|
2138
|
+
ctx.fill();
|
|
2139
|
+
ctx.stroke();
|
|
2140
|
+
break;
|
|
2141
|
+
}
|
|
2142
|
+
case "line": {
|
|
2143
|
+
const halfW = r / 12;
|
|
2144
|
+
const cos = Math.cos(template.angle);
|
|
2145
|
+
const sin = Math.sin(template.angle);
|
|
2146
|
+
const perpX = -sin * halfW;
|
|
2147
|
+
const perpY = cos * halfW;
|
|
2148
|
+
ctx.beginPath();
|
|
2149
|
+
ctx.moveTo(cx + perpX, cy + perpY);
|
|
2150
|
+
ctx.lineTo(cx + r * cos + perpX, cy + r * sin + perpY);
|
|
2151
|
+
ctx.lineTo(cx + r * cos - perpX, cy + r * sin - perpY);
|
|
2152
|
+
ctx.lineTo(cx - perpX, cy - perpY);
|
|
2153
|
+
ctx.closePath();
|
|
2154
|
+
ctx.fill();
|
|
2155
|
+
ctx.stroke();
|
|
2156
|
+
break;
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
ctx.restore();
|
|
2160
|
+
}
|
|
2161
|
+
renderHexTemplate(ctx, template, cellSize, orientation) {
|
|
2162
|
+
const snapUnit = Math.sqrt(3) * cellSize;
|
|
2163
|
+
const radiusCells = template.radius / snapUnit;
|
|
2164
|
+
const center = template.position;
|
|
2165
|
+
let cells;
|
|
2166
|
+
switch (template.templateShape) {
|
|
2167
|
+
case "circle":
|
|
2168
|
+
cells = getHexCellsInRadius(center, radiusCells, cellSize, orientation);
|
|
2169
|
+
break;
|
|
2170
|
+
case "cone":
|
|
2171
|
+
cells = getHexCellsInCone(center, template.angle, radiusCells, cellSize, orientation);
|
|
2172
|
+
break;
|
|
2173
|
+
case "line":
|
|
2174
|
+
cells = getHexCellsInLine(center, template.angle, radiusCells, cellSize, orientation);
|
|
2175
|
+
break;
|
|
2176
|
+
case "square":
|
|
2177
|
+
cells = getHexCellsInSquare(center, radiusCells, cellSize, orientation);
|
|
2178
|
+
break;
|
|
2179
|
+
}
|
|
2180
|
+
ctx.save();
|
|
2181
|
+
ctx.globalAlpha = template.opacity;
|
|
2182
|
+
ctx.beginPath();
|
|
2183
|
+
for (const cell of cells) {
|
|
2184
|
+
drawHexPath(ctx, cell.x, cell.y, cellSize, orientation);
|
|
2185
|
+
}
|
|
2186
|
+
ctx.fillStyle = template.fillColor;
|
|
2187
|
+
ctx.fill();
|
|
2188
|
+
ctx.beginPath();
|
|
2189
|
+
for (const cell of cells) {
|
|
2190
|
+
drawHexPath(ctx, cell.x, cell.y, cellSize, orientation);
|
|
2191
|
+
}
|
|
2192
|
+
ctx.strokeStyle = template.strokeColor;
|
|
2193
|
+
ctx.lineWidth = template.strokeWidth;
|
|
2194
|
+
ctx.stroke();
|
|
2195
|
+
{
|
|
2196
|
+
ctx.globalAlpha = Math.min(template.opacity + 0.1, 1);
|
|
2197
|
+
ctx.beginPath();
|
|
2198
|
+
drawHexPath(ctx, center.x, center.y, cellSize, orientation);
|
|
2199
|
+
ctx.fillStyle = template.strokeColor;
|
|
2200
|
+
ctx.fill();
|
|
2201
|
+
ctx.strokeStyle = template.strokeColor;
|
|
2202
|
+
ctx.lineWidth = template.strokeWidth;
|
|
2203
|
+
ctx.stroke();
|
|
2204
|
+
}
|
|
2205
|
+
if (template.templateShape === "circle" && template.radiusFeet != null && template.radiusFeet > 0) {
|
|
2206
|
+
const r = template.radius;
|
|
2207
|
+
this.renderRadiusMarker(ctx, center.x, center.y, r, template.radiusFeet);
|
|
2208
|
+
}
|
|
2209
|
+
ctx.restore();
|
|
2210
|
+
}
|
|
2211
|
+
renderRadiusMarker(ctx, cx, cy, r, feet) {
|
|
2212
|
+
const markerColor = ctx.strokeStyle;
|
|
2213
|
+
ctx.save();
|
|
2214
|
+
ctx.globalAlpha = 1;
|
|
2215
|
+
ctx.beginPath();
|
|
2216
|
+
ctx.setLineDash([4, 4]);
|
|
2217
|
+
ctx.strokeStyle = markerColor;
|
|
2218
|
+
ctx.lineWidth = 1.5;
|
|
2219
|
+
ctx.moveTo(cx, cy);
|
|
2220
|
+
ctx.lineTo(cx + r, cy);
|
|
2221
|
+
ctx.stroke();
|
|
2222
|
+
ctx.setLineDash([]);
|
|
2223
|
+
const label = `${Math.round(feet)} ft`;
|
|
2224
|
+
const fontSize = Math.max(10, Math.min(14, r * 0.15));
|
|
2225
|
+
ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
|
|
2226
|
+
ctx.textAlign = "center";
|
|
2227
|
+
ctx.textBaseline = "bottom";
|
|
2228
|
+
const textX = cx + r / 2;
|
|
2229
|
+
const textY = cy - 4;
|
|
2230
|
+
const metrics = ctx.measureText(label);
|
|
2231
|
+
const padX = 4;
|
|
2232
|
+
const padY = 2;
|
|
2233
|
+
const textW = metrics.width + padX * 2;
|
|
2234
|
+
const textH = fontSize + padY * 2;
|
|
2235
|
+
ctx.fillStyle = "rgba(255, 255, 255, 0.85)";
|
|
2236
|
+
ctx.beginPath();
|
|
2237
|
+
ctx.roundRect(textX - textW / 2, textY - textH, textW, textH, 3);
|
|
2238
|
+
ctx.fill();
|
|
2239
|
+
ctx.fillStyle = markerColor;
|
|
2240
|
+
ctx.fillText(label, textX, textY - padY);
|
|
2241
|
+
ctx.restore();
|
|
2242
|
+
}
|
|
1821
2243
|
renderImage(ctx, image) {
|
|
1822
2244
|
const img = this.getImage(image.src);
|
|
1823
2245
|
if (!img) return;
|
|
@@ -2325,6 +2747,25 @@ function createText(input) {
|
|
|
2325
2747
|
textAlign: input.textAlign ?? "left"
|
|
2326
2748
|
};
|
|
2327
2749
|
}
|
|
2750
|
+
function createTemplate(input) {
|
|
2751
|
+
return {
|
|
2752
|
+
id: createId("template"),
|
|
2753
|
+
type: "template",
|
|
2754
|
+
position: input.position,
|
|
2755
|
+
zIndex: input.zIndex ?? 0,
|
|
2756
|
+
locked: input.locked ?? false,
|
|
2757
|
+
layerId: input.layerId ?? "",
|
|
2758
|
+
templateShape: input.templateShape,
|
|
2759
|
+
radius: input.radius,
|
|
2760
|
+
angle: input.angle ?? 0,
|
|
2761
|
+
fillColor: input.fillColor ?? "rgba(255, 87, 34, 0.2)",
|
|
2762
|
+
strokeColor: input.strokeColor ?? "#FF5722",
|
|
2763
|
+
strokeWidth: input.strokeWidth ?? 2,
|
|
2764
|
+
opacity: input.opacity ?? 0.6,
|
|
2765
|
+
feetPerCell: input.feetPerCell,
|
|
2766
|
+
radiusFeet: input.radiusFeet
|
|
2767
|
+
};
|
|
2768
|
+
}
|
|
2328
2769
|
|
|
2329
2770
|
// src/canvas/export-image.ts
|
|
2330
2771
|
function getStrokeBounds(el) {
|
|
@@ -2361,6 +2802,11 @@ function getElementRect(el) {
|
|
|
2361
2802
|
}
|
|
2362
2803
|
case "grid":
|
|
2363
2804
|
return null;
|
|
2805
|
+
case "template": {
|
|
2806
|
+
const bounds = getElementBounds(el);
|
|
2807
|
+
if (!bounds) return null;
|
|
2808
|
+
return bounds;
|
|
2809
|
+
}
|
|
2364
2810
|
case "note":
|
|
2365
2811
|
case "image":
|
|
2366
2812
|
case "html":
|
|
@@ -3105,7 +3551,15 @@ var RenderLoop = class {
|
|
|
3105
3551
|
ctx.save();
|
|
3106
3552
|
ctx.scale(dpr, dpr);
|
|
3107
3553
|
this.renderer.setCanvasSize(cssWidth, cssHeight);
|
|
3108
|
-
this.
|
|
3554
|
+
const hasGridElement = this.store.getElementsByType("grid").length > 0;
|
|
3555
|
+
if (hasGridElement) {
|
|
3556
|
+
ctx.save();
|
|
3557
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
3558
|
+
ctx.clearRect(0, 0, cssWidth, cssHeight);
|
|
3559
|
+
ctx.restore();
|
|
3560
|
+
} else {
|
|
3561
|
+
this.background.render(ctx, this.camera);
|
|
3562
|
+
}
|
|
3109
3563
|
ctx.save();
|
|
3110
3564
|
ctx.translate(this.camera.position.x, this.camera.position.y);
|
|
3111
3565
|
ctx.scale(this.camera.zoom, this.camera.zoom);
|
|
@@ -3366,16 +3820,19 @@ var Viewport = class {
|
|
|
3366
3820
|
});
|
|
3367
3821
|
this.unsubStore = [
|
|
3368
3822
|
this.store.on("add", (el) => {
|
|
3823
|
+
if (el.type === "grid") this.syncGridContext();
|
|
3369
3824
|
this.renderLoop.markLayerDirty(el.layerId);
|
|
3370
3825
|
this.requestRender();
|
|
3371
3826
|
}),
|
|
3372
3827
|
this.store.on("remove", (el) => {
|
|
3828
|
+
if (el.type === "grid") this.syncGridContext();
|
|
3373
3829
|
this.unbindArrowsFrom(el);
|
|
3374
3830
|
this.domNodeManager.removeDomNode(el.id);
|
|
3375
3831
|
this.renderLoop.markLayerDirty(el.layerId);
|
|
3376
3832
|
this.requestRender();
|
|
3377
3833
|
}),
|
|
3378
3834
|
this.store.on("update", ({ previous, current }) => {
|
|
3835
|
+
if (current.type === "grid") this.syncGridContext();
|
|
3379
3836
|
this.renderLoop.markLayerDirty(current.layerId);
|
|
3380
3837
|
if (previous.layerId !== current.layerId) {
|
|
3381
3838
|
this.renderLoop.markLayerDirty(previous.layerId);
|
|
@@ -3385,6 +3842,7 @@ var Viewport = class {
|
|
|
3385
3842
|
this.store.on("clear", () => {
|
|
3386
3843
|
this.domNodeManager.clearDomNodes();
|
|
3387
3844
|
this.renderLoop.markAllLayersDirty();
|
|
3845
|
+
this.syncGridContext();
|
|
3388
3846
|
this.requestRender();
|
|
3389
3847
|
})
|
|
3390
3848
|
];
|
|
@@ -3398,6 +3856,7 @@ var Viewport = class {
|
|
|
3398
3856
|
this.observeResize();
|
|
3399
3857
|
this.syncCanvasSize();
|
|
3400
3858
|
this.renderLoop.start();
|
|
3859
|
+
this.syncGridContext();
|
|
3401
3860
|
}
|
|
3402
3861
|
camera;
|
|
3403
3862
|
store;
|
|
@@ -3714,6 +4173,18 @@ var Viewport = class {
|
|
|
3714
4173
|
this.renderLoop.setCanvasSize(rect.width * dpr, rect.height * dpr);
|
|
3715
4174
|
this.requestRender();
|
|
3716
4175
|
}
|
|
4176
|
+
syncGridContext() {
|
|
4177
|
+
const grid = this.store.getElementsByType("grid")[0];
|
|
4178
|
+
if (grid) {
|
|
4179
|
+
this.toolContext.gridSize = grid.cellSize;
|
|
4180
|
+
this.toolContext.gridType = grid.gridType;
|
|
4181
|
+
this.toolContext.hexOrientation = grid.hexOrientation;
|
|
4182
|
+
} else {
|
|
4183
|
+
this.toolContext.gridSize = this._gridSize;
|
|
4184
|
+
this.toolContext.gridType = void 0;
|
|
4185
|
+
this.toolContext.hexOrientation = void 0;
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
3717
4188
|
observeResize() {
|
|
3718
4189
|
if (typeof ResizeObserver === "undefined") return;
|
|
3719
4190
|
this.resizeObserver = new ResizeObserver(() => this.syncCanvasSize());
|
|
@@ -4088,7 +4559,7 @@ var SelectTool = class {
|
|
|
4088
4559
|
ctx.setCursor?.("default");
|
|
4089
4560
|
}
|
|
4090
4561
|
snap(point, ctx) {
|
|
4091
|
-
return
|
|
4562
|
+
return smartSnap(point, ctx);
|
|
4092
4563
|
}
|
|
4093
4564
|
onPointerDown(state, ctx) {
|
|
4094
4565
|
this.ctx = ctx;
|
|
@@ -4105,6 +4576,12 @@ var SelectTool = class {
|
|
|
4105
4576
|
ctx.requestRender();
|
|
4106
4577
|
return;
|
|
4107
4578
|
}
|
|
4579
|
+
const templateResizeHit = this.hitTestTemplateResizeHandle(world, ctx);
|
|
4580
|
+
if (templateResizeHit) {
|
|
4581
|
+
this.mode = { type: "resizing-template", elementId: templateResizeHit };
|
|
4582
|
+
ctx.requestRender();
|
|
4583
|
+
return;
|
|
4584
|
+
}
|
|
4108
4585
|
const resizeHit = this.hitTestResizeHandle(world, ctx);
|
|
4109
4586
|
if (resizeHit) {
|
|
4110
4587
|
const el = ctx.store.getById(resizeHit.elementId);
|
|
@@ -4139,6 +4616,11 @@ var SelectTool = class {
|
|
|
4139
4616
|
applyArrowHandleDrag(this.mode.handle, this.mode.elementId, world, ctx);
|
|
4140
4617
|
return;
|
|
4141
4618
|
}
|
|
4619
|
+
if (this.mode.type === "resizing-template") {
|
|
4620
|
+
ctx.setCursor?.("nwse-resize");
|
|
4621
|
+
this.handleTemplateResize(world, ctx);
|
|
4622
|
+
return;
|
|
4623
|
+
}
|
|
4142
4624
|
if (this.mode.type === "resizing") {
|
|
4143
4625
|
ctx.setCursor?.(HANDLE_CURSORS[this.mode.handle]);
|
|
4144
4626
|
this.handleResize(world, ctx);
|
|
@@ -4162,6 +4644,16 @@ var SelectTool = class {
|
|
|
4162
4644
|
from: { x: el.from.x + dx, y: el.from.y + dy },
|
|
4163
4645
|
to: { x: el.to.x + dx, y: el.to.y + dy }
|
|
4164
4646
|
});
|
|
4647
|
+
} else if (ctx.gridType && "size" in el) {
|
|
4648
|
+
const centerX = el.position.x + el.size.w / 2 + dx;
|
|
4649
|
+
const centerY = el.position.y + el.size.h / 2 + dy;
|
|
4650
|
+
const snappedCenter = this.snap({ x: centerX, y: centerY }, ctx);
|
|
4651
|
+
ctx.store.update(id, {
|
|
4652
|
+
position: {
|
|
4653
|
+
x: snappedCenter.x - el.size.w / 2,
|
|
4654
|
+
y: snappedCenter.y - el.size.h / 2
|
|
4655
|
+
}
|
|
4656
|
+
});
|
|
4165
4657
|
} else {
|
|
4166
4658
|
ctx.store.update(id, {
|
|
4167
4659
|
position: { x: el.position.x + dx, y: el.position.y + dy }
|
|
@@ -4236,6 +4728,11 @@ var SelectTool = class {
|
|
|
4236
4728
|
ctx.setCursor?.(getArrowHandleCursor(arrowHit.handle, false));
|
|
4237
4729
|
return;
|
|
4238
4730
|
}
|
|
4731
|
+
const templateResizeHit = this.hitTestTemplateResizeHandle(world, ctx);
|
|
4732
|
+
if (templateResizeHit) {
|
|
4733
|
+
ctx.setCursor?.("nwse-resize");
|
|
4734
|
+
return;
|
|
4735
|
+
}
|
|
4239
4736
|
const resizeHit = this.hitTestResizeHandle(world, ctx);
|
|
4240
4737
|
if (resizeHit) {
|
|
4241
4738
|
ctx.setCursor?.(HANDLE_CURSORS[resizeHit.handle]);
|
|
@@ -4372,6 +4869,24 @@ var SelectTool = class {
|
|
|
4372
4869
|
);
|
|
4373
4870
|
}
|
|
4374
4871
|
canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
|
|
4872
|
+
} else if (el.type === "template") {
|
|
4873
|
+
canvasCtx.setLineDash([]);
|
|
4874
|
+
canvasCtx.fillStyle = "#ffffff";
|
|
4875
|
+
const hx = bounds.x + bounds.w;
|
|
4876
|
+
const hy = bounds.y + bounds.h;
|
|
4877
|
+
canvasCtx.fillRect(
|
|
4878
|
+
hx - handleWorldSize / 2,
|
|
4879
|
+
hy - handleWorldSize / 2,
|
|
4880
|
+
handleWorldSize,
|
|
4881
|
+
handleWorldSize
|
|
4882
|
+
);
|
|
4883
|
+
canvasCtx.strokeRect(
|
|
4884
|
+
hx - handleWorldSize / 2,
|
|
4885
|
+
hy - handleWorldSize / 2,
|
|
4886
|
+
handleWorldSize,
|
|
4887
|
+
handleWorldSize
|
|
4888
|
+
);
|
|
4889
|
+
canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
|
|
4375
4890
|
}
|
|
4376
4891
|
}
|
|
4377
4892
|
canvasCtx.restore();
|
|
@@ -4396,6 +4911,43 @@ var SelectTool = class {
|
|
|
4396
4911
|
}
|
|
4397
4912
|
canvasCtx.restore();
|
|
4398
4913
|
}
|
|
4914
|
+
hitTestTemplateResizeHandle(world, ctx) {
|
|
4915
|
+
if (this._selectedIds.length === 0) return null;
|
|
4916
|
+
const zoom = ctx.camera.zoom;
|
|
4917
|
+
const handleHalf = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
|
|
4918
|
+
for (const id of this._selectedIds) {
|
|
4919
|
+
const el = ctx.store.getById(id);
|
|
4920
|
+
if (!el || el.type !== "template") continue;
|
|
4921
|
+
const bounds = getElementBounds(el);
|
|
4922
|
+
if (!bounds) continue;
|
|
4923
|
+
const hx = bounds.x + bounds.w;
|
|
4924
|
+
const hy = bounds.y + bounds.h;
|
|
4925
|
+
if (Math.abs(world.x - hx) <= handleHalf && Math.abs(world.y - hy) <= handleHalf) {
|
|
4926
|
+
return id;
|
|
4927
|
+
}
|
|
4928
|
+
}
|
|
4929
|
+
return null;
|
|
4930
|
+
}
|
|
4931
|
+
handleTemplateResize(world, ctx) {
|
|
4932
|
+
if (this.mode.type !== "resizing-template") return;
|
|
4933
|
+
const el = ctx.store.getById(this.mode.elementId);
|
|
4934
|
+
if (!el || el.type !== "template" || el.locked) return;
|
|
4935
|
+
const dx = world.x - el.position.x;
|
|
4936
|
+
const dy = world.y - el.position.y;
|
|
4937
|
+
let newRadius = Math.sqrt(dx * dx + dy * dy);
|
|
4938
|
+
if (ctx.snapToGrid && ctx.gridSize && ctx.gridSize > 0) {
|
|
4939
|
+
const snapUnit = ctx.gridType === "hex" ? Math.sqrt(3) * ctx.gridSize : ctx.gridSize;
|
|
4940
|
+
newRadius = Math.max(snapUnit, Math.round(newRadius / snapUnit) * snapUnit);
|
|
4941
|
+
}
|
|
4942
|
+
newRadius = Math.max(MIN_ELEMENT_SIZE, newRadius);
|
|
4943
|
+
const updates = { radius: newRadius };
|
|
4944
|
+
if (el.feetPerCell != null && ctx.gridSize && ctx.gridSize > 0) {
|
|
4945
|
+
const snapUnit = ctx.gridType === "hex" ? Math.sqrt(3) * ctx.gridSize : ctx.gridSize;
|
|
4946
|
+
updates.radiusFeet = newRadius / snapUnit * el.feetPerCell;
|
|
4947
|
+
}
|
|
4948
|
+
ctx.store.update(this.mode.elementId, updates);
|
|
4949
|
+
ctx.requestRender();
|
|
4950
|
+
}
|
|
4399
4951
|
getMarqueeRect() {
|
|
4400
4952
|
if (this.mode.type !== "marquee") return null;
|
|
4401
4953
|
const { start } = this.mode;
|
|
@@ -4452,6 +5004,11 @@ var SelectTool = class {
|
|
|
4452
5004
|
if (el.type === "arrow") {
|
|
4453
5005
|
return isNearBezier(point, el.from, el.to, el.bend, 10);
|
|
4454
5006
|
}
|
|
5007
|
+
if (el.type === "template") {
|
|
5008
|
+
const bounds = getElementBounds(el);
|
|
5009
|
+
if (!bounds) return false;
|
|
5010
|
+
return point.x >= bounds.x && point.x <= bounds.x + bounds.w && point.y >= bounds.y && point.y <= bounds.y + bounds.h;
|
|
5011
|
+
}
|
|
4455
5012
|
return false;
|
|
4456
5013
|
}
|
|
4457
5014
|
};
|
|
@@ -4509,7 +5066,7 @@ var ArrowTool = class {
|
|
|
4509
5066
|
this.fromBinding = { elementId: target.id };
|
|
4510
5067
|
this.fromTarget = target;
|
|
4511
5068
|
} else {
|
|
4512
|
-
this.start =
|
|
5069
|
+
this.start = smartSnap(world, ctx);
|
|
4513
5070
|
this.fromBinding = void 0;
|
|
4514
5071
|
this.fromTarget = null;
|
|
4515
5072
|
}
|
|
@@ -4527,7 +5084,7 @@ var ArrowTool = class {
|
|
|
4527
5084
|
this.end = getElementCenter(target);
|
|
4528
5085
|
this.toTarget = target;
|
|
4529
5086
|
} else {
|
|
4530
|
-
this.end =
|
|
5087
|
+
this.end = smartSnap(world, ctx);
|
|
4531
5088
|
this.toTarget = null;
|
|
4532
5089
|
}
|
|
4533
5090
|
ctx.requestRender();
|
|
@@ -4640,9 +5197,7 @@ var NoteTool = class {
|
|
|
4640
5197
|
}
|
|
4641
5198
|
onPointerUp(state, ctx) {
|
|
4642
5199
|
let world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
4643
|
-
|
|
4644
|
-
world = snapPoint(world, ctx.gridSize);
|
|
4645
|
-
}
|
|
5200
|
+
world = smartSnap(world, ctx);
|
|
4646
5201
|
const note = createNote({
|
|
4647
5202
|
position: world,
|
|
4648
5203
|
size: { ...this.size },
|
|
@@ -4697,9 +5252,7 @@ var TextTool = class {
|
|
|
4697
5252
|
}
|
|
4698
5253
|
onPointerUp(state, ctx) {
|
|
4699
5254
|
let world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
4700
|
-
|
|
4701
|
-
world = snapPoint(world, ctx.gridSize);
|
|
4702
|
-
}
|
|
5255
|
+
world = smartSnap(world, ctx);
|
|
4703
5256
|
const textEl = createText({
|
|
4704
5257
|
position: world,
|
|
4705
5258
|
fontSize: this.fontSize,
|
|
@@ -4732,8 +5285,12 @@ var ImageTool = class {
|
|
|
4732
5285
|
onPointerUp(state, ctx) {
|
|
4733
5286
|
if (!this.src) return;
|
|
4734
5287
|
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
5288
|
+
const snapped = smartSnap(world, ctx);
|
|
4735
5289
|
const image = createImage({
|
|
4736
|
-
position:
|
|
5290
|
+
position: {
|
|
5291
|
+
x: snapped.x - this.size.w / 2,
|
|
5292
|
+
y: snapped.y - this.size.h / 2
|
|
5293
|
+
},
|
|
4737
5294
|
size: { ...this.size },
|
|
4738
5295
|
src: this.src
|
|
4739
5296
|
});
|
|
@@ -4870,7 +5427,7 @@ var ShapeTool = class {
|
|
|
4870
5427
|
for (const listener of this.optionListeners) listener();
|
|
4871
5428
|
}
|
|
4872
5429
|
snap(point, ctx) {
|
|
4873
|
-
return
|
|
5430
|
+
return smartSnap(point, ctx);
|
|
4874
5431
|
}
|
|
4875
5432
|
onKeyDown = (e) => {
|
|
4876
5433
|
if (e.key === "Shift") this.shiftHeld = true;
|
|
@@ -4880,6 +5437,398 @@ var ShapeTool = class {
|
|
|
4880
5437
|
};
|
|
4881
5438
|
};
|
|
4882
5439
|
|
|
5440
|
+
// src/tools/measure-tool.ts
|
|
5441
|
+
var MeasureTool = class {
|
|
5442
|
+
name = "measure";
|
|
5443
|
+
start = null;
|
|
5444
|
+
end = null;
|
|
5445
|
+
gridSize = 1;
|
|
5446
|
+
gridType;
|
|
5447
|
+
hexOrientation;
|
|
5448
|
+
feetPerCell;
|
|
5449
|
+
optionListeners = /* @__PURE__ */ new Set();
|
|
5450
|
+
constructor(options = {}) {
|
|
5451
|
+
this.feetPerCell = options.feetPerCell ?? 5;
|
|
5452
|
+
}
|
|
5453
|
+
getOptions() {
|
|
5454
|
+
return { feetPerCell: this.feetPerCell };
|
|
5455
|
+
}
|
|
5456
|
+
setOptions(options) {
|
|
5457
|
+
if (options.feetPerCell !== void 0) this.feetPerCell = options.feetPerCell;
|
|
5458
|
+
this.notifyOptionsChange();
|
|
5459
|
+
}
|
|
5460
|
+
onOptionsChange(listener) {
|
|
5461
|
+
this.optionListeners.add(listener);
|
|
5462
|
+
return () => this.optionListeners.delete(listener);
|
|
5463
|
+
}
|
|
5464
|
+
onPointerDown(state, ctx) {
|
|
5465
|
+
this.gridSize = ctx.gridSize ?? 1;
|
|
5466
|
+
this.gridType = ctx.gridType;
|
|
5467
|
+
this.hexOrientation = ctx.hexOrientation;
|
|
5468
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
5469
|
+
this.start = this.snapToGrid(world, ctx);
|
|
5470
|
+
this.end = { ...this.start };
|
|
5471
|
+
}
|
|
5472
|
+
onPointerMove(state, ctx) {
|
|
5473
|
+
if (!this.start) return;
|
|
5474
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
5475
|
+
this.end = this.snapToGrid(world, ctx);
|
|
5476
|
+
ctx.requestRender();
|
|
5477
|
+
}
|
|
5478
|
+
onPointerUp(_state, ctx) {
|
|
5479
|
+
if (!this.start) return;
|
|
5480
|
+
this.start = null;
|
|
5481
|
+
this.end = null;
|
|
5482
|
+
ctx.requestRender();
|
|
5483
|
+
}
|
|
5484
|
+
onDeactivate(_ctx) {
|
|
5485
|
+
this.start = null;
|
|
5486
|
+
this.end = null;
|
|
5487
|
+
}
|
|
5488
|
+
getMeasurement() {
|
|
5489
|
+
if (!this.start || !this.end) return null;
|
|
5490
|
+
const dx = this.end.x - this.start.x;
|
|
5491
|
+
const dy = this.end.y - this.start.y;
|
|
5492
|
+
const worldDistance = Math.sqrt(dx * dx + dy * dy);
|
|
5493
|
+
let cells;
|
|
5494
|
+
if (this.gridType === "hex" && this.hexOrientation) {
|
|
5495
|
+
cells = getHexDistance(this.start, this.end, this.gridSize, this.hexOrientation);
|
|
5496
|
+
} else {
|
|
5497
|
+
const snapUnit = this.gridSize;
|
|
5498
|
+
cells = worldDistance / snapUnit;
|
|
5499
|
+
}
|
|
5500
|
+
const feet = cells * this.feetPerCell;
|
|
5501
|
+
return {
|
|
5502
|
+
start: { ...this.start },
|
|
5503
|
+
end: { ...this.end },
|
|
5504
|
+
worldDistance,
|
|
5505
|
+
cells,
|
|
5506
|
+
feet
|
|
5507
|
+
};
|
|
5508
|
+
}
|
|
5509
|
+
renderOverlay(ctx) {
|
|
5510
|
+
const m = this.getMeasurement();
|
|
5511
|
+
if (!m) return;
|
|
5512
|
+
ctx.save();
|
|
5513
|
+
ctx.strokeStyle = "#FF5722";
|
|
5514
|
+
ctx.setLineDash([8, 4]);
|
|
5515
|
+
ctx.lineWidth = 2;
|
|
5516
|
+
ctx.beginPath();
|
|
5517
|
+
ctx.moveTo(m.start.x, m.start.y);
|
|
5518
|
+
ctx.lineTo(m.end.x, m.end.y);
|
|
5519
|
+
ctx.stroke();
|
|
5520
|
+
ctx.setLineDash([]);
|
|
5521
|
+
ctx.fillStyle = "#FF5722";
|
|
5522
|
+
const dotRadius = 4;
|
|
5523
|
+
ctx.beginPath();
|
|
5524
|
+
ctx.arc(m.start.x, m.start.y, dotRadius, 0, Math.PI * 2);
|
|
5525
|
+
ctx.fill();
|
|
5526
|
+
ctx.beginPath();
|
|
5527
|
+
ctx.arc(m.end.x, m.end.y, dotRadius, 0, Math.PI * 2);
|
|
5528
|
+
ctx.fill();
|
|
5529
|
+
const label = `${Math.round(m.feet)} ft`;
|
|
5530
|
+
const midX = (m.start.x + m.end.x) / 2;
|
|
5531
|
+
const midY = (m.start.y + m.end.y) / 2;
|
|
5532
|
+
ctx.font = "14px sans-serif";
|
|
5533
|
+
const metrics = ctx.measureText(label);
|
|
5534
|
+
const padX = 6;
|
|
5535
|
+
const padY = 4;
|
|
5536
|
+
const textH = 14;
|
|
5537
|
+
ctx.fillStyle = "rgba(0, 0, 0, 0.75)";
|
|
5538
|
+
ctx.beginPath();
|
|
5539
|
+
ctx.roundRect(
|
|
5540
|
+
midX - metrics.width / 2 - padX,
|
|
5541
|
+
midY - textH / 2 - padY,
|
|
5542
|
+
metrics.width + padX * 2,
|
|
5543
|
+
textH + padY * 2,
|
|
5544
|
+
4
|
|
5545
|
+
);
|
|
5546
|
+
ctx.fill();
|
|
5547
|
+
ctx.fillStyle = "#FFFFFF";
|
|
5548
|
+
ctx.textAlign = "center";
|
|
5549
|
+
ctx.textBaseline = "middle";
|
|
5550
|
+
ctx.fillText(label, midX, midY);
|
|
5551
|
+
ctx.restore();
|
|
5552
|
+
}
|
|
5553
|
+
snapToGrid(point, ctx) {
|
|
5554
|
+
if (!ctx.gridSize) return point;
|
|
5555
|
+
if (ctx.gridType === "hex" && ctx.hexOrientation) {
|
|
5556
|
+
return snapToHexCenter(point, ctx.gridSize, ctx.hexOrientation);
|
|
5557
|
+
}
|
|
5558
|
+
if (ctx.gridType === "square") {
|
|
5559
|
+
return snapPoint(point, ctx.gridSize);
|
|
5560
|
+
}
|
|
5561
|
+
if (ctx.snapToGrid) {
|
|
5562
|
+
return snapPoint(point, ctx.gridSize);
|
|
5563
|
+
}
|
|
5564
|
+
return point;
|
|
5565
|
+
}
|
|
5566
|
+
notifyOptionsChange() {
|
|
5567
|
+
for (const listener of this.optionListeners) listener();
|
|
5568
|
+
}
|
|
5569
|
+
};
|
|
5570
|
+
|
|
5571
|
+
// src/tools/template-tool.ts
|
|
5572
|
+
var TemplateTool = class {
|
|
5573
|
+
name = "template";
|
|
5574
|
+
drawing = false;
|
|
5575
|
+
origin = { x: 0, y: 0 };
|
|
5576
|
+
current = { x: 0, y: 0 };
|
|
5577
|
+
gridSize = 1;
|
|
5578
|
+
gridType;
|
|
5579
|
+
hexOrientation;
|
|
5580
|
+
snapEnabled = false;
|
|
5581
|
+
templateShape;
|
|
5582
|
+
fillColor;
|
|
5583
|
+
strokeColor;
|
|
5584
|
+
strokeWidth;
|
|
5585
|
+
opacity;
|
|
5586
|
+
feetPerCell;
|
|
5587
|
+
optionListeners = /* @__PURE__ */ new Set();
|
|
5588
|
+
constructor(options = {}) {
|
|
5589
|
+
this.templateShape = options.templateShape ?? "circle";
|
|
5590
|
+
this.fillColor = options.fillColor ?? "rgba(255, 87, 34, 0.2)";
|
|
5591
|
+
this.strokeColor = options.strokeColor ?? "#FF5722";
|
|
5592
|
+
this.strokeWidth = options.strokeWidth ?? 2;
|
|
5593
|
+
this.opacity = options.opacity ?? 0.6;
|
|
5594
|
+
this.feetPerCell = options.feetPerCell ?? 5;
|
|
5595
|
+
}
|
|
5596
|
+
getOptions() {
|
|
5597
|
+
return {
|
|
5598
|
+
templateShape: this.templateShape,
|
|
5599
|
+
fillColor: this.fillColor,
|
|
5600
|
+
strokeColor: this.strokeColor,
|
|
5601
|
+
strokeWidth: this.strokeWidth,
|
|
5602
|
+
opacity: this.opacity,
|
|
5603
|
+
feetPerCell: this.feetPerCell
|
|
5604
|
+
};
|
|
5605
|
+
}
|
|
5606
|
+
setOptions(options) {
|
|
5607
|
+
if (options.templateShape !== void 0) this.templateShape = options.templateShape;
|
|
5608
|
+
if (options.fillColor !== void 0) this.fillColor = options.fillColor;
|
|
5609
|
+
if (options.strokeColor !== void 0) this.strokeColor = options.strokeColor;
|
|
5610
|
+
if (options.strokeWidth !== void 0) this.strokeWidth = options.strokeWidth;
|
|
5611
|
+
if (options.opacity !== void 0) this.opacity = options.opacity;
|
|
5612
|
+
if (options.feetPerCell !== void 0) this.feetPerCell = options.feetPerCell;
|
|
5613
|
+
this.notifyOptionsChange();
|
|
5614
|
+
}
|
|
5615
|
+
onOptionsChange(listener) {
|
|
5616
|
+
this.optionListeners.add(listener);
|
|
5617
|
+
return () => this.optionListeners.delete(listener);
|
|
5618
|
+
}
|
|
5619
|
+
onPointerDown(state, ctx) {
|
|
5620
|
+
this.drawing = true;
|
|
5621
|
+
this.gridSize = ctx.gridSize ?? 1;
|
|
5622
|
+
this.gridType = ctx.gridType;
|
|
5623
|
+
this.hexOrientation = ctx.hexOrientation;
|
|
5624
|
+
this.snapEnabled = !!ctx.gridType || (ctx.snapToGrid ?? false);
|
|
5625
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
5626
|
+
this.origin = this.snapToGrid(world, ctx);
|
|
5627
|
+
this.current = { ...this.origin };
|
|
5628
|
+
}
|
|
5629
|
+
onPointerMove(state, ctx) {
|
|
5630
|
+
if (!this.drawing) return;
|
|
5631
|
+
this.current = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
5632
|
+
ctx.requestRender();
|
|
5633
|
+
}
|
|
5634
|
+
onPointerUp(_state, ctx) {
|
|
5635
|
+
if (!this.drawing) return;
|
|
5636
|
+
this.drawing = false;
|
|
5637
|
+
const radius = this.computeRadius();
|
|
5638
|
+
if (radius <= 0) return;
|
|
5639
|
+
const angle = this.computeAngle();
|
|
5640
|
+
const gridSize = ctx.gridSize;
|
|
5641
|
+
const snapUnit = gridSize && gridSize > 0 ? ctx.gridType === "hex" ? Math.sqrt(3) * gridSize : gridSize : 0;
|
|
5642
|
+
const cells = snapUnit > 0 ? radius / snapUnit : 0;
|
|
5643
|
+
const radiusFeet = cells * this.feetPerCell;
|
|
5644
|
+
const element = createTemplate({
|
|
5645
|
+
position: { ...this.origin },
|
|
5646
|
+
templateShape: this.templateShape,
|
|
5647
|
+
radius,
|
|
5648
|
+
angle,
|
|
5649
|
+
fillColor: this.fillColor,
|
|
5650
|
+
strokeColor: this.strokeColor,
|
|
5651
|
+
strokeWidth: this.strokeWidth,
|
|
5652
|
+
opacity: this.opacity,
|
|
5653
|
+
feetPerCell: this.feetPerCell,
|
|
5654
|
+
radiusFeet: radiusFeet > 0 ? radiusFeet : void 0,
|
|
5655
|
+
layerId: ctx.activeLayerId ?? ""
|
|
5656
|
+
});
|
|
5657
|
+
ctx.store.add(element);
|
|
5658
|
+
ctx.requestRender();
|
|
5659
|
+
ctx.switchTool?.("select");
|
|
5660
|
+
}
|
|
5661
|
+
onDeactivate(_ctx) {
|
|
5662
|
+
this.drawing = false;
|
|
5663
|
+
this.origin = { x: 0, y: 0 };
|
|
5664
|
+
this.current = { x: 0, y: 0 };
|
|
5665
|
+
}
|
|
5666
|
+
renderOverlay(ctx) {
|
|
5667
|
+
if (!this.drawing) return;
|
|
5668
|
+
const radius = this.computeRadius();
|
|
5669
|
+
if (radius <= 0) return;
|
|
5670
|
+
if (this.gridType === "hex" && this.hexOrientation) {
|
|
5671
|
+
this.renderHexOverlay(ctx, radius);
|
|
5672
|
+
return;
|
|
5673
|
+
}
|
|
5674
|
+
this.renderGeometricOverlay(ctx, radius);
|
|
5675
|
+
}
|
|
5676
|
+
renderGeometricOverlay(ctx, radius) {
|
|
5677
|
+
const cx = this.origin.x;
|
|
5678
|
+
const cy = this.origin.y;
|
|
5679
|
+
const angle = this.computeAngle();
|
|
5680
|
+
ctx.save();
|
|
5681
|
+
ctx.globalAlpha = 0.4;
|
|
5682
|
+
ctx.fillStyle = this.fillColor;
|
|
5683
|
+
ctx.strokeStyle = this.strokeColor;
|
|
5684
|
+
ctx.lineWidth = this.strokeWidth;
|
|
5685
|
+
switch (this.templateShape) {
|
|
5686
|
+
case "circle":
|
|
5687
|
+
ctx.beginPath();
|
|
5688
|
+
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
|
|
5689
|
+
ctx.fill();
|
|
5690
|
+
ctx.stroke();
|
|
5691
|
+
break;
|
|
5692
|
+
case "square":
|
|
5693
|
+
ctx.fillRect(cx - radius / 2, cy - radius / 2, radius, radius);
|
|
5694
|
+
ctx.strokeRect(cx - radius / 2, cy - radius / 2, radius, radius);
|
|
5695
|
+
break;
|
|
5696
|
+
case "cone": {
|
|
5697
|
+
const halfAngle = Math.atan(0.5);
|
|
5698
|
+
ctx.beginPath();
|
|
5699
|
+
ctx.moveTo(cx, cy);
|
|
5700
|
+
ctx.arc(cx, cy, radius, angle - halfAngle, angle + halfAngle);
|
|
5701
|
+
ctx.closePath();
|
|
5702
|
+
ctx.fill();
|
|
5703
|
+
ctx.stroke();
|
|
5704
|
+
break;
|
|
5705
|
+
}
|
|
5706
|
+
case "line": {
|
|
5707
|
+
const halfW = radius / 12;
|
|
5708
|
+
const cos = Math.cos(angle);
|
|
5709
|
+
const sin = Math.sin(angle);
|
|
5710
|
+
const perpX = -sin * halfW;
|
|
5711
|
+
const perpY = cos * halfW;
|
|
5712
|
+
ctx.beginPath();
|
|
5713
|
+
ctx.moveTo(cx + perpX, cy + perpY);
|
|
5714
|
+
ctx.lineTo(cx + radius * cos + perpX, cy + radius * sin + perpY);
|
|
5715
|
+
ctx.lineTo(cx + radius * cos - perpX, cy + radius * sin - perpY);
|
|
5716
|
+
ctx.lineTo(cx - perpX, cy - perpY);
|
|
5717
|
+
ctx.closePath();
|
|
5718
|
+
ctx.fill();
|
|
5719
|
+
ctx.stroke();
|
|
5720
|
+
break;
|
|
5721
|
+
}
|
|
5722
|
+
}
|
|
5723
|
+
ctx.restore();
|
|
5724
|
+
}
|
|
5725
|
+
renderHexOverlay(ctx, radius) {
|
|
5726
|
+
const orientation = this.hexOrientation;
|
|
5727
|
+
if (!orientation) return;
|
|
5728
|
+
const cellSize = this.gridSize;
|
|
5729
|
+
const snapUnit = Math.sqrt(3) * cellSize;
|
|
5730
|
+
const radiusCells = radius / snapUnit;
|
|
5731
|
+
const angle = this.computeAngle();
|
|
5732
|
+
const center = this.origin;
|
|
5733
|
+
let hexCells;
|
|
5734
|
+
switch (this.templateShape) {
|
|
5735
|
+
case "circle":
|
|
5736
|
+
hexCells = getHexCellsInRadius(center, radiusCells, cellSize, orientation);
|
|
5737
|
+
break;
|
|
5738
|
+
case "cone":
|
|
5739
|
+
hexCells = getHexCellsInCone(center, angle, radiusCells, cellSize, orientation);
|
|
5740
|
+
break;
|
|
5741
|
+
case "line":
|
|
5742
|
+
hexCells = getHexCellsInLine(center, angle, radiusCells, cellSize, orientation);
|
|
5743
|
+
break;
|
|
5744
|
+
case "square":
|
|
5745
|
+
hexCells = getHexCellsInSquare(center, radiusCells, cellSize, orientation);
|
|
5746
|
+
break;
|
|
5747
|
+
}
|
|
5748
|
+
ctx.save();
|
|
5749
|
+
ctx.globalAlpha = 0.4;
|
|
5750
|
+
ctx.beginPath();
|
|
5751
|
+
for (const cell of hexCells) {
|
|
5752
|
+
drawHexPath(ctx, cell.x, cell.y, cellSize, orientation);
|
|
5753
|
+
}
|
|
5754
|
+
ctx.fillStyle = this.fillColor;
|
|
5755
|
+
ctx.fill();
|
|
5756
|
+
ctx.beginPath();
|
|
5757
|
+
for (const cell of hexCells) {
|
|
5758
|
+
drawHexPath(ctx, cell.x, cell.y, cellSize, orientation);
|
|
5759
|
+
}
|
|
5760
|
+
ctx.strokeStyle = this.strokeColor;
|
|
5761
|
+
ctx.lineWidth = this.strokeWidth;
|
|
5762
|
+
ctx.stroke();
|
|
5763
|
+
if (this.templateShape === "cone" || this.templateShape === "line" || this.templateShape === "circle" || this.templateShape === "square") {
|
|
5764
|
+
ctx.globalAlpha = 0.5;
|
|
5765
|
+
ctx.beginPath();
|
|
5766
|
+
drawHexPath(ctx, center.x, center.y, cellSize, orientation);
|
|
5767
|
+
ctx.fillStyle = this.strokeColor;
|
|
5768
|
+
ctx.fill();
|
|
5769
|
+
ctx.strokeStyle = this.strokeColor;
|
|
5770
|
+
ctx.lineWidth = this.strokeWidth;
|
|
5771
|
+
ctx.stroke();
|
|
5772
|
+
}
|
|
5773
|
+
if (this.templateShape === "circle") {
|
|
5774
|
+
const feet = radiusCells * this.feetPerCell;
|
|
5775
|
+
if (feet > 0) {
|
|
5776
|
+
ctx.globalAlpha = 1;
|
|
5777
|
+
const label = `${Math.round(feet)} ft`;
|
|
5778
|
+
const fontSize = Math.max(10, Math.min(14, radius * 0.15));
|
|
5779
|
+
ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
|
|
5780
|
+
ctx.textAlign = "center";
|
|
5781
|
+
ctx.textBaseline = "bottom";
|
|
5782
|
+
const textX = center.x;
|
|
5783
|
+
const textY = center.y - 4;
|
|
5784
|
+
const metrics = ctx.measureText(label);
|
|
5785
|
+
const padX = 4;
|
|
5786
|
+
const padY = 2;
|
|
5787
|
+
const textW = metrics.width + padX * 2;
|
|
5788
|
+
const textH = fontSize + padY * 2;
|
|
5789
|
+
ctx.fillStyle = "rgba(255, 255, 255, 0.85)";
|
|
5790
|
+
ctx.beginPath();
|
|
5791
|
+
ctx.roundRect(textX - textW / 2, textY - textH, textW, textH, 3);
|
|
5792
|
+
ctx.fill();
|
|
5793
|
+
ctx.fillStyle = this.strokeColor;
|
|
5794
|
+
ctx.fillText(label, textX, textY - padY);
|
|
5795
|
+
}
|
|
5796
|
+
}
|
|
5797
|
+
ctx.restore();
|
|
5798
|
+
}
|
|
5799
|
+
computeRadius() {
|
|
5800
|
+
const dx = this.current.x - this.origin.x;
|
|
5801
|
+
const dy = this.current.y - this.origin.y;
|
|
5802
|
+
const raw = Math.sqrt(dx * dx + dy * dy);
|
|
5803
|
+
if (this.snapEnabled && this.gridSize > 0) {
|
|
5804
|
+
const snapUnit = this.gridType === "hex" ? Math.sqrt(3) * this.gridSize : this.gridSize;
|
|
5805
|
+
return Math.max(snapUnit, Math.round(raw / snapUnit) * snapUnit);
|
|
5806
|
+
}
|
|
5807
|
+
return raw;
|
|
5808
|
+
}
|
|
5809
|
+
computeAngle() {
|
|
5810
|
+
const dx = this.current.x - this.origin.x;
|
|
5811
|
+
const dy = this.current.y - this.origin.y;
|
|
5812
|
+
return Math.atan2(dy, dx);
|
|
5813
|
+
}
|
|
5814
|
+
snapToGrid(point, ctx) {
|
|
5815
|
+
if (!ctx.gridSize) return point;
|
|
5816
|
+
if (ctx.gridType === "hex" && ctx.hexOrientation) {
|
|
5817
|
+
return snapToHexCenter(point, ctx.gridSize, ctx.hexOrientation);
|
|
5818
|
+
}
|
|
5819
|
+
if (ctx.gridType === "square") {
|
|
5820
|
+
return snapPoint(point, ctx.gridSize);
|
|
5821
|
+
}
|
|
5822
|
+
if (ctx.snapToGrid) {
|
|
5823
|
+
return snapPoint(point, ctx.gridSize);
|
|
5824
|
+
}
|
|
5825
|
+
return point;
|
|
5826
|
+
}
|
|
5827
|
+
notifyOptionsChange() {
|
|
5828
|
+
for (const listener of this.optionListeners) listener();
|
|
5829
|
+
}
|
|
5830
|
+
};
|
|
5831
|
+
|
|
4883
5832
|
// src/history/layer-commands.ts
|
|
4884
5833
|
var CreateLayerCommand = class {
|
|
4885
5834
|
constructor(manager, layer) {
|
|
@@ -4921,7 +5870,7 @@ var UpdateLayerCommand = class {
|
|
|
4921
5870
|
};
|
|
4922
5871
|
|
|
4923
5872
|
// src/index.ts
|
|
4924
|
-
var VERSION = "0.
|
|
5873
|
+
var VERSION = "0.9.0";
|
|
4925
5874
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4926
5875
|
0 && (module.exports = {
|
|
4927
5876
|
AddElementCommand,
|
|
@@ -4941,6 +5890,7 @@ var VERSION = "0.8.11";
|
|
|
4941
5890
|
ImageTool,
|
|
4942
5891
|
InputHandler,
|
|
4943
5892
|
LayerManager,
|
|
5893
|
+
MeasureTool,
|
|
4944
5894
|
NoteEditor,
|
|
4945
5895
|
NoteTool,
|
|
4946
5896
|
PencilTool,
|
|
@@ -4949,6 +5899,7 @@ var VERSION = "0.8.11";
|
|
|
4949
5899
|
RemoveLayerCommand,
|
|
4950
5900
|
SelectTool,
|
|
4951
5901
|
ShapeTool,
|
|
5902
|
+
TemplateTool,
|
|
4952
5903
|
TextTool,
|
|
4953
5904
|
ToolManager,
|
|
4954
5905
|
UpdateElementCommand,
|
|
@@ -4965,7 +5916,9 @@ var VERSION = "0.8.11";
|
|
|
4965
5916
|
createNote,
|
|
4966
5917
|
createShape,
|
|
4967
5918
|
createStroke,
|
|
5919
|
+
createTemplate,
|
|
4968
5920
|
createText,
|
|
5921
|
+
drawHexPath,
|
|
4969
5922
|
exportImage,
|
|
4970
5923
|
exportState,
|
|
4971
5924
|
findBindTarget,
|
|
@@ -4978,10 +5931,17 @@ var VERSION = "0.8.11";
|
|
|
4978
5931
|
getEdgeIntersection,
|
|
4979
5932
|
getElementBounds,
|
|
4980
5933
|
getElementCenter,
|
|
5934
|
+
getHexCellsInCone,
|
|
5935
|
+
getHexCellsInLine,
|
|
5936
|
+
getHexCellsInRadius,
|
|
5937
|
+
getHexCellsInSquare,
|
|
5938
|
+
getHexDistance,
|
|
4981
5939
|
isBindable,
|
|
4982
5940
|
isNearBezier,
|
|
4983
5941
|
parseState,
|
|
5942
|
+
smartSnap,
|
|
4984
5943
|
snapPoint,
|
|
5944
|
+
snapToHexCenter,
|
|
4985
5945
|
unbindArrow,
|
|
4986
5946
|
updateBoundArrow
|
|
4987
5947
|
});
|