@fieldnotes/core 0.39.0 → 0.40.1

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 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,
@@ -913,6 +914,11 @@ var KeyboardActions = class {
913
914
  if (tool?.name !== "select") return null;
914
915
  return { tool, ctx };
915
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
+ }
916
922
  nudge(dx, dy, byCell) {
917
923
  if (this.deps.isToolActive()) return false;
918
924
  const sel = this.selectTool();
@@ -1055,12 +1061,28 @@ var KeyboardActions = class {
1055
1061
  }
1056
1062
  const sel = this.selectTool();
1057
1063
  if (!sel) return;
1058
- const ids = sel.ctx.store.getAll().filter(
1059
- (el) => !el.locked && (sel.ctx.isLayerVisible?.(el.layerId) ?? true) && !(sel.ctx.isLayerLocked?.(el.layerId) ?? false)
1060
- ).map((el) => el.id);
1064
+ const ids = this.selectableElements(sel.ctx).map((el) => el.id);
1061
1065
  sel.tool.setSelection(ids);
1062
1066
  sel.ctx.requestRender();
1063
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
+ }
1064
1086
  zoomToFit() {
1065
1087
  if (this.deps.isToolActive()) return;
1066
1088
  this.deps.fitToContent?.();
@@ -1165,6 +1187,8 @@ var DEFAULT_BINDINGS = [
1165
1187
  ["undo", ["mod+z"]],
1166
1188
  ["redo", ["mod+y", "mod+shift+z"]],
1167
1189
  ["select-all", ["mod+a"]],
1190
+ ["cycle-selection", ["tab"]],
1191
+ ["cycle-selection-reverse", ["shift+tab"]],
1168
1192
  ["copy", ["mod+c"]],
1169
1193
  ["duplicate", ["mod+d"]],
1170
1194
  ["z-forward", ["]"]],
@@ -1435,6 +1459,14 @@ var KeyboardHandler = class {
1435
1459
  e?.preventDefault();
1436
1460
  this.deps.actions.selectAll();
1437
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;
1438
1470
  case "copy":
1439
1471
  e?.preventDefault();
1440
1472
  this.deps.actions.copy();
@@ -4733,6 +4765,16 @@ function renderStyledRuns(ctx, runs, startX, startY, maxWidth) {
4733
4765
  }
4734
4766
  }
4735
4767
 
4768
+ // src/canvas/text-canvas-renderer.ts
4769
+ function renderTextOnCanvas(ctx, text) {
4770
+ const pad = 2;
4771
+ ctx.save();
4772
+ ctx.fillStyle = text.color;
4773
+ const runs = parseStyledRuns(text.text ?? "", text.fontSize);
4774
+ renderStyledRuns(ctx, runs, text.position.x + pad, text.position.y + pad, text.size.w - pad * 2);
4775
+ ctx.restore();
4776
+ }
4777
+
4736
4778
  // src/canvas/export-image.ts
4737
4779
  var center = (b) => ({ x: b.x + b.w / 2, y: b.y + b.h / 2 });
4738
4780
  function getStrokeBounds(el) {
@@ -4815,30 +4857,6 @@ function computeBounds(elements, padding) {
4815
4857
  h: maxY - minY + padding * 2
4816
4858
  };
4817
4859
  }
4818
- function renderTextOnCanvas(ctx, text) {
4819
- if (!text.text) return;
4820
- ctx.save();
4821
- ctx.fillStyle = text.color;
4822
- ctx.font = `${text.fontSize}px system-ui, sans-serif`;
4823
- ctx.textBaseline = "top";
4824
- ctx.textAlign = text.textAlign;
4825
- const pad = 2;
4826
- let textX = text.position.x + pad;
4827
- if (text.textAlign === "center") {
4828
- textX = text.position.x + text.size.w / 2;
4829
- } else if (text.textAlign === "right") {
4830
- textX = text.position.x + text.size.w - pad;
4831
- }
4832
- const lineHeight = text.fontSize * 1.4;
4833
- const lines = text.text.split("\n");
4834
- for (let i = 0; i < lines.length; i++) {
4835
- const line = lines[i];
4836
- if (line !== void 0) {
4837
- ctx.fillText(line, textX, text.position.y + pad + i * lineHeight);
4838
- }
4839
- }
4840
- ctx.restore();
4841
- }
4842
4860
  function renderGridForBounds(ctx, grid, bounds) {
4843
4861
  const visibleBounds = {
4844
4862
  minX: bounds.x,
@@ -5060,28 +5078,27 @@ function emitImage(image, dataUri) {
5060
5078
  const { w, h } = image.size;
5061
5079
  return `<image href="${esc(href)}" x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" />`;
5062
5080
  }
5063
- function emitText(text) {
5081
+ function emitText(text, rasterScale) {
5064
5082
  if (!text.text) return "";
5065
- const pad = 2;
5066
- let anchor = "start";
5067
- let textX = text.position.x + pad;
5068
- if (text.textAlign === "center") {
5069
- anchor = "middle";
5070
- textX = text.position.x + text.size.w / 2;
5071
- } else if (text.textAlign === "right") {
5072
- anchor = "end";
5073
- textX = text.position.x + text.size.w - pad;
5074
- }
5075
- const lineHeight = text.fontSize * 1.4;
5076
- const lines = text.text.split("\n");
5077
- let out = "";
5078
- for (let i = 0; i < lines.length; i++) {
5079
- const line = lines[i];
5080
- if (line === void 0) continue;
5081
- const y = text.position.y + pad + i * lineHeight;
5082
- 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>`;
5083
+ const { x, y } = text.position;
5084
+ const { w, h } = text.size;
5085
+ if (typeof document === "undefined") return "";
5086
+ const canvas = document.createElement("canvas");
5087
+ canvas.width = Math.max(1, Math.ceil(w * rasterScale));
5088
+ canvas.height = Math.max(1, Math.ceil(h * rasterScale));
5089
+ const ctx = canvas.getContext("2d");
5090
+ if (!ctx) return "";
5091
+ ctx.scale(rasterScale, rasterScale);
5092
+ ctx.translate(-x, -y);
5093
+ renderTextOnCanvas(ctx, text);
5094
+ let dataUri;
5095
+ try {
5096
+ dataUri = canvas.toDataURL();
5097
+ } catch {
5098
+ return "";
5083
5099
  }
5084
- return out;
5100
+ if (!dataUri || !dataUri.startsWith("data:")) return "";
5101
+ return `<image href="${esc(dataUri)}" x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" />`;
5085
5102
  }
5086
5103
  function emitNote(note, rasterScale) {
5087
5104
  const { x, y } = note.position;
@@ -5264,7 +5281,7 @@ function emitElement(el, imageDataUris, rasterScale, firstGrid, store) {
5264
5281
  case "image":
5265
5282
  return withRotationSvg(el, emitImage(el, imageDataUris.get(el.id)));
5266
5283
  case "text":
5267
- return withRotationSvg(el, emitText(el));
5284
+ return withRotationSvg(el, emitText(el, rasterScale));
5268
5285
  case "note":
5269
5286
  return withRotationSvg(el, emitNote(el, rasterScale));
5270
5287
  case "template":
@@ -5727,10 +5744,9 @@ var DomNodeManager = class {
5727
5744
  cursor: "default",
5728
5745
  userSelect: "none",
5729
5746
  wordWrap: "break-word",
5730
- whiteSpace: "pre-wrap",
5731
5747
  lineHeight: "1.4"
5732
5748
  });
5733
- node.textContent = element.text || "";
5749
+ node.innerHTML = element.text || "";
5734
5750
  const detector = new DoubleTapDetector();
5735
5751
  node.addEventListener("pointerup", (e) => {
5736
5752
  if (detector.feed(e)) {
@@ -5741,8 +5757,9 @@ var DomNodeManager = class {
5741
5757
  });
5742
5758
  }
5743
5759
  if (!this.isEditingElement(element.id)) {
5744
- if (node.textContent !== element.text) {
5745
- node.textContent = element.text || "";
5760
+ const text = element.text || "";
5761
+ if (node.innerHTML !== text) {
5762
+ node.innerHTML = text;
5746
5763
  }
5747
5764
  Object.assign(node.style, {
5748
5765
  fontSize: `${element.fontSize}px`,
@@ -7773,6 +7790,17 @@ function renderArrowHandles(canvasCtx, arrow, zoom) {
7773
7790
  canvasCtx.stroke();
7774
7791
  }
7775
7792
  }
7793
+ function renderArrowHoverHandle(canvasCtx, arrow, zoom) {
7794
+ const mid = getArrowMidpoint(arrow.from, arrow.to, arrow.bend);
7795
+ const radius = HANDLE_RADIUS / zoom;
7796
+ canvasCtx.fillStyle = "#2196F3";
7797
+ canvasCtx.strokeStyle = "#2196F3";
7798
+ canvasCtx.lineWidth = 1.5 / zoom;
7799
+ canvasCtx.beginPath();
7800
+ canvasCtx.arc(mid.x, mid.y, radius, 0, Math.PI * 2);
7801
+ canvasCtx.fill();
7802
+ canvasCtx.stroke();
7803
+ }
7776
7804
 
7777
7805
  // src/elements/snap-guides.ts
7778
7806
  function xAnchors(b) {
@@ -8621,7 +8649,13 @@ var SelectTool = class {
8621
8649
  if (this.hoveredId && this.ctx && this.mode.type === "idle") {
8622
8650
  if (!this._selectedIds.includes(this.hoveredId)) {
8623
8651
  const el = this.ctx.store.getById(this.hoveredId);
8624
- if (el) {
8652
+ if (el?.type === "arrow") {
8653
+ canvasCtx.save();
8654
+ canvasCtx.globalAlpha = 0.35;
8655
+ canvasCtx.setLineDash([]);
8656
+ renderArrowHoverHandle(canvasCtx, el, this.ctx.camera.zoom);
8657
+ canvasCtx.restore();
8658
+ } else if (el) {
8625
8659
  const b = getElementBounds(el);
8626
8660
  if (b) {
8627
8661
  canvasCtx.save();
@@ -9592,8 +9626,118 @@ var TemplateTool = class {
9592
9626
  }
9593
9627
  };
9594
9628
 
9629
+ // src/tools/laser-tool.ts
9630
+ var DEFAULT_COLOR = "#ff3b30";
9631
+ var DEFAULT_WIDTH = 4;
9632
+ var DEFAULT_FADE_MS = 1200;
9633
+ var LaserTool = class {
9634
+ name;
9635
+ color;
9636
+ width;
9637
+ fadeMs;
9638
+ trail = [];
9639
+ rafId = null;
9640
+ drawing = false;
9641
+ optionListeners = /* @__PURE__ */ new Set();
9642
+ constructor(options = {}) {
9643
+ this.name = options.name ?? "laser";
9644
+ this.color = options.color ?? DEFAULT_COLOR;
9645
+ this.width = options.width ?? DEFAULT_WIDTH;
9646
+ this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS;
9647
+ }
9648
+ now() {
9649
+ return performance.now();
9650
+ }
9651
+ onActivate(ctx) {
9652
+ ctx.setCursor?.("crosshair");
9653
+ }
9654
+ onDeactivate(ctx) {
9655
+ if (this.rafId !== null) {
9656
+ cancelAnimationFrame(this.rafId);
9657
+ this.rafId = null;
9658
+ }
9659
+ this.trail = [];
9660
+ this.drawing = false;
9661
+ ctx.setCursor?.("default");
9662
+ ctx.requestRender();
9663
+ }
9664
+ getOptions() {
9665
+ return {
9666
+ name: this.name,
9667
+ color: this.color,
9668
+ width: this.width,
9669
+ fadeMs: this.fadeMs
9670
+ };
9671
+ }
9672
+ onOptionsChange(listener) {
9673
+ this.optionListeners.add(listener);
9674
+ return () => this.optionListeners.delete(listener);
9675
+ }
9676
+ setOptions(options) {
9677
+ if (options.color !== void 0) this.color = options.color;
9678
+ if (options.width !== void 0) this.width = options.width;
9679
+ if (options.fadeMs !== void 0) this.fadeMs = options.fadeMs;
9680
+ this.notifyOptionsChange();
9681
+ }
9682
+ onPointerDown(state, ctx) {
9683
+ this.drawing = true;
9684
+ const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
9685
+ this.trail.push({ x: world.x, y: world.y, t: this.now() });
9686
+ this.ensureAnimating(ctx);
9687
+ }
9688
+ onPointerMove(state, ctx) {
9689
+ if (!this.drawing) return;
9690
+ const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
9691
+ this.trail.push({ x: world.x, y: world.y, t: this.now() });
9692
+ this.ensureAnimating(ctx);
9693
+ }
9694
+ onPointerUp(_state, _ctx) {
9695
+ this.drawing = false;
9696
+ }
9697
+ renderOverlay(ctx) {
9698
+ if (this.trail.length < 2) return;
9699
+ ctx.save();
9700
+ ctx.strokeStyle = this.color;
9701
+ ctx.lineWidth = this.width;
9702
+ ctx.lineCap = "round";
9703
+ ctx.lineJoin = "round";
9704
+ const now = this.now();
9705
+ for (let i = 0; i < this.trail.length - 1; i++) {
9706
+ const a = this.trail[i];
9707
+ const b = this.trail[i + 1];
9708
+ if (!a || !b) continue;
9709
+ const age = now - b.t;
9710
+ ctx.globalAlpha = Math.max(0, 1 - age / this.fadeMs);
9711
+ ctx.beginPath();
9712
+ ctx.moveTo(a.x, a.y);
9713
+ ctx.lineTo(b.x, b.y);
9714
+ ctx.stroke();
9715
+ }
9716
+ ctx.restore();
9717
+ }
9718
+ ensureAnimating(ctx) {
9719
+ if (this.rafId === null) {
9720
+ this.rafId = requestAnimationFrame(() => this.tick(ctx));
9721
+ }
9722
+ }
9723
+ tick(ctx) {
9724
+ const cutoff = this.now() - this.fadeMs;
9725
+ this.trail = this.trail.filter((p) => p.t >= cutoff);
9726
+ if (this.trail.length > 0) {
9727
+ ctx.requestRender();
9728
+ this.rafId = requestAnimationFrame(() => this.tick(ctx));
9729
+ } else {
9730
+ ctx.requestRender();
9731
+ this.rafId = null;
9732
+ }
9733
+ }
9734
+ notifyOptionsChange() {
9735
+ for (const listener of this.optionListeners) listener();
9736
+ }
9737
+ };
9738
+
9595
9739
  // src/index.ts
9596
- var VERSION = "0.39.0";
9740
+ var VERSION = "0.40.1";
9597
9741
  // Annotate the CommonJS export names for ESM import in node:
9598
9742
  0 && (module.exports = {
9599
9743
  ArrowTool,
@@ -9605,6 +9749,7 @@ var VERSION = "0.39.0";
9605
9749
  HandTool,
9606
9750
  HistoryStack,
9607
9751
  ImageTool,
9752
+ LaserTool,
9608
9753
  LayerManager,
9609
9754
  MeasureTool,
9610
9755
  NoteTool,