@fieldnotes/core 0.38.7 → 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
@@ -1084,7 +1084,6 @@ var DEFAULT_BINDINGS = [
1084
1084
  ["redo", ["mod+y", "mod+shift+z"]],
1085
1085
  ["select-all", ["mod+a"]],
1086
1086
  ["copy", ["mod+c"]],
1087
- ["paste", ["mod+v"]],
1088
1087
  ["duplicate", ["mod+d"]],
1089
1088
  ["z-forward", ["]"]],
1090
1089
  ["z-backward", ["["]],
@@ -1259,6 +1258,7 @@ var KeyboardHandler = class {
1259
1258
  this.shortcutMap = new ShortcutMap(deps.shortcuts?.bindings);
1260
1259
  window.addEventListener("keydown", this.onKeyDown, { signal: deps.abortSignal });
1261
1260
  window.addEventListener("keyup", this.onKeyUp, { signal: deps.abortSignal });
1261
+ window.addEventListener("paste", this.onPaste, { signal: deps.abortSignal });
1262
1262
  }
1263
1263
  shortcutMap;
1264
1264
  get shortcuts() {
@@ -1274,12 +1274,15 @@ var KeyboardHandler = class {
1274
1274
  zoomToLevel(level) {
1275
1275
  this.deps.camera.zoomAt(level, this.viewportCenter());
1276
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
+ }
1277
1284
  onKeyDown = (e) => {
1278
- const target = e.target;
1279
- if (target?.isContentEditable) return;
1280
- const tag = target?.tagName;
1281
- if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
1282
- if (!this.isInScope()) return;
1285
+ if (!this.shouldHandle(e.target)) return;
1283
1286
  if (e.key === " ") {
1284
1287
  this.deps.setSpaceHeld(true);
1285
1288
  }
@@ -1301,6 +1304,34 @@ var KeyboardHandler = class {
1301
1304
  }
1302
1305
  }
1303
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
+ };
1304
1335
  runAction(action, e) {
1305
1336
  switch (action) {
1306
1337
  case "delete":
@@ -1449,7 +1480,11 @@ var InputHandler = class {
1449
1480
  this.spaceHeld = v;
1450
1481
  },
1451
1482
  getActivePointerCount: () => this.activePointers.size,
1452
- 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
1453
1488
  });
1454
1489
  this.element.style.touchAction = "none";
1455
1490
  if (this.scope === "focus") {
@@ -2109,12 +2144,12 @@ function smoothToSegments(points) {
2109
2144
  return [{ start: p0, cp1: { x: mx, y: my }, cp2: { x: mx, y: my }, end: p1 }];
2110
2145
  }
2111
2146
  const segments = [];
2112
- const n = points.length;
2113
- for (let i = 0; i < n - 1; i++) {
2147
+ const n2 = points.length;
2148
+ for (let i = 0; i < n2 - 1; i++) {
2114
2149
  const p0 = points[Math.max(0, i - 1)];
2115
2150
  const p1 = points[i];
2116
2151
  const p2 = points[i + 1];
2117
- const p3 = points[Math.min(n - 1, i + 2)];
2152
+ const p3 = points[Math.min(n2 - 1, i + 2)];
2118
2153
  if (!p0 || !p1 || !p2 || !p3) continue;
2119
2154
  const cp1 = {
2120
2155
  x: p1.x + (p2.x - p0.x) / 6,
@@ -2804,11 +2839,11 @@ function pixelToOffset(x, y, cellSize, orientation) {
2804
2839
  const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
2805
2840
  return { col, row: Math.round((y - offsetY) / hexH) };
2806
2841
  }
2807
- function enumerateHexRing(centerQ, centerR, n, orientation, cellSize) {
2842
+ function enumerateHexRing(centerQ, centerR, n2, orientation, cellSize) {
2808
2843
  const cells = [];
2809
- for (let dq = -n; dq <= n; dq++) {
2810
- const rMin = Math.max(-n, -dq - n);
2811
- 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);
2812
2847
  for (let dr = rMin; dr <= rMax; dr++) {
2813
2848
  const absQ = centerQ + dq;
2814
2849
  const absR = centerR + dr;
@@ -2829,28 +2864,28 @@ function getHexDistance(a, b, cellSize, orientation) {
2829
2864
  return Math.max(Math.abs(dq), Math.abs(dr), Math.abs(ds));
2830
2865
  }
2831
2866
  function getHexCellsInRadius(center2, radiusCells, cellSize, orientation) {
2832
- const n = Math.round(radiusCells);
2867
+ const n2 = Math.round(radiusCells);
2833
2868
  const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2834
2869
  const cube = offsetToCube(off.col, off.row, orientation);
2835
- if (n <= 0) {
2870
+ if (n2 <= 0) {
2836
2871
  return [offsetToPixel(off.col, off.row, cellSize, orientation)];
2837
2872
  }
2838
- return enumerateHexRing(cube.q, cube.r, n, orientation, cellSize);
2873
+ return enumerateHexRing(cube.q, cube.r, n2, orientation, cellSize);
2839
2874
  }
2840
2875
  function getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation) {
2841
- const n = Math.round(radiusCells);
2876
+ const n2 = Math.round(radiusCells);
2842
2877
  const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2843
2878
  const cube = offsetToCube(off.col, off.row, orientation);
2844
2879
  const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2845
- if (n <= 0) return [centerPixel];
2880
+ if (n2 <= 0) return [centerPixel];
2846
2881
  const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
2847
2882
  const step = Math.PI / 3;
2848
2883
  const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
2849
2884
  const halfAngle = Math.PI / 6 + 1e-6;
2850
2885
  const cells = [centerPixel];
2851
- for (let dq = -n; dq <= n; dq++) {
2852
- const rMin = Math.max(-n, -dq - n);
2853
- 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);
2854
2889
  for (let dr = rMin; dr <= rMax; dr++) {
2855
2890
  if (dq === 0 && dr === 0) continue;
2856
2891
  const absQ = cube.q + dq;
@@ -2874,23 +2909,23 @@ function getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation) {
2874
2909
  return cells;
2875
2910
  }
2876
2911
  function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
2877
- const n = Math.round(radiusCells);
2912
+ const n2 = Math.round(radiusCells);
2878
2913
  const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2879
2914
  const cube = offsetToCube(off.col, off.row, orientation);
2880
2915
  const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2881
- if (n <= 0) return [centerPixel];
2916
+ if (n2 <= 0) return [centerPixel];
2882
2917
  const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
2883
2918
  const step = Math.PI / 3;
2884
2919
  const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
2885
2920
  const cos = Math.cos(snappedAngle);
2886
2921
  const sin = Math.sin(snappedAngle);
2887
2922
  const snapUnit = Math.sqrt(3) * cellSize;
2888
- const lineLength = n * snapUnit;
2923
+ const lineLength = n2 * snapUnit;
2889
2924
  const halfWidth = snapUnit * 0.5 + 1e-6;
2890
2925
  const cells = [];
2891
- for (let dq = -n; dq <= n; dq++) {
2892
- const rMin = Math.max(-n, -dq - n);
2893
- 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);
2894
2929
  for (let dr = rMin; dr <= rMax; dr++) {
2895
2930
  const absQ = cube.q + dq;
2896
2931
  const absR = cube.r + dr;
@@ -2912,17 +2947,17 @@ function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
2912
2947
  return cells;
2913
2948
  }
2914
2949
  function getHexCellsInSquare(center2, radiusCells, cellSize, orientation) {
2915
- const n = Math.round(radiusCells);
2950
+ const n2 = Math.round(radiusCells);
2916
2951
  const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2917
2952
  const cube = offsetToCube(off.col, off.row, orientation);
2918
2953
  const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2919
- if (n <= 0) return [centerPixel];
2954
+ if (n2 <= 0) return [centerPixel];
2920
2955
  const snapUnit = Math.sqrt(3) * cellSize;
2921
- const halfSide = n * snapUnit / 2;
2956
+ const halfSide = n2 * snapUnit / 2;
2922
2957
  const cells = [];
2923
- for (let dq = -n; dq <= n; dq++) {
2924
- const rMin = Math.max(-n, -dq - n);
2925
- 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);
2926
2961
  for (let dr = rMin; dr <= rMax; dr++) {
2927
2962
  const absQ = cube.q + dq;
2928
2963
  const absR = cube.r + dr;
@@ -3109,6 +3144,58 @@ function getSquareGridLines(bounds, cellSize) {
3109
3144
  }
3110
3145
  return { verticals, horizontals };
3111
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
+ }
3112
3199
  function renderSquareGrid(ctx, bounds, cellSize, strokeColor, strokeWidth, opacity) {
3113
3200
  if (cellSize <= 0) return;
3114
3201
  const { verticals, horizontals } = getSquareGridLines(bounds, cellSize);
@@ -3507,6 +3594,8 @@ function createHtmlElement(input) {
3507
3594
  };
3508
3595
  if (input.domId) el.domId = input.domId;
3509
3596
  if (input.interactive) el.interactive = input.interactive;
3597
+ if (input.htmlType) el.htmlType = input.htmlType;
3598
+ if (input.data) el.data = input.data;
3510
3599
  return el;
3511
3600
  }
3512
3601
  function createShape(input) {
@@ -4787,6 +4876,350 @@ async function exportImage(store, options = {}, layerManager) {
4787
4876
  });
4788
4877
  }
4789
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
+
4790
5223
  // src/layers/layer-manager.ts
4791
5224
  var LayerManager = class {
4792
5225
  constructor(store) {
@@ -5947,13 +6380,13 @@ var SelectionOps = class {
5947
6380
  if (!first || !last) return;
5948
6381
  const c0 = center2(first.bounds);
5949
6382
  const cN = center2(last.bounds);
5950
- const n = sorted.length;
6383
+ const n2 = sorted.length;
5951
6384
  this.deps.recorder.begin();
5952
6385
  const moved = [];
5953
- for (let i = 1; i < n - 1; i++) {
6386
+ for (let i = 1; i < n2 - 1; i++) {
5954
6387
  const item = sorted[i];
5955
6388
  if (!item || !this.isMovable(item.el)) continue;
5956
- const target = c0 + i * (cN - c0) / (n - 1);
6389
+ const target = c0 + i * (cN - c0) / (n2 - 1);
5957
6390
  const delta = target - center2(item.bounds);
5958
6391
  if (delta === 0) continue;
5959
6392
  const [dx, dy] = axis === "horizontal" ? [delta, 0] : [0, delta];
@@ -6291,7 +6724,10 @@ var Viewport = class {
6291
6724
  this.getSelectTool()?.selectAtPoint(world, this.toolContext);
6292
6725
  this.openContextMenu(screenPos);
6293
6726
  },
6294
- 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
6295
6731
  });
6296
6732
  if (options.contextMenu !== false) {
6297
6733
  this.contextMenu = new ContextMenu({
@@ -6431,6 +6867,7 @@ var Viewport = class {
6431
6867
  gridController;
6432
6868
  interactions;
6433
6869
  contextMenu = null;
6870
+ htmlRenderers = /* @__PURE__ */ new Map();
6434
6871
  get ctx() {
6435
6872
  return this.canvasEl.getContext("2d");
6436
6873
  }
@@ -6472,6 +6909,9 @@ var Viewport = class {
6472
6909
  async exportImage(options) {
6473
6910
  return exportImage(this.store, options, this.layerManager);
6474
6911
  }
6912
+ async exportSVG(options) {
6913
+ return exportSvg(this.store, options, this.layerManager);
6914
+ }
6475
6915
  loadState(state) {
6476
6916
  this.inputHandler.flushPendingHistory();
6477
6917
  this.historyRecorder.pause();
@@ -6485,19 +6925,24 @@ var Viewport = class {
6485
6925
  this.layerManager.setActiveLayer(state.activeLayerId);
6486
6926
  }
6487
6927
  this.domNodeManager.reattachHtmlContent(this.store);
6488
- if (this.onHtmlElementMount) {
6489
- for (const el of this.store.getElementsByType("html")) {
6490
- if (!this.domNodeManager.hasContent(el.id)) {
6491
- this.domNodeManager.syncDomNode(el);
6492
- const node = this.domNodeManager.getNode(el.id);
6493
- if (node) {
6494
- this.onHtmlElementMount(el.id, el.domId, node);
6495
- node.dataset["initialized"] = "true";
6496
- Object.assign(node.style, {
6497
- overflow: "hidden",
6498
- pointerEvents: el.interactive ? "auto" : "none"
6499
- });
6500
- }
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
+ });
6501
6946
  }
6502
6947
  }
6503
6948
  }
@@ -6543,12 +6988,14 @@ var Viewport = class {
6543
6988
  this.requestRender();
6544
6989
  return image.id;
6545
6990
  }
6546
- addHtmlElement(dom, position, size = { w: 200, h: 150 }) {
6991
+ addHtmlElement(dom, position, size = { w: 200, h: 150 }, opts) {
6547
6992
  const domId = dom.id || void 0;
6548
6993
  const el = createHtmlElement({
6549
6994
  position,
6550
6995
  size,
6551
6996
  domId,
6997
+ htmlType: opts?.htmlType,
6998
+ data: opts?.data,
6552
6999
  layerId: this.layerManager.activeLayerId
6553
7000
  });
6554
7001
  this.domNodeManager.storeHtmlContent(el.id, dom);
@@ -6589,6 +7036,9 @@ var Viewport = class {
6589
7036
  this.layerManager.removeLayer(id);
6590
7037
  this.historyRecorder.commit();
6591
7038
  }
7039
+ registerHtmlRenderer(htmlType, factory) {
7040
+ this.htmlRenderers.set(htmlType, factory);
7041
+ }
6592
7042
  updateHtmlElement(id, newContent) {
6593
7043
  const el = this.store.getById(id);
6594
7044
  if (!el) throw new Error(`Element not found: ${id}`);
@@ -9061,7 +9511,7 @@ var TemplateTool = class {
9061
9511
  };
9062
9512
 
9063
9513
  // src/index.ts
9064
- var VERSION = "0.38.7";
9514
+ var VERSION = "0.39.0";
9065
9515
  export {
9066
9516
  ArrowTool,
9067
9517
  AutoSave,
@@ -9095,6 +9545,7 @@ export {
9095
9545
  createText,
9096
9546
  drawHexPath,
9097
9547
  exportImage,
9548
+ exportSvg,
9098
9549
  getActiveFormats,
9099
9550
  getArrowBounds,
9100
9551
  getArrowControlPoint,