@fieldnotes/core 0.27.0 → 0.29.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.d.cts CHANGED
@@ -50,6 +50,8 @@ interface ArrowElement extends BaseElement {
50
50
  toBinding?: Binding;
51
51
  /** Derived from from/to/bend. Redundant in serialized state — safe to omit. */
52
52
  cachedControlPoint?: Point;
53
+ /** Optional text rendered at the curve midpoint. */
54
+ label?: string;
53
55
  }
54
56
  interface ImageElement extends BaseElement {
55
57
  type: 'image';
@@ -446,6 +448,7 @@ declare class Viewport {
446
448
  private readonly background;
447
449
  private readonly renderer;
448
450
  private readonly noteEditor;
451
+ private readonly arrowLabelEditor;
449
452
  private readonly historyRecorder;
450
453
  readonly toolContext: ToolContext;
451
454
  private readonly marginViewport;
@@ -517,6 +520,8 @@ declare class Viewport {
517
520
  private onTextEditStop;
518
521
  private onTapDown;
519
522
  private onDoubleTap;
523
+ private findArrowAt;
524
+ private startArrowLabelEdit;
520
525
  private hitTestWorld;
521
526
  stopInteracting(): void;
522
527
  private onDragOver;
@@ -574,6 +579,7 @@ interface ArrowInput extends BaseDefaults {
574
579
  width?: number;
575
580
  fromBinding?: Binding;
576
581
  toBinding?: Binding;
582
+ label?: string;
577
583
  }
578
584
  interface ImageInput extends BaseDefaults {
579
585
  position: Point;
@@ -695,14 +701,17 @@ declare class PencilTool implements Tool {
695
701
 
696
702
  interface EraserToolOptions {
697
703
  radius?: number;
704
+ mode?: 'partial' | 'stroke';
698
705
  }
699
706
  declare class EraserTool implements Tool {
700
707
  readonly name = "eraser";
701
708
  private erasing;
702
- private readonly radius;
703
- private readonly cursor;
709
+ private radius;
710
+ private cursor;
711
+ private mode;
704
712
  constructor(options?: EraserToolOptions);
705
713
  getOptions(): EraserToolOptions;
714
+ setOptions(options: EraserToolOptions): void;
706
715
  onActivate(ctx: ToolContext): void;
707
716
  onDeactivate(ctx: ToolContext): void;
708
717
  onPointerDown(state: PointerState, ctx: ToolContext): void;
@@ -951,6 +960,6 @@ declare class TemplateTool implements Tool {
951
960
  private notifyOptionsChange;
952
961
  }
953
962
 
954
- declare const VERSION = "0.27.0";
963
+ declare const VERSION = "0.29.0";
955
964
 
956
965
  export { type ActiveFormats, 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, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportImageOptions, 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, 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
@@ -50,6 +50,8 @@ interface ArrowElement extends BaseElement {
50
50
  toBinding?: Binding;
51
51
  /** Derived from from/to/bend. Redundant in serialized state — safe to omit. */
52
52
  cachedControlPoint?: Point;
53
+ /** Optional text rendered at the curve midpoint. */
54
+ label?: string;
53
55
  }
54
56
  interface ImageElement extends BaseElement {
55
57
  type: 'image';
@@ -446,6 +448,7 @@ declare class Viewport {
446
448
  private readonly background;
447
449
  private readonly renderer;
448
450
  private readonly noteEditor;
451
+ private readonly arrowLabelEditor;
449
452
  private readonly historyRecorder;
450
453
  readonly toolContext: ToolContext;
451
454
  private readonly marginViewport;
@@ -517,6 +520,8 @@ declare class Viewport {
517
520
  private onTextEditStop;
518
521
  private onTapDown;
519
522
  private onDoubleTap;
523
+ private findArrowAt;
524
+ private startArrowLabelEdit;
520
525
  private hitTestWorld;
521
526
  stopInteracting(): void;
522
527
  private onDragOver;
@@ -574,6 +579,7 @@ interface ArrowInput extends BaseDefaults {
574
579
  width?: number;
575
580
  fromBinding?: Binding;
576
581
  toBinding?: Binding;
582
+ label?: string;
577
583
  }
578
584
  interface ImageInput extends BaseDefaults {
579
585
  position: Point;
@@ -695,14 +701,17 @@ declare class PencilTool implements Tool {
695
701
 
696
702
  interface EraserToolOptions {
697
703
  radius?: number;
704
+ mode?: 'partial' | 'stroke';
698
705
  }
699
706
  declare class EraserTool implements Tool {
700
707
  readonly name = "eraser";
701
708
  private erasing;
702
- private readonly radius;
703
- private readonly cursor;
709
+ private radius;
710
+ private cursor;
711
+ private mode;
704
712
  constructor(options?: EraserToolOptions);
705
713
  getOptions(): EraserToolOptions;
714
+ setOptions(options: EraserToolOptions): void;
706
715
  onActivate(ctx: ToolContext): void;
707
716
  onDeactivate(ctx: ToolContext): void;
708
717
  onPointerDown(state: PointerState, ctx: ToolContext): void;
@@ -951,6 +960,6 @@ declare class TemplateTool implements Tool {
951
960
  private notifyOptionsChange;
952
961
  }
953
962
 
954
- declare const VERSION = "0.27.0";
963
+ declare const VERSION = "0.29.0";
955
964
 
956
965
  export { type ActiveFormats, 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, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportImageOptions, 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, 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
@@ -2658,6 +2658,7 @@ function drawHexPath(ctx, cx, cy, cellSize, orientation) {
2658
2658
  var DOM_ELEMENT_TYPES = /* @__PURE__ */ new Set(["note", "html", "text"]);
2659
2659
  var ARROWHEAD_LENGTH = 12;
2660
2660
  var ARROWHEAD_ANGLE = Math.PI / 6;
2661
+ var ARROW_LABEL_FONT_SIZE = 14;
2661
2662
  var ElementRenderer = class {
2662
2663
  store = null;
2663
2664
  imageCache = /* @__PURE__ */ new Map();
@@ -2668,6 +2669,7 @@ var ElementRenderer = class {
2668
2669
  hexTileCache = null;
2669
2670
  hexTileCacheKey = "";
2670
2671
  gridBoundsOverride = null;
2672
+ labelEditingId = null;
2671
2673
  setStore(store) {
2672
2674
  this.store = store;
2673
2675
  }
@@ -2686,6 +2688,9 @@ var ElementRenderer = class {
2686
2688
  setGridBoundsOverride(bounds) {
2687
2689
  this.gridBoundsOverride = bounds;
2688
2690
  }
2691
+ setLabelEditingId(id) {
2692
+ this.labelEditingId = id;
2693
+ }
2689
2694
  isDomElement(element) {
2690
2695
  return DOM_ELEMENT_TYPES.has(element.type);
2691
2696
  }
@@ -2762,6 +2767,28 @@ var ElementRenderer = class {
2762
2767
  ctx.stroke();
2763
2768
  this.renderArrowhead(ctx, arrow, visualTo, geometry.tangentEnd);
2764
2769
  ctx.restore();
2770
+ this.renderArrowLabel(ctx, arrow);
2771
+ }
2772
+ renderArrowLabel(ctx, arrow) {
2773
+ if (!arrow.label || arrow.label.length === 0) return;
2774
+ if (arrow.id === this.labelEditingId) return;
2775
+ const mid = getArrowMidpoint(arrow.from, arrow.to, arrow.bend);
2776
+ ctx.save();
2777
+ ctx.font = `${ARROW_LABEL_FONT_SIZE}px system-ui, sans-serif`;
2778
+ const metrics = ctx.measureText(arrow.label);
2779
+ const padX = 6;
2780
+ const padY = 4;
2781
+ const w = metrics.width + padX * 2;
2782
+ const h = ARROW_LABEL_FONT_SIZE + padY * 2;
2783
+ ctx.fillStyle = "rgba(255, 255, 255, 0.9)";
2784
+ ctx.beginPath();
2785
+ ctx.roundRect(mid.x - w / 2, mid.y - h / 2, w, h, 4);
2786
+ ctx.fill();
2787
+ ctx.fillStyle = "#1a1a1a";
2788
+ ctx.textAlign = "center";
2789
+ ctx.textBaseline = "middle";
2790
+ ctx.fillText(arrow.label, mid.x, mid.y);
2791
+ ctx.restore();
2765
2792
  }
2766
2793
  renderArrowhead(ctx, arrow, tip, angle) {
2767
2794
  ctx.beginPath();
@@ -3172,6 +3199,7 @@ function createArrow(input) {
3172
3199
  };
3173
3200
  if (input.fromBinding) result.fromBinding = input.fromBinding;
3174
3201
  if (input.toBinding) result.toBinding = input.toBinding;
3202
+ if (input.label !== void 0) result.label = input.label;
3175
3203
  return result;
3176
3204
  }
3177
3205
  function createImage(input) {
@@ -3635,6 +3663,84 @@ var NoteEditor = class {
3635
3663
  }
3636
3664
  };
3637
3665
 
3666
+ // src/elements/arrow-label-editor.ts
3667
+ var ArrowLabelEditor = class {
3668
+ input = null;
3669
+ done = false;
3670
+ get isEditing() {
3671
+ return this.input !== null;
3672
+ }
3673
+ startEditing(start) {
3674
+ if (this.input) this.cleanup();
3675
+ const { arrow, layer, store, recorder, onDone } = start;
3676
+ const mid = getArrowMidpoint(arrow.from, arrow.to, arrow.bend);
3677
+ const input = document.createElement("input");
3678
+ input.type = "text";
3679
+ input.value = arrow.label ?? "";
3680
+ Object.assign(input.style, {
3681
+ position: "absolute",
3682
+ left: `${mid.x}px`,
3683
+ top: `${mid.y}px`,
3684
+ transform: "translate(-50%, -50%)",
3685
+ // domLayer is pointer-events:none; the input must opt back in to receive taps/clicks.
3686
+ pointerEvents: "auto",
3687
+ font: "14px system-ui, sans-serif",
3688
+ padding: "2px 6px",
3689
+ border: "1px solid #2196F3",
3690
+ borderRadius: "4px",
3691
+ background: "#ffffff",
3692
+ color: "#1a1a1a",
3693
+ outline: "none",
3694
+ minWidth: "40px"
3695
+ });
3696
+ this.done = false;
3697
+ const commit = () => {
3698
+ if (this.done) return;
3699
+ this.done = true;
3700
+ const next = input.value.trim() || void 0;
3701
+ if (next !== arrow.label) {
3702
+ recorder.begin();
3703
+ store.update(arrow.id, { label: next });
3704
+ recorder.commit();
3705
+ }
3706
+ this.cleanup();
3707
+ onDone();
3708
+ };
3709
+ const cancel = () => {
3710
+ if (this.done) return;
3711
+ this.done = true;
3712
+ this.cleanup();
3713
+ onDone();
3714
+ };
3715
+ input.addEventListener("keydown", (e) => {
3716
+ if (e.key === "Enter") {
3717
+ e.preventDefault();
3718
+ commit();
3719
+ } else if (e.key === "Escape") {
3720
+ e.preventDefault();
3721
+ cancel();
3722
+ }
3723
+ e.stopPropagation();
3724
+ });
3725
+ input.addEventListener("blur", commit);
3726
+ layer.appendChild(input);
3727
+ this.input = input;
3728
+ input.focus();
3729
+ input.select();
3730
+ }
3731
+ /** Abort any in-progress edit without committing (e.g. on viewport teardown). */
3732
+ cancel() {
3733
+ this.done = true;
3734
+ this.cleanup();
3735
+ }
3736
+ cleanup() {
3737
+ if (this.input) {
3738
+ this.input.remove();
3739
+ this.input = null;
3740
+ }
3741
+ }
3742
+ };
3743
+
3638
3744
  // src/tools/tool-manager.ts
3639
3745
  var ToolManager = class {
3640
3746
  tools = /* @__PURE__ */ new Map();
@@ -5244,6 +5350,7 @@ function getElementStyle(element) {
5244
5350
 
5245
5351
  // src/canvas/viewport.ts
5246
5352
  var EMPTY_IDS = [];
5353
+ var ARROW_HIT_THRESHOLD = 10;
5247
5354
  function noop() {
5248
5355
  }
5249
5356
  function sharedValue(values) {
@@ -5285,6 +5392,7 @@ var Viewport = class {
5285
5392
  placeholder: options.placeholder
5286
5393
  });
5287
5394
  this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
5395
+ this.arrowLabelEditor = new ArrowLabelEditor();
5288
5396
  this.noteEditor.setHistoryHooks(
5289
5397
  () => this.historyRecorder.begin(),
5290
5398
  () => this.historyRecorder.commit()
@@ -5411,6 +5519,7 @@ var Viewport = class {
5411
5519
  background;
5412
5520
  renderer;
5413
5521
  noteEditor;
5522
+ arrowLabelEditor;
5414
5523
  historyRecorder;
5415
5524
  toolContext;
5416
5525
  marginViewport;
@@ -5665,6 +5774,7 @@ var Viewport = class {
5665
5774
  this.renderLoop.stop();
5666
5775
  this.interactMode.destroy();
5667
5776
  this.noteEditor.destroy(this.store);
5777
+ this.arrowLabelEditor.cancel();
5668
5778
  this.historyRecorder.destroy();
5669
5779
  this.wrapper.removeEventListener("pointerdown", this.onTapDown);
5670
5780
  this.wrapper.removeEventListener("pointerup", this.onDoubleTap);
@@ -5750,8 +5860,35 @@ var Viewport = class {
5750
5860
  const hit = this.hitTestWorld(world);
5751
5861
  if (hit?.type === "html") {
5752
5862
  this.interactMode.startInteracting(hit.id);
5863
+ return;
5864
+ }
5865
+ const arrow = this.findArrowAt(world);
5866
+ if (arrow) {
5867
+ this.startArrowLabelEdit(arrow);
5753
5868
  }
5754
5869
  };
5870
+ findArrowAt(world) {
5871
+ const candidates = this.store.queryPoint(world).reverse();
5872
+ for (const el of candidates) {
5873
+ if (el.type === "arrow" && isNearBezier(world, el.from, el.to, el.bend, ARROW_HIT_THRESHOLD)) {
5874
+ return el;
5875
+ }
5876
+ }
5877
+ return void 0;
5878
+ }
5879
+ startArrowLabelEdit(arrow) {
5880
+ this.arrowLabelEditor.startEditing({
5881
+ arrow,
5882
+ layer: this.domLayer,
5883
+ store: this.store,
5884
+ recorder: this.historyRecorder,
5885
+ onDone: () => {
5886
+ this.renderer.setLabelEditingId(null);
5887
+ this.requestRender();
5888
+ }
5889
+ });
5890
+ this.renderer.setLabelEditingId(arrow.id);
5891
+ }
5755
5892
  hitTestWorld(world) {
5756
5893
  const candidates = this.store.queryPoint(world).reverse();
5757
5894
  for (const el of candidates) {
@@ -6073,6 +6210,79 @@ function hitTestStroke(stroke, point, radius) {
6073
6210
  return false;
6074
6211
  }
6075
6212
 
6213
+ // src/elements/stroke-erase.ts
6214
+ function lerp(a, b, t) {
6215
+ return {
6216
+ x: a.x + (b.x - a.x) * t,
6217
+ y: a.y + (b.y - a.y) * t,
6218
+ pressure: a.pressure + (b.pressure - a.pressure) * t
6219
+ };
6220
+ }
6221
+ function erasePoints(points, eraser, radius) {
6222
+ const r2 = radius * radius;
6223
+ if (points.length < 2) {
6224
+ const p = points[0];
6225
+ if (p && (p.x - eraser.x) ** 2 + (p.y - eraser.y) ** 2 <= r2) return [];
6226
+ return null;
6227
+ }
6228
+ const runs = [];
6229
+ let current = [];
6230
+ let erased = false;
6231
+ const flush = () => {
6232
+ if (current.length >= 2) runs.push(current);
6233
+ current = [];
6234
+ };
6235
+ for (let i = 0; i < points.length - 1; i++) {
6236
+ const a = points[i];
6237
+ const b = points[i + 1];
6238
+ if (!a || !b) continue;
6239
+ const dx = b.x - a.x;
6240
+ const dy = b.y - a.y;
6241
+ const fx = a.x - eraser.x;
6242
+ const fy = a.y - eraser.y;
6243
+ const A = dx * dx + dy * dy;
6244
+ const B = 2 * (fx * dx + fy * dy);
6245
+ const C = fx * fx + fy * fy - r2;
6246
+ let tLo = 1;
6247
+ let tHi = 0;
6248
+ if (A === 0) {
6249
+ if (C <= 0) {
6250
+ tLo = 0;
6251
+ tHi = 1;
6252
+ }
6253
+ } else {
6254
+ const disc = B * B - 4 * A * C;
6255
+ if (disc >= 0) {
6256
+ const sq = Math.sqrt(disc);
6257
+ const lo = Math.max(0, (-B - sq) / (2 * A));
6258
+ const hi = Math.min(1, (-B + sq) / (2 * A));
6259
+ if (lo < hi) {
6260
+ tLo = lo;
6261
+ tHi = hi;
6262
+ }
6263
+ }
6264
+ }
6265
+ if (tLo > tHi) {
6266
+ if (current.length === 0) current.push(a);
6267
+ current.push(b);
6268
+ continue;
6269
+ }
6270
+ erased = true;
6271
+ if (tLo > 0) {
6272
+ if (current.length === 0) current.push(a);
6273
+ current.push(lerp(a, b, tLo));
6274
+ flush();
6275
+ } else {
6276
+ flush();
6277
+ }
6278
+ if (tHi < 1) {
6279
+ current = [lerp(a, b, tHi), b];
6280
+ }
6281
+ }
6282
+ flush();
6283
+ return erased ? runs : null;
6284
+ }
6285
+
6076
6286
  // src/tools/eraser-tool.ts
6077
6287
  var DEFAULT_RADIUS = 20;
6078
6288
  function makeEraserCursor(radius) {
@@ -6085,12 +6295,21 @@ var EraserTool = class {
6085
6295
  erasing = false;
6086
6296
  radius;
6087
6297
  cursor;
6298
+ mode;
6088
6299
  constructor(options = {}) {
6089
6300
  this.radius = options.radius ?? DEFAULT_RADIUS;
6090
6301
  this.cursor = makeEraserCursor(this.radius);
6302
+ this.mode = options.mode ?? "partial";
6091
6303
  }
6092
6304
  getOptions() {
6093
- return { radius: this.radius };
6305
+ return { radius: this.radius, mode: this.mode };
6306
+ }
6307
+ setOptions(options) {
6308
+ if (options.mode !== void 0) this.mode = options.mode;
6309
+ if (options.radius !== void 0) {
6310
+ this.radius = options.radius;
6311
+ this.cursor = makeEraserCursor(this.radius);
6312
+ }
6094
6313
  }
6095
6314
  onActivate(ctx) {
6096
6315
  ctx.setCursor?.(this.cursor);
@@ -6123,10 +6342,30 @@ var EraserTool = class {
6123
6342
  if (el.type !== "stroke") continue;
6124
6343
  if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
6125
6344
  if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
6126
- if (this.strokeIntersects(el, world)) {
6345
+ if (!this.strokeIntersects(el, world)) continue;
6346
+ if (this.mode === "stroke") {
6127
6347
  ctx.store.remove(el.id);
6128
6348
  erased = true;
6349
+ continue;
6350
+ }
6351
+ const localEraser = { x: world.x - el.position.x, y: world.y - el.position.y };
6352
+ const runs = erasePoints(el.points, localEraser, this.radius);
6353
+ if (runs === null) continue;
6354
+ ctx.store.remove(el.id);
6355
+ for (const run of runs) {
6356
+ ctx.store.add(
6357
+ createStroke({
6358
+ points: run,
6359
+ color: el.color,
6360
+ width: el.width,
6361
+ opacity: el.opacity,
6362
+ layerId: el.layerId,
6363
+ zIndex: el.zIndex,
6364
+ position: el.position
6365
+ })
6366
+ );
6129
6367
  }
6368
+ erased = true;
6130
6369
  }
6131
6370
  if (erased) ctx.requestRender();
6132
6371
  }
@@ -7665,7 +7904,7 @@ var TemplateTool = class {
7665
7904
  };
7666
7905
 
7667
7906
  // src/index.ts
7668
- var VERSION = "0.27.0";
7907
+ var VERSION = "0.29.0";
7669
7908
  export {
7670
7909
  ArrowTool,
7671
7910
  AutoSave,