@fieldnotes/core 0.8.10 → 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 +977 -14
- package/dist/index.d.cts +183 -65
- package/dist/index.d.ts +183 -65
- package/dist/index.js +966 -14
- 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);
|
|
@@ -3302,7 +3756,10 @@ var Viewport = class {
|
|
|
3302
3756
|
this.renderer = new ElementRenderer();
|
|
3303
3757
|
this.renderer.setStore(this.store);
|
|
3304
3758
|
this.renderer.setCamera(this.camera);
|
|
3305
|
-
this.renderer.setOnImageLoad(() =>
|
|
3759
|
+
this.renderer.setOnImageLoad(() => {
|
|
3760
|
+
this.renderLoop.markAllLayersDirty();
|
|
3761
|
+
this.requestRender();
|
|
3762
|
+
});
|
|
3306
3763
|
this.noteEditor = new NoteEditor();
|
|
3307
3764
|
this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
|
|
3308
3765
|
this.history = new HistoryStack();
|
|
@@ -3363,16 +3820,19 @@ var Viewport = class {
|
|
|
3363
3820
|
});
|
|
3364
3821
|
this.unsubStore = [
|
|
3365
3822
|
this.store.on("add", (el) => {
|
|
3823
|
+
if (el.type === "grid") this.syncGridContext();
|
|
3366
3824
|
this.renderLoop.markLayerDirty(el.layerId);
|
|
3367
3825
|
this.requestRender();
|
|
3368
3826
|
}),
|
|
3369
3827
|
this.store.on("remove", (el) => {
|
|
3828
|
+
if (el.type === "grid") this.syncGridContext();
|
|
3370
3829
|
this.unbindArrowsFrom(el);
|
|
3371
3830
|
this.domNodeManager.removeDomNode(el.id);
|
|
3372
3831
|
this.renderLoop.markLayerDirty(el.layerId);
|
|
3373
3832
|
this.requestRender();
|
|
3374
3833
|
}),
|
|
3375
3834
|
this.store.on("update", ({ previous, current }) => {
|
|
3835
|
+
if (current.type === "grid") this.syncGridContext();
|
|
3376
3836
|
this.renderLoop.markLayerDirty(current.layerId);
|
|
3377
3837
|
if (previous.layerId !== current.layerId) {
|
|
3378
3838
|
this.renderLoop.markLayerDirty(previous.layerId);
|
|
@@ -3382,6 +3842,7 @@ var Viewport = class {
|
|
|
3382
3842
|
this.store.on("clear", () => {
|
|
3383
3843
|
this.domNodeManager.clearDomNodes();
|
|
3384
3844
|
this.renderLoop.markAllLayersDirty();
|
|
3845
|
+
this.syncGridContext();
|
|
3385
3846
|
this.requestRender();
|
|
3386
3847
|
})
|
|
3387
3848
|
];
|
|
@@ -3395,6 +3856,7 @@ var Viewport = class {
|
|
|
3395
3856
|
this.observeResize();
|
|
3396
3857
|
this.syncCanvasSize();
|
|
3397
3858
|
this.renderLoop.start();
|
|
3859
|
+
this.syncGridContext();
|
|
3398
3860
|
}
|
|
3399
3861
|
camera;
|
|
3400
3862
|
store;
|
|
@@ -3711,6 +4173,18 @@ var Viewport = class {
|
|
|
3711
4173
|
this.renderLoop.setCanvasSize(rect.width * dpr, rect.height * dpr);
|
|
3712
4174
|
this.requestRender();
|
|
3713
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
|
+
}
|
|
3714
4188
|
observeResize() {
|
|
3715
4189
|
if (typeof ResizeObserver === "undefined") return;
|
|
3716
4190
|
this.resizeObserver = new ResizeObserver(() => this.syncCanvasSize());
|
|
@@ -4085,7 +4559,7 @@ var SelectTool = class {
|
|
|
4085
4559
|
ctx.setCursor?.("default");
|
|
4086
4560
|
}
|
|
4087
4561
|
snap(point, ctx) {
|
|
4088
|
-
return
|
|
4562
|
+
return smartSnap(point, ctx);
|
|
4089
4563
|
}
|
|
4090
4564
|
onPointerDown(state, ctx) {
|
|
4091
4565
|
this.ctx = ctx;
|
|
@@ -4102,6 +4576,12 @@ var SelectTool = class {
|
|
|
4102
4576
|
ctx.requestRender();
|
|
4103
4577
|
return;
|
|
4104
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
|
+
}
|
|
4105
4585
|
const resizeHit = this.hitTestResizeHandle(world, ctx);
|
|
4106
4586
|
if (resizeHit) {
|
|
4107
4587
|
const el = ctx.store.getById(resizeHit.elementId);
|
|
@@ -4136,6 +4616,11 @@ var SelectTool = class {
|
|
|
4136
4616
|
applyArrowHandleDrag(this.mode.handle, this.mode.elementId, world, ctx);
|
|
4137
4617
|
return;
|
|
4138
4618
|
}
|
|
4619
|
+
if (this.mode.type === "resizing-template") {
|
|
4620
|
+
ctx.setCursor?.("nwse-resize");
|
|
4621
|
+
this.handleTemplateResize(world, ctx);
|
|
4622
|
+
return;
|
|
4623
|
+
}
|
|
4139
4624
|
if (this.mode.type === "resizing") {
|
|
4140
4625
|
ctx.setCursor?.(HANDLE_CURSORS[this.mode.handle]);
|
|
4141
4626
|
this.handleResize(world, ctx);
|
|
@@ -4159,6 +4644,16 @@ var SelectTool = class {
|
|
|
4159
4644
|
from: { x: el.from.x + dx, y: el.from.y + dy },
|
|
4160
4645
|
to: { x: el.to.x + dx, y: el.to.y + dy }
|
|
4161
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
|
+
});
|
|
4162
4657
|
} else {
|
|
4163
4658
|
ctx.store.update(id, {
|
|
4164
4659
|
position: { x: el.position.x + dx, y: el.position.y + dy }
|
|
@@ -4233,6 +4728,11 @@ var SelectTool = class {
|
|
|
4233
4728
|
ctx.setCursor?.(getArrowHandleCursor(arrowHit.handle, false));
|
|
4234
4729
|
return;
|
|
4235
4730
|
}
|
|
4731
|
+
const templateResizeHit = this.hitTestTemplateResizeHandle(world, ctx);
|
|
4732
|
+
if (templateResizeHit) {
|
|
4733
|
+
ctx.setCursor?.("nwse-resize");
|
|
4734
|
+
return;
|
|
4735
|
+
}
|
|
4236
4736
|
const resizeHit = this.hitTestResizeHandle(world, ctx);
|
|
4237
4737
|
if (resizeHit) {
|
|
4238
4738
|
ctx.setCursor?.(HANDLE_CURSORS[resizeHit.handle]);
|
|
@@ -4369,6 +4869,24 @@ var SelectTool = class {
|
|
|
4369
4869
|
);
|
|
4370
4870
|
}
|
|
4371
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]);
|
|
4372
4890
|
}
|
|
4373
4891
|
}
|
|
4374
4892
|
canvasCtx.restore();
|
|
@@ -4393,6 +4911,43 @@ var SelectTool = class {
|
|
|
4393
4911
|
}
|
|
4394
4912
|
canvasCtx.restore();
|
|
4395
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
|
+
}
|
|
4396
4951
|
getMarqueeRect() {
|
|
4397
4952
|
if (this.mode.type !== "marquee") return null;
|
|
4398
4953
|
const { start } = this.mode;
|
|
@@ -4449,6 +5004,11 @@ var SelectTool = class {
|
|
|
4449
5004
|
if (el.type === "arrow") {
|
|
4450
5005
|
return isNearBezier(point, el.from, el.to, el.bend, 10);
|
|
4451
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
|
+
}
|
|
4452
5012
|
return false;
|
|
4453
5013
|
}
|
|
4454
5014
|
};
|
|
@@ -4506,7 +5066,7 @@ var ArrowTool = class {
|
|
|
4506
5066
|
this.fromBinding = { elementId: target.id };
|
|
4507
5067
|
this.fromTarget = target;
|
|
4508
5068
|
} else {
|
|
4509
|
-
this.start =
|
|
5069
|
+
this.start = smartSnap(world, ctx);
|
|
4510
5070
|
this.fromBinding = void 0;
|
|
4511
5071
|
this.fromTarget = null;
|
|
4512
5072
|
}
|
|
@@ -4524,7 +5084,7 @@ var ArrowTool = class {
|
|
|
4524
5084
|
this.end = getElementCenter(target);
|
|
4525
5085
|
this.toTarget = target;
|
|
4526
5086
|
} else {
|
|
4527
|
-
this.end =
|
|
5087
|
+
this.end = smartSnap(world, ctx);
|
|
4528
5088
|
this.toTarget = null;
|
|
4529
5089
|
}
|
|
4530
5090
|
ctx.requestRender();
|
|
@@ -4637,9 +5197,7 @@ var NoteTool = class {
|
|
|
4637
5197
|
}
|
|
4638
5198
|
onPointerUp(state, ctx) {
|
|
4639
5199
|
let world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
4640
|
-
|
|
4641
|
-
world = snapPoint(world, ctx.gridSize);
|
|
4642
|
-
}
|
|
5200
|
+
world = smartSnap(world, ctx);
|
|
4643
5201
|
const note = createNote({
|
|
4644
5202
|
position: world,
|
|
4645
5203
|
size: { ...this.size },
|
|
@@ -4694,9 +5252,7 @@ var TextTool = class {
|
|
|
4694
5252
|
}
|
|
4695
5253
|
onPointerUp(state, ctx) {
|
|
4696
5254
|
let world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
4697
|
-
|
|
4698
|
-
world = snapPoint(world, ctx.gridSize);
|
|
4699
|
-
}
|
|
5255
|
+
world = smartSnap(world, ctx);
|
|
4700
5256
|
const textEl = createText({
|
|
4701
5257
|
position: world,
|
|
4702
5258
|
fontSize: this.fontSize,
|
|
@@ -4729,8 +5285,12 @@ var ImageTool = class {
|
|
|
4729
5285
|
onPointerUp(state, ctx) {
|
|
4730
5286
|
if (!this.src) return;
|
|
4731
5287
|
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
5288
|
+
const snapped = smartSnap(world, ctx);
|
|
4732
5289
|
const image = createImage({
|
|
4733
|
-
position:
|
|
5290
|
+
position: {
|
|
5291
|
+
x: snapped.x - this.size.w / 2,
|
|
5292
|
+
y: snapped.y - this.size.h / 2
|
|
5293
|
+
},
|
|
4734
5294
|
size: { ...this.size },
|
|
4735
5295
|
src: this.src
|
|
4736
5296
|
});
|
|
@@ -4867,7 +5427,7 @@ var ShapeTool = class {
|
|
|
4867
5427
|
for (const listener of this.optionListeners) listener();
|
|
4868
5428
|
}
|
|
4869
5429
|
snap(point, ctx) {
|
|
4870
|
-
return
|
|
5430
|
+
return smartSnap(point, ctx);
|
|
4871
5431
|
}
|
|
4872
5432
|
onKeyDown = (e) => {
|
|
4873
5433
|
if (e.key === "Shift") this.shiftHeld = true;
|
|
@@ -4877,6 +5437,398 @@ var ShapeTool = class {
|
|
|
4877
5437
|
};
|
|
4878
5438
|
};
|
|
4879
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
|
+
|
|
4880
5832
|
// src/history/layer-commands.ts
|
|
4881
5833
|
var CreateLayerCommand = class {
|
|
4882
5834
|
constructor(manager, layer) {
|
|
@@ -4918,7 +5870,7 @@ var UpdateLayerCommand = class {
|
|
|
4918
5870
|
};
|
|
4919
5871
|
|
|
4920
5872
|
// src/index.ts
|
|
4921
|
-
var VERSION = "0.
|
|
5873
|
+
var VERSION = "0.9.0";
|
|
4922
5874
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4923
5875
|
0 && (module.exports = {
|
|
4924
5876
|
AddElementCommand,
|
|
@@ -4938,6 +5890,7 @@ var VERSION = "0.8.10";
|
|
|
4938
5890
|
ImageTool,
|
|
4939
5891
|
InputHandler,
|
|
4940
5892
|
LayerManager,
|
|
5893
|
+
MeasureTool,
|
|
4941
5894
|
NoteEditor,
|
|
4942
5895
|
NoteTool,
|
|
4943
5896
|
PencilTool,
|
|
@@ -4946,6 +5899,7 @@ var VERSION = "0.8.10";
|
|
|
4946
5899
|
RemoveLayerCommand,
|
|
4947
5900
|
SelectTool,
|
|
4948
5901
|
ShapeTool,
|
|
5902
|
+
TemplateTool,
|
|
4949
5903
|
TextTool,
|
|
4950
5904
|
ToolManager,
|
|
4951
5905
|
UpdateElementCommand,
|
|
@@ -4962,7 +5916,9 @@ var VERSION = "0.8.10";
|
|
|
4962
5916
|
createNote,
|
|
4963
5917
|
createShape,
|
|
4964
5918
|
createStroke,
|
|
5919
|
+
createTemplate,
|
|
4965
5920
|
createText,
|
|
5921
|
+
drawHexPath,
|
|
4966
5922
|
exportImage,
|
|
4967
5923
|
exportState,
|
|
4968
5924
|
findBindTarget,
|
|
@@ -4975,10 +5931,17 @@ var VERSION = "0.8.10";
|
|
|
4975
5931
|
getEdgeIntersection,
|
|
4976
5932
|
getElementBounds,
|
|
4977
5933
|
getElementCenter,
|
|
5934
|
+
getHexCellsInCone,
|
|
5935
|
+
getHexCellsInLine,
|
|
5936
|
+
getHexCellsInRadius,
|
|
5937
|
+
getHexCellsInSquare,
|
|
5938
|
+
getHexDistance,
|
|
4978
5939
|
isBindable,
|
|
4979
5940
|
isNearBezier,
|
|
4980
5941
|
parseState,
|
|
5942
|
+
smartSnap,
|
|
4981
5943
|
snapPoint,
|
|
5944
|
+
snapToHexCenter,
|
|
4982
5945
|
unbindArrow,
|
|
4983
5946
|
updateBoundArrow
|
|
4984
5947
|
});
|