@fieldnotes/core 0.30.0 → 0.31.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/README.md CHANGED
@@ -439,7 +439,7 @@ new Viewport(container, {
439
439
 
440
440
  ```typescript
441
441
  new PencilTool({ color: '#ff0000', width: 3, smoothing: 1.5 });
442
- new EraserTool({ radius: 30 }); // mode: 'partial' (default) splits strokes at the erased span; mode: 'stroke' deletes the whole stroke
442
+ new EraserTool({ radius: 30 }); // radius is screen pixels (converted to world units per zoom); mode: 'partial' (default) splits strokes at the erased span; mode: 'stroke' deletes the whole stroke
443
443
  new ArrowTool({ color: '#333', width: 2 });
444
444
  ```
445
445
 
@@ -453,7 +453,7 @@ viewport.toolManager.register(
453
453
  viewport.setTool('highlighter');
454
454
  ```
455
455
 
456
- `ShapeTool` supports a `'line'` shape kind that draws a straight segment between two points. Hold **Shift** while drawing to snap to 45° increments. Lines are hit-tested by proximity to the segment, and `ShapeElement.flip` records which diagonal of the bounding box the line runs along.
456
+ `ShapeTool` supports a `'line'` shape kind that draws a straight segment between two points. Hold **Shift** while drawing to snap to 45° increments. Lines are hit-tested by proximity to the segment, and `ShapeElement.flip` records which diagonal of the bounding box the line runs along. When a line is selected, it shows two endpoint drag-handles instead of bounding-box resize handles — drag one endpoint to reshape the line while the other stays anchored.
457
457
 
458
458
  ### Arrow Labels
459
459
 
package/dist/index.cjs CHANGED
@@ -2277,6 +2277,13 @@ function getArrowRenderGeometry(arrow) {
2277
2277
  }
2278
2278
 
2279
2279
  // src/elements/shape-geometry.ts
2280
+ function lineFromEndpoints(a, b) {
2281
+ return {
2282
+ position: { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y) },
2283
+ size: { w: Math.abs(b.x - a.x), h: Math.abs(b.y - a.y) },
2284
+ flip: b.x > a.x !== b.y > a.y
2285
+ };
2286
+ }
2280
2287
  function lineEndpoints(shape) {
2281
2288
  const { x, y } = shape.position;
2282
2289
  const { w, h } = shape.size;
@@ -6450,11 +6457,12 @@ var EraserTool = class {
6450
6457
  }
6451
6458
  eraseAt(state, ctx) {
6452
6459
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
6460
+ const worldRadius = this.radius / ctx.camera.zoom;
6453
6461
  const queryBounds = {
6454
- x: world.x - this.radius,
6455
- y: world.y - this.radius,
6456
- w: this.radius * 2,
6457
- h: this.radius * 2
6462
+ x: world.x - worldRadius,
6463
+ y: world.y - worldRadius,
6464
+ w: worldRadius * 2,
6465
+ h: worldRadius * 2
6458
6466
  };
6459
6467
  const candidates = ctx.store.queryRect(queryBounds);
6460
6468
  let erased = false;
@@ -6462,14 +6470,14 @@ var EraserTool = class {
6462
6470
  if (el.type !== "stroke") continue;
6463
6471
  if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
6464
6472
  if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
6465
- if (!this.strokeIntersects(el, world)) continue;
6473
+ if (!this.strokeIntersects(el, world, worldRadius)) continue;
6466
6474
  if (this.mode === "stroke") {
6467
6475
  ctx.store.remove(el.id);
6468
6476
  erased = true;
6469
6477
  continue;
6470
6478
  }
6471
6479
  const localEraser = { x: world.x - el.position.x, y: world.y - el.position.y };
6472
- const runs = erasePoints(el.points, localEraser, this.radius);
6480
+ const runs = erasePoints(el.points, localEraser, worldRadius);
6473
6481
  if (runs === null) continue;
6474
6482
  ctx.store.remove(el.id);
6475
6483
  for (const run of runs) {
@@ -6489,8 +6497,8 @@ var EraserTool = class {
6489
6497
  }
6490
6498
  if (erased) ctx.requestRender();
6491
6499
  }
6492
- strokeIntersects(stroke, point) {
6493
- return hitTestStroke(stroke, point, this.radius);
6500
+ strokeIntersects(stroke, point, worldRadius) {
6501
+ return hitTestStroke(stroke, point, worldRadius);
6494
6502
  }
6495
6503
  };
6496
6504
 
@@ -6682,6 +6690,12 @@ var SelectTool = class {
6682
6690
  ctx.requestRender();
6683
6691
  return;
6684
6692
  }
6693
+ const lineHit = this.hitTestLineHandles(world, ctx);
6694
+ if (lineHit) {
6695
+ this.mode = { type: "line-handle", elementId: lineHit.elementId, fixed: lineHit.fixed };
6696
+ ctx.requestRender();
6697
+ return;
6698
+ }
6685
6699
  const templateResizeHit = this.hitTestTemplateResizeHandle(world, ctx);
6686
6700
  if (templateResizeHit) {
6687
6701
  this.mode = { type: "resizing-template", elementId: templateResizeHit };
@@ -6737,6 +6751,15 @@ var SelectTool = class {
6737
6751
  applyArrowHandleDrag(this.mode.handle, this.mode.elementId, world, ctx);
6738
6752
  return;
6739
6753
  }
6754
+ if (this.mode.type === "line-handle") {
6755
+ ctx.setCursor?.("grabbing");
6756
+ const el = ctx.store.getById(this.mode.elementId);
6757
+ if (el && el.type === "shape") {
6758
+ ctx.store.update(el.id, lineFromEndpoints(this.mode.fixed, world));
6759
+ }
6760
+ ctx.requestRender();
6761
+ return;
6762
+ }
6740
6763
  if (this.mode.type === "resizing-template") {
6741
6764
  ctx.setCursor?.("nwse-resize");
6742
6765
  this.handleTemplateResize(world, ctx);
@@ -6905,6 +6928,10 @@ var SelectTool = class {
6905
6928
  ctx.setCursor?.(getArrowHandleCursor(arrowHit.handle, false));
6906
6929
  return null;
6907
6930
  }
6931
+ if (this.hitTestLineHandles(world, ctx)) {
6932
+ ctx.setCursor?.("grab");
6933
+ return null;
6934
+ }
6908
6935
  const templateResizeHit = this.hitTestTemplateResizeHandle(world, ctx);
6909
6936
  if (templateResizeHit) {
6910
6937
  ctx.setCursor?.("nwse-resize");
@@ -6992,6 +7019,7 @@ var SelectTool = class {
6992
7019
  for (const id of this._selectedIds) {
6993
7020
  const el = ctx.store.getById(id);
6994
7021
  if (!el || !("size" in el)) continue;
7022
+ if (el.type === "shape" && el.shape === "line") continue;
6995
7023
  const bounds = getElementBounds(el);
6996
7024
  if (!bounds) continue;
6997
7025
  const corners = this.getHandlePositions(bounds);
@@ -7003,6 +7031,20 @@ var SelectTool = class {
7003
7031
  }
7004
7032
  return null;
7005
7033
  }
7034
+ hitTestLineHandles(world, ctx) {
7035
+ if (this._selectedIds.length === 0) return null;
7036
+ const zoom = ctx.camera.zoom;
7037
+ const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
7038
+ const r2 = r * r;
7039
+ for (const id of this._selectedIds) {
7040
+ const el = ctx.store.getById(id);
7041
+ if (!el || el.type !== "shape" || el.shape !== "line") continue;
7042
+ const [a, b] = lineEndpoints(el);
7043
+ if ((world.x - a.x) ** 2 + (world.y - a.y) ** 2 <= r2) return { elementId: id, fixed: b };
7044
+ if ((world.x - b.x) ** 2 + (world.y - b.y) ** 2 <= r2) return { elementId: id, fixed: a };
7045
+ }
7046
+ return null;
7047
+ }
7006
7048
  getHandlePositions(bounds) {
7007
7049
  return [
7008
7050
  ["nw", { x: bounds.x, y: bounds.y }],
@@ -7040,6 +7082,19 @@ var SelectTool = class {
7040
7082
  this.renderBindingHighlights(canvasCtx, el, zoom);
7041
7083
  continue;
7042
7084
  }
7085
+ if (el.type === "shape" && el.shape === "line") {
7086
+ canvasCtx.setLineDash([]);
7087
+ canvasCtx.fillStyle = "#ffffff";
7088
+ const r = handleWorldSize / 2;
7089
+ for (const p of lineEndpoints(el)) {
7090
+ canvasCtx.beginPath();
7091
+ canvasCtx.arc(p.x, p.y, r, 0, Math.PI * 2);
7092
+ canvasCtx.fill();
7093
+ canvasCtx.stroke();
7094
+ }
7095
+ canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7096
+ continue;
7097
+ }
7043
7098
  const bounds = getElementBounds(el);
7044
7099
  if (!bounds) continue;
7045
7100
  const pad = SELECTION_PAD / zoom;
@@ -8050,7 +8105,7 @@ var TemplateTool = class {
8050
8105
  };
8051
8106
 
8052
8107
  // src/index.ts
8053
- var VERSION = "0.30.0";
8108
+ var VERSION = "0.31.1";
8054
8109
  // Annotate the CommonJS export names for ESM import in node:
8055
8110
  0 && (module.exports = {
8056
8111
  ArrowTool,