@fieldnotes/core 0.38.6 → 0.39.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.js CHANGED
@@ -843,6 +843,11 @@ var KeyboardActions = class {
843
843
  this.nudgeTxId = recorder?.currentTransactionId ?? null;
844
844
  } else {
845
845
  clearTimeout(this.nudgeTimer);
846
+ const recorder = this.deps.getHistoryRecorder();
847
+ if (recorder?.currentTransactionId !== this.nudgeTxId) {
848
+ recorder?.begin();
849
+ this.nudgeTxId = recorder?.currentTransactionId ?? null;
850
+ }
846
851
  }
847
852
  const moved = sel.tool.nudgeSelection(dx * step, dy * step, sel.ctx);
848
853
  this.nudgeTimer = setTimeout(() => this.flushPendingNudge(), 400);
@@ -853,7 +858,7 @@ var KeyboardActions = class {
853
858
  clearTimeout(this.nudgeTimer);
854
859
  this.nudgeTimer = null;
855
860
  const recorder = this.deps.getHistoryRecorder();
856
- if (this.nudgeTxId === null || recorder?.currentTransactionId === this.nudgeTxId) {
861
+ if (recorder?.currentTransactionId === this.nudgeTxId) {
857
862
  recorder?.commit();
858
863
  }
859
864
  this.nudgeTxId = null;
@@ -1079,7 +1084,6 @@ var DEFAULT_BINDINGS = [
1079
1084
  ["redo", ["mod+y", "mod+shift+z"]],
1080
1085
  ["select-all", ["mod+a"]],
1081
1086
  ["copy", ["mod+c"]],
1082
- ["paste", ["mod+v"]],
1083
1087
  ["duplicate", ["mod+d"]],
1084
1088
  ["z-forward", ["]"]],
1085
1089
  ["z-backward", ["["]],
@@ -1164,6 +1168,9 @@ function bindingMatches(p, e, allowShift) {
1164
1168
  if (e.altKey !== p.alt) return false;
1165
1169
  return p.digit ? e.code === `Digit${p.key}` : e.key.toLowerCase() === p.key;
1166
1170
  }
1171
+ function sameBinding(a, b) {
1172
+ return a.key === b.key && a.mod === b.mod && a.ctrl === b.ctrl && a.meta === b.meta && a.shift === b.shift && a.alt === b.alt;
1173
+ }
1167
1174
  function toArray(bindings) {
1168
1175
  if (bindings === null) return [];
1169
1176
  return Array.isArray(bindings) ? [...bindings] : [bindings];
@@ -1192,6 +1199,16 @@ var ShortcutMap = class {
1192
1199
  rebind(action, bindings) {
1193
1200
  const list = toArray(bindings);
1194
1201
  const parsedList = list.map(parseBinding);
1202
+ for (const p of parsedList) {
1203
+ for (const [otherAction, otherList] of this.parsed) {
1204
+ if (otherAction === action) continue;
1205
+ if (otherList.some((q) => sameBinding(p, q))) {
1206
+ console.warn(
1207
+ `[fieldnotes] shortcut binding for "${action}" conflicts with "${otherAction}"; first registered wins`
1208
+ );
1209
+ }
1210
+ }
1211
+ }
1195
1212
  this.raw.set(action, list);
1196
1213
  this.parsed.set(action, parsedList);
1197
1214
  }
@@ -1241,6 +1258,7 @@ var KeyboardHandler = class {
1241
1258
  this.shortcutMap = new ShortcutMap(deps.shortcuts?.bindings);
1242
1259
  window.addEventListener("keydown", this.onKeyDown, { signal: deps.abortSignal });
1243
1260
  window.addEventListener("keyup", this.onKeyUp, { signal: deps.abortSignal });
1261
+ window.addEventListener("paste", this.onPaste, { signal: deps.abortSignal });
1244
1262
  }
1245
1263
  shortcutMap;
1246
1264
  get shortcuts() {
@@ -1256,12 +1274,15 @@ var KeyboardHandler = class {
1256
1274
  zoomToLevel(level) {
1257
1275
  this.deps.camera.zoomAt(level, this.viewportCenter());
1258
1276
  }
1277
+ shouldHandle(target) {
1278
+ const el = target;
1279
+ if (el?.isContentEditable) return false;
1280
+ const tag = el?.tagName;
1281
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return false;
1282
+ return this.isInScope();
1283
+ }
1259
1284
  onKeyDown = (e) => {
1260
- const target = e.target;
1261
- if (target?.isContentEditable) return;
1262
- const tag = target?.tagName;
1263
- if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
1264
- if (!this.isInScope()) return;
1285
+ if (!this.shouldHandle(e.target)) return;
1265
1286
  if (e.key === " ") {
1266
1287
  this.deps.setSpaceHeld(true);
1267
1288
  }
@@ -1283,6 +1304,34 @@ var KeyboardHandler = class {
1283
1304
  }
1284
1305
  }
1285
1306
  };
1307
+ onPaste = (e) => {
1308
+ if (!this.shouldHandle(e.target)) return;
1309
+ const items = e.clipboardData?.items;
1310
+ let file = null;
1311
+ if (items) {
1312
+ for (const it of items) {
1313
+ if (it.kind === "file" && it.type.startsWith("image/")) {
1314
+ file = it.getAsFile();
1315
+ break;
1316
+ }
1317
+ }
1318
+ }
1319
+ if (file) {
1320
+ e.preventDefault();
1321
+ const world = this.deps.getLastPointerWorld() ?? this.deps.getCenteredWorld();
1322
+ if (this.deps.onPaste) {
1323
+ this.deps.onPaste(e, world);
1324
+ return;
1325
+ }
1326
+ const reader = new FileReader();
1327
+ reader.onload = () => {
1328
+ if (typeof reader.result === "string") this.deps.addImage(reader.result, world);
1329
+ };
1330
+ reader.readAsDataURL(file);
1331
+ return;
1332
+ }
1333
+ this.deps.actions.paste();
1334
+ };
1286
1335
  runAction(action, e) {
1287
1336
  switch (action) {
1288
1337
  case "delete":
@@ -1431,7 +1480,11 @@ var InputHandler = class {
1431
1480
  this.spaceHeld = v;
1432
1481
  },
1433
1482
  getActivePointerCount: () => this.activePointers.size,
1434
- dispatchToolHover: (e) => this.dispatchToolHover(e)
1483
+ dispatchToolHover: (e) => this.dispatchToolHover(e),
1484
+ addImage: options.addImage ?? (() => ""),
1485
+ getLastPointerWorld: () => this.lastPointerWorld(),
1486
+ getCenteredWorld: options.getCenteredWorld ?? (() => ({ x: 0, y: 0 })),
1487
+ onPaste: options.onPaste
1435
1488
  });
1436
1489
  this.element.style.touchAction = "none";
1437
1490
  if (this.scope === "focus") {
@@ -2091,12 +2144,12 @@ function smoothToSegments(points) {
2091
2144
  return [{ start: p0, cp1: { x: mx, y: my }, cp2: { x: mx, y: my }, end: p1 }];
2092
2145
  }
2093
2146
  const segments = [];
2094
- const n = points.length;
2095
- for (let i = 0; i < n - 1; i++) {
2147
+ const n2 = points.length;
2148
+ for (let i = 0; i < n2 - 1; i++) {
2096
2149
  const p0 = points[Math.max(0, i - 1)];
2097
2150
  const p1 = points[i];
2098
2151
  const p2 = points[i + 1];
2099
- const p3 = points[Math.min(n - 1, i + 2)];
2152
+ const p3 = points[Math.min(n2 - 1, i + 2)];
2100
2153
  if (!p0 || !p1 || !p2 || !p3) continue;
2101
2154
  const cp1 = {
2102
2155
  x: p1.x + (p2.x - p0.x) / 6,
@@ -2786,11 +2839,11 @@ function pixelToOffset(x, y, cellSize, orientation) {
2786
2839
  const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
2787
2840
  return { col, row: Math.round((y - offsetY) / hexH) };
2788
2841
  }
2789
- function enumerateHexRing(centerQ, centerR, n, orientation, cellSize) {
2842
+ function enumerateHexRing(centerQ, centerR, n2, orientation, cellSize) {
2790
2843
  const cells = [];
2791
- for (let dq = -n; dq <= n; dq++) {
2792
- const rMin = Math.max(-n, -dq - n);
2793
- const rMax = Math.min(n, -dq + n);
2844
+ for (let dq = -n2; dq <= n2; dq++) {
2845
+ const rMin = Math.max(-n2, -dq - n2);
2846
+ const rMax = Math.min(n2, -dq + n2);
2794
2847
  for (let dr = rMin; dr <= rMax; dr++) {
2795
2848
  const absQ = centerQ + dq;
2796
2849
  const absR = centerR + dr;
@@ -2811,28 +2864,28 @@ function getHexDistance(a, b, cellSize, orientation) {
2811
2864
  return Math.max(Math.abs(dq), Math.abs(dr), Math.abs(ds));
2812
2865
  }
2813
2866
  function getHexCellsInRadius(center2, radiusCells, cellSize, orientation) {
2814
- const n = Math.round(radiusCells);
2867
+ const n2 = Math.round(radiusCells);
2815
2868
  const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2816
2869
  const cube = offsetToCube(off.col, off.row, orientation);
2817
- if (n <= 0) {
2870
+ if (n2 <= 0) {
2818
2871
  return [offsetToPixel(off.col, off.row, cellSize, orientation)];
2819
2872
  }
2820
- return enumerateHexRing(cube.q, cube.r, n, orientation, cellSize);
2873
+ return enumerateHexRing(cube.q, cube.r, n2, orientation, cellSize);
2821
2874
  }
2822
2875
  function getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation) {
2823
- const n = Math.round(radiusCells);
2876
+ const n2 = Math.round(radiusCells);
2824
2877
  const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2825
2878
  const cube = offsetToCube(off.col, off.row, orientation);
2826
2879
  const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2827
- if (n <= 0) return [centerPixel];
2880
+ if (n2 <= 0) return [centerPixel];
2828
2881
  const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
2829
2882
  const step = Math.PI / 3;
2830
2883
  const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
2831
2884
  const halfAngle = Math.PI / 6 + 1e-6;
2832
2885
  const cells = [centerPixel];
2833
- for (let dq = -n; dq <= n; dq++) {
2834
- const rMin = Math.max(-n, -dq - n);
2835
- const rMax = Math.min(n, -dq + n);
2886
+ for (let dq = -n2; dq <= n2; dq++) {
2887
+ const rMin = Math.max(-n2, -dq - n2);
2888
+ const rMax = Math.min(n2, -dq + n2);
2836
2889
  for (let dr = rMin; dr <= rMax; dr++) {
2837
2890
  if (dq === 0 && dr === 0) continue;
2838
2891
  const absQ = cube.q + dq;
@@ -2856,23 +2909,23 @@ function getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation) {
2856
2909
  return cells;
2857
2910
  }
2858
2911
  function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
2859
- const n = Math.round(radiusCells);
2912
+ const n2 = Math.round(radiusCells);
2860
2913
  const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2861
2914
  const cube = offsetToCube(off.col, off.row, orientation);
2862
2915
  const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2863
- if (n <= 0) return [centerPixel];
2916
+ if (n2 <= 0) return [centerPixel];
2864
2917
  const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
2865
2918
  const step = Math.PI / 3;
2866
2919
  const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
2867
2920
  const cos = Math.cos(snappedAngle);
2868
2921
  const sin = Math.sin(snappedAngle);
2869
2922
  const snapUnit = Math.sqrt(3) * cellSize;
2870
- const lineLength = n * snapUnit;
2923
+ const lineLength = n2 * snapUnit;
2871
2924
  const halfWidth = snapUnit * 0.5 + 1e-6;
2872
2925
  const cells = [];
2873
- for (let dq = -n; dq <= n; dq++) {
2874
- const rMin = Math.max(-n, -dq - n);
2875
- const rMax = Math.min(n, -dq + n);
2926
+ for (let dq = -n2; dq <= n2; dq++) {
2927
+ const rMin = Math.max(-n2, -dq - n2);
2928
+ const rMax = Math.min(n2, -dq + n2);
2876
2929
  for (let dr = rMin; dr <= rMax; dr++) {
2877
2930
  const absQ = cube.q + dq;
2878
2931
  const absR = cube.r + dr;
@@ -2894,17 +2947,17 @@ function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
2894
2947
  return cells;
2895
2948
  }
2896
2949
  function getHexCellsInSquare(center2, radiusCells, cellSize, orientation) {
2897
- const n = Math.round(radiusCells);
2950
+ const n2 = Math.round(radiusCells);
2898
2951
  const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2899
2952
  const cube = offsetToCube(off.col, off.row, orientation);
2900
2953
  const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2901
- if (n <= 0) return [centerPixel];
2954
+ if (n2 <= 0) return [centerPixel];
2902
2955
  const snapUnit = Math.sqrt(3) * cellSize;
2903
- const halfSide = n * snapUnit / 2;
2956
+ const halfSide = n2 * snapUnit / 2;
2904
2957
  const cells = [];
2905
- for (let dq = -n; dq <= n; dq++) {
2906
- const rMin = Math.max(-n, -dq - n);
2907
- const rMax = Math.min(n, -dq + n);
2958
+ for (let dq = -n2; dq <= n2; dq++) {
2959
+ const rMin = Math.max(-n2, -dq - n2);
2960
+ const rMax = Math.min(n2, -dq + n2);
2908
2961
  for (let dr = rMin; dr <= rMax; dr++) {
2909
2962
  const absQ = cube.q + dq;
2910
2963
  const absR = cube.r + dr;
@@ -3091,6 +3144,58 @@ function getSquareGridLines(bounds, cellSize) {
3091
3144
  }
3092
3145
  return { verticals, horizontals };
3093
3146
  }
3147
+ function getHexVertices(cx, cy, circumradius, orientation) {
3148
+ const vertices = [];
3149
+ const angleOffset = orientation === "pointy" ? -Math.PI / 2 : 0;
3150
+ for (let i = 0; i < 6; i++) {
3151
+ const angle = Math.PI / 3 * i + angleOffset;
3152
+ vertices.push({
3153
+ x: cx + circumradius * Math.cos(angle),
3154
+ y: cy + circumradius * Math.sin(angle)
3155
+ });
3156
+ }
3157
+ return vertices;
3158
+ }
3159
+ function getHexCenters(bounds, circumradius, orientation) {
3160
+ if (circumradius <= 0) return [];
3161
+ const centers = [];
3162
+ if (orientation === "pointy") {
3163
+ const hexW = Math.sqrt(3) * circumradius;
3164
+ const hexH = 2 * circumradius;
3165
+ const rowH = hexH * 0.75;
3166
+ const startRow = Math.floor((bounds.minY - circumradius) / rowH);
3167
+ const endRow = Math.ceil((bounds.maxY + circumradius) / rowH);
3168
+ const startCol = Math.floor((bounds.minX - hexW) / hexW);
3169
+ const endCol = Math.ceil((bounds.maxX + hexW) / hexW);
3170
+ for (let row = startRow; row <= endRow; row++) {
3171
+ const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
3172
+ for (let col = startCol; col <= endCol; col++) {
3173
+ centers.push({
3174
+ x: col * hexW + offsetX,
3175
+ y: row * rowH
3176
+ });
3177
+ }
3178
+ }
3179
+ } else {
3180
+ const hexW = 2 * circumradius;
3181
+ const hexH = Math.sqrt(3) * circumradius;
3182
+ const colW = hexW * 0.75;
3183
+ const startCol = Math.floor((bounds.minX - circumradius) / colW);
3184
+ const endCol = Math.ceil((bounds.maxX + circumradius) / colW);
3185
+ const startRow = Math.floor((bounds.minY - hexH) / hexH);
3186
+ const endRow = Math.ceil((bounds.maxY + hexH) / hexH);
3187
+ for (let col = startCol; col <= endCol; col++) {
3188
+ const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
3189
+ for (let row = startRow; row <= endRow; row++) {
3190
+ centers.push({
3191
+ x: col * colW,
3192
+ y: row * hexH + offsetY
3193
+ });
3194
+ }
3195
+ }
3196
+ }
3197
+ return centers;
3198
+ }
3094
3199
  function renderSquareGrid(ctx, bounds, cellSize, strokeColor, strokeWidth, opacity) {
3095
3200
  if (cellSize <= 0) return;
3096
3201
  const { verticals, horizontals } = getSquareGridLines(bounds, cellSize);
@@ -3489,6 +3594,8 @@ function createHtmlElement(input) {
3489
3594
  };
3490
3595
  if (input.domId) el.domId = input.domId;
3491
3596
  if (input.interactive) el.interactive = input.interactive;
3597
+ if (input.htmlType) el.htmlType = input.htmlType;
3598
+ if (input.data) el.data = input.data;
3492
3599
  return el;
3493
3600
  }
3494
3601
  function createShape(input) {
@@ -4769,6 +4876,350 @@ async function exportImage(store, options = {}, layerManager) {
4769
4876
  });
4770
4877
  }
4771
4878
 
4879
+ // src/canvas/export-svg.ts
4880
+ var ARROWHEAD_LENGTH2 = 12;
4881
+ var ARROWHEAD_ANGLE2 = Math.PI / 6;
4882
+ var ARROW_LABEL_FONT_SIZE2 = 14;
4883
+ function esc(s) {
4884
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
4885
+ }
4886
+ var n = (v) => Number.isFinite(v) ? `${Math.round(v * 1e3) / 1e3}` : "0";
4887
+ function elementCenter(el) {
4888
+ const b = getElementBounds(el);
4889
+ if (!b) return null;
4890
+ return { x: b.x + b.w / 2, y: b.y + b.h / 2 };
4891
+ }
4892
+ function withRotationSvg(el, fragment) {
4893
+ const angle = el.rotation ?? 0;
4894
+ if (!angle || !fragment) return fragment;
4895
+ const c = elementCenter(el);
4896
+ if (!c) return fragment;
4897
+ const deg = angle * 180 / Math.PI;
4898
+ return `<g transform="rotate(${n(deg)} ${n(c.x)} ${n(c.y)})">${fragment}</g>`;
4899
+ }
4900
+ var WIDTH_QUANTUM2 = 0.25;
4901
+ function emitStroke(stroke) {
4902
+ if (stroke.points.length < 2) return "";
4903
+ const data = getStrokeRenderData(stroke);
4904
+ const { x: ox, y: oy } = stroke.position;
4905
+ const byWidth = /* @__PURE__ */ new Map();
4906
+ for (let i = 0; i < data.segments.length; i++) {
4907
+ const seg = data.segments[i];
4908
+ const w = data.widths[i];
4909
+ if (!seg || w === void 0) continue;
4910
+ const q = Math.max(WIDTH_QUANTUM2, Math.round(w / WIDTH_QUANTUM2) * WIDTH_QUANTUM2);
4911
+ let parts = byWidth.get(q);
4912
+ if (!parts) {
4913
+ parts = [];
4914
+ byWidth.set(q, parts);
4915
+ }
4916
+ parts.push(
4917
+ `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)}`
4918
+ );
4919
+ }
4920
+ const blend = stroke.blendMode === "multiply" ? ' style="mix-blend-mode:multiply"' : "";
4921
+ let out = "";
4922
+ for (const [width, parts] of byWidth) {
4923
+ 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} />`;
4924
+ }
4925
+ return out;
4926
+ }
4927
+ function emitShape(shape) {
4928
+ const { x, y } = shape.position;
4929
+ const { w, h } = shape.size;
4930
+ const fill = shape.fillColor !== "none" && shape.shape !== "line" ? esc(shape.fillColor) : "none";
4931
+ const stroke = shape.strokeWidth > 0 ? esc(shape.strokeColor) : "none";
4932
+ const sw = shape.strokeWidth > 0 ? ` stroke-width="${n(shape.strokeWidth)}"` : "";
4933
+ switch (shape.shape) {
4934
+ case "rectangle":
4935
+ return `<rect x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" fill="${fill}" stroke="${stroke}"${sw} />`;
4936
+ case "ellipse":
4937
+ 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} />`;
4938
+ case "line": {
4939
+ const [a, b] = lineEndpoints(shape);
4940
+ return `<line x1="${n(a.x)}" y1="${n(a.y)}" x2="${n(b.x)}" y2="${n(b.y)}" stroke="${stroke}"${sw} stroke-linecap="round" />`;
4941
+ }
4942
+ }
4943
+ }
4944
+ function emitArrow(arrow, store) {
4945
+ const geometry = getArrowRenderGeometry(arrow);
4946
+ const { visualFrom: from, visualTo: to } = getVisualEndpoints(arrow, geometry, store);
4947
+ let d;
4948
+ if (arrow.bend !== 0) {
4949
+ const cp = geometry.controlPoint ?? getArrowControlPoint(from, to, arrow.bend);
4950
+ d = `M${n(from.x)} ${n(from.y)} Q${n(cp.x)} ${n(cp.y)} ${n(to.x)} ${n(to.y)}`;
4951
+ } else {
4952
+ d = `M${n(from.x)} ${n(from.y)} L${n(to.x)} ${n(to.y)}`;
4953
+ }
4954
+ const dash = arrow.fromBinding || arrow.toBinding ? ' stroke-dasharray="8 4"' : "";
4955
+ let out = `<path d="${d}" fill="none" stroke="${esc(arrow.color)}" stroke-width="${n(arrow.width)}" stroke-linecap="round"${dash} />`;
4956
+ const angle = geometry.tangentEnd;
4957
+ const p1x = to.x - ARROWHEAD_LENGTH2 * Math.cos(angle - ARROWHEAD_ANGLE2);
4958
+ const p1y = to.y - ARROWHEAD_LENGTH2 * Math.sin(angle - ARROWHEAD_ANGLE2);
4959
+ const p2x = to.x - ARROWHEAD_LENGTH2 * Math.cos(angle + ARROWHEAD_ANGLE2);
4960
+ const p2y = to.y - ARROWHEAD_LENGTH2 * Math.sin(angle + ARROWHEAD_ANGLE2);
4961
+ out += `<polygon points="${n(to.x)},${n(to.y)} ${n(p1x)},${n(p1y)} ${n(p2x)},${n(p2y)}" fill="${esc(arrow.color)}" />`;
4962
+ if (arrow.label && arrow.label.length > 0) {
4963
+ const mid = getArrowMidpoint(arrow.from, arrow.to, arrow.bend);
4964
+ const approxW = arrow.label.length * ARROW_LABEL_FONT_SIZE2 * 0.6;
4965
+ const padX = 6;
4966
+ const padY = 4;
4967
+ const lw = approxW + padX * 2;
4968
+ const lh = ARROW_LABEL_FONT_SIZE2 + padY * 2;
4969
+ 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)" />`;
4970
+ 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>`;
4971
+ }
4972
+ return out;
4973
+ }
4974
+ function emitImage(image, dataUri) {
4975
+ const href = dataUri ?? image.src;
4976
+ if (!href) return "";
4977
+ const { x, y } = image.position;
4978
+ const { w, h } = image.size;
4979
+ return `<image href="${esc(href)}" x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" />`;
4980
+ }
4981
+ function emitText(text) {
4982
+ if (!text.text) return "";
4983
+ const pad = 2;
4984
+ let anchor = "start";
4985
+ let textX = text.position.x + pad;
4986
+ if (text.textAlign === "center") {
4987
+ anchor = "middle";
4988
+ textX = text.position.x + text.size.w / 2;
4989
+ } else if (text.textAlign === "right") {
4990
+ anchor = "end";
4991
+ textX = text.position.x + text.size.w - pad;
4992
+ }
4993
+ const lineHeight = text.fontSize * 1.4;
4994
+ const lines = text.text.split("\n");
4995
+ let out = "";
4996
+ for (let i = 0; i < lines.length; i++) {
4997
+ const line = lines[i];
4998
+ if (line === void 0) continue;
4999
+ const y = text.position.y + pad + i * lineHeight;
5000
+ 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>`;
5001
+ }
5002
+ return out;
5003
+ }
5004
+ function emitNote(note, rasterScale) {
5005
+ const { x, y } = note.position;
5006
+ const { w, h } = note.size;
5007
+ if (typeof document === "undefined") return emitNotePlaceholder(note);
5008
+ const canvas = document.createElement("canvas");
5009
+ canvas.width = Math.max(1, Math.ceil(w * rasterScale));
5010
+ canvas.height = Math.max(1, Math.ceil(h * rasterScale));
5011
+ const ctx = canvas.getContext("2d");
5012
+ if (!ctx) return emitNotePlaceholder(note);
5013
+ ctx.scale(rasterScale, rasterScale);
5014
+ ctx.translate(-x, -y);
5015
+ renderNoteOnCanvas(ctx, note);
5016
+ let dataUri;
5017
+ try {
5018
+ dataUri = canvas.toDataURL();
5019
+ } catch {
5020
+ return emitNotePlaceholder(note);
5021
+ }
5022
+ if (!dataUri || !dataUri.startsWith("data:")) return emitNotePlaceholder(note);
5023
+ return `<image href="${esc(dataUri)}" x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" />`;
5024
+ }
5025
+ function emitNotePlaceholder(note) {
5026
+ const { x, y } = note.position;
5027
+ const { w, h } = note.size;
5028
+ return `<rect x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" rx="4" fill="${esc(note.backgroundColor)}" />`;
5029
+ }
5030
+ function emitGrid(grid, bounds) {
5031
+ if (grid.cellSize <= 0) return "";
5032
+ const vb = {
5033
+ minX: bounds.x,
5034
+ minY: bounds.y,
5035
+ maxX: bounds.x + bounds.w,
5036
+ maxY: bounds.y + bounds.h
5037
+ };
5038
+ const stroke = esc(grid.strokeColor);
5039
+ const sw = n(grid.strokeWidth);
5040
+ const op = n(grid.opacity);
5041
+ if (grid.gridType === "hex") {
5042
+ const centers = getHexCenters(vb, grid.cellSize, grid.hexOrientation);
5043
+ let d2 = "";
5044
+ for (const c of centers) {
5045
+ const verts = getHexVertices(c.x, c.y, grid.cellSize, grid.hexOrientation);
5046
+ const first = verts[0];
5047
+ if (!first) continue;
5048
+ d2 += `M${n(first.x)} ${n(first.y)}`;
5049
+ for (let i = 1; i < verts.length; i++) {
5050
+ const v = verts[i];
5051
+ if (v) d2 += `L${n(v.x)} ${n(v.y)}`;
5052
+ }
5053
+ d2 += "Z";
5054
+ }
5055
+ return `<path d="${d2}" fill="none" stroke="${stroke}" stroke-width="${sw}" opacity="${op}" />`;
5056
+ }
5057
+ const { verticals, horizontals } = getSquareGridLines(vb, grid.cellSize);
5058
+ let d = "";
5059
+ for (const gx of verticals) d += `M${n(gx)} ${n(vb.minY)}L${n(gx)} ${n(vb.maxY)}`;
5060
+ for (const gy of horizontals) d += `M${n(vb.minX)} ${n(gy)}L${n(vb.maxX)} ${n(gy)}`;
5061
+ return `<path d="${d}" fill="none" stroke="${stroke}" stroke-width="${sw}" opacity="${op}" />`;
5062
+ }
5063
+ function emitTemplate(template, grid) {
5064
+ if (grid && grid.gridType === "hex") {
5065
+ return emitHexTemplate(template, grid);
5066
+ }
5067
+ return emitGeometricTemplate(template);
5068
+ }
5069
+ function emitGeometricTemplate(t) {
5070
+ const { x: cx, y: cy } = t.position;
5071
+ const r = t.radius;
5072
+ const fill = esc(t.fillColor);
5073
+ const stroke = esc(t.strokeColor);
5074
+ const sw = n(t.strokeWidth);
5075
+ const op = n(t.opacity);
5076
+ const attrs = `fill="${fill}" stroke="${stroke}" stroke-width="${sw}" opacity="${op}"`;
5077
+ switch (t.templateShape) {
5078
+ case "circle":
5079
+ return `<circle cx="${n(cx)}" cy="${n(cy)}" r="${n(r)}" ${attrs} />`;
5080
+ case "square":
5081
+ return `<rect x="${n(cx - r / 2)}" y="${n(cy - r / 2)}" width="${n(r)}" height="${n(r)}" ${attrs} />`;
5082
+ case "cone": {
5083
+ const halfAngle = Math.atan(0.5);
5084
+ const a0 = t.angle - halfAngle;
5085
+ const a1 = t.angle + halfAngle;
5086
+ const p0x = cx + r * Math.cos(a0);
5087
+ const p0y = cy + r * Math.sin(a0);
5088
+ const p1x = cx + r * Math.cos(a1);
5089
+ const p1y = cy + r * Math.sin(a1);
5090
+ const large = a1 - a0 > Math.PI ? 1 : 0;
5091
+ 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} />`;
5092
+ }
5093
+ case "line": {
5094
+ const halfW = r / 12;
5095
+ const cos = Math.cos(t.angle);
5096
+ const sin = Math.sin(t.angle);
5097
+ const perpX = -sin * halfW;
5098
+ const perpY = cos * halfW;
5099
+ const pts = [
5100
+ [cx + perpX, cy + perpY],
5101
+ [cx + r * cos + perpX, cy + r * sin + perpY],
5102
+ [cx + r * cos - perpX, cy + r * sin - perpY],
5103
+ [cx - perpX, cy - perpY]
5104
+ ].map(([px, py]) => `${n(px ?? 0)},${n(py ?? 0)}`).join(" ");
5105
+ return `<polygon points="${pts}" ${attrs} />`;
5106
+ }
5107
+ }
5108
+ }
5109
+ function emitHexTemplate(t, grid) {
5110
+ const cellSize = grid.cellSize;
5111
+ const orientation = grid.hexOrientation;
5112
+ const snapUnit = Math.sqrt(3) * cellSize;
5113
+ const radiusCells = t.radius / snapUnit;
5114
+ const center2 = t.position;
5115
+ let cells;
5116
+ switch (t.templateShape) {
5117
+ case "circle":
5118
+ cells = getHexCellsInRadius(center2, radiusCells, cellSize, orientation);
5119
+ break;
5120
+ case "cone":
5121
+ cells = getHexCellsInCone(center2, t.angle, radiusCells, cellSize, orientation);
5122
+ break;
5123
+ case "line":
5124
+ cells = getHexCellsInLine(center2, t.angle, radiusCells, cellSize, orientation);
5125
+ break;
5126
+ case "square":
5127
+ cells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
5128
+ break;
5129
+ }
5130
+ let d = "";
5131
+ for (const cell of cells) {
5132
+ const verts = getHexVertices(cell.x, cell.y, cellSize, orientation);
5133
+ const first = verts[0];
5134
+ if (!first) continue;
5135
+ d += `M${n(first.x)} ${n(first.y)}`;
5136
+ for (let i = 1; i < verts.length; i++) {
5137
+ const v = verts[i];
5138
+ if (v) d += `L${n(v.x)} ${n(v.y)}`;
5139
+ }
5140
+ d += "Z";
5141
+ }
5142
+ return `<path d="${d}" fill="${esc(t.fillColor)}" stroke="${esc(t.strokeColor)}" stroke-width="${n(t.strokeWidth)}" opacity="${n(t.opacity)}" />`;
5143
+ }
5144
+ async function exportSvg(store, options = {}, layerManager) {
5145
+ const padding = options.padding ?? 0;
5146
+ const rasterScale = options.rasterScale ?? 2;
5147
+ const filter = options.filter;
5148
+ const allElements = store.getAll();
5149
+ let visibleElements = layerManager ? allElements.filter((el) => layerManager.isLayerVisible(el.layerId)) : allElements;
5150
+ if (filter) visibleElements = visibleElements.filter(filter);
5151
+ const bounds = computeBounds(visibleElements, padding);
5152
+ if (!bounds) {
5153
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
5154
+ }
5155
+ const remoteImages = visibleElements.filter(
5156
+ (el) => el.type === "image" && !el.src.startsWith("data:")
5157
+ );
5158
+ const imageCache = await loadImages(remoteImages);
5159
+ const imageDataUris = encodeImages(visibleElements, imageCache, rasterScale);
5160
+ const grids = visibleElements.filter((el) => el.type === "grid");
5161
+ const firstGrid = grids[0];
5162
+ let body = "";
5163
+ if (options.background) {
5164
+ body += `<rect x="${n(bounds.x)}" y="${n(bounds.y)}" width="${n(bounds.w)}" height="${n(bounds.h)}" fill="${esc(options.background)}" />`;
5165
+ }
5166
+ for (const el of visibleElements) {
5167
+ body += emitElement(el, imageDataUris, rasterScale, firstGrid, store);
5168
+ }
5169
+ for (const grid of grids) {
5170
+ body += emitGrid(grid, bounds);
5171
+ }
5172
+ 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>`;
5173
+ }
5174
+ function emitElement(el, imageDataUris, rasterScale, firstGrid, store) {
5175
+ switch (el.type) {
5176
+ case "stroke":
5177
+ return withRotationSvg(el, emitStroke(el));
5178
+ case "shape":
5179
+ return withRotationSvg(el, emitShape(el));
5180
+ case "arrow":
5181
+ return emitArrow(el, store);
5182
+ case "image":
5183
+ return withRotationSvg(el, emitImage(el, imageDataUris.get(el.id)));
5184
+ case "text":
5185
+ return withRotationSvg(el, emitText(el));
5186
+ case "note":
5187
+ return withRotationSvg(el, emitNote(el, rasterScale));
5188
+ case "template":
5189
+ return emitTemplate(el, firstGrid);
5190
+ case "grid":
5191
+ return "";
5192
+ case "html":
5193
+ return "";
5194
+ default:
5195
+ return "";
5196
+ }
5197
+ }
5198
+ function encodeImages(elements, imageCache, rasterScale) {
5199
+ const out = /* @__PURE__ */ new Map();
5200
+ for (const el of elements) {
5201
+ if (el.type !== "image") continue;
5202
+ if (el.src.startsWith("data:")) {
5203
+ out.set(el.id, el.src);
5204
+ continue;
5205
+ }
5206
+ const img = imageCache.get(el.id);
5207
+ if (!img || typeof document === "undefined") continue;
5208
+ const canvas = document.createElement("canvas");
5209
+ canvas.width = Math.max(1, Math.ceil(el.size.w * rasterScale));
5210
+ canvas.height = Math.max(1, Math.ceil(el.size.h * rasterScale));
5211
+ const ctx = canvas.getContext("2d");
5212
+ if (!ctx) continue;
5213
+ try {
5214
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
5215
+ const uri = canvas.toDataURL();
5216
+ if (uri.startsWith("data:")) out.set(el.id, uri);
5217
+ } catch {
5218
+ }
5219
+ }
5220
+ return out;
5221
+ }
5222
+
4772
5223
  // src/layers/layer-manager.ts
4773
5224
  var LayerManager = class {
4774
5225
  constructor(store) {
@@ -5929,13 +6380,13 @@ var SelectionOps = class {
5929
6380
  if (!first || !last) return;
5930
6381
  const c0 = center2(first.bounds);
5931
6382
  const cN = center2(last.bounds);
5932
- const n = sorted.length;
6383
+ const n2 = sorted.length;
5933
6384
  this.deps.recorder.begin();
5934
6385
  const moved = [];
5935
- for (let i = 1; i < n - 1; i++) {
6386
+ for (let i = 1; i < n2 - 1; i++) {
5936
6387
  const item = sorted[i];
5937
6388
  if (!item || !this.isMovable(item.el)) continue;
5938
- const target = c0 + i * (cN - c0) / (n - 1);
6389
+ const target = c0 + i * (cN - c0) / (n2 - 1);
5939
6390
  const delta = target - center2(item.bounds);
5940
6391
  if (delta === 0) continue;
5941
6392
  const [dx, dy] = axis === "horizontal" ? [delta, 0] : [0, delta];
@@ -6273,7 +6724,10 @@ var Viewport = class {
6273
6724
  this.getSelectTool()?.selectAtPoint(world, this.toolContext);
6274
6725
  this.openContextMenu(screenPos);
6275
6726
  },
6276
- shortcuts: options.shortcuts
6727
+ shortcuts: options.shortcuts,
6728
+ addImage: (src, world) => this.addImage(src, world),
6729
+ getCenteredWorld: () => this.centeredPosition({ w: 300, h: 200 }),
6730
+ onPaste: options.onPaste
6277
6731
  });
6278
6732
  if (options.contextMenu !== false) {
6279
6733
  this.contextMenu = new ContextMenu({
@@ -6413,6 +6867,7 @@ var Viewport = class {
6413
6867
  gridController;
6414
6868
  interactions;
6415
6869
  contextMenu = null;
6870
+ htmlRenderers = /* @__PURE__ */ new Map();
6416
6871
  get ctx() {
6417
6872
  return this.canvasEl.getContext("2d");
6418
6873
  }
@@ -6454,6 +6909,9 @@ var Viewport = class {
6454
6909
  async exportImage(options) {
6455
6910
  return exportImage(this.store, options, this.layerManager);
6456
6911
  }
6912
+ async exportSVG(options) {
6913
+ return exportSvg(this.store, options, this.layerManager);
6914
+ }
6457
6915
  loadState(state) {
6458
6916
  this.inputHandler.flushPendingHistory();
6459
6917
  this.historyRecorder.pause();
@@ -6467,19 +6925,24 @@ var Viewport = class {
6467
6925
  this.layerManager.setActiveLayer(state.activeLayerId);
6468
6926
  }
6469
6927
  this.domNodeManager.reattachHtmlContent(this.store);
6470
- if (this.onHtmlElementMount) {
6471
- for (const el of this.store.getElementsByType("html")) {
6472
- if (!this.domNodeManager.hasContent(el.id)) {
6473
- this.domNodeManager.syncDomNode(el);
6474
- const node = this.domNodeManager.getNode(el.id);
6475
- if (node) {
6476
- this.onHtmlElementMount(el.id, el.domId, node);
6477
- node.dataset["initialized"] = "true";
6478
- Object.assign(node.style, {
6479
- overflow: "hidden",
6480
- pointerEvents: el.interactive ? "auto" : "none"
6481
- });
6482
- }
6928
+ for (const el of this.store.getElementsByType("html")) {
6929
+ if (this.domNodeManager.hasContent(el.id)) continue;
6930
+ const factory = el.htmlType ? this.htmlRenderers.get(el.htmlType) : void 0;
6931
+ const rebuilt = factory ? factory(el) : null;
6932
+ if (rebuilt) {
6933
+ this.domNodeManager.storeHtmlContent(el.id, rebuilt);
6934
+ this.domNodeManager.syncDomNode(el);
6935
+ }
6936
+ if (this.onHtmlElementMount) {
6937
+ if (!rebuilt) this.domNodeManager.syncDomNode(el);
6938
+ const node = this.domNodeManager.getNode(el.id);
6939
+ if (node) {
6940
+ this.onHtmlElementMount(el.id, el.domId, node);
6941
+ node.dataset["initialized"] = "true";
6942
+ Object.assign(node.style, {
6943
+ overflow: "hidden",
6944
+ pointerEvents: el.interactive ? "auto" : "none"
6945
+ });
6483
6946
  }
6484
6947
  }
6485
6948
  }
@@ -6525,12 +6988,14 @@ var Viewport = class {
6525
6988
  this.requestRender();
6526
6989
  return image.id;
6527
6990
  }
6528
- addHtmlElement(dom, position, size = { w: 200, h: 150 }) {
6991
+ addHtmlElement(dom, position, size = { w: 200, h: 150 }, opts) {
6529
6992
  const domId = dom.id || void 0;
6530
6993
  const el = createHtmlElement({
6531
6994
  position,
6532
6995
  size,
6533
6996
  domId,
6997
+ htmlType: opts?.htmlType,
6998
+ data: opts?.data,
6534
6999
  layerId: this.layerManager.activeLayerId
6535
7000
  });
6536
7001
  this.domNodeManager.storeHtmlContent(el.id, dom);
@@ -6571,6 +7036,9 @@ var Viewport = class {
6571
7036
  this.layerManager.removeLayer(id);
6572
7037
  this.historyRecorder.commit();
6573
7038
  }
7039
+ registerHtmlRenderer(htmlType, factory) {
7040
+ this.htmlRenderers.set(htmlType, factory);
7041
+ }
6574
7042
  updateHtmlElement(id, newContent) {
6575
7043
  const el = this.store.getById(id);
6576
7044
  if (!el) throw new Error(`Element not found: ${id}`);
@@ -9043,7 +9511,7 @@ var TemplateTool = class {
9043
9511
  };
9044
9512
 
9045
9513
  // src/index.ts
9046
- var VERSION = "0.38.6";
9514
+ var VERSION = "0.39.0";
9047
9515
  export {
9048
9516
  ArrowTool,
9049
9517
  AutoSave,
@@ -9077,6 +9545,7 @@ export {
9077
9545
  createText,
9078
9546
  drawHexPath,
9079
9547
  exportImage,
9548
+ exportSvg,
9080
9549
  getActiveFormats,
9081
9550
  getArrowBounds,
9082
9551
  getArrowControlPoint,