@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.cjs
CHANGED
|
@@ -29,6 +29,7 @@ __export(index_exports, {
|
|
|
29
29
|
HandTool: () => HandTool,
|
|
30
30
|
HistoryStack: () => HistoryStack,
|
|
31
31
|
ImageTool: () => ImageTool,
|
|
32
|
+
LaserTool: () => LaserTool,
|
|
32
33
|
LayerManager: () => LayerManager,
|
|
33
34
|
MeasureTool: () => MeasureTool,
|
|
34
35
|
NoteTool: () => NoteTool,
|
|
@@ -52,6 +53,7 @@ __export(index_exports, {
|
|
|
52
53
|
createText: () => createText,
|
|
53
54
|
drawHexPath: () => drawHexPath,
|
|
54
55
|
exportImage: () => exportImage,
|
|
56
|
+
exportSvg: () => exportSvg,
|
|
55
57
|
getActiveFormats: () => getActiveFormats,
|
|
56
58
|
getArrowBounds: () => getArrowBounds,
|
|
57
59
|
getArrowControlPoint: () => getArrowControlPoint,
|
|
@@ -912,6 +914,11 @@ var KeyboardActions = class {
|
|
|
912
914
|
if (tool?.name !== "select") return null;
|
|
913
915
|
return { tool, ctx };
|
|
914
916
|
}
|
|
917
|
+
selectableElements(ctx) {
|
|
918
|
+
return ctx.store.getAll().filter(
|
|
919
|
+
(el) => !el.locked && (ctx.isLayerVisible?.(el.layerId) ?? true) && !(ctx.isLayerLocked?.(el.layerId) ?? false)
|
|
920
|
+
);
|
|
921
|
+
}
|
|
915
922
|
nudge(dx, dy, byCell) {
|
|
916
923
|
if (this.deps.isToolActive()) return false;
|
|
917
924
|
const sel = this.selectTool();
|
|
@@ -1054,12 +1061,28 @@ var KeyboardActions = class {
|
|
|
1054
1061
|
}
|
|
1055
1062
|
const sel = this.selectTool();
|
|
1056
1063
|
if (!sel) return;
|
|
1057
|
-
const ids = sel.ctx.
|
|
1058
|
-
(el) => !el.locked && (sel.ctx.isLayerVisible?.(el.layerId) ?? true) && !(sel.ctx.isLayerLocked?.(el.layerId) ?? false)
|
|
1059
|
-
).map((el) => el.id);
|
|
1064
|
+
const ids = this.selectableElements(sel.ctx).map((el) => el.id);
|
|
1060
1065
|
sel.tool.setSelection(ids);
|
|
1061
1066
|
sel.ctx.requestRender();
|
|
1062
1067
|
}
|
|
1068
|
+
cycleSelection(direction) {
|
|
1069
|
+
if (this.deps.isToolActive()) return;
|
|
1070
|
+
const tm = this.deps.getToolManager();
|
|
1071
|
+
const ctx = this.deps.getToolContext();
|
|
1072
|
+
if (!tm || !ctx) return;
|
|
1073
|
+
if (tm.activeTool?.name !== "select") ctx.switchTool?.("select");
|
|
1074
|
+
const sel = this.selectTool();
|
|
1075
|
+
if (!sel) return;
|
|
1076
|
+
const eligible = this.selectableElements(sel.ctx).filter((el) => el.type !== "grid");
|
|
1077
|
+
if (eligible.length === 0) return;
|
|
1078
|
+
const idxs = sel.tool.selectedIds.map((id) => eligible.findIndex((e) => e.id === id)).filter((i) => i >= 0);
|
|
1079
|
+
const anchor = idxs.length === 0 ? direction > 0 ? -1 : 0 : direction > 0 ? Math.max(...idxs) : Math.min(...idxs);
|
|
1080
|
+
const next = (anchor + direction + eligible.length) % eligible.length;
|
|
1081
|
+
const target = eligible[next];
|
|
1082
|
+
if (!target) return;
|
|
1083
|
+
sel.tool.setSelection([target.id]);
|
|
1084
|
+
sel.ctx.requestRender();
|
|
1085
|
+
}
|
|
1063
1086
|
zoomToFit() {
|
|
1064
1087
|
if (this.deps.isToolActive()) return;
|
|
1065
1088
|
this.deps.fitToContent?.();
|
|
@@ -1164,8 +1187,9 @@ var DEFAULT_BINDINGS = [
|
|
|
1164
1187
|
["undo", ["mod+z"]],
|
|
1165
1188
|
["redo", ["mod+y", "mod+shift+z"]],
|
|
1166
1189
|
["select-all", ["mod+a"]],
|
|
1190
|
+
["cycle-selection", ["tab"]],
|
|
1191
|
+
["cycle-selection-reverse", ["shift+tab"]],
|
|
1167
1192
|
["copy", ["mod+c"]],
|
|
1168
|
-
["paste", ["mod+v"]],
|
|
1169
1193
|
["duplicate", ["mod+d"]],
|
|
1170
1194
|
["z-forward", ["]"]],
|
|
1171
1195
|
["z-backward", ["["]],
|
|
@@ -1340,6 +1364,7 @@ var KeyboardHandler = class {
|
|
|
1340
1364
|
this.shortcutMap = new ShortcutMap(deps.shortcuts?.bindings);
|
|
1341
1365
|
window.addEventListener("keydown", this.onKeyDown, { signal: deps.abortSignal });
|
|
1342
1366
|
window.addEventListener("keyup", this.onKeyUp, { signal: deps.abortSignal });
|
|
1367
|
+
window.addEventListener("paste", this.onPaste, { signal: deps.abortSignal });
|
|
1343
1368
|
}
|
|
1344
1369
|
shortcutMap;
|
|
1345
1370
|
get shortcuts() {
|
|
@@ -1355,12 +1380,15 @@ var KeyboardHandler = class {
|
|
|
1355
1380
|
zoomToLevel(level) {
|
|
1356
1381
|
this.deps.camera.zoomAt(level, this.viewportCenter());
|
|
1357
1382
|
}
|
|
1383
|
+
shouldHandle(target) {
|
|
1384
|
+
const el = target;
|
|
1385
|
+
if (el?.isContentEditable) return false;
|
|
1386
|
+
const tag = el?.tagName;
|
|
1387
|
+
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return false;
|
|
1388
|
+
return this.isInScope();
|
|
1389
|
+
}
|
|
1358
1390
|
onKeyDown = (e) => {
|
|
1359
|
-
|
|
1360
|
-
if (target?.isContentEditable) return;
|
|
1361
|
-
const tag = target?.tagName;
|
|
1362
|
-
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
|
|
1363
|
-
if (!this.isInScope()) return;
|
|
1391
|
+
if (!this.shouldHandle(e.target)) return;
|
|
1364
1392
|
if (e.key === " ") {
|
|
1365
1393
|
this.deps.setSpaceHeld(true);
|
|
1366
1394
|
}
|
|
@@ -1382,6 +1410,34 @@ var KeyboardHandler = class {
|
|
|
1382
1410
|
}
|
|
1383
1411
|
}
|
|
1384
1412
|
};
|
|
1413
|
+
onPaste = (e) => {
|
|
1414
|
+
if (!this.shouldHandle(e.target)) return;
|
|
1415
|
+
const items = e.clipboardData?.items;
|
|
1416
|
+
let file = null;
|
|
1417
|
+
if (items) {
|
|
1418
|
+
for (const it of items) {
|
|
1419
|
+
if (it.kind === "file" && it.type.startsWith("image/")) {
|
|
1420
|
+
file = it.getAsFile();
|
|
1421
|
+
break;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
if (file) {
|
|
1426
|
+
e.preventDefault();
|
|
1427
|
+
const world = this.deps.getLastPointerWorld() ?? this.deps.getCenteredWorld();
|
|
1428
|
+
if (this.deps.onPaste) {
|
|
1429
|
+
this.deps.onPaste(e, world);
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
const reader = new FileReader();
|
|
1433
|
+
reader.onload = () => {
|
|
1434
|
+
if (typeof reader.result === "string") this.deps.addImage(reader.result, world);
|
|
1435
|
+
};
|
|
1436
|
+
reader.readAsDataURL(file);
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
this.deps.actions.paste();
|
|
1440
|
+
};
|
|
1385
1441
|
runAction(action, e) {
|
|
1386
1442
|
switch (action) {
|
|
1387
1443
|
case "delete":
|
|
@@ -1403,6 +1459,14 @@ var KeyboardHandler = class {
|
|
|
1403
1459
|
e?.preventDefault();
|
|
1404
1460
|
this.deps.actions.selectAll();
|
|
1405
1461
|
return;
|
|
1462
|
+
case "cycle-selection":
|
|
1463
|
+
e?.preventDefault();
|
|
1464
|
+
this.deps.actions.cycleSelection(1);
|
|
1465
|
+
return;
|
|
1466
|
+
case "cycle-selection-reverse":
|
|
1467
|
+
e?.preventDefault();
|
|
1468
|
+
this.deps.actions.cycleSelection(-1);
|
|
1469
|
+
return;
|
|
1406
1470
|
case "copy":
|
|
1407
1471
|
e?.preventDefault();
|
|
1408
1472
|
this.deps.actions.copy();
|
|
@@ -1530,7 +1594,11 @@ var InputHandler = class {
|
|
|
1530
1594
|
this.spaceHeld = v;
|
|
1531
1595
|
},
|
|
1532
1596
|
getActivePointerCount: () => this.activePointers.size,
|
|
1533
|
-
dispatchToolHover: (e) => this.dispatchToolHover(e)
|
|
1597
|
+
dispatchToolHover: (e) => this.dispatchToolHover(e),
|
|
1598
|
+
addImage: options.addImage ?? (() => ""),
|
|
1599
|
+
getLastPointerWorld: () => this.lastPointerWorld(),
|
|
1600
|
+
getCenteredWorld: options.getCenteredWorld ?? (() => ({ x: 0, y: 0 })),
|
|
1601
|
+
onPaste: options.onPaste
|
|
1534
1602
|
});
|
|
1535
1603
|
this.element.style.touchAction = "none";
|
|
1536
1604
|
if (this.scope === "focus") {
|
|
@@ -2190,12 +2258,12 @@ function smoothToSegments(points) {
|
|
|
2190
2258
|
return [{ start: p0, cp1: { x: mx, y: my }, cp2: { x: mx, y: my }, end: p1 }];
|
|
2191
2259
|
}
|
|
2192
2260
|
const segments = [];
|
|
2193
|
-
const
|
|
2194
|
-
for (let i = 0; i <
|
|
2261
|
+
const n2 = points.length;
|
|
2262
|
+
for (let i = 0; i < n2 - 1; i++) {
|
|
2195
2263
|
const p0 = points[Math.max(0, i - 1)];
|
|
2196
2264
|
const p1 = points[i];
|
|
2197
2265
|
const p2 = points[i + 1];
|
|
2198
|
-
const p3 = points[Math.min(
|
|
2266
|
+
const p3 = points[Math.min(n2 - 1, i + 2)];
|
|
2199
2267
|
if (!p0 || !p1 || !p2 || !p3) continue;
|
|
2200
2268
|
const cp1 = {
|
|
2201
2269
|
x: p1.x + (p2.x - p0.x) / 6,
|
|
@@ -2885,11 +2953,11 @@ function pixelToOffset(x, y, cellSize, orientation) {
|
|
|
2885
2953
|
const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
|
|
2886
2954
|
return { col, row: Math.round((y - offsetY) / hexH) };
|
|
2887
2955
|
}
|
|
2888
|
-
function enumerateHexRing(centerQ, centerR,
|
|
2956
|
+
function enumerateHexRing(centerQ, centerR, n2, orientation, cellSize) {
|
|
2889
2957
|
const cells = [];
|
|
2890
|
-
for (let dq = -
|
|
2891
|
-
const rMin = Math.max(-
|
|
2892
|
-
const rMax = Math.min(
|
|
2958
|
+
for (let dq = -n2; dq <= n2; dq++) {
|
|
2959
|
+
const rMin = Math.max(-n2, -dq - n2);
|
|
2960
|
+
const rMax = Math.min(n2, -dq + n2);
|
|
2893
2961
|
for (let dr = rMin; dr <= rMax; dr++) {
|
|
2894
2962
|
const absQ = centerQ + dq;
|
|
2895
2963
|
const absR = centerR + dr;
|
|
@@ -2910,28 +2978,28 @@ function getHexDistance(a, b, cellSize, orientation) {
|
|
|
2910
2978
|
return Math.max(Math.abs(dq), Math.abs(dr), Math.abs(ds));
|
|
2911
2979
|
}
|
|
2912
2980
|
function getHexCellsInRadius(center2, radiusCells, cellSize, orientation) {
|
|
2913
|
-
const
|
|
2981
|
+
const n2 = Math.round(radiusCells);
|
|
2914
2982
|
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2915
2983
|
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2916
|
-
if (
|
|
2984
|
+
if (n2 <= 0) {
|
|
2917
2985
|
return [offsetToPixel(off.col, off.row, cellSize, orientation)];
|
|
2918
2986
|
}
|
|
2919
|
-
return enumerateHexRing(cube.q, cube.r,
|
|
2987
|
+
return enumerateHexRing(cube.q, cube.r, n2, orientation, cellSize);
|
|
2920
2988
|
}
|
|
2921
2989
|
function getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation) {
|
|
2922
|
-
const
|
|
2990
|
+
const n2 = Math.round(radiusCells);
|
|
2923
2991
|
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2924
2992
|
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2925
2993
|
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
2926
|
-
if (
|
|
2994
|
+
if (n2 <= 0) return [centerPixel];
|
|
2927
2995
|
const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
2928
2996
|
const step = Math.PI / 3;
|
|
2929
2997
|
const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
|
|
2930
2998
|
const halfAngle = Math.PI / 6 + 1e-6;
|
|
2931
2999
|
const cells = [centerPixel];
|
|
2932
|
-
for (let dq = -
|
|
2933
|
-
const rMin = Math.max(-
|
|
2934
|
-
const rMax = Math.min(
|
|
3000
|
+
for (let dq = -n2; dq <= n2; dq++) {
|
|
3001
|
+
const rMin = Math.max(-n2, -dq - n2);
|
|
3002
|
+
const rMax = Math.min(n2, -dq + n2);
|
|
2935
3003
|
for (let dr = rMin; dr <= rMax; dr++) {
|
|
2936
3004
|
if (dq === 0 && dr === 0) continue;
|
|
2937
3005
|
const absQ = cube.q + dq;
|
|
@@ -2955,23 +3023,23 @@ function getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation) {
|
|
|
2955
3023
|
return cells;
|
|
2956
3024
|
}
|
|
2957
3025
|
function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
|
|
2958
|
-
const
|
|
3026
|
+
const n2 = Math.round(radiusCells);
|
|
2959
3027
|
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2960
3028
|
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2961
3029
|
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
2962
|
-
if (
|
|
3030
|
+
if (n2 <= 0) return [centerPixel];
|
|
2963
3031
|
const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
2964
3032
|
const step = Math.PI / 3;
|
|
2965
3033
|
const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
|
|
2966
3034
|
const cos = Math.cos(snappedAngle);
|
|
2967
3035
|
const sin = Math.sin(snappedAngle);
|
|
2968
3036
|
const snapUnit = Math.sqrt(3) * cellSize;
|
|
2969
|
-
const lineLength =
|
|
3037
|
+
const lineLength = n2 * snapUnit;
|
|
2970
3038
|
const halfWidth = snapUnit * 0.5 + 1e-6;
|
|
2971
3039
|
const cells = [];
|
|
2972
|
-
for (let dq = -
|
|
2973
|
-
const rMin = Math.max(-
|
|
2974
|
-
const rMax = Math.min(
|
|
3040
|
+
for (let dq = -n2; dq <= n2; dq++) {
|
|
3041
|
+
const rMin = Math.max(-n2, -dq - n2);
|
|
3042
|
+
const rMax = Math.min(n2, -dq + n2);
|
|
2975
3043
|
for (let dr = rMin; dr <= rMax; dr++) {
|
|
2976
3044
|
const absQ = cube.q + dq;
|
|
2977
3045
|
const absR = cube.r + dr;
|
|
@@ -2993,17 +3061,17 @@ function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
|
|
|
2993
3061
|
return cells;
|
|
2994
3062
|
}
|
|
2995
3063
|
function getHexCellsInSquare(center2, radiusCells, cellSize, orientation) {
|
|
2996
|
-
const
|
|
3064
|
+
const n2 = Math.round(radiusCells);
|
|
2997
3065
|
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2998
3066
|
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2999
3067
|
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
3000
|
-
if (
|
|
3068
|
+
if (n2 <= 0) return [centerPixel];
|
|
3001
3069
|
const snapUnit = Math.sqrt(3) * cellSize;
|
|
3002
|
-
const halfSide =
|
|
3070
|
+
const halfSide = n2 * snapUnit / 2;
|
|
3003
3071
|
const cells = [];
|
|
3004
|
-
for (let dq = -
|
|
3005
|
-
const rMin = Math.max(-
|
|
3006
|
-
const rMax = Math.min(
|
|
3072
|
+
for (let dq = -n2; dq <= n2; dq++) {
|
|
3073
|
+
const rMin = Math.max(-n2, -dq - n2);
|
|
3074
|
+
const rMax = Math.min(n2, -dq + n2);
|
|
3007
3075
|
for (let dr = rMin; dr <= rMax; dr++) {
|
|
3008
3076
|
const absQ = cube.q + dq;
|
|
3009
3077
|
const absR = cube.r + dr;
|
|
@@ -3190,6 +3258,58 @@ function getSquareGridLines(bounds, cellSize) {
|
|
|
3190
3258
|
}
|
|
3191
3259
|
return { verticals, horizontals };
|
|
3192
3260
|
}
|
|
3261
|
+
function getHexVertices(cx, cy, circumradius, orientation) {
|
|
3262
|
+
const vertices = [];
|
|
3263
|
+
const angleOffset = orientation === "pointy" ? -Math.PI / 2 : 0;
|
|
3264
|
+
for (let i = 0; i < 6; i++) {
|
|
3265
|
+
const angle = Math.PI / 3 * i + angleOffset;
|
|
3266
|
+
vertices.push({
|
|
3267
|
+
x: cx + circumradius * Math.cos(angle),
|
|
3268
|
+
y: cy + circumradius * Math.sin(angle)
|
|
3269
|
+
});
|
|
3270
|
+
}
|
|
3271
|
+
return vertices;
|
|
3272
|
+
}
|
|
3273
|
+
function getHexCenters(bounds, circumradius, orientation) {
|
|
3274
|
+
if (circumradius <= 0) return [];
|
|
3275
|
+
const centers = [];
|
|
3276
|
+
if (orientation === "pointy") {
|
|
3277
|
+
const hexW = Math.sqrt(3) * circumradius;
|
|
3278
|
+
const hexH = 2 * circumradius;
|
|
3279
|
+
const rowH = hexH * 0.75;
|
|
3280
|
+
const startRow = Math.floor((bounds.minY - circumradius) / rowH);
|
|
3281
|
+
const endRow = Math.ceil((bounds.maxY + circumradius) / rowH);
|
|
3282
|
+
const startCol = Math.floor((bounds.minX - hexW) / hexW);
|
|
3283
|
+
const endCol = Math.ceil((bounds.maxX + hexW) / hexW);
|
|
3284
|
+
for (let row = startRow; row <= endRow; row++) {
|
|
3285
|
+
const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
|
|
3286
|
+
for (let col = startCol; col <= endCol; col++) {
|
|
3287
|
+
centers.push({
|
|
3288
|
+
x: col * hexW + offsetX,
|
|
3289
|
+
y: row * rowH
|
|
3290
|
+
});
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
} else {
|
|
3294
|
+
const hexW = 2 * circumradius;
|
|
3295
|
+
const hexH = Math.sqrt(3) * circumradius;
|
|
3296
|
+
const colW = hexW * 0.75;
|
|
3297
|
+
const startCol = Math.floor((bounds.minX - circumradius) / colW);
|
|
3298
|
+
const endCol = Math.ceil((bounds.maxX + circumradius) / colW);
|
|
3299
|
+
const startRow = Math.floor((bounds.minY - hexH) / hexH);
|
|
3300
|
+
const endRow = Math.ceil((bounds.maxY + hexH) / hexH);
|
|
3301
|
+
for (let col = startCol; col <= endCol; col++) {
|
|
3302
|
+
const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
|
|
3303
|
+
for (let row = startRow; row <= endRow; row++) {
|
|
3304
|
+
centers.push({
|
|
3305
|
+
x: col * colW,
|
|
3306
|
+
y: row * hexH + offsetY
|
|
3307
|
+
});
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
return centers;
|
|
3312
|
+
}
|
|
3193
3313
|
function renderSquareGrid(ctx, bounds, cellSize, strokeColor, strokeWidth, opacity) {
|
|
3194
3314
|
if (cellSize <= 0) return;
|
|
3195
3315
|
const { verticals, horizontals } = getSquareGridLines(bounds, cellSize);
|
|
@@ -3588,6 +3708,8 @@ function createHtmlElement(input) {
|
|
|
3588
3708
|
};
|
|
3589
3709
|
if (input.domId) el.domId = input.domId;
|
|
3590
3710
|
if (input.interactive) el.interactive = input.interactive;
|
|
3711
|
+
if (input.htmlType) el.htmlType = input.htmlType;
|
|
3712
|
+
if (input.data) el.data = input.data;
|
|
3591
3713
|
return el;
|
|
3592
3714
|
}
|
|
3593
3715
|
function createShape(input) {
|
|
@@ -4868,6 +4990,350 @@ async function exportImage(store, options = {}, layerManager) {
|
|
|
4868
4990
|
});
|
|
4869
4991
|
}
|
|
4870
4992
|
|
|
4993
|
+
// src/canvas/export-svg.ts
|
|
4994
|
+
var ARROWHEAD_LENGTH2 = 12;
|
|
4995
|
+
var ARROWHEAD_ANGLE2 = Math.PI / 6;
|
|
4996
|
+
var ARROW_LABEL_FONT_SIZE2 = 14;
|
|
4997
|
+
function esc(s) {
|
|
4998
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
4999
|
+
}
|
|
5000
|
+
var n = (v) => Number.isFinite(v) ? `${Math.round(v * 1e3) / 1e3}` : "0";
|
|
5001
|
+
function elementCenter(el) {
|
|
5002
|
+
const b = getElementBounds(el);
|
|
5003
|
+
if (!b) return null;
|
|
5004
|
+
return { x: b.x + b.w / 2, y: b.y + b.h / 2 };
|
|
5005
|
+
}
|
|
5006
|
+
function withRotationSvg(el, fragment) {
|
|
5007
|
+
const angle = el.rotation ?? 0;
|
|
5008
|
+
if (!angle || !fragment) return fragment;
|
|
5009
|
+
const c = elementCenter(el);
|
|
5010
|
+
if (!c) return fragment;
|
|
5011
|
+
const deg = angle * 180 / Math.PI;
|
|
5012
|
+
return `<g transform="rotate(${n(deg)} ${n(c.x)} ${n(c.y)})">${fragment}</g>`;
|
|
5013
|
+
}
|
|
5014
|
+
var WIDTH_QUANTUM2 = 0.25;
|
|
5015
|
+
function emitStroke(stroke) {
|
|
5016
|
+
if (stroke.points.length < 2) return "";
|
|
5017
|
+
const data = getStrokeRenderData(stroke);
|
|
5018
|
+
const { x: ox, y: oy } = stroke.position;
|
|
5019
|
+
const byWidth = /* @__PURE__ */ new Map();
|
|
5020
|
+
for (let i = 0; i < data.segments.length; i++) {
|
|
5021
|
+
const seg = data.segments[i];
|
|
5022
|
+
const w = data.widths[i];
|
|
5023
|
+
if (!seg || w === void 0) continue;
|
|
5024
|
+
const q = Math.max(WIDTH_QUANTUM2, Math.round(w / WIDTH_QUANTUM2) * WIDTH_QUANTUM2);
|
|
5025
|
+
let parts = byWidth.get(q);
|
|
5026
|
+
if (!parts) {
|
|
5027
|
+
parts = [];
|
|
5028
|
+
byWidth.set(q, parts);
|
|
5029
|
+
}
|
|
5030
|
+
parts.push(
|
|
5031
|
+
`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)}`
|
|
5032
|
+
);
|
|
5033
|
+
}
|
|
5034
|
+
const blend = stroke.blendMode === "multiply" ? ' style="mix-blend-mode:multiply"' : "";
|
|
5035
|
+
let out = "";
|
|
5036
|
+
for (const [width, parts] of byWidth) {
|
|
5037
|
+
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} />`;
|
|
5038
|
+
}
|
|
5039
|
+
return out;
|
|
5040
|
+
}
|
|
5041
|
+
function emitShape(shape) {
|
|
5042
|
+
const { x, y } = shape.position;
|
|
5043
|
+
const { w, h } = shape.size;
|
|
5044
|
+
const fill = shape.fillColor !== "none" && shape.shape !== "line" ? esc(shape.fillColor) : "none";
|
|
5045
|
+
const stroke = shape.strokeWidth > 0 ? esc(shape.strokeColor) : "none";
|
|
5046
|
+
const sw = shape.strokeWidth > 0 ? ` stroke-width="${n(shape.strokeWidth)}"` : "";
|
|
5047
|
+
switch (shape.shape) {
|
|
5048
|
+
case "rectangle":
|
|
5049
|
+
return `<rect x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" fill="${fill}" stroke="${stroke}"${sw} />`;
|
|
5050
|
+
case "ellipse":
|
|
5051
|
+
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} />`;
|
|
5052
|
+
case "line": {
|
|
5053
|
+
const [a, b] = lineEndpoints(shape);
|
|
5054
|
+
return `<line x1="${n(a.x)}" y1="${n(a.y)}" x2="${n(b.x)}" y2="${n(b.y)}" stroke="${stroke}"${sw} stroke-linecap="round" />`;
|
|
5055
|
+
}
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
function emitArrow(arrow, store) {
|
|
5059
|
+
const geometry = getArrowRenderGeometry(arrow);
|
|
5060
|
+
const { visualFrom: from, visualTo: to } = getVisualEndpoints(arrow, geometry, store);
|
|
5061
|
+
let d;
|
|
5062
|
+
if (arrow.bend !== 0) {
|
|
5063
|
+
const cp = geometry.controlPoint ?? getArrowControlPoint(from, to, arrow.bend);
|
|
5064
|
+
d = `M${n(from.x)} ${n(from.y)} Q${n(cp.x)} ${n(cp.y)} ${n(to.x)} ${n(to.y)}`;
|
|
5065
|
+
} else {
|
|
5066
|
+
d = `M${n(from.x)} ${n(from.y)} L${n(to.x)} ${n(to.y)}`;
|
|
5067
|
+
}
|
|
5068
|
+
const dash = arrow.fromBinding || arrow.toBinding ? ' stroke-dasharray="8 4"' : "";
|
|
5069
|
+
let out = `<path d="${d}" fill="none" stroke="${esc(arrow.color)}" stroke-width="${n(arrow.width)}" stroke-linecap="round"${dash} />`;
|
|
5070
|
+
const angle = geometry.tangentEnd;
|
|
5071
|
+
const p1x = to.x - ARROWHEAD_LENGTH2 * Math.cos(angle - ARROWHEAD_ANGLE2);
|
|
5072
|
+
const p1y = to.y - ARROWHEAD_LENGTH2 * Math.sin(angle - ARROWHEAD_ANGLE2);
|
|
5073
|
+
const p2x = to.x - ARROWHEAD_LENGTH2 * Math.cos(angle + ARROWHEAD_ANGLE2);
|
|
5074
|
+
const p2y = to.y - ARROWHEAD_LENGTH2 * Math.sin(angle + ARROWHEAD_ANGLE2);
|
|
5075
|
+
out += `<polygon points="${n(to.x)},${n(to.y)} ${n(p1x)},${n(p1y)} ${n(p2x)},${n(p2y)}" fill="${esc(arrow.color)}" />`;
|
|
5076
|
+
if (arrow.label && arrow.label.length > 0) {
|
|
5077
|
+
const mid = getArrowMidpoint(arrow.from, arrow.to, arrow.bend);
|
|
5078
|
+
const approxW = arrow.label.length * ARROW_LABEL_FONT_SIZE2 * 0.6;
|
|
5079
|
+
const padX = 6;
|
|
5080
|
+
const padY = 4;
|
|
5081
|
+
const lw = approxW + padX * 2;
|
|
5082
|
+
const lh = ARROW_LABEL_FONT_SIZE2 + padY * 2;
|
|
5083
|
+
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)" />`;
|
|
5084
|
+
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>`;
|
|
5085
|
+
}
|
|
5086
|
+
return out;
|
|
5087
|
+
}
|
|
5088
|
+
function emitImage(image, dataUri) {
|
|
5089
|
+
const href = dataUri ?? image.src;
|
|
5090
|
+
if (!href) return "";
|
|
5091
|
+
const { x, y } = image.position;
|
|
5092
|
+
const { w, h } = image.size;
|
|
5093
|
+
return `<image href="${esc(href)}" x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" />`;
|
|
5094
|
+
}
|
|
5095
|
+
function emitText(text) {
|
|
5096
|
+
if (!text.text) return "";
|
|
5097
|
+
const pad = 2;
|
|
5098
|
+
let anchor = "start";
|
|
5099
|
+
let textX = text.position.x + pad;
|
|
5100
|
+
if (text.textAlign === "center") {
|
|
5101
|
+
anchor = "middle";
|
|
5102
|
+
textX = text.position.x + text.size.w / 2;
|
|
5103
|
+
} else if (text.textAlign === "right") {
|
|
5104
|
+
anchor = "end";
|
|
5105
|
+
textX = text.position.x + text.size.w - pad;
|
|
5106
|
+
}
|
|
5107
|
+
const lineHeight = text.fontSize * 1.4;
|
|
5108
|
+
const lines = text.text.split("\n");
|
|
5109
|
+
let out = "";
|
|
5110
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5111
|
+
const line = lines[i];
|
|
5112
|
+
if (line === void 0) continue;
|
|
5113
|
+
const y = text.position.y + pad + i * lineHeight;
|
|
5114
|
+
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>`;
|
|
5115
|
+
}
|
|
5116
|
+
return out;
|
|
5117
|
+
}
|
|
5118
|
+
function emitNote(note, rasterScale) {
|
|
5119
|
+
const { x, y } = note.position;
|
|
5120
|
+
const { w, h } = note.size;
|
|
5121
|
+
if (typeof document === "undefined") return emitNotePlaceholder(note);
|
|
5122
|
+
const canvas = document.createElement("canvas");
|
|
5123
|
+
canvas.width = Math.max(1, Math.ceil(w * rasterScale));
|
|
5124
|
+
canvas.height = Math.max(1, Math.ceil(h * rasterScale));
|
|
5125
|
+
const ctx = canvas.getContext("2d");
|
|
5126
|
+
if (!ctx) return emitNotePlaceholder(note);
|
|
5127
|
+
ctx.scale(rasterScale, rasterScale);
|
|
5128
|
+
ctx.translate(-x, -y);
|
|
5129
|
+
renderNoteOnCanvas(ctx, note);
|
|
5130
|
+
let dataUri;
|
|
5131
|
+
try {
|
|
5132
|
+
dataUri = canvas.toDataURL();
|
|
5133
|
+
} catch {
|
|
5134
|
+
return emitNotePlaceholder(note);
|
|
5135
|
+
}
|
|
5136
|
+
if (!dataUri || !dataUri.startsWith("data:")) return emitNotePlaceholder(note);
|
|
5137
|
+
return `<image href="${esc(dataUri)}" x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" />`;
|
|
5138
|
+
}
|
|
5139
|
+
function emitNotePlaceholder(note) {
|
|
5140
|
+
const { x, y } = note.position;
|
|
5141
|
+
const { w, h } = note.size;
|
|
5142
|
+
return `<rect x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" rx="4" fill="${esc(note.backgroundColor)}" />`;
|
|
5143
|
+
}
|
|
5144
|
+
function emitGrid(grid, bounds) {
|
|
5145
|
+
if (grid.cellSize <= 0) return "";
|
|
5146
|
+
const vb = {
|
|
5147
|
+
minX: bounds.x,
|
|
5148
|
+
minY: bounds.y,
|
|
5149
|
+
maxX: bounds.x + bounds.w,
|
|
5150
|
+
maxY: bounds.y + bounds.h
|
|
5151
|
+
};
|
|
5152
|
+
const stroke = esc(grid.strokeColor);
|
|
5153
|
+
const sw = n(grid.strokeWidth);
|
|
5154
|
+
const op = n(grid.opacity);
|
|
5155
|
+
if (grid.gridType === "hex") {
|
|
5156
|
+
const centers = getHexCenters(vb, grid.cellSize, grid.hexOrientation);
|
|
5157
|
+
let d2 = "";
|
|
5158
|
+
for (const c of centers) {
|
|
5159
|
+
const verts = getHexVertices(c.x, c.y, grid.cellSize, grid.hexOrientation);
|
|
5160
|
+
const first = verts[0];
|
|
5161
|
+
if (!first) continue;
|
|
5162
|
+
d2 += `M${n(first.x)} ${n(first.y)}`;
|
|
5163
|
+
for (let i = 1; i < verts.length; i++) {
|
|
5164
|
+
const v = verts[i];
|
|
5165
|
+
if (v) d2 += `L${n(v.x)} ${n(v.y)}`;
|
|
5166
|
+
}
|
|
5167
|
+
d2 += "Z";
|
|
5168
|
+
}
|
|
5169
|
+
return `<path d="${d2}" fill="none" stroke="${stroke}" stroke-width="${sw}" opacity="${op}" />`;
|
|
5170
|
+
}
|
|
5171
|
+
const { verticals, horizontals } = getSquareGridLines(vb, grid.cellSize);
|
|
5172
|
+
let d = "";
|
|
5173
|
+
for (const gx of verticals) d += `M${n(gx)} ${n(vb.minY)}L${n(gx)} ${n(vb.maxY)}`;
|
|
5174
|
+
for (const gy of horizontals) d += `M${n(vb.minX)} ${n(gy)}L${n(vb.maxX)} ${n(gy)}`;
|
|
5175
|
+
return `<path d="${d}" fill="none" stroke="${stroke}" stroke-width="${sw}" opacity="${op}" />`;
|
|
5176
|
+
}
|
|
5177
|
+
function emitTemplate(template, grid) {
|
|
5178
|
+
if (grid && grid.gridType === "hex") {
|
|
5179
|
+
return emitHexTemplate(template, grid);
|
|
5180
|
+
}
|
|
5181
|
+
return emitGeometricTemplate(template);
|
|
5182
|
+
}
|
|
5183
|
+
function emitGeometricTemplate(t) {
|
|
5184
|
+
const { x: cx, y: cy } = t.position;
|
|
5185
|
+
const r = t.radius;
|
|
5186
|
+
const fill = esc(t.fillColor);
|
|
5187
|
+
const stroke = esc(t.strokeColor);
|
|
5188
|
+
const sw = n(t.strokeWidth);
|
|
5189
|
+
const op = n(t.opacity);
|
|
5190
|
+
const attrs = `fill="${fill}" stroke="${stroke}" stroke-width="${sw}" opacity="${op}"`;
|
|
5191
|
+
switch (t.templateShape) {
|
|
5192
|
+
case "circle":
|
|
5193
|
+
return `<circle cx="${n(cx)}" cy="${n(cy)}" r="${n(r)}" ${attrs} />`;
|
|
5194
|
+
case "square":
|
|
5195
|
+
return `<rect x="${n(cx - r / 2)}" y="${n(cy - r / 2)}" width="${n(r)}" height="${n(r)}" ${attrs} />`;
|
|
5196
|
+
case "cone": {
|
|
5197
|
+
const halfAngle = Math.atan(0.5);
|
|
5198
|
+
const a0 = t.angle - halfAngle;
|
|
5199
|
+
const a1 = t.angle + halfAngle;
|
|
5200
|
+
const p0x = cx + r * Math.cos(a0);
|
|
5201
|
+
const p0y = cy + r * Math.sin(a0);
|
|
5202
|
+
const p1x = cx + r * Math.cos(a1);
|
|
5203
|
+
const p1y = cy + r * Math.sin(a1);
|
|
5204
|
+
const large = a1 - a0 > Math.PI ? 1 : 0;
|
|
5205
|
+
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} />`;
|
|
5206
|
+
}
|
|
5207
|
+
case "line": {
|
|
5208
|
+
const halfW = r / 12;
|
|
5209
|
+
const cos = Math.cos(t.angle);
|
|
5210
|
+
const sin = Math.sin(t.angle);
|
|
5211
|
+
const perpX = -sin * halfW;
|
|
5212
|
+
const perpY = cos * halfW;
|
|
5213
|
+
const pts = [
|
|
5214
|
+
[cx + perpX, cy + perpY],
|
|
5215
|
+
[cx + r * cos + perpX, cy + r * sin + perpY],
|
|
5216
|
+
[cx + r * cos - perpX, cy + r * sin - perpY],
|
|
5217
|
+
[cx - perpX, cy - perpY]
|
|
5218
|
+
].map(([px, py]) => `${n(px ?? 0)},${n(py ?? 0)}`).join(" ");
|
|
5219
|
+
return `<polygon points="${pts}" ${attrs} />`;
|
|
5220
|
+
}
|
|
5221
|
+
}
|
|
5222
|
+
}
|
|
5223
|
+
function emitHexTemplate(t, grid) {
|
|
5224
|
+
const cellSize = grid.cellSize;
|
|
5225
|
+
const orientation = grid.hexOrientation;
|
|
5226
|
+
const snapUnit = Math.sqrt(3) * cellSize;
|
|
5227
|
+
const radiusCells = t.radius / snapUnit;
|
|
5228
|
+
const center2 = t.position;
|
|
5229
|
+
let cells;
|
|
5230
|
+
switch (t.templateShape) {
|
|
5231
|
+
case "circle":
|
|
5232
|
+
cells = getHexCellsInRadius(center2, radiusCells, cellSize, orientation);
|
|
5233
|
+
break;
|
|
5234
|
+
case "cone":
|
|
5235
|
+
cells = getHexCellsInCone(center2, t.angle, radiusCells, cellSize, orientation);
|
|
5236
|
+
break;
|
|
5237
|
+
case "line":
|
|
5238
|
+
cells = getHexCellsInLine(center2, t.angle, radiusCells, cellSize, orientation);
|
|
5239
|
+
break;
|
|
5240
|
+
case "square":
|
|
5241
|
+
cells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
|
|
5242
|
+
break;
|
|
5243
|
+
}
|
|
5244
|
+
let d = "";
|
|
5245
|
+
for (const cell of cells) {
|
|
5246
|
+
const verts = getHexVertices(cell.x, cell.y, cellSize, orientation);
|
|
5247
|
+
const first = verts[0];
|
|
5248
|
+
if (!first) continue;
|
|
5249
|
+
d += `M${n(first.x)} ${n(first.y)}`;
|
|
5250
|
+
for (let i = 1; i < verts.length; i++) {
|
|
5251
|
+
const v = verts[i];
|
|
5252
|
+
if (v) d += `L${n(v.x)} ${n(v.y)}`;
|
|
5253
|
+
}
|
|
5254
|
+
d += "Z";
|
|
5255
|
+
}
|
|
5256
|
+
return `<path d="${d}" fill="${esc(t.fillColor)}" stroke="${esc(t.strokeColor)}" stroke-width="${n(t.strokeWidth)}" opacity="${n(t.opacity)}" />`;
|
|
5257
|
+
}
|
|
5258
|
+
async function exportSvg(store, options = {}, layerManager) {
|
|
5259
|
+
const padding = options.padding ?? 0;
|
|
5260
|
+
const rasterScale = options.rasterScale ?? 2;
|
|
5261
|
+
const filter = options.filter;
|
|
5262
|
+
const allElements = store.getAll();
|
|
5263
|
+
let visibleElements = layerManager ? allElements.filter((el) => layerManager.isLayerVisible(el.layerId)) : allElements;
|
|
5264
|
+
if (filter) visibleElements = visibleElements.filter(filter);
|
|
5265
|
+
const bounds = computeBounds(visibleElements, padding);
|
|
5266
|
+
if (!bounds) {
|
|
5267
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
|
|
5268
|
+
}
|
|
5269
|
+
const remoteImages = visibleElements.filter(
|
|
5270
|
+
(el) => el.type === "image" && !el.src.startsWith("data:")
|
|
5271
|
+
);
|
|
5272
|
+
const imageCache = await loadImages(remoteImages);
|
|
5273
|
+
const imageDataUris = encodeImages(visibleElements, imageCache, rasterScale);
|
|
5274
|
+
const grids = visibleElements.filter((el) => el.type === "grid");
|
|
5275
|
+
const firstGrid = grids[0];
|
|
5276
|
+
let body = "";
|
|
5277
|
+
if (options.background) {
|
|
5278
|
+
body += `<rect x="${n(bounds.x)}" y="${n(bounds.y)}" width="${n(bounds.w)}" height="${n(bounds.h)}" fill="${esc(options.background)}" />`;
|
|
5279
|
+
}
|
|
5280
|
+
for (const el of visibleElements) {
|
|
5281
|
+
body += emitElement(el, imageDataUris, rasterScale, firstGrid, store);
|
|
5282
|
+
}
|
|
5283
|
+
for (const grid of grids) {
|
|
5284
|
+
body += emitGrid(grid, bounds);
|
|
5285
|
+
}
|
|
5286
|
+
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>`;
|
|
5287
|
+
}
|
|
5288
|
+
function emitElement(el, imageDataUris, rasterScale, firstGrid, store) {
|
|
5289
|
+
switch (el.type) {
|
|
5290
|
+
case "stroke":
|
|
5291
|
+
return withRotationSvg(el, emitStroke(el));
|
|
5292
|
+
case "shape":
|
|
5293
|
+
return withRotationSvg(el, emitShape(el));
|
|
5294
|
+
case "arrow":
|
|
5295
|
+
return emitArrow(el, store);
|
|
5296
|
+
case "image":
|
|
5297
|
+
return withRotationSvg(el, emitImage(el, imageDataUris.get(el.id)));
|
|
5298
|
+
case "text":
|
|
5299
|
+
return withRotationSvg(el, emitText(el));
|
|
5300
|
+
case "note":
|
|
5301
|
+
return withRotationSvg(el, emitNote(el, rasterScale));
|
|
5302
|
+
case "template":
|
|
5303
|
+
return emitTemplate(el, firstGrid);
|
|
5304
|
+
case "grid":
|
|
5305
|
+
return "";
|
|
5306
|
+
case "html":
|
|
5307
|
+
return "";
|
|
5308
|
+
default:
|
|
5309
|
+
return "";
|
|
5310
|
+
}
|
|
5311
|
+
}
|
|
5312
|
+
function encodeImages(elements, imageCache, rasterScale) {
|
|
5313
|
+
const out = /* @__PURE__ */ new Map();
|
|
5314
|
+
for (const el of elements) {
|
|
5315
|
+
if (el.type !== "image") continue;
|
|
5316
|
+
if (el.src.startsWith("data:")) {
|
|
5317
|
+
out.set(el.id, el.src);
|
|
5318
|
+
continue;
|
|
5319
|
+
}
|
|
5320
|
+
const img = imageCache.get(el.id);
|
|
5321
|
+
if (!img || typeof document === "undefined") continue;
|
|
5322
|
+
const canvas = document.createElement("canvas");
|
|
5323
|
+
canvas.width = Math.max(1, Math.ceil(el.size.w * rasterScale));
|
|
5324
|
+
canvas.height = Math.max(1, Math.ceil(el.size.h * rasterScale));
|
|
5325
|
+
const ctx = canvas.getContext("2d");
|
|
5326
|
+
if (!ctx) continue;
|
|
5327
|
+
try {
|
|
5328
|
+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
5329
|
+
const uri = canvas.toDataURL();
|
|
5330
|
+
if (uri.startsWith("data:")) out.set(el.id, uri);
|
|
5331
|
+
} catch {
|
|
5332
|
+
}
|
|
5333
|
+
}
|
|
5334
|
+
return out;
|
|
5335
|
+
}
|
|
5336
|
+
|
|
4871
5337
|
// src/layers/layer-manager.ts
|
|
4872
5338
|
var LayerManager = class {
|
|
4873
5339
|
constructor(store) {
|
|
@@ -6028,13 +6494,13 @@ var SelectionOps = class {
|
|
|
6028
6494
|
if (!first || !last) return;
|
|
6029
6495
|
const c0 = center2(first.bounds);
|
|
6030
6496
|
const cN = center2(last.bounds);
|
|
6031
|
-
const
|
|
6497
|
+
const n2 = sorted.length;
|
|
6032
6498
|
this.deps.recorder.begin();
|
|
6033
6499
|
const moved = [];
|
|
6034
|
-
for (let i = 1; i <
|
|
6500
|
+
for (let i = 1; i < n2 - 1; i++) {
|
|
6035
6501
|
const item = sorted[i];
|
|
6036
6502
|
if (!item || !this.isMovable(item.el)) continue;
|
|
6037
|
-
const target = c0 + i * (cN - c0) / (
|
|
6503
|
+
const target = c0 + i * (cN - c0) / (n2 - 1);
|
|
6038
6504
|
const delta = target - center2(item.bounds);
|
|
6039
6505
|
if (delta === 0) continue;
|
|
6040
6506
|
const [dx, dy] = axis === "horizontal" ? [delta, 0] : [0, delta];
|
|
@@ -6372,7 +6838,10 @@ var Viewport = class {
|
|
|
6372
6838
|
this.getSelectTool()?.selectAtPoint(world, this.toolContext);
|
|
6373
6839
|
this.openContextMenu(screenPos);
|
|
6374
6840
|
},
|
|
6375
|
-
shortcuts: options.shortcuts
|
|
6841
|
+
shortcuts: options.shortcuts,
|
|
6842
|
+
addImage: (src, world) => this.addImage(src, world),
|
|
6843
|
+
getCenteredWorld: () => this.centeredPosition({ w: 300, h: 200 }),
|
|
6844
|
+
onPaste: options.onPaste
|
|
6376
6845
|
});
|
|
6377
6846
|
if (options.contextMenu !== false) {
|
|
6378
6847
|
this.contextMenu = new ContextMenu({
|
|
@@ -6512,6 +6981,7 @@ var Viewport = class {
|
|
|
6512
6981
|
gridController;
|
|
6513
6982
|
interactions;
|
|
6514
6983
|
contextMenu = null;
|
|
6984
|
+
htmlRenderers = /* @__PURE__ */ new Map();
|
|
6515
6985
|
get ctx() {
|
|
6516
6986
|
return this.canvasEl.getContext("2d");
|
|
6517
6987
|
}
|
|
@@ -6553,6 +7023,9 @@ var Viewport = class {
|
|
|
6553
7023
|
async exportImage(options) {
|
|
6554
7024
|
return exportImage(this.store, options, this.layerManager);
|
|
6555
7025
|
}
|
|
7026
|
+
async exportSVG(options) {
|
|
7027
|
+
return exportSvg(this.store, options, this.layerManager);
|
|
7028
|
+
}
|
|
6556
7029
|
loadState(state) {
|
|
6557
7030
|
this.inputHandler.flushPendingHistory();
|
|
6558
7031
|
this.historyRecorder.pause();
|
|
@@ -6566,19 +7039,24 @@ var Viewport = class {
|
|
|
6566
7039
|
this.layerManager.setActiveLayer(state.activeLayerId);
|
|
6567
7040
|
}
|
|
6568
7041
|
this.domNodeManager.reattachHtmlContent(this.store);
|
|
6569
|
-
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
7042
|
+
for (const el of this.store.getElementsByType("html")) {
|
|
7043
|
+
if (this.domNodeManager.hasContent(el.id)) continue;
|
|
7044
|
+
const factory = el.htmlType ? this.htmlRenderers.get(el.htmlType) : void 0;
|
|
7045
|
+
const rebuilt = factory ? factory(el) : null;
|
|
7046
|
+
if (rebuilt) {
|
|
7047
|
+
this.domNodeManager.storeHtmlContent(el.id, rebuilt);
|
|
7048
|
+
this.domNodeManager.syncDomNode(el);
|
|
7049
|
+
}
|
|
7050
|
+
if (this.onHtmlElementMount) {
|
|
7051
|
+
if (!rebuilt) this.domNodeManager.syncDomNode(el);
|
|
7052
|
+
const node = this.domNodeManager.getNode(el.id);
|
|
7053
|
+
if (node) {
|
|
7054
|
+
this.onHtmlElementMount(el.id, el.domId, node);
|
|
7055
|
+
node.dataset["initialized"] = "true";
|
|
7056
|
+
Object.assign(node.style, {
|
|
7057
|
+
overflow: "hidden",
|
|
7058
|
+
pointerEvents: el.interactive ? "auto" : "none"
|
|
7059
|
+
});
|
|
6582
7060
|
}
|
|
6583
7061
|
}
|
|
6584
7062
|
}
|
|
@@ -6624,12 +7102,14 @@ var Viewport = class {
|
|
|
6624
7102
|
this.requestRender();
|
|
6625
7103
|
return image.id;
|
|
6626
7104
|
}
|
|
6627
|
-
addHtmlElement(dom, position, size = { w: 200, h: 150 }) {
|
|
7105
|
+
addHtmlElement(dom, position, size = { w: 200, h: 150 }, opts) {
|
|
6628
7106
|
const domId = dom.id || void 0;
|
|
6629
7107
|
const el = createHtmlElement({
|
|
6630
7108
|
position,
|
|
6631
7109
|
size,
|
|
6632
7110
|
domId,
|
|
7111
|
+
htmlType: opts?.htmlType,
|
|
7112
|
+
data: opts?.data,
|
|
6633
7113
|
layerId: this.layerManager.activeLayerId
|
|
6634
7114
|
});
|
|
6635
7115
|
this.domNodeManager.storeHtmlContent(el.id, dom);
|
|
@@ -6670,6 +7150,9 @@ var Viewport = class {
|
|
|
6670
7150
|
this.layerManager.removeLayer(id);
|
|
6671
7151
|
this.historyRecorder.commit();
|
|
6672
7152
|
}
|
|
7153
|
+
registerHtmlRenderer(htmlType, factory) {
|
|
7154
|
+
this.htmlRenderers.set(htmlType, factory);
|
|
7155
|
+
}
|
|
6673
7156
|
updateHtmlElement(id, newContent) {
|
|
6674
7157
|
const el = this.store.getById(id);
|
|
6675
7158
|
if (!el) throw new Error(`Element not found: ${id}`);
|
|
@@ -7322,6 +7805,17 @@ function renderArrowHandles(canvasCtx, arrow, zoom) {
|
|
|
7322
7805
|
canvasCtx.stroke();
|
|
7323
7806
|
}
|
|
7324
7807
|
}
|
|
7808
|
+
function renderArrowHoverHandle(canvasCtx, arrow, zoom) {
|
|
7809
|
+
const mid = getArrowMidpoint(arrow.from, arrow.to, arrow.bend);
|
|
7810
|
+
const radius = HANDLE_RADIUS / zoom;
|
|
7811
|
+
canvasCtx.fillStyle = "#2196F3";
|
|
7812
|
+
canvasCtx.strokeStyle = "#2196F3";
|
|
7813
|
+
canvasCtx.lineWidth = 1.5 / zoom;
|
|
7814
|
+
canvasCtx.beginPath();
|
|
7815
|
+
canvasCtx.arc(mid.x, mid.y, radius, 0, Math.PI * 2);
|
|
7816
|
+
canvasCtx.fill();
|
|
7817
|
+
canvasCtx.stroke();
|
|
7818
|
+
}
|
|
7325
7819
|
|
|
7326
7820
|
// src/elements/snap-guides.ts
|
|
7327
7821
|
function xAnchors(b) {
|
|
@@ -8170,7 +8664,13 @@ var SelectTool = class {
|
|
|
8170
8664
|
if (this.hoveredId && this.ctx && this.mode.type === "idle") {
|
|
8171
8665
|
if (!this._selectedIds.includes(this.hoveredId)) {
|
|
8172
8666
|
const el = this.ctx.store.getById(this.hoveredId);
|
|
8173
|
-
if (el) {
|
|
8667
|
+
if (el?.type === "arrow") {
|
|
8668
|
+
canvasCtx.save();
|
|
8669
|
+
canvasCtx.globalAlpha = 0.35;
|
|
8670
|
+
canvasCtx.setLineDash([]);
|
|
8671
|
+
renderArrowHoverHandle(canvasCtx, el, this.ctx.camera.zoom);
|
|
8672
|
+
canvasCtx.restore();
|
|
8673
|
+
} else if (el) {
|
|
8174
8674
|
const b = getElementBounds(el);
|
|
8175
8675
|
if (b) {
|
|
8176
8676
|
canvasCtx.save();
|
|
@@ -9141,8 +9641,118 @@ var TemplateTool = class {
|
|
|
9141
9641
|
}
|
|
9142
9642
|
};
|
|
9143
9643
|
|
|
9644
|
+
// src/tools/laser-tool.ts
|
|
9645
|
+
var DEFAULT_COLOR = "#ff3b30";
|
|
9646
|
+
var DEFAULT_WIDTH = 4;
|
|
9647
|
+
var DEFAULT_FADE_MS = 1200;
|
|
9648
|
+
var LaserTool = class {
|
|
9649
|
+
name;
|
|
9650
|
+
color;
|
|
9651
|
+
width;
|
|
9652
|
+
fadeMs;
|
|
9653
|
+
trail = [];
|
|
9654
|
+
rafId = null;
|
|
9655
|
+
drawing = false;
|
|
9656
|
+
optionListeners = /* @__PURE__ */ new Set();
|
|
9657
|
+
constructor(options = {}) {
|
|
9658
|
+
this.name = options.name ?? "laser";
|
|
9659
|
+
this.color = options.color ?? DEFAULT_COLOR;
|
|
9660
|
+
this.width = options.width ?? DEFAULT_WIDTH;
|
|
9661
|
+
this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS;
|
|
9662
|
+
}
|
|
9663
|
+
now() {
|
|
9664
|
+
return performance.now();
|
|
9665
|
+
}
|
|
9666
|
+
onActivate(ctx) {
|
|
9667
|
+
ctx.setCursor?.("crosshair");
|
|
9668
|
+
}
|
|
9669
|
+
onDeactivate(ctx) {
|
|
9670
|
+
if (this.rafId !== null) {
|
|
9671
|
+
cancelAnimationFrame(this.rafId);
|
|
9672
|
+
this.rafId = null;
|
|
9673
|
+
}
|
|
9674
|
+
this.trail = [];
|
|
9675
|
+
this.drawing = false;
|
|
9676
|
+
ctx.setCursor?.("default");
|
|
9677
|
+
ctx.requestRender();
|
|
9678
|
+
}
|
|
9679
|
+
getOptions() {
|
|
9680
|
+
return {
|
|
9681
|
+
name: this.name,
|
|
9682
|
+
color: this.color,
|
|
9683
|
+
width: this.width,
|
|
9684
|
+
fadeMs: this.fadeMs
|
|
9685
|
+
};
|
|
9686
|
+
}
|
|
9687
|
+
onOptionsChange(listener) {
|
|
9688
|
+
this.optionListeners.add(listener);
|
|
9689
|
+
return () => this.optionListeners.delete(listener);
|
|
9690
|
+
}
|
|
9691
|
+
setOptions(options) {
|
|
9692
|
+
if (options.color !== void 0) this.color = options.color;
|
|
9693
|
+
if (options.width !== void 0) this.width = options.width;
|
|
9694
|
+
if (options.fadeMs !== void 0) this.fadeMs = options.fadeMs;
|
|
9695
|
+
this.notifyOptionsChange();
|
|
9696
|
+
}
|
|
9697
|
+
onPointerDown(state, ctx) {
|
|
9698
|
+
this.drawing = true;
|
|
9699
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
9700
|
+
this.trail.push({ x: world.x, y: world.y, t: this.now() });
|
|
9701
|
+
this.ensureAnimating(ctx);
|
|
9702
|
+
}
|
|
9703
|
+
onPointerMove(state, ctx) {
|
|
9704
|
+
if (!this.drawing) return;
|
|
9705
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
9706
|
+
this.trail.push({ x: world.x, y: world.y, t: this.now() });
|
|
9707
|
+
this.ensureAnimating(ctx);
|
|
9708
|
+
}
|
|
9709
|
+
onPointerUp(_state, _ctx) {
|
|
9710
|
+
this.drawing = false;
|
|
9711
|
+
}
|
|
9712
|
+
renderOverlay(ctx) {
|
|
9713
|
+
if (this.trail.length < 2) return;
|
|
9714
|
+
ctx.save();
|
|
9715
|
+
ctx.strokeStyle = this.color;
|
|
9716
|
+
ctx.lineWidth = this.width;
|
|
9717
|
+
ctx.lineCap = "round";
|
|
9718
|
+
ctx.lineJoin = "round";
|
|
9719
|
+
const now = this.now();
|
|
9720
|
+
for (let i = 0; i < this.trail.length - 1; i++) {
|
|
9721
|
+
const a = this.trail[i];
|
|
9722
|
+
const b = this.trail[i + 1];
|
|
9723
|
+
if (!a || !b) continue;
|
|
9724
|
+
const age = now - b.t;
|
|
9725
|
+
ctx.globalAlpha = Math.max(0, 1 - age / this.fadeMs);
|
|
9726
|
+
ctx.beginPath();
|
|
9727
|
+
ctx.moveTo(a.x, a.y);
|
|
9728
|
+
ctx.lineTo(b.x, b.y);
|
|
9729
|
+
ctx.stroke();
|
|
9730
|
+
}
|
|
9731
|
+
ctx.restore();
|
|
9732
|
+
}
|
|
9733
|
+
ensureAnimating(ctx) {
|
|
9734
|
+
if (this.rafId === null) {
|
|
9735
|
+
this.rafId = requestAnimationFrame(() => this.tick(ctx));
|
|
9736
|
+
}
|
|
9737
|
+
}
|
|
9738
|
+
tick(ctx) {
|
|
9739
|
+
const cutoff = this.now() - this.fadeMs;
|
|
9740
|
+
this.trail = this.trail.filter((p) => p.t >= cutoff);
|
|
9741
|
+
if (this.trail.length > 0) {
|
|
9742
|
+
ctx.requestRender();
|
|
9743
|
+
this.rafId = requestAnimationFrame(() => this.tick(ctx));
|
|
9744
|
+
} else {
|
|
9745
|
+
ctx.requestRender();
|
|
9746
|
+
this.rafId = null;
|
|
9747
|
+
}
|
|
9748
|
+
}
|
|
9749
|
+
notifyOptionsChange() {
|
|
9750
|
+
for (const listener of this.optionListeners) listener();
|
|
9751
|
+
}
|
|
9752
|
+
};
|
|
9753
|
+
|
|
9144
9754
|
// src/index.ts
|
|
9145
|
-
var VERSION = "0.
|
|
9755
|
+
var VERSION = "0.40.0";
|
|
9146
9756
|
// Annotate the CommonJS export names for ESM import in node:
|
|
9147
9757
|
0 && (module.exports = {
|
|
9148
9758
|
ArrowTool,
|
|
@@ -9154,6 +9764,7 @@ var VERSION = "0.38.7";
|
|
|
9154
9764
|
HandTool,
|
|
9155
9765
|
HistoryStack,
|
|
9156
9766
|
ImageTool,
|
|
9767
|
+
LaserTool,
|
|
9157
9768
|
LayerManager,
|
|
9158
9769
|
MeasureTool,
|
|
9159
9770
|
NoteTool,
|
|
@@ -9177,6 +9788,7 @@ var VERSION = "0.38.7";
|
|
|
9177
9788
|
createText,
|
|
9178
9789
|
drawHexPath,
|
|
9179
9790
|
exportImage,
|
|
9791
|
+
exportSvg,
|
|
9180
9792
|
getActiveFormats,
|
|
9181
9793
|
getArrowBounds,
|
|
9182
9794
|
getArrowControlPoint,
|