@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.d.cts CHANGED
@@ -233,7 +233,7 @@ interface Tool {
233
233
  setOptions?(options: object): void;
234
234
  onOptionsChange?(listener: () => void): () => void;
235
235
  }
236
- type ToolName = 'hand' | 'select' | 'pencil' | 'eraser' | 'arrow' | 'note' | 'image' | 'text' | 'shape' | 'measure' | 'template';
236
+ type ToolName = 'hand' | 'select' | 'pencil' | 'eraser' | 'arrow' | 'note' | 'image' | 'text' | 'shape' | 'measure' | 'template' | 'laser';
237
237
 
238
238
  declare function snapPoint(point: Point, gridSize: number): Point;
239
239
  declare function snapToHexCenter(point: Point, cellSize: number, orientation: HexOrientation): Point;
@@ -1017,6 +1017,37 @@ declare class TemplateTool implements Tool {
1017
1017
  private notifyOptionsChange;
1018
1018
  }
1019
1019
 
1020
- declare const VERSION = "0.39.0";
1020
+ interface LaserToolOptions {
1021
+ name?: string;
1022
+ color?: string;
1023
+ width?: number;
1024
+ fadeMs?: number;
1025
+ }
1026
+ declare class LaserTool implements Tool {
1027
+ readonly name: string;
1028
+ private color;
1029
+ private width;
1030
+ private fadeMs;
1031
+ private trail;
1032
+ private rafId;
1033
+ private drawing;
1034
+ private optionListeners;
1035
+ constructor(options?: LaserToolOptions);
1036
+ private now;
1037
+ onActivate(ctx: ToolContext): void;
1038
+ onDeactivate(ctx: ToolContext): void;
1039
+ getOptions(): LaserToolOptions;
1040
+ onOptionsChange(listener: () => void): () => void;
1041
+ setOptions(options: LaserToolOptions): void;
1042
+ onPointerDown(state: PointerState, ctx: ToolContext): void;
1043
+ onPointerMove(state: PointerState, ctx: ToolContext): void;
1044
+ onPointerUp(_state: PointerState, _ctx: ToolContext): void;
1045
+ renderOverlay(ctx: CanvasRenderingContext2D): void;
1046
+ private ensureAnimating;
1047
+ private tick;
1048
+ private notifyOptionsChange;
1049
+ }
1050
+
1051
+ declare const VERSION = "0.40.1";
1021
1052
 
1022
- export { type ActiveFormats, type AlignEdge, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, type DistributeAxis, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportImageOptions, type ExportSvgOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, type NoteElement, NoteTool, type NoteToolOptions, PencilTool, type PencilToolOptions, type Point, type PointerState, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, boundsIntersect, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportSvg, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isNearBezier, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
1053
+ export { type ActiveFormats, type AlignEdge, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, type DistributeAxis, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportImageOptions, type ExportSvgOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, LaserTool, type LaserToolOptions, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, type NoteElement, NoteTool, type NoteToolOptions, PencilTool, type PencilToolOptions, type Point, type PointerState, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, boundsIntersect, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportSvg, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isNearBezier, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
package/dist/index.d.ts CHANGED
@@ -233,7 +233,7 @@ interface Tool {
233
233
  setOptions?(options: object): void;
234
234
  onOptionsChange?(listener: () => void): () => void;
235
235
  }
236
- type ToolName = 'hand' | 'select' | 'pencil' | 'eraser' | 'arrow' | 'note' | 'image' | 'text' | 'shape' | 'measure' | 'template';
236
+ type ToolName = 'hand' | 'select' | 'pencil' | 'eraser' | 'arrow' | 'note' | 'image' | 'text' | 'shape' | 'measure' | 'template' | 'laser';
237
237
 
238
238
  declare function snapPoint(point: Point, gridSize: number): Point;
239
239
  declare function snapToHexCenter(point: Point, cellSize: number, orientation: HexOrientation): Point;
@@ -1017,6 +1017,37 @@ declare class TemplateTool implements Tool {
1017
1017
  private notifyOptionsChange;
1018
1018
  }
1019
1019
 
1020
- declare const VERSION = "0.39.0";
1020
+ interface LaserToolOptions {
1021
+ name?: string;
1022
+ color?: string;
1023
+ width?: number;
1024
+ fadeMs?: number;
1025
+ }
1026
+ declare class LaserTool implements Tool {
1027
+ readonly name: string;
1028
+ private color;
1029
+ private width;
1030
+ private fadeMs;
1031
+ private trail;
1032
+ private rafId;
1033
+ private drawing;
1034
+ private optionListeners;
1035
+ constructor(options?: LaserToolOptions);
1036
+ private now;
1037
+ onActivate(ctx: ToolContext): void;
1038
+ onDeactivate(ctx: ToolContext): void;
1039
+ getOptions(): LaserToolOptions;
1040
+ onOptionsChange(listener: () => void): () => void;
1041
+ setOptions(options: LaserToolOptions): void;
1042
+ onPointerDown(state: PointerState, ctx: ToolContext): void;
1043
+ onPointerMove(state: PointerState, ctx: ToolContext): void;
1044
+ onPointerUp(_state: PointerState, _ctx: ToolContext): void;
1045
+ renderOverlay(ctx: CanvasRenderingContext2D): void;
1046
+ private ensureAnimating;
1047
+ private tick;
1048
+ private notifyOptionsChange;
1049
+ }
1050
+
1051
+ declare const VERSION = "0.40.1";
1021
1052
 
1022
- export { type ActiveFormats, type AlignEdge, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, type DistributeAxis, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportImageOptions, type ExportSvgOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, type NoteElement, NoteTool, type NoteToolOptions, PencilTool, type PencilToolOptions, type Point, type PointerState, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, boundsIntersect, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportSvg, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isNearBezier, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
1053
+ export { type ActiveFormats, type AlignEdge, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, type DistributeAxis, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportImageOptions, type ExportSvgOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, LaserTool, type LaserToolOptions, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, type NoteElement, NoteTool, type NoteToolOptions, PencilTool, type PencilToolOptions, type Point, type PointerState, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, boundsIntersect, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportSvg, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isNearBezier, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
package/dist/index.js CHANGED
@@ -831,6 +831,11 @@ var KeyboardActions = class {
831
831
  if (tool?.name !== "select") return null;
832
832
  return { tool, ctx };
833
833
  }
834
+ selectableElements(ctx) {
835
+ return ctx.store.getAll().filter(
836
+ (el) => !el.locked && (ctx.isLayerVisible?.(el.layerId) ?? true) && !(ctx.isLayerLocked?.(el.layerId) ?? false)
837
+ );
838
+ }
834
839
  nudge(dx, dy, byCell) {
835
840
  if (this.deps.isToolActive()) return false;
836
841
  const sel = this.selectTool();
@@ -973,12 +978,28 @@ var KeyboardActions = class {
973
978
  }
974
979
  const sel = this.selectTool();
975
980
  if (!sel) return;
976
- const ids = sel.ctx.store.getAll().filter(
977
- (el) => !el.locked && (sel.ctx.isLayerVisible?.(el.layerId) ?? true) && !(sel.ctx.isLayerLocked?.(el.layerId) ?? false)
978
- ).map((el) => el.id);
981
+ const ids = this.selectableElements(sel.ctx).map((el) => el.id);
979
982
  sel.tool.setSelection(ids);
980
983
  sel.ctx.requestRender();
981
984
  }
985
+ cycleSelection(direction) {
986
+ if (this.deps.isToolActive()) return;
987
+ const tm = this.deps.getToolManager();
988
+ const ctx = this.deps.getToolContext();
989
+ if (!tm || !ctx) return;
990
+ if (tm.activeTool?.name !== "select") ctx.switchTool?.("select");
991
+ const sel = this.selectTool();
992
+ if (!sel) return;
993
+ const eligible = this.selectableElements(sel.ctx).filter((el) => el.type !== "grid");
994
+ if (eligible.length === 0) return;
995
+ const idxs = sel.tool.selectedIds.map((id) => eligible.findIndex((e) => e.id === id)).filter((i) => i >= 0);
996
+ const anchor = idxs.length === 0 ? direction > 0 ? -1 : 0 : direction > 0 ? Math.max(...idxs) : Math.min(...idxs);
997
+ const next = (anchor + direction + eligible.length) % eligible.length;
998
+ const target = eligible[next];
999
+ if (!target) return;
1000
+ sel.tool.setSelection([target.id]);
1001
+ sel.ctx.requestRender();
1002
+ }
982
1003
  zoomToFit() {
983
1004
  if (this.deps.isToolActive()) return;
984
1005
  this.deps.fitToContent?.();
@@ -1083,6 +1104,8 @@ var DEFAULT_BINDINGS = [
1083
1104
  ["undo", ["mod+z"]],
1084
1105
  ["redo", ["mod+y", "mod+shift+z"]],
1085
1106
  ["select-all", ["mod+a"]],
1107
+ ["cycle-selection", ["tab"]],
1108
+ ["cycle-selection-reverse", ["shift+tab"]],
1086
1109
  ["copy", ["mod+c"]],
1087
1110
  ["duplicate", ["mod+d"]],
1088
1111
  ["z-forward", ["]"]],
@@ -1353,6 +1376,14 @@ var KeyboardHandler = class {
1353
1376
  e?.preventDefault();
1354
1377
  this.deps.actions.selectAll();
1355
1378
  return;
1379
+ case "cycle-selection":
1380
+ e?.preventDefault();
1381
+ this.deps.actions.cycleSelection(1);
1382
+ return;
1383
+ case "cycle-selection-reverse":
1384
+ e?.preventDefault();
1385
+ this.deps.actions.cycleSelection(-1);
1386
+ return;
1356
1387
  case "copy":
1357
1388
  e?.preventDefault();
1358
1389
  this.deps.actions.copy();
@@ -4651,6 +4682,16 @@ function renderStyledRuns(ctx, runs, startX, startY, maxWidth) {
4651
4682
  }
4652
4683
  }
4653
4684
 
4685
+ // src/canvas/text-canvas-renderer.ts
4686
+ function renderTextOnCanvas(ctx, text) {
4687
+ const pad = 2;
4688
+ ctx.save();
4689
+ ctx.fillStyle = text.color;
4690
+ const runs = parseStyledRuns(text.text ?? "", text.fontSize);
4691
+ renderStyledRuns(ctx, runs, text.position.x + pad, text.position.y + pad, text.size.w - pad * 2);
4692
+ ctx.restore();
4693
+ }
4694
+
4654
4695
  // src/canvas/export-image.ts
4655
4696
  var center = (b) => ({ x: b.x + b.w / 2, y: b.y + b.h / 2 });
4656
4697
  function getStrokeBounds(el) {
@@ -4733,30 +4774,6 @@ function computeBounds(elements, padding) {
4733
4774
  h: maxY - minY + padding * 2
4734
4775
  };
4735
4776
  }
4736
- function renderTextOnCanvas(ctx, text) {
4737
- if (!text.text) return;
4738
- ctx.save();
4739
- ctx.fillStyle = text.color;
4740
- ctx.font = `${text.fontSize}px system-ui, sans-serif`;
4741
- ctx.textBaseline = "top";
4742
- ctx.textAlign = text.textAlign;
4743
- const pad = 2;
4744
- let textX = text.position.x + pad;
4745
- if (text.textAlign === "center") {
4746
- textX = text.position.x + text.size.w / 2;
4747
- } else if (text.textAlign === "right") {
4748
- textX = text.position.x + text.size.w - pad;
4749
- }
4750
- const lineHeight = text.fontSize * 1.4;
4751
- const lines = text.text.split("\n");
4752
- for (let i = 0; i < lines.length; i++) {
4753
- const line = lines[i];
4754
- if (line !== void 0) {
4755
- ctx.fillText(line, textX, text.position.y + pad + i * lineHeight);
4756
- }
4757
- }
4758
- ctx.restore();
4759
- }
4760
4777
  function renderGridForBounds(ctx, grid, bounds) {
4761
4778
  const visibleBounds = {
4762
4779
  minX: bounds.x,
@@ -4978,28 +4995,27 @@ function emitImage(image, dataUri) {
4978
4995
  const { w, h } = image.size;
4979
4996
  return `<image href="${esc(href)}" x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" />`;
4980
4997
  }
4981
- function emitText(text) {
4998
+ function emitText(text, rasterScale) {
4982
4999
  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>`;
5000
+ const { x, y } = text.position;
5001
+ const { w, h } = text.size;
5002
+ if (typeof document === "undefined") return "";
5003
+ const canvas = document.createElement("canvas");
5004
+ canvas.width = Math.max(1, Math.ceil(w * rasterScale));
5005
+ canvas.height = Math.max(1, Math.ceil(h * rasterScale));
5006
+ const ctx = canvas.getContext("2d");
5007
+ if (!ctx) return "";
5008
+ ctx.scale(rasterScale, rasterScale);
5009
+ ctx.translate(-x, -y);
5010
+ renderTextOnCanvas(ctx, text);
5011
+ let dataUri;
5012
+ try {
5013
+ dataUri = canvas.toDataURL();
5014
+ } catch {
5015
+ return "";
5001
5016
  }
5002
- return out;
5017
+ if (!dataUri || !dataUri.startsWith("data:")) return "";
5018
+ return `<image href="${esc(dataUri)}" x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" />`;
5003
5019
  }
5004
5020
  function emitNote(note, rasterScale) {
5005
5021
  const { x, y } = note.position;
@@ -5182,7 +5198,7 @@ function emitElement(el, imageDataUris, rasterScale, firstGrid, store) {
5182
5198
  case "image":
5183
5199
  return withRotationSvg(el, emitImage(el, imageDataUris.get(el.id)));
5184
5200
  case "text":
5185
- return withRotationSvg(el, emitText(el));
5201
+ return withRotationSvg(el, emitText(el, rasterScale));
5186
5202
  case "note":
5187
5203
  return withRotationSvg(el, emitNote(el, rasterScale));
5188
5204
  case "template":
@@ -5645,10 +5661,9 @@ var DomNodeManager = class {
5645
5661
  cursor: "default",
5646
5662
  userSelect: "none",
5647
5663
  wordWrap: "break-word",
5648
- whiteSpace: "pre-wrap",
5649
5664
  lineHeight: "1.4"
5650
5665
  });
5651
- node.textContent = element.text || "";
5666
+ node.innerHTML = element.text || "";
5652
5667
  const detector = new DoubleTapDetector();
5653
5668
  node.addEventListener("pointerup", (e) => {
5654
5669
  if (detector.feed(e)) {
@@ -5659,8 +5674,9 @@ var DomNodeManager = class {
5659
5674
  });
5660
5675
  }
5661
5676
  if (!this.isEditingElement(element.id)) {
5662
- if (node.textContent !== element.text) {
5663
- node.textContent = element.text || "";
5677
+ const text = element.text || "";
5678
+ if (node.innerHTML !== text) {
5679
+ node.innerHTML = text;
5664
5680
  }
5665
5681
  Object.assign(node.style, {
5666
5682
  fontSize: `${element.fontSize}px`,
@@ -7691,6 +7707,17 @@ function renderArrowHandles(canvasCtx, arrow, zoom) {
7691
7707
  canvasCtx.stroke();
7692
7708
  }
7693
7709
  }
7710
+ function renderArrowHoverHandle(canvasCtx, arrow, zoom) {
7711
+ const mid = getArrowMidpoint(arrow.from, arrow.to, arrow.bend);
7712
+ const radius = HANDLE_RADIUS / zoom;
7713
+ canvasCtx.fillStyle = "#2196F3";
7714
+ canvasCtx.strokeStyle = "#2196F3";
7715
+ canvasCtx.lineWidth = 1.5 / zoom;
7716
+ canvasCtx.beginPath();
7717
+ canvasCtx.arc(mid.x, mid.y, radius, 0, Math.PI * 2);
7718
+ canvasCtx.fill();
7719
+ canvasCtx.stroke();
7720
+ }
7694
7721
 
7695
7722
  // src/elements/snap-guides.ts
7696
7723
  function xAnchors(b) {
@@ -8539,7 +8566,13 @@ var SelectTool = class {
8539
8566
  if (this.hoveredId && this.ctx && this.mode.type === "idle") {
8540
8567
  if (!this._selectedIds.includes(this.hoveredId)) {
8541
8568
  const el = this.ctx.store.getById(this.hoveredId);
8542
- if (el) {
8569
+ if (el?.type === "arrow") {
8570
+ canvasCtx.save();
8571
+ canvasCtx.globalAlpha = 0.35;
8572
+ canvasCtx.setLineDash([]);
8573
+ renderArrowHoverHandle(canvasCtx, el, this.ctx.camera.zoom);
8574
+ canvasCtx.restore();
8575
+ } else if (el) {
8543
8576
  const b = getElementBounds(el);
8544
8577
  if (b) {
8545
8578
  canvasCtx.save();
@@ -9510,8 +9543,118 @@ var TemplateTool = class {
9510
9543
  }
9511
9544
  };
9512
9545
 
9546
+ // src/tools/laser-tool.ts
9547
+ var DEFAULT_COLOR = "#ff3b30";
9548
+ var DEFAULT_WIDTH = 4;
9549
+ var DEFAULT_FADE_MS = 1200;
9550
+ var LaserTool = class {
9551
+ name;
9552
+ color;
9553
+ width;
9554
+ fadeMs;
9555
+ trail = [];
9556
+ rafId = null;
9557
+ drawing = false;
9558
+ optionListeners = /* @__PURE__ */ new Set();
9559
+ constructor(options = {}) {
9560
+ this.name = options.name ?? "laser";
9561
+ this.color = options.color ?? DEFAULT_COLOR;
9562
+ this.width = options.width ?? DEFAULT_WIDTH;
9563
+ this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS;
9564
+ }
9565
+ now() {
9566
+ return performance.now();
9567
+ }
9568
+ onActivate(ctx) {
9569
+ ctx.setCursor?.("crosshair");
9570
+ }
9571
+ onDeactivate(ctx) {
9572
+ if (this.rafId !== null) {
9573
+ cancelAnimationFrame(this.rafId);
9574
+ this.rafId = null;
9575
+ }
9576
+ this.trail = [];
9577
+ this.drawing = false;
9578
+ ctx.setCursor?.("default");
9579
+ ctx.requestRender();
9580
+ }
9581
+ getOptions() {
9582
+ return {
9583
+ name: this.name,
9584
+ color: this.color,
9585
+ width: this.width,
9586
+ fadeMs: this.fadeMs
9587
+ };
9588
+ }
9589
+ onOptionsChange(listener) {
9590
+ this.optionListeners.add(listener);
9591
+ return () => this.optionListeners.delete(listener);
9592
+ }
9593
+ setOptions(options) {
9594
+ if (options.color !== void 0) this.color = options.color;
9595
+ if (options.width !== void 0) this.width = options.width;
9596
+ if (options.fadeMs !== void 0) this.fadeMs = options.fadeMs;
9597
+ this.notifyOptionsChange();
9598
+ }
9599
+ onPointerDown(state, ctx) {
9600
+ this.drawing = true;
9601
+ const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
9602
+ this.trail.push({ x: world.x, y: world.y, t: this.now() });
9603
+ this.ensureAnimating(ctx);
9604
+ }
9605
+ onPointerMove(state, ctx) {
9606
+ if (!this.drawing) return;
9607
+ const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
9608
+ this.trail.push({ x: world.x, y: world.y, t: this.now() });
9609
+ this.ensureAnimating(ctx);
9610
+ }
9611
+ onPointerUp(_state, _ctx) {
9612
+ this.drawing = false;
9613
+ }
9614
+ renderOverlay(ctx) {
9615
+ if (this.trail.length < 2) return;
9616
+ ctx.save();
9617
+ ctx.strokeStyle = this.color;
9618
+ ctx.lineWidth = this.width;
9619
+ ctx.lineCap = "round";
9620
+ ctx.lineJoin = "round";
9621
+ const now = this.now();
9622
+ for (let i = 0; i < this.trail.length - 1; i++) {
9623
+ const a = this.trail[i];
9624
+ const b = this.trail[i + 1];
9625
+ if (!a || !b) continue;
9626
+ const age = now - b.t;
9627
+ ctx.globalAlpha = Math.max(0, 1 - age / this.fadeMs);
9628
+ ctx.beginPath();
9629
+ ctx.moveTo(a.x, a.y);
9630
+ ctx.lineTo(b.x, b.y);
9631
+ ctx.stroke();
9632
+ }
9633
+ ctx.restore();
9634
+ }
9635
+ ensureAnimating(ctx) {
9636
+ if (this.rafId === null) {
9637
+ this.rafId = requestAnimationFrame(() => this.tick(ctx));
9638
+ }
9639
+ }
9640
+ tick(ctx) {
9641
+ const cutoff = this.now() - this.fadeMs;
9642
+ this.trail = this.trail.filter((p) => p.t >= cutoff);
9643
+ if (this.trail.length > 0) {
9644
+ ctx.requestRender();
9645
+ this.rafId = requestAnimationFrame(() => this.tick(ctx));
9646
+ } else {
9647
+ ctx.requestRender();
9648
+ this.rafId = null;
9649
+ }
9650
+ }
9651
+ notifyOptionsChange() {
9652
+ for (const listener of this.optionListeners) listener();
9653
+ }
9654
+ };
9655
+
9513
9656
  // src/index.ts
9514
- var VERSION = "0.39.0";
9657
+ var VERSION = "0.40.1";
9515
9658
  export {
9516
9659
  ArrowTool,
9517
9660
  AutoSave,
@@ -9522,6 +9665,7 @@ export {
9522
9665
  HandTool,
9523
9666
  HistoryStack,
9524
9667
  ImageTool,
9668
+ LaserTool,
9525
9669
  LayerManager,
9526
9670
  MeasureTool,
9527
9671
  NoteTool,