@fieldnotes/core 0.39.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs 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();
@@ -7773,6 +7805,17 @@ function renderArrowHandles(canvasCtx, arrow, zoom) {
7773
7805
  canvasCtx.stroke();
7774
7806
  }
7775
7807
  }
7808
+ function renderArrowHoverHandle(canvasCtx, arrow, zoom) {
7809
+ const mid = getArrowMidpoint(arrow.from, arrow.to, arrow.bend);
7810
+ const radius = HANDLE_RADIUS / zoom;
7811
+ canvasCtx.fillStyle = "#2196F3";
7812
+ canvasCtx.strokeStyle = "#2196F3";
7813
+ canvasCtx.lineWidth = 1.5 / zoom;
7814
+ canvasCtx.beginPath();
7815
+ canvasCtx.arc(mid.x, mid.y, radius, 0, Math.PI * 2);
7816
+ canvasCtx.fill();
7817
+ canvasCtx.stroke();
7818
+ }
7776
7819
 
7777
7820
  // src/elements/snap-guides.ts
7778
7821
  function xAnchors(b) {
@@ -8621,7 +8664,13 @@ var SelectTool = class {
8621
8664
  if (this.hoveredId && this.ctx && this.mode.type === "idle") {
8622
8665
  if (!this._selectedIds.includes(this.hoveredId)) {
8623
8666
  const el = this.ctx.store.getById(this.hoveredId);
8624
- if (el) {
8667
+ if (el?.type === "arrow") {
8668
+ canvasCtx.save();
8669
+ canvasCtx.globalAlpha = 0.35;
8670
+ canvasCtx.setLineDash([]);
8671
+ renderArrowHoverHandle(canvasCtx, el, this.ctx.camera.zoom);
8672
+ canvasCtx.restore();
8673
+ } else if (el) {
8625
8674
  const b = getElementBounds(el);
8626
8675
  if (b) {
8627
8676
  canvasCtx.save();
@@ -9592,8 +9641,118 @@ var TemplateTool = class {
9592
9641
  }
9593
9642
  };
9594
9643
 
9644
+ // src/tools/laser-tool.ts
9645
+ var DEFAULT_COLOR = "#ff3b30";
9646
+ var DEFAULT_WIDTH = 4;
9647
+ var DEFAULT_FADE_MS = 1200;
9648
+ var LaserTool = class {
9649
+ name;
9650
+ color;
9651
+ width;
9652
+ fadeMs;
9653
+ trail = [];
9654
+ rafId = null;
9655
+ drawing = false;
9656
+ optionListeners = /* @__PURE__ */ new Set();
9657
+ constructor(options = {}) {
9658
+ this.name = options.name ?? "laser";
9659
+ this.color = options.color ?? DEFAULT_COLOR;
9660
+ this.width = options.width ?? DEFAULT_WIDTH;
9661
+ this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS;
9662
+ }
9663
+ now() {
9664
+ return performance.now();
9665
+ }
9666
+ onActivate(ctx) {
9667
+ ctx.setCursor?.("crosshair");
9668
+ }
9669
+ onDeactivate(ctx) {
9670
+ if (this.rafId !== null) {
9671
+ cancelAnimationFrame(this.rafId);
9672
+ this.rafId = null;
9673
+ }
9674
+ this.trail = [];
9675
+ this.drawing = false;
9676
+ ctx.setCursor?.("default");
9677
+ ctx.requestRender();
9678
+ }
9679
+ getOptions() {
9680
+ return {
9681
+ name: this.name,
9682
+ color: this.color,
9683
+ width: this.width,
9684
+ fadeMs: this.fadeMs
9685
+ };
9686
+ }
9687
+ onOptionsChange(listener) {
9688
+ this.optionListeners.add(listener);
9689
+ return () => this.optionListeners.delete(listener);
9690
+ }
9691
+ setOptions(options) {
9692
+ if (options.color !== void 0) this.color = options.color;
9693
+ if (options.width !== void 0) this.width = options.width;
9694
+ if (options.fadeMs !== void 0) this.fadeMs = options.fadeMs;
9695
+ this.notifyOptionsChange();
9696
+ }
9697
+ onPointerDown(state, ctx) {
9698
+ this.drawing = true;
9699
+ const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
9700
+ this.trail.push({ x: world.x, y: world.y, t: this.now() });
9701
+ this.ensureAnimating(ctx);
9702
+ }
9703
+ onPointerMove(state, ctx) {
9704
+ if (!this.drawing) return;
9705
+ const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
9706
+ this.trail.push({ x: world.x, y: world.y, t: this.now() });
9707
+ this.ensureAnimating(ctx);
9708
+ }
9709
+ onPointerUp(_state, _ctx) {
9710
+ this.drawing = false;
9711
+ }
9712
+ renderOverlay(ctx) {
9713
+ if (this.trail.length < 2) return;
9714
+ ctx.save();
9715
+ ctx.strokeStyle = this.color;
9716
+ ctx.lineWidth = this.width;
9717
+ ctx.lineCap = "round";
9718
+ ctx.lineJoin = "round";
9719
+ const now = this.now();
9720
+ for (let i = 0; i < this.trail.length - 1; i++) {
9721
+ const a = this.trail[i];
9722
+ const b = this.trail[i + 1];
9723
+ if (!a || !b) continue;
9724
+ const age = now - b.t;
9725
+ ctx.globalAlpha = Math.max(0, 1 - age / this.fadeMs);
9726
+ ctx.beginPath();
9727
+ ctx.moveTo(a.x, a.y);
9728
+ ctx.lineTo(b.x, b.y);
9729
+ ctx.stroke();
9730
+ }
9731
+ ctx.restore();
9732
+ }
9733
+ ensureAnimating(ctx) {
9734
+ if (this.rafId === null) {
9735
+ this.rafId = requestAnimationFrame(() => this.tick(ctx));
9736
+ }
9737
+ }
9738
+ tick(ctx) {
9739
+ const cutoff = this.now() - this.fadeMs;
9740
+ this.trail = this.trail.filter((p) => p.t >= cutoff);
9741
+ if (this.trail.length > 0) {
9742
+ ctx.requestRender();
9743
+ this.rafId = requestAnimationFrame(() => this.tick(ctx));
9744
+ } else {
9745
+ ctx.requestRender();
9746
+ this.rafId = null;
9747
+ }
9748
+ }
9749
+ notifyOptionsChange() {
9750
+ for (const listener of this.optionListeners) listener();
9751
+ }
9752
+ };
9753
+
9595
9754
  // src/index.ts
9596
- var VERSION = "0.39.0";
9755
+ var VERSION = "0.40.0";
9597
9756
  // Annotate the CommonJS export names for ESM import in node:
9598
9757
  0 && (module.exports = {
9599
9758
  ArrowTool,
@@ -9605,6 +9764,7 @@ var VERSION = "0.39.0";
9605
9764
  HandTool,
9606
9765
  HistoryStack,
9607
9766
  ImageTool,
9767
+ LaserTool,
9608
9768
  LayerManager,
9609
9769
  MeasureTool,
9610
9770
  NoteTool,