@fieldnotes/core 0.38.7 → 0.40.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/dist/index.cjs +669 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -3
- package/dist/index.d.ts +58 -3
- package/dist/index.js +667 -57
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -831,6 +831,11 @@ var KeyboardActions = class {
|
|
|
831
831
|
if (tool?.name !== "select") return null;
|
|
832
832
|
return { tool, ctx };
|
|
833
833
|
}
|
|
834
|
+
selectableElements(ctx) {
|
|
835
|
+
return ctx.store.getAll().filter(
|
|
836
|
+
(el) => !el.locked && (ctx.isLayerVisible?.(el.layerId) ?? true) && !(ctx.isLayerLocked?.(el.layerId) ?? false)
|
|
837
|
+
);
|
|
838
|
+
}
|
|
834
839
|
nudge(dx, dy, byCell) {
|
|
835
840
|
if (this.deps.isToolActive()) return false;
|
|
836
841
|
const sel = this.selectTool();
|
|
@@ -973,12 +978,28 @@ var KeyboardActions = class {
|
|
|
973
978
|
}
|
|
974
979
|
const sel = this.selectTool();
|
|
975
980
|
if (!sel) return;
|
|
976
|
-
const ids = sel.ctx.
|
|
977
|
-
(el) => !el.locked && (sel.ctx.isLayerVisible?.(el.layerId) ?? true) && !(sel.ctx.isLayerLocked?.(el.layerId) ?? false)
|
|
978
|
-
).map((el) => el.id);
|
|
981
|
+
const ids = this.selectableElements(sel.ctx).map((el) => el.id);
|
|
979
982
|
sel.tool.setSelection(ids);
|
|
980
983
|
sel.ctx.requestRender();
|
|
981
984
|
}
|
|
985
|
+
cycleSelection(direction) {
|
|
986
|
+
if (this.deps.isToolActive()) return;
|
|
987
|
+
const tm = this.deps.getToolManager();
|
|
988
|
+
const ctx = this.deps.getToolContext();
|
|
989
|
+
if (!tm || !ctx) return;
|
|
990
|
+
if (tm.activeTool?.name !== "select") ctx.switchTool?.("select");
|
|
991
|
+
const sel = this.selectTool();
|
|
992
|
+
if (!sel) return;
|
|
993
|
+
const eligible = this.selectableElements(sel.ctx).filter((el) => el.type !== "grid");
|
|
994
|
+
if (eligible.length === 0) return;
|
|
995
|
+
const idxs = sel.tool.selectedIds.map((id) => eligible.findIndex((e) => e.id === id)).filter((i) => i >= 0);
|
|
996
|
+
const anchor = idxs.length === 0 ? direction > 0 ? -1 : 0 : direction > 0 ? Math.max(...idxs) : Math.min(...idxs);
|
|
997
|
+
const next = (anchor + direction + eligible.length) % eligible.length;
|
|
998
|
+
const target = eligible[next];
|
|
999
|
+
if (!target) return;
|
|
1000
|
+
sel.tool.setSelection([target.id]);
|
|
1001
|
+
sel.ctx.requestRender();
|
|
1002
|
+
}
|
|
982
1003
|
zoomToFit() {
|
|
983
1004
|
if (this.deps.isToolActive()) return;
|
|
984
1005
|
this.deps.fitToContent?.();
|
|
@@ -1083,8 +1104,9 @@ var DEFAULT_BINDINGS = [
|
|
|
1083
1104
|
["undo", ["mod+z"]],
|
|
1084
1105
|
["redo", ["mod+y", "mod+shift+z"]],
|
|
1085
1106
|
["select-all", ["mod+a"]],
|
|
1107
|
+
["cycle-selection", ["tab"]],
|
|
1108
|
+
["cycle-selection-reverse", ["shift+tab"]],
|
|
1086
1109
|
["copy", ["mod+c"]],
|
|
1087
|
-
["paste", ["mod+v"]],
|
|
1088
1110
|
["duplicate", ["mod+d"]],
|
|
1089
1111
|
["z-forward", ["]"]],
|
|
1090
1112
|
["z-backward", ["["]],
|
|
@@ -1259,6 +1281,7 @@ var KeyboardHandler = class {
|
|
|
1259
1281
|
this.shortcutMap = new ShortcutMap(deps.shortcuts?.bindings);
|
|
1260
1282
|
window.addEventListener("keydown", this.onKeyDown, { signal: deps.abortSignal });
|
|
1261
1283
|
window.addEventListener("keyup", this.onKeyUp, { signal: deps.abortSignal });
|
|
1284
|
+
window.addEventListener("paste", this.onPaste, { signal: deps.abortSignal });
|
|
1262
1285
|
}
|
|
1263
1286
|
shortcutMap;
|
|
1264
1287
|
get shortcuts() {
|
|
@@ -1274,12 +1297,15 @@ var KeyboardHandler = class {
|
|
|
1274
1297
|
zoomToLevel(level) {
|
|
1275
1298
|
this.deps.camera.zoomAt(level, this.viewportCenter());
|
|
1276
1299
|
}
|
|
1300
|
+
shouldHandle(target) {
|
|
1301
|
+
const el = target;
|
|
1302
|
+
if (el?.isContentEditable) return false;
|
|
1303
|
+
const tag = el?.tagName;
|
|
1304
|
+
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return false;
|
|
1305
|
+
return this.isInScope();
|
|
1306
|
+
}
|
|
1277
1307
|
onKeyDown = (e) => {
|
|
1278
|
-
|
|
1279
|
-
if (target?.isContentEditable) return;
|
|
1280
|
-
const tag = target?.tagName;
|
|
1281
|
-
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
|
|
1282
|
-
if (!this.isInScope()) return;
|
|
1308
|
+
if (!this.shouldHandle(e.target)) return;
|
|
1283
1309
|
if (e.key === " ") {
|
|
1284
1310
|
this.deps.setSpaceHeld(true);
|
|
1285
1311
|
}
|
|
@@ -1301,6 +1327,34 @@ var KeyboardHandler = class {
|
|
|
1301
1327
|
}
|
|
1302
1328
|
}
|
|
1303
1329
|
};
|
|
1330
|
+
onPaste = (e) => {
|
|
1331
|
+
if (!this.shouldHandle(e.target)) return;
|
|
1332
|
+
const items = e.clipboardData?.items;
|
|
1333
|
+
let file = null;
|
|
1334
|
+
if (items) {
|
|
1335
|
+
for (const it of items) {
|
|
1336
|
+
if (it.kind === "file" && it.type.startsWith("image/")) {
|
|
1337
|
+
file = it.getAsFile();
|
|
1338
|
+
break;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
if (file) {
|
|
1343
|
+
e.preventDefault();
|
|
1344
|
+
const world = this.deps.getLastPointerWorld() ?? this.deps.getCenteredWorld();
|
|
1345
|
+
if (this.deps.onPaste) {
|
|
1346
|
+
this.deps.onPaste(e, world);
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
const reader = new FileReader();
|
|
1350
|
+
reader.onload = () => {
|
|
1351
|
+
if (typeof reader.result === "string") this.deps.addImage(reader.result, world);
|
|
1352
|
+
};
|
|
1353
|
+
reader.readAsDataURL(file);
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
this.deps.actions.paste();
|
|
1357
|
+
};
|
|
1304
1358
|
runAction(action, e) {
|
|
1305
1359
|
switch (action) {
|
|
1306
1360
|
case "delete":
|
|
@@ -1322,6 +1376,14 @@ var KeyboardHandler = class {
|
|
|
1322
1376
|
e?.preventDefault();
|
|
1323
1377
|
this.deps.actions.selectAll();
|
|
1324
1378
|
return;
|
|
1379
|
+
case "cycle-selection":
|
|
1380
|
+
e?.preventDefault();
|
|
1381
|
+
this.deps.actions.cycleSelection(1);
|
|
1382
|
+
return;
|
|
1383
|
+
case "cycle-selection-reverse":
|
|
1384
|
+
e?.preventDefault();
|
|
1385
|
+
this.deps.actions.cycleSelection(-1);
|
|
1386
|
+
return;
|
|
1325
1387
|
case "copy":
|
|
1326
1388
|
e?.preventDefault();
|
|
1327
1389
|
this.deps.actions.copy();
|
|
@@ -1449,7 +1511,11 @@ var InputHandler = class {
|
|
|
1449
1511
|
this.spaceHeld = v;
|
|
1450
1512
|
},
|
|
1451
1513
|
getActivePointerCount: () => this.activePointers.size,
|
|
1452
|
-
dispatchToolHover: (e) => this.dispatchToolHover(e)
|
|
1514
|
+
dispatchToolHover: (e) => this.dispatchToolHover(e),
|
|
1515
|
+
addImage: options.addImage ?? (() => ""),
|
|
1516
|
+
getLastPointerWorld: () => this.lastPointerWorld(),
|
|
1517
|
+
getCenteredWorld: options.getCenteredWorld ?? (() => ({ x: 0, y: 0 })),
|
|
1518
|
+
onPaste: options.onPaste
|
|
1453
1519
|
});
|
|
1454
1520
|
this.element.style.touchAction = "none";
|
|
1455
1521
|
if (this.scope === "focus") {
|
|
@@ -2109,12 +2175,12 @@ function smoothToSegments(points) {
|
|
|
2109
2175
|
return [{ start: p0, cp1: { x: mx, y: my }, cp2: { x: mx, y: my }, end: p1 }];
|
|
2110
2176
|
}
|
|
2111
2177
|
const segments = [];
|
|
2112
|
-
const
|
|
2113
|
-
for (let i = 0; i <
|
|
2178
|
+
const n2 = points.length;
|
|
2179
|
+
for (let i = 0; i < n2 - 1; i++) {
|
|
2114
2180
|
const p0 = points[Math.max(0, i - 1)];
|
|
2115
2181
|
const p1 = points[i];
|
|
2116
2182
|
const p2 = points[i + 1];
|
|
2117
|
-
const p3 = points[Math.min(
|
|
2183
|
+
const p3 = points[Math.min(n2 - 1, i + 2)];
|
|
2118
2184
|
if (!p0 || !p1 || !p2 || !p3) continue;
|
|
2119
2185
|
const cp1 = {
|
|
2120
2186
|
x: p1.x + (p2.x - p0.x) / 6,
|
|
@@ -2804,11 +2870,11 @@ function pixelToOffset(x, y, cellSize, orientation) {
|
|
|
2804
2870
|
const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
|
|
2805
2871
|
return { col, row: Math.round((y - offsetY) / hexH) };
|
|
2806
2872
|
}
|
|
2807
|
-
function enumerateHexRing(centerQ, centerR,
|
|
2873
|
+
function enumerateHexRing(centerQ, centerR, n2, orientation, cellSize) {
|
|
2808
2874
|
const cells = [];
|
|
2809
|
-
for (let dq = -
|
|
2810
|
-
const rMin = Math.max(-
|
|
2811
|
-
const rMax = Math.min(
|
|
2875
|
+
for (let dq = -n2; dq <= n2; dq++) {
|
|
2876
|
+
const rMin = Math.max(-n2, -dq - n2);
|
|
2877
|
+
const rMax = Math.min(n2, -dq + n2);
|
|
2812
2878
|
for (let dr = rMin; dr <= rMax; dr++) {
|
|
2813
2879
|
const absQ = centerQ + dq;
|
|
2814
2880
|
const absR = centerR + dr;
|
|
@@ -2829,28 +2895,28 @@ function getHexDistance(a, b, cellSize, orientation) {
|
|
|
2829
2895
|
return Math.max(Math.abs(dq), Math.abs(dr), Math.abs(ds));
|
|
2830
2896
|
}
|
|
2831
2897
|
function getHexCellsInRadius(center2, radiusCells, cellSize, orientation) {
|
|
2832
|
-
const
|
|
2898
|
+
const n2 = Math.round(radiusCells);
|
|
2833
2899
|
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2834
2900
|
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2835
|
-
if (
|
|
2901
|
+
if (n2 <= 0) {
|
|
2836
2902
|
return [offsetToPixel(off.col, off.row, cellSize, orientation)];
|
|
2837
2903
|
}
|
|
2838
|
-
return enumerateHexRing(cube.q, cube.r,
|
|
2904
|
+
return enumerateHexRing(cube.q, cube.r, n2, orientation, cellSize);
|
|
2839
2905
|
}
|
|
2840
2906
|
function getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation) {
|
|
2841
|
-
const
|
|
2907
|
+
const n2 = Math.round(radiusCells);
|
|
2842
2908
|
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2843
2909
|
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2844
2910
|
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
2845
|
-
if (
|
|
2911
|
+
if (n2 <= 0) return [centerPixel];
|
|
2846
2912
|
const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
2847
2913
|
const step = Math.PI / 3;
|
|
2848
2914
|
const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
|
|
2849
2915
|
const halfAngle = Math.PI / 6 + 1e-6;
|
|
2850
2916
|
const cells = [centerPixel];
|
|
2851
|
-
for (let dq = -
|
|
2852
|
-
const rMin = Math.max(-
|
|
2853
|
-
const rMax = Math.min(
|
|
2917
|
+
for (let dq = -n2; dq <= n2; dq++) {
|
|
2918
|
+
const rMin = Math.max(-n2, -dq - n2);
|
|
2919
|
+
const rMax = Math.min(n2, -dq + n2);
|
|
2854
2920
|
for (let dr = rMin; dr <= rMax; dr++) {
|
|
2855
2921
|
if (dq === 0 && dr === 0) continue;
|
|
2856
2922
|
const absQ = cube.q + dq;
|
|
@@ -2874,23 +2940,23 @@ function getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation) {
|
|
|
2874
2940
|
return cells;
|
|
2875
2941
|
}
|
|
2876
2942
|
function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
|
|
2877
|
-
const
|
|
2943
|
+
const n2 = Math.round(radiusCells);
|
|
2878
2944
|
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2879
2945
|
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2880
2946
|
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
2881
|
-
if (
|
|
2947
|
+
if (n2 <= 0) return [centerPixel];
|
|
2882
2948
|
const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
2883
2949
|
const step = Math.PI / 3;
|
|
2884
2950
|
const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
|
|
2885
2951
|
const cos = Math.cos(snappedAngle);
|
|
2886
2952
|
const sin = Math.sin(snappedAngle);
|
|
2887
2953
|
const snapUnit = Math.sqrt(3) * cellSize;
|
|
2888
|
-
const lineLength =
|
|
2954
|
+
const lineLength = n2 * snapUnit;
|
|
2889
2955
|
const halfWidth = snapUnit * 0.5 + 1e-6;
|
|
2890
2956
|
const cells = [];
|
|
2891
|
-
for (let dq = -
|
|
2892
|
-
const rMin = Math.max(-
|
|
2893
|
-
const rMax = Math.min(
|
|
2957
|
+
for (let dq = -n2; dq <= n2; dq++) {
|
|
2958
|
+
const rMin = Math.max(-n2, -dq - n2);
|
|
2959
|
+
const rMax = Math.min(n2, -dq + n2);
|
|
2894
2960
|
for (let dr = rMin; dr <= rMax; dr++) {
|
|
2895
2961
|
const absQ = cube.q + dq;
|
|
2896
2962
|
const absR = cube.r + dr;
|
|
@@ -2912,17 +2978,17 @@ function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
|
|
|
2912
2978
|
return cells;
|
|
2913
2979
|
}
|
|
2914
2980
|
function getHexCellsInSquare(center2, radiusCells, cellSize, orientation) {
|
|
2915
|
-
const
|
|
2981
|
+
const n2 = Math.round(radiusCells);
|
|
2916
2982
|
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2917
2983
|
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2918
2984
|
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
2919
|
-
if (
|
|
2985
|
+
if (n2 <= 0) return [centerPixel];
|
|
2920
2986
|
const snapUnit = Math.sqrt(3) * cellSize;
|
|
2921
|
-
const halfSide =
|
|
2987
|
+
const halfSide = n2 * snapUnit / 2;
|
|
2922
2988
|
const cells = [];
|
|
2923
|
-
for (let dq = -
|
|
2924
|
-
const rMin = Math.max(-
|
|
2925
|
-
const rMax = Math.min(
|
|
2989
|
+
for (let dq = -n2; dq <= n2; dq++) {
|
|
2990
|
+
const rMin = Math.max(-n2, -dq - n2);
|
|
2991
|
+
const rMax = Math.min(n2, -dq + n2);
|
|
2926
2992
|
for (let dr = rMin; dr <= rMax; dr++) {
|
|
2927
2993
|
const absQ = cube.q + dq;
|
|
2928
2994
|
const absR = cube.r + dr;
|
|
@@ -3109,6 +3175,58 @@ function getSquareGridLines(bounds, cellSize) {
|
|
|
3109
3175
|
}
|
|
3110
3176
|
return { verticals, horizontals };
|
|
3111
3177
|
}
|
|
3178
|
+
function getHexVertices(cx, cy, circumradius, orientation) {
|
|
3179
|
+
const vertices = [];
|
|
3180
|
+
const angleOffset = orientation === "pointy" ? -Math.PI / 2 : 0;
|
|
3181
|
+
for (let i = 0; i < 6; i++) {
|
|
3182
|
+
const angle = Math.PI / 3 * i + angleOffset;
|
|
3183
|
+
vertices.push({
|
|
3184
|
+
x: cx + circumradius * Math.cos(angle),
|
|
3185
|
+
y: cy + circumradius * Math.sin(angle)
|
|
3186
|
+
});
|
|
3187
|
+
}
|
|
3188
|
+
return vertices;
|
|
3189
|
+
}
|
|
3190
|
+
function getHexCenters(bounds, circumradius, orientation) {
|
|
3191
|
+
if (circumradius <= 0) return [];
|
|
3192
|
+
const centers = [];
|
|
3193
|
+
if (orientation === "pointy") {
|
|
3194
|
+
const hexW = Math.sqrt(3) * circumradius;
|
|
3195
|
+
const hexH = 2 * circumradius;
|
|
3196
|
+
const rowH = hexH * 0.75;
|
|
3197
|
+
const startRow = Math.floor((bounds.minY - circumradius) / rowH);
|
|
3198
|
+
const endRow = Math.ceil((bounds.maxY + circumradius) / rowH);
|
|
3199
|
+
const startCol = Math.floor((bounds.minX - hexW) / hexW);
|
|
3200
|
+
const endCol = Math.ceil((bounds.maxX + hexW) / hexW);
|
|
3201
|
+
for (let row = startRow; row <= endRow; row++) {
|
|
3202
|
+
const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
|
|
3203
|
+
for (let col = startCol; col <= endCol; col++) {
|
|
3204
|
+
centers.push({
|
|
3205
|
+
x: col * hexW + offsetX,
|
|
3206
|
+
y: row * rowH
|
|
3207
|
+
});
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
} else {
|
|
3211
|
+
const hexW = 2 * circumradius;
|
|
3212
|
+
const hexH = Math.sqrt(3) * circumradius;
|
|
3213
|
+
const colW = hexW * 0.75;
|
|
3214
|
+
const startCol = Math.floor((bounds.minX - circumradius) / colW);
|
|
3215
|
+
const endCol = Math.ceil((bounds.maxX + circumradius) / colW);
|
|
3216
|
+
const startRow = Math.floor((bounds.minY - hexH) / hexH);
|
|
3217
|
+
const endRow = Math.ceil((bounds.maxY + hexH) / hexH);
|
|
3218
|
+
for (let col = startCol; col <= endCol; col++) {
|
|
3219
|
+
const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
|
|
3220
|
+
for (let row = startRow; row <= endRow; row++) {
|
|
3221
|
+
centers.push({
|
|
3222
|
+
x: col * colW,
|
|
3223
|
+
y: row * hexH + offsetY
|
|
3224
|
+
});
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
return centers;
|
|
3229
|
+
}
|
|
3112
3230
|
function renderSquareGrid(ctx, bounds, cellSize, strokeColor, strokeWidth, opacity) {
|
|
3113
3231
|
if (cellSize <= 0) return;
|
|
3114
3232
|
const { verticals, horizontals } = getSquareGridLines(bounds, cellSize);
|
|
@@ -3507,6 +3625,8 @@ function createHtmlElement(input) {
|
|
|
3507
3625
|
};
|
|
3508
3626
|
if (input.domId) el.domId = input.domId;
|
|
3509
3627
|
if (input.interactive) el.interactive = input.interactive;
|
|
3628
|
+
if (input.htmlType) el.htmlType = input.htmlType;
|
|
3629
|
+
if (input.data) el.data = input.data;
|
|
3510
3630
|
return el;
|
|
3511
3631
|
}
|
|
3512
3632
|
function createShape(input) {
|
|
@@ -4787,6 +4907,350 @@ async function exportImage(store, options = {}, layerManager) {
|
|
|
4787
4907
|
});
|
|
4788
4908
|
}
|
|
4789
4909
|
|
|
4910
|
+
// src/canvas/export-svg.ts
|
|
4911
|
+
var ARROWHEAD_LENGTH2 = 12;
|
|
4912
|
+
var ARROWHEAD_ANGLE2 = Math.PI / 6;
|
|
4913
|
+
var ARROW_LABEL_FONT_SIZE2 = 14;
|
|
4914
|
+
function esc(s) {
|
|
4915
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
4916
|
+
}
|
|
4917
|
+
var n = (v) => Number.isFinite(v) ? `${Math.round(v * 1e3) / 1e3}` : "0";
|
|
4918
|
+
function elementCenter(el) {
|
|
4919
|
+
const b = getElementBounds(el);
|
|
4920
|
+
if (!b) return null;
|
|
4921
|
+
return { x: b.x + b.w / 2, y: b.y + b.h / 2 };
|
|
4922
|
+
}
|
|
4923
|
+
function withRotationSvg(el, fragment) {
|
|
4924
|
+
const angle = el.rotation ?? 0;
|
|
4925
|
+
if (!angle || !fragment) return fragment;
|
|
4926
|
+
const c = elementCenter(el);
|
|
4927
|
+
if (!c) return fragment;
|
|
4928
|
+
const deg = angle * 180 / Math.PI;
|
|
4929
|
+
return `<g transform="rotate(${n(deg)} ${n(c.x)} ${n(c.y)})">${fragment}</g>`;
|
|
4930
|
+
}
|
|
4931
|
+
var WIDTH_QUANTUM2 = 0.25;
|
|
4932
|
+
function emitStroke(stroke) {
|
|
4933
|
+
if (stroke.points.length < 2) return "";
|
|
4934
|
+
const data = getStrokeRenderData(stroke);
|
|
4935
|
+
const { x: ox, y: oy } = stroke.position;
|
|
4936
|
+
const byWidth = /* @__PURE__ */ new Map();
|
|
4937
|
+
for (let i = 0; i < data.segments.length; i++) {
|
|
4938
|
+
const seg = data.segments[i];
|
|
4939
|
+
const w = data.widths[i];
|
|
4940
|
+
if (!seg || w === void 0) continue;
|
|
4941
|
+
const q = Math.max(WIDTH_QUANTUM2, Math.round(w / WIDTH_QUANTUM2) * WIDTH_QUANTUM2);
|
|
4942
|
+
let parts = byWidth.get(q);
|
|
4943
|
+
if (!parts) {
|
|
4944
|
+
parts = [];
|
|
4945
|
+
byWidth.set(q, parts);
|
|
4946
|
+
}
|
|
4947
|
+
parts.push(
|
|
4948
|
+
`M${n(ox + seg.start.x)} ${n(oy + seg.start.y)} C${n(ox + seg.cp1.x)} ${n(oy + seg.cp1.y)} ${n(ox + seg.cp2.x)} ${n(oy + seg.cp2.y)} ${n(ox + seg.end.x)} ${n(oy + seg.end.y)}`
|
|
4949
|
+
);
|
|
4950
|
+
}
|
|
4951
|
+
const blend = stroke.blendMode === "multiply" ? ' style="mix-blend-mode:multiply"' : "";
|
|
4952
|
+
let out = "";
|
|
4953
|
+
for (const [width, parts] of byWidth) {
|
|
4954
|
+
out += `<path d="${parts.join(" ")}" fill="none" stroke="${esc(stroke.color)}" stroke-width="${n(width)}" stroke-linecap="round" stroke-linejoin="round" opacity="${n(stroke.opacity)}"${blend} />`;
|
|
4955
|
+
}
|
|
4956
|
+
return out;
|
|
4957
|
+
}
|
|
4958
|
+
function emitShape(shape) {
|
|
4959
|
+
const { x, y } = shape.position;
|
|
4960
|
+
const { w, h } = shape.size;
|
|
4961
|
+
const fill = shape.fillColor !== "none" && shape.shape !== "line" ? esc(shape.fillColor) : "none";
|
|
4962
|
+
const stroke = shape.strokeWidth > 0 ? esc(shape.strokeColor) : "none";
|
|
4963
|
+
const sw = shape.strokeWidth > 0 ? ` stroke-width="${n(shape.strokeWidth)}"` : "";
|
|
4964
|
+
switch (shape.shape) {
|
|
4965
|
+
case "rectangle":
|
|
4966
|
+
return `<rect x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" fill="${fill}" stroke="${stroke}"${sw} />`;
|
|
4967
|
+
case "ellipse":
|
|
4968
|
+
return `<ellipse cx="${n(x + w / 2)}" cy="${n(y + h / 2)}" rx="${n(w / 2)}" ry="${n(h / 2)}" fill="${fill}" stroke="${stroke}"${sw} />`;
|
|
4969
|
+
case "line": {
|
|
4970
|
+
const [a, b] = lineEndpoints(shape);
|
|
4971
|
+
return `<line x1="${n(a.x)}" y1="${n(a.y)}" x2="${n(b.x)}" y2="${n(b.y)}" stroke="${stroke}"${sw} stroke-linecap="round" />`;
|
|
4972
|
+
}
|
|
4973
|
+
}
|
|
4974
|
+
}
|
|
4975
|
+
function emitArrow(arrow, store) {
|
|
4976
|
+
const geometry = getArrowRenderGeometry(arrow);
|
|
4977
|
+
const { visualFrom: from, visualTo: to } = getVisualEndpoints(arrow, geometry, store);
|
|
4978
|
+
let d;
|
|
4979
|
+
if (arrow.bend !== 0) {
|
|
4980
|
+
const cp = geometry.controlPoint ?? getArrowControlPoint(from, to, arrow.bend);
|
|
4981
|
+
d = `M${n(from.x)} ${n(from.y)} Q${n(cp.x)} ${n(cp.y)} ${n(to.x)} ${n(to.y)}`;
|
|
4982
|
+
} else {
|
|
4983
|
+
d = `M${n(from.x)} ${n(from.y)} L${n(to.x)} ${n(to.y)}`;
|
|
4984
|
+
}
|
|
4985
|
+
const dash = arrow.fromBinding || arrow.toBinding ? ' stroke-dasharray="8 4"' : "";
|
|
4986
|
+
let out = `<path d="${d}" fill="none" stroke="${esc(arrow.color)}" stroke-width="${n(arrow.width)}" stroke-linecap="round"${dash} />`;
|
|
4987
|
+
const angle = geometry.tangentEnd;
|
|
4988
|
+
const p1x = to.x - ARROWHEAD_LENGTH2 * Math.cos(angle - ARROWHEAD_ANGLE2);
|
|
4989
|
+
const p1y = to.y - ARROWHEAD_LENGTH2 * Math.sin(angle - ARROWHEAD_ANGLE2);
|
|
4990
|
+
const p2x = to.x - ARROWHEAD_LENGTH2 * Math.cos(angle + ARROWHEAD_ANGLE2);
|
|
4991
|
+
const p2y = to.y - ARROWHEAD_LENGTH2 * Math.sin(angle + ARROWHEAD_ANGLE2);
|
|
4992
|
+
out += `<polygon points="${n(to.x)},${n(to.y)} ${n(p1x)},${n(p1y)} ${n(p2x)},${n(p2y)}" fill="${esc(arrow.color)}" />`;
|
|
4993
|
+
if (arrow.label && arrow.label.length > 0) {
|
|
4994
|
+
const mid = getArrowMidpoint(arrow.from, arrow.to, arrow.bend);
|
|
4995
|
+
const approxW = arrow.label.length * ARROW_LABEL_FONT_SIZE2 * 0.6;
|
|
4996
|
+
const padX = 6;
|
|
4997
|
+
const padY = 4;
|
|
4998
|
+
const lw = approxW + padX * 2;
|
|
4999
|
+
const lh = ARROW_LABEL_FONT_SIZE2 + padY * 2;
|
|
5000
|
+
out += `<rect x="${n(mid.x - lw / 2)}" y="${n(mid.y - lh / 2)}" width="${n(lw)}" height="${n(lh)}" rx="4" fill="rgba(255,255,255,0.9)" />`;
|
|
5001
|
+
out += `<text x="${n(mid.x)}" y="${n(mid.y)}" font-family="system-ui, sans-serif" font-size="${ARROW_LABEL_FONT_SIZE2}" fill="#1a1a1a" text-anchor="middle" dominant-baseline="central">${esc(arrow.label)}</text>`;
|
|
5002
|
+
}
|
|
5003
|
+
return out;
|
|
5004
|
+
}
|
|
5005
|
+
function emitImage(image, dataUri) {
|
|
5006
|
+
const href = dataUri ?? image.src;
|
|
5007
|
+
if (!href) return "";
|
|
5008
|
+
const { x, y } = image.position;
|
|
5009
|
+
const { w, h } = image.size;
|
|
5010
|
+
return `<image href="${esc(href)}" x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" />`;
|
|
5011
|
+
}
|
|
5012
|
+
function emitText(text) {
|
|
5013
|
+
if (!text.text) return "";
|
|
5014
|
+
const pad = 2;
|
|
5015
|
+
let anchor = "start";
|
|
5016
|
+
let textX = text.position.x + pad;
|
|
5017
|
+
if (text.textAlign === "center") {
|
|
5018
|
+
anchor = "middle";
|
|
5019
|
+
textX = text.position.x + text.size.w / 2;
|
|
5020
|
+
} else if (text.textAlign === "right") {
|
|
5021
|
+
anchor = "end";
|
|
5022
|
+
textX = text.position.x + text.size.w - pad;
|
|
5023
|
+
}
|
|
5024
|
+
const lineHeight = text.fontSize * 1.4;
|
|
5025
|
+
const lines = text.text.split("\n");
|
|
5026
|
+
let out = "";
|
|
5027
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5028
|
+
const line = lines[i];
|
|
5029
|
+
if (line === void 0) continue;
|
|
5030
|
+
const y = text.position.y + pad + i * lineHeight;
|
|
5031
|
+
out += `<text x="${n(textX)}" y="${n(y)}" font-family="system-ui, sans-serif" font-size="${n(text.fontSize)}" fill="${esc(text.color)}" text-anchor="${anchor}" dominant-baseline="text-before-edge">${esc(line)}</text>`;
|
|
5032
|
+
}
|
|
5033
|
+
return out;
|
|
5034
|
+
}
|
|
5035
|
+
function emitNote(note, rasterScale) {
|
|
5036
|
+
const { x, y } = note.position;
|
|
5037
|
+
const { w, h } = note.size;
|
|
5038
|
+
if (typeof document === "undefined") return emitNotePlaceholder(note);
|
|
5039
|
+
const canvas = document.createElement("canvas");
|
|
5040
|
+
canvas.width = Math.max(1, Math.ceil(w * rasterScale));
|
|
5041
|
+
canvas.height = Math.max(1, Math.ceil(h * rasterScale));
|
|
5042
|
+
const ctx = canvas.getContext("2d");
|
|
5043
|
+
if (!ctx) return emitNotePlaceholder(note);
|
|
5044
|
+
ctx.scale(rasterScale, rasterScale);
|
|
5045
|
+
ctx.translate(-x, -y);
|
|
5046
|
+
renderNoteOnCanvas(ctx, note);
|
|
5047
|
+
let dataUri;
|
|
5048
|
+
try {
|
|
5049
|
+
dataUri = canvas.toDataURL();
|
|
5050
|
+
} catch {
|
|
5051
|
+
return emitNotePlaceholder(note);
|
|
5052
|
+
}
|
|
5053
|
+
if (!dataUri || !dataUri.startsWith("data:")) return emitNotePlaceholder(note);
|
|
5054
|
+
return `<image href="${esc(dataUri)}" x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" />`;
|
|
5055
|
+
}
|
|
5056
|
+
function emitNotePlaceholder(note) {
|
|
5057
|
+
const { x, y } = note.position;
|
|
5058
|
+
const { w, h } = note.size;
|
|
5059
|
+
return `<rect x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" rx="4" fill="${esc(note.backgroundColor)}" />`;
|
|
5060
|
+
}
|
|
5061
|
+
function emitGrid(grid, bounds) {
|
|
5062
|
+
if (grid.cellSize <= 0) return "";
|
|
5063
|
+
const vb = {
|
|
5064
|
+
minX: bounds.x,
|
|
5065
|
+
minY: bounds.y,
|
|
5066
|
+
maxX: bounds.x + bounds.w,
|
|
5067
|
+
maxY: bounds.y + bounds.h
|
|
5068
|
+
};
|
|
5069
|
+
const stroke = esc(grid.strokeColor);
|
|
5070
|
+
const sw = n(grid.strokeWidth);
|
|
5071
|
+
const op = n(grid.opacity);
|
|
5072
|
+
if (grid.gridType === "hex") {
|
|
5073
|
+
const centers = getHexCenters(vb, grid.cellSize, grid.hexOrientation);
|
|
5074
|
+
let d2 = "";
|
|
5075
|
+
for (const c of centers) {
|
|
5076
|
+
const verts = getHexVertices(c.x, c.y, grid.cellSize, grid.hexOrientation);
|
|
5077
|
+
const first = verts[0];
|
|
5078
|
+
if (!first) continue;
|
|
5079
|
+
d2 += `M${n(first.x)} ${n(first.y)}`;
|
|
5080
|
+
for (let i = 1; i < verts.length; i++) {
|
|
5081
|
+
const v = verts[i];
|
|
5082
|
+
if (v) d2 += `L${n(v.x)} ${n(v.y)}`;
|
|
5083
|
+
}
|
|
5084
|
+
d2 += "Z";
|
|
5085
|
+
}
|
|
5086
|
+
return `<path d="${d2}" fill="none" stroke="${stroke}" stroke-width="${sw}" opacity="${op}" />`;
|
|
5087
|
+
}
|
|
5088
|
+
const { verticals, horizontals } = getSquareGridLines(vb, grid.cellSize);
|
|
5089
|
+
let d = "";
|
|
5090
|
+
for (const gx of verticals) d += `M${n(gx)} ${n(vb.minY)}L${n(gx)} ${n(vb.maxY)}`;
|
|
5091
|
+
for (const gy of horizontals) d += `M${n(vb.minX)} ${n(gy)}L${n(vb.maxX)} ${n(gy)}`;
|
|
5092
|
+
return `<path d="${d}" fill="none" stroke="${stroke}" stroke-width="${sw}" opacity="${op}" />`;
|
|
5093
|
+
}
|
|
5094
|
+
function emitTemplate(template, grid) {
|
|
5095
|
+
if (grid && grid.gridType === "hex") {
|
|
5096
|
+
return emitHexTemplate(template, grid);
|
|
5097
|
+
}
|
|
5098
|
+
return emitGeometricTemplate(template);
|
|
5099
|
+
}
|
|
5100
|
+
function emitGeometricTemplate(t) {
|
|
5101
|
+
const { x: cx, y: cy } = t.position;
|
|
5102
|
+
const r = t.radius;
|
|
5103
|
+
const fill = esc(t.fillColor);
|
|
5104
|
+
const stroke = esc(t.strokeColor);
|
|
5105
|
+
const sw = n(t.strokeWidth);
|
|
5106
|
+
const op = n(t.opacity);
|
|
5107
|
+
const attrs = `fill="${fill}" stroke="${stroke}" stroke-width="${sw}" opacity="${op}"`;
|
|
5108
|
+
switch (t.templateShape) {
|
|
5109
|
+
case "circle":
|
|
5110
|
+
return `<circle cx="${n(cx)}" cy="${n(cy)}" r="${n(r)}" ${attrs} />`;
|
|
5111
|
+
case "square":
|
|
5112
|
+
return `<rect x="${n(cx - r / 2)}" y="${n(cy - r / 2)}" width="${n(r)}" height="${n(r)}" ${attrs} />`;
|
|
5113
|
+
case "cone": {
|
|
5114
|
+
const halfAngle = Math.atan(0.5);
|
|
5115
|
+
const a0 = t.angle - halfAngle;
|
|
5116
|
+
const a1 = t.angle + halfAngle;
|
|
5117
|
+
const p0x = cx + r * Math.cos(a0);
|
|
5118
|
+
const p0y = cy + r * Math.sin(a0);
|
|
5119
|
+
const p1x = cx + r * Math.cos(a1);
|
|
5120
|
+
const p1y = cy + r * Math.sin(a1);
|
|
5121
|
+
const large = a1 - a0 > Math.PI ? 1 : 0;
|
|
5122
|
+
return `<path d="M${n(cx)} ${n(cy)} L${n(p0x)} ${n(p0y)} A${n(r)} ${n(r)} 0 ${large} 1 ${n(p1x)} ${n(p1y)} Z" ${attrs} />`;
|
|
5123
|
+
}
|
|
5124
|
+
case "line": {
|
|
5125
|
+
const halfW = r / 12;
|
|
5126
|
+
const cos = Math.cos(t.angle);
|
|
5127
|
+
const sin = Math.sin(t.angle);
|
|
5128
|
+
const perpX = -sin * halfW;
|
|
5129
|
+
const perpY = cos * halfW;
|
|
5130
|
+
const pts = [
|
|
5131
|
+
[cx + perpX, cy + perpY],
|
|
5132
|
+
[cx + r * cos + perpX, cy + r * sin + perpY],
|
|
5133
|
+
[cx + r * cos - perpX, cy + r * sin - perpY],
|
|
5134
|
+
[cx - perpX, cy - perpY]
|
|
5135
|
+
].map(([px, py]) => `${n(px ?? 0)},${n(py ?? 0)}`).join(" ");
|
|
5136
|
+
return `<polygon points="${pts}" ${attrs} />`;
|
|
5137
|
+
}
|
|
5138
|
+
}
|
|
5139
|
+
}
|
|
5140
|
+
function emitHexTemplate(t, grid) {
|
|
5141
|
+
const cellSize = grid.cellSize;
|
|
5142
|
+
const orientation = grid.hexOrientation;
|
|
5143
|
+
const snapUnit = Math.sqrt(3) * cellSize;
|
|
5144
|
+
const radiusCells = t.radius / snapUnit;
|
|
5145
|
+
const center2 = t.position;
|
|
5146
|
+
let cells;
|
|
5147
|
+
switch (t.templateShape) {
|
|
5148
|
+
case "circle":
|
|
5149
|
+
cells = getHexCellsInRadius(center2, radiusCells, cellSize, orientation);
|
|
5150
|
+
break;
|
|
5151
|
+
case "cone":
|
|
5152
|
+
cells = getHexCellsInCone(center2, t.angle, radiusCells, cellSize, orientation);
|
|
5153
|
+
break;
|
|
5154
|
+
case "line":
|
|
5155
|
+
cells = getHexCellsInLine(center2, t.angle, radiusCells, cellSize, orientation);
|
|
5156
|
+
break;
|
|
5157
|
+
case "square":
|
|
5158
|
+
cells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
|
|
5159
|
+
break;
|
|
5160
|
+
}
|
|
5161
|
+
let d = "";
|
|
5162
|
+
for (const cell of cells) {
|
|
5163
|
+
const verts = getHexVertices(cell.x, cell.y, cellSize, orientation);
|
|
5164
|
+
const first = verts[0];
|
|
5165
|
+
if (!first) continue;
|
|
5166
|
+
d += `M${n(first.x)} ${n(first.y)}`;
|
|
5167
|
+
for (let i = 1; i < verts.length; i++) {
|
|
5168
|
+
const v = verts[i];
|
|
5169
|
+
if (v) d += `L${n(v.x)} ${n(v.y)}`;
|
|
5170
|
+
}
|
|
5171
|
+
d += "Z";
|
|
5172
|
+
}
|
|
5173
|
+
return `<path d="${d}" fill="${esc(t.fillColor)}" stroke="${esc(t.strokeColor)}" stroke-width="${n(t.strokeWidth)}" opacity="${n(t.opacity)}" />`;
|
|
5174
|
+
}
|
|
5175
|
+
async function exportSvg(store, options = {}, layerManager) {
|
|
5176
|
+
const padding = options.padding ?? 0;
|
|
5177
|
+
const rasterScale = options.rasterScale ?? 2;
|
|
5178
|
+
const filter = options.filter;
|
|
5179
|
+
const allElements = store.getAll();
|
|
5180
|
+
let visibleElements = layerManager ? allElements.filter((el) => layerManager.isLayerVisible(el.layerId)) : allElements;
|
|
5181
|
+
if (filter) visibleElements = visibleElements.filter(filter);
|
|
5182
|
+
const bounds = computeBounds(visibleElements, padding);
|
|
5183
|
+
if (!bounds) {
|
|
5184
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
|
|
5185
|
+
}
|
|
5186
|
+
const remoteImages = visibleElements.filter(
|
|
5187
|
+
(el) => el.type === "image" && !el.src.startsWith("data:")
|
|
5188
|
+
);
|
|
5189
|
+
const imageCache = await loadImages(remoteImages);
|
|
5190
|
+
const imageDataUris = encodeImages(visibleElements, imageCache, rasterScale);
|
|
5191
|
+
const grids = visibleElements.filter((el) => el.type === "grid");
|
|
5192
|
+
const firstGrid = grids[0];
|
|
5193
|
+
let body = "";
|
|
5194
|
+
if (options.background) {
|
|
5195
|
+
body += `<rect x="${n(bounds.x)}" y="${n(bounds.y)}" width="${n(bounds.w)}" height="${n(bounds.h)}" fill="${esc(options.background)}" />`;
|
|
5196
|
+
}
|
|
5197
|
+
for (const el of visibleElements) {
|
|
5198
|
+
body += emitElement(el, imageDataUris, rasterScale, firstGrid, store);
|
|
5199
|
+
}
|
|
5200
|
+
for (const grid of grids) {
|
|
5201
|
+
body += emitGrid(grid, bounds);
|
|
5202
|
+
}
|
|
5203
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${n(bounds.w)}" height="${n(bounds.h)}" viewBox="${n(bounds.x)} ${n(bounds.y)} ${n(bounds.w)} ${n(bounds.h)}">${body}</svg>`;
|
|
5204
|
+
}
|
|
5205
|
+
function emitElement(el, imageDataUris, rasterScale, firstGrid, store) {
|
|
5206
|
+
switch (el.type) {
|
|
5207
|
+
case "stroke":
|
|
5208
|
+
return withRotationSvg(el, emitStroke(el));
|
|
5209
|
+
case "shape":
|
|
5210
|
+
return withRotationSvg(el, emitShape(el));
|
|
5211
|
+
case "arrow":
|
|
5212
|
+
return emitArrow(el, store);
|
|
5213
|
+
case "image":
|
|
5214
|
+
return withRotationSvg(el, emitImage(el, imageDataUris.get(el.id)));
|
|
5215
|
+
case "text":
|
|
5216
|
+
return withRotationSvg(el, emitText(el));
|
|
5217
|
+
case "note":
|
|
5218
|
+
return withRotationSvg(el, emitNote(el, rasterScale));
|
|
5219
|
+
case "template":
|
|
5220
|
+
return emitTemplate(el, firstGrid);
|
|
5221
|
+
case "grid":
|
|
5222
|
+
return "";
|
|
5223
|
+
case "html":
|
|
5224
|
+
return "";
|
|
5225
|
+
default:
|
|
5226
|
+
return "";
|
|
5227
|
+
}
|
|
5228
|
+
}
|
|
5229
|
+
function encodeImages(elements, imageCache, rasterScale) {
|
|
5230
|
+
const out = /* @__PURE__ */ new Map();
|
|
5231
|
+
for (const el of elements) {
|
|
5232
|
+
if (el.type !== "image") continue;
|
|
5233
|
+
if (el.src.startsWith("data:")) {
|
|
5234
|
+
out.set(el.id, el.src);
|
|
5235
|
+
continue;
|
|
5236
|
+
}
|
|
5237
|
+
const img = imageCache.get(el.id);
|
|
5238
|
+
if (!img || typeof document === "undefined") continue;
|
|
5239
|
+
const canvas = document.createElement("canvas");
|
|
5240
|
+
canvas.width = Math.max(1, Math.ceil(el.size.w * rasterScale));
|
|
5241
|
+
canvas.height = Math.max(1, Math.ceil(el.size.h * rasterScale));
|
|
5242
|
+
const ctx = canvas.getContext("2d");
|
|
5243
|
+
if (!ctx) continue;
|
|
5244
|
+
try {
|
|
5245
|
+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
5246
|
+
const uri = canvas.toDataURL();
|
|
5247
|
+
if (uri.startsWith("data:")) out.set(el.id, uri);
|
|
5248
|
+
} catch {
|
|
5249
|
+
}
|
|
5250
|
+
}
|
|
5251
|
+
return out;
|
|
5252
|
+
}
|
|
5253
|
+
|
|
4790
5254
|
// src/layers/layer-manager.ts
|
|
4791
5255
|
var LayerManager = class {
|
|
4792
5256
|
constructor(store) {
|
|
@@ -5947,13 +6411,13 @@ var SelectionOps = class {
|
|
|
5947
6411
|
if (!first || !last) return;
|
|
5948
6412
|
const c0 = center2(first.bounds);
|
|
5949
6413
|
const cN = center2(last.bounds);
|
|
5950
|
-
const
|
|
6414
|
+
const n2 = sorted.length;
|
|
5951
6415
|
this.deps.recorder.begin();
|
|
5952
6416
|
const moved = [];
|
|
5953
|
-
for (let i = 1; i <
|
|
6417
|
+
for (let i = 1; i < n2 - 1; i++) {
|
|
5954
6418
|
const item = sorted[i];
|
|
5955
6419
|
if (!item || !this.isMovable(item.el)) continue;
|
|
5956
|
-
const target = c0 + i * (cN - c0) / (
|
|
6420
|
+
const target = c0 + i * (cN - c0) / (n2 - 1);
|
|
5957
6421
|
const delta = target - center2(item.bounds);
|
|
5958
6422
|
if (delta === 0) continue;
|
|
5959
6423
|
const [dx, dy] = axis === "horizontal" ? [delta, 0] : [0, delta];
|
|
@@ -6291,7 +6755,10 @@ var Viewport = class {
|
|
|
6291
6755
|
this.getSelectTool()?.selectAtPoint(world, this.toolContext);
|
|
6292
6756
|
this.openContextMenu(screenPos);
|
|
6293
6757
|
},
|
|
6294
|
-
shortcuts: options.shortcuts
|
|
6758
|
+
shortcuts: options.shortcuts,
|
|
6759
|
+
addImage: (src, world) => this.addImage(src, world),
|
|
6760
|
+
getCenteredWorld: () => this.centeredPosition({ w: 300, h: 200 }),
|
|
6761
|
+
onPaste: options.onPaste
|
|
6295
6762
|
});
|
|
6296
6763
|
if (options.contextMenu !== false) {
|
|
6297
6764
|
this.contextMenu = new ContextMenu({
|
|
@@ -6431,6 +6898,7 @@ var Viewport = class {
|
|
|
6431
6898
|
gridController;
|
|
6432
6899
|
interactions;
|
|
6433
6900
|
contextMenu = null;
|
|
6901
|
+
htmlRenderers = /* @__PURE__ */ new Map();
|
|
6434
6902
|
get ctx() {
|
|
6435
6903
|
return this.canvasEl.getContext("2d");
|
|
6436
6904
|
}
|
|
@@ -6472,6 +6940,9 @@ var Viewport = class {
|
|
|
6472
6940
|
async exportImage(options) {
|
|
6473
6941
|
return exportImage(this.store, options, this.layerManager);
|
|
6474
6942
|
}
|
|
6943
|
+
async exportSVG(options) {
|
|
6944
|
+
return exportSvg(this.store, options, this.layerManager);
|
|
6945
|
+
}
|
|
6475
6946
|
loadState(state) {
|
|
6476
6947
|
this.inputHandler.flushPendingHistory();
|
|
6477
6948
|
this.historyRecorder.pause();
|
|
@@ -6485,19 +6956,24 @@ var Viewport = class {
|
|
|
6485
6956
|
this.layerManager.setActiveLayer(state.activeLayerId);
|
|
6486
6957
|
}
|
|
6487
6958
|
this.domNodeManager.reattachHtmlContent(this.store);
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6959
|
+
for (const el of this.store.getElementsByType("html")) {
|
|
6960
|
+
if (this.domNodeManager.hasContent(el.id)) continue;
|
|
6961
|
+
const factory = el.htmlType ? this.htmlRenderers.get(el.htmlType) : void 0;
|
|
6962
|
+
const rebuilt = factory ? factory(el) : null;
|
|
6963
|
+
if (rebuilt) {
|
|
6964
|
+
this.domNodeManager.storeHtmlContent(el.id, rebuilt);
|
|
6965
|
+
this.domNodeManager.syncDomNode(el);
|
|
6966
|
+
}
|
|
6967
|
+
if (this.onHtmlElementMount) {
|
|
6968
|
+
if (!rebuilt) this.domNodeManager.syncDomNode(el);
|
|
6969
|
+
const node = this.domNodeManager.getNode(el.id);
|
|
6970
|
+
if (node) {
|
|
6971
|
+
this.onHtmlElementMount(el.id, el.domId, node);
|
|
6972
|
+
node.dataset["initialized"] = "true";
|
|
6973
|
+
Object.assign(node.style, {
|
|
6974
|
+
overflow: "hidden",
|
|
6975
|
+
pointerEvents: el.interactive ? "auto" : "none"
|
|
6976
|
+
});
|
|
6501
6977
|
}
|
|
6502
6978
|
}
|
|
6503
6979
|
}
|
|
@@ -6543,12 +7019,14 @@ var Viewport = class {
|
|
|
6543
7019
|
this.requestRender();
|
|
6544
7020
|
return image.id;
|
|
6545
7021
|
}
|
|
6546
|
-
addHtmlElement(dom, position, size = { w: 200, h: 150 }) {
|
|
7022
|
+
addHtmlElement(dom, position, size = { w: 200, h: 150 }, opts) {
|
|
6547
7023
|
const domId = dom.id || void 0;
|
|
6548
7024
|
const el = createHtmlElement({
|
|
6549
7025
|
position,
|
|
6550
7026
|
size,
|
|
6551
7027
|
domId,
|
|
7028
|
+
htmlType: opts?.htmlType,
|
|
7029
|
+
data: opts?.data,
|
|
6552
7030
|
layerId: this.layerManager.activeLayerId
|
|
6553
7031
|
});
|
|
6554
7032
|
this.domNodeManager.storeHtmlContent(el.id, dom);
|
|
@@ -6589,6 +7067,9 @@ var Viewport = class {
|
|
|
6589
7067
|
this.layerManager.removeLayer(id);
|
|
6590
7068
|
this.historyRecorder.commit();
|
|
6591
7069
|
}
|
|
7070
|
+
registerHtmlRenderer(htmlType, factory) {
|
|
7071
|
+
this.htmlRenderers.set(htmlType, factory);
|
|
7072
|
+
}
|
|
6592
7073
|
updateHtmlElement(id, newContent) {
|
|
6593
7074
|
const el = this.store.getById(id);
|
|
6594
7075
|
if (!el) throw new Error(`Element not found: ${id}`);
|
|
@@ -7241,6 +7722,17 @@ function renderArrowHandles(canvasCtx, arrow, zoom) {
|
|
|
7241
7722
|
canvasCtx.stroke();
|
|
7242
7723
|
}
|
|
7243
7724
|
}
|
|
7725
|
+
function renderArrowHoverHandle(canvasCtx, arrow, zoom) {
|
|
7726
|
+
const mid = getArrowMidpoint(arrow.from, arrow.to, arrow.bend);
|
|
7727
|
+
const radius = HANDLE_RADIUS / zoom;
|
|
7728
|
+
canvasCtx.fillStyle = "#2196F3";
|
|
7729
|
+
canvasCtx.strokeStyle = "#2196F3";
|
|
7730
|
+
canvasCtx.lineWidth = 1.5 / zoom;
|
|
7731
|
+
canvasCtx.beginPath();
|
|
7732
|
+
canvasCtx.arc(mid.x, mid.y, radius, 0, Math.PI * 2);
|
|
7733
|
+
canvasCtx.fill();
|
|
7734
|
+
canvasCtx.stroke();
|
|
7735
|
+
}
|
|
7244
7736
|
|
|
7245
7737
|
// src/elements/snap-guides.ts
|
|
7246
7738
|
function xAnchors(b) {
|
|
@@ -8089,7 +8581,13 @@ var SelectTool = class {
|
|
|
8089
8581
|
if (this.hoveredId && this.ctx && this.mode.type === "idle") {
|
|
8090
8582
|
if (!this._selectedIds.includes(this.hoveredId)) {
|
|
8091
8583
|
const el = this.ctx.store.getById(this.hoveredId);
|
|
8092
|
-
if (el) {
|
|
8584
|
+
if (el?.type === "arrow") {
|
|
8585
|
+
canvasCtx.save();
|
|
8586
|
+
canvasCtx.globalAlpha = 0.35;
|
|
8587
|
+
canvasCtx.setLineDash([]);
|
|
8588
|
+
renderArrowHoverHandle(canvasCtx, el, this.ctx.camera.zoom);
|
|
8589
|
+
canvasCtx.restore();
|
|
8590
|
+
} else if (el) {
|
|
8093
8591
|
const b = getElementBounds(el);
|
|
8094
8592
|
if (b) {
|
|
8095
8593
|
canvasCtx.save();
|
|
@@ -9060,8 +9558,118 @@ var TemplateTool = class {
|
|
|
9060
9558
|
}
|
|
9061
9559
|
};
|
|
9062
9560
|
|
|
9561
|
+
// src/tools/laser-tool.ts
|
|
9562
|
+
var DEFAULT_COLOR = "#ff3b30";
|
|
9563
|
+
var DEFAULT_WIDTH = 4;
|
|
9564
|
+
var DEFAULT_FADE_MS = 1200;
|
|
9565
|
+
var LaserTool = class {
|
|
9566
|
+
name;
|
|
9567
|
+
color;
|
|
9568
|
+
width;
|
|
9569
|
+
fadeMs;
|
|
9570
|
+
trail = [];
|
|
9571
|
+
rafId = null;
|
|
9572
|
+
drawing = false;
|
|
9573
|
+
optionListeners = /* @__PURE__ */ new Set();
|
|
9574
|
+
constructor(options = {}) {
|
|
9575
|
+
this.name = options.name ?? "laser";
|
|
9576
|
+
this.color = options.color ?? DEFAULT_COLOR;
|
|
9577
|
+
this.width = options.width ?? DEFAULT_WIDTH;
|
|
9578
|
+
this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS;
|
|
9579
|
+
}
|
|
9580
|
+
now() {
|
|
9581
|
+
return performance.now();
|
|
9582
|
+
}
|
|
9583
|
+
onActivate(ctx) {
|
|
9584
|
+
ctx.setCursor?.("crosshair");
|
|
9585
|
+
}
|
|
9586
|
+
onDeactivate(ctx) {
|
|
9587
|
+
if (this.rafId !== null) {
|
|
9588
|
+
cancelAnimationFrame(this.rafId);
|
|
9589
|
+
this.rafId = null;
|
|
9590
|
+
}
|
|
9591
|
+
this.trail = [];
|
|
9592
|
+
this.drawing = false;
|
|
9593
|
+
ctx.setCursor?.("default");
|
|
9594
|
+
ctx.requestRender();
|
|
9595
|
+
}
|
|
9596
|
+
getOptions() {
|
|
9597
|
+
return {
|
|
9598
|
+
name: this.name,
|
|
9599
|
+
color: this.color,
|
|
9600
|
+
width: this.width,
|
|
9601
|
+
fadeMs: this.fadeMs
|
|
9602
|
+
};
|
|
9603
|
+
}
|
|
9604
|
+
onOptionsChange(listener) {
|
|
9605
|
+
this.optionListeners.add(listener);
|
|
9606
|
+
return () => this.optionListeners.delete(listener);
|
|
9607
|
+
}
|
|
9608
|
+
setOptions(options) {
|
|
9609
|
+
if (options.color !== void 0) this.color = options.color;
|
|
9610
|
+
if (options.width !== void 0) this.width = options.width;
|
|
9611
|
+
if (options.fadeMs !== void 0) this.fadeMs = options.fadeMs;
|
|
9612
|
+
this.notifyOptionsChange();
|
|
9613
|
+
}
|
|
9614
|
+
onPointerDown(state, ctx) {
|
|
9615
|
+
this.drawing = true;
|
|
9616
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
9617
|
+
this.trail.push({ x: world.x, y: world.y, t: this.now() });
|
|
9618
|
+
this.ensureAnimating(ctx);
|
|
9619
|
+
}
|
|
9620
|
+
onPointerMove(state, ctx) {
|
|
9621
|
+
if (!this.drawing) return;
|
|
9622
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
9623
|
+
this.trail.push({ x: world.x, y: world.y, t: this.now() });
|
|
9624
|
+
this.ensureAnimating(ctx);
|
|
9625
|
+
}
|
|
9626
|
+
onPointerUp(_state, _ctx) {
|
|
9627
|
+
this.drawing = false;
|
|
9628
|
+
}
|
|
9629
|
+
renderOverlay(ctx) {
|
|
9630
|
+
if (this.trail.length < 2) return;
|
|
9631
|
+
ctx.save();
|
|
9632
|
+
ctx.strokeStyle = this.color;
|
|
9633
|
+
ctx.lineWidth = this.width;
|
|
9634
|
+
ctx.lineCap = "round";
|
|
9635
|
+
ctx.lineJoin = "round";
|
|
9636
|
+
const now = this.now();
|
|
9637
|
+
for (let i = 0; i < this.trail.length - 1; i++) {
|
|
9638
|
+
const a = this.trail[i];
|
|
9639
|
+
const b = this.trail[i + 1];
|
|
9640
|
+
if (!a || !b) continue;
|
|
9641
|
+
const age = now - b.t;
|
|
9642
|
+
ctx.globalAlpha = Math.max(0, 1 - age / this.fadeMs);
|
|
9643
|
+
ctx.beginPath();
|
|
9644
|
+
ctx.moveTo(a.x, a.y);
|
|
9645
|
+
ctx.lineTo(b.x, b.y);
|
|
9646
|
+
ctx.stroke();
|
|
9647
|
+
}
|
|
9648
|
+
ctx.restore();
|
|
9649
|
+
}
|
|
9650
|
+
ensureAnimating(ctx) {
|
|
9651
|
+
if (this.rafId === null) {
|
|
9652
|
+
this.rafId = requestAnimationFrame(() => this.tick(ctx));
|
|
9653
|
+
}
|
|
9654
|
+
}
|
|
9655
|
+
tick(ctx) {
|
|
9656
|
+
const cutoff = this.now() - this.fadeMs;
|
|
9657
|
+
this.trail = this.trail.filter((p) => p.t >= cutoff);
|
|
9658
|
+
if (this.trail.length > 0) {
|
|
9659
|
+
ctx.requestRender();
|
|
9660
|
+
this.rafId = requestAnimationFrame(() => this.tick(ctx));
|
|
9661
|
+
} else {
|
|
9662
|
+
ctx.requestRender();
|
|
9663
|
+
this.rafId = null;
|
|
9664
|
+
}
|
|
9665
|
+
}
|
|
9666
|
+
notifyOptionsChange() {
|
|
9667
|
+
for (const listener of this.optionListeners) listener();
|
|
9668
|
+
}
|
|
9669
|
+
};
|
|
9670
|
+
|
|
9063
9671
|
// src/index.ts
|
|
9064
|
-
var VERSION = "0.
|
|
9672
|
+
var VERSION = "0.40.0";
|
|
9065
9673
|
export {
|
|
9066
9674
|
ArrowTool,
|
|
9067
9675
|
AutoSave,
|
|
@@ -9072,6 +9680,7 @@ export {
|
|
|
9072
9680
|
HandTool,
|
|
9073
9681
|
HistoryStack,
|
|
9074
9682
|
ImageTool,
|
|
9683
|
+
LaserTool,
|
|
9075
9684
|
LayerManager,
|
|
9076
9685
|
MeasureTool,
|
|
9077
9686
|
NoteTool,
|
|
@@ -9095,6 +9704,7 @@ export {
|
|
|
9095
9704
|
createText,
|
|
9096
9705
|
drawHexPath,
|
|
9097
9706
|
exportImage,
|
|
9707
|
+
exportSvg,
|
|
9098
9708
|
getActiveFormats,
|
|
9099
9709
|
getArrowBounds,
|
|
9100
9710
|
getArrowControlPoint,
|