@fieldnotes/core 0.29.0 → 0.31.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
@@ -2276,6 +2276,26 @@ function getArrowRenderGeometry(arrow) {
2276
2276
  return geometry;
2277
2277
  }
2278
2278
 
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
+ }
2287
+ function lineEndpoints(shape) {
2288
+ const { x, y } = shape.position;
2289
+ const { w, h } = shape.size;
2290
+ return shape.flip ? [
2291
+ { x, y: y + h },
2292
+ { x: x + w, y }
2293
+ ] : [
2294
+ { x, y },
2295
+ { x: x + w, y: y + h }
2296
+ ];
2297
+ }
2298
+
2279
2299
  // src/elements/arrow-binding.ts
2280
2300
  var BINDABLE_TYPES = /* @__PURE__ */ new Set(["note", "text", "image", "html", "shape"]);
2281
2301
  function isBindable(element) {
@@ -2800,6 +2820,7 @@ var ElementRenderer = class {
2800
2820
  renderStroke(ctx, stroke) {
2801
2821
  if (stroke.points.length < 2) return;
2802
2822
  ctx.save();
2823
+ if (stroke.blendMode) ctx.globalCompositeOperation = stroke.blendMode;
2803
2824
  ctx.translate(stroke.position.x, stroke.position.y);
2804
2825
  ctx.strokeStyle = stroke.color;
2805
2826
  ctx.lineCap = "round";
@@ -2922,7 +2943,7 @@ var ElementRenderer = class {
2922
2943
  }
2923
2944
  renderShape(ctx, shape) {
2924
2945
  ctx.save();
2925
- if (shape.fillColor !== "none") {
2946
+ if (shape.fillColor !== "none" && shape.shape !== "line") {
2926
2947
  ctx.fillStyle = shape.fillColor;
2927
2948
  this.fillShapePath(ctx, shape);
2928
2949
  }
@@ -2961,6 +2982,15 @@ var ElementRenderer = class {
2961
2982
  ctx.stroke();
2962
2983
  break;
2963
2984
  }
2985
+ case "line": {
2986
+ const [a, b] = lineEndpoints(shape);
2987
+ ctx.lineCap = "round";
2988
+ ctx.beginPath();
2989
+ ctx.moveTo(a.x, a.y);
2990
+ ctx.lineTo(b.x, b.y);
2991
+ ctx.stroke();
2992
+ break;
2993
+ }
2964
2994
  }
2965
2995
  }
2966
2996
  renderGrid(ctx, grid) {
@@ -3234,7 +3264,7 @@ var ElementRenderer = class {
3234
3264
  // src/elements/element-factory.ts
3235
3265
  var DEFAULT_NOTE_FONT_SIZE = 18;
3236
3266
  function createStroke(input) {
3237
- return {
3267
+ const result = {
3238
3268
  id: createId("stroke"),
3239
3269
  type: "stroke",
3240
3270
  position: input.position ?? { x: 0, y: 0 },
@@ -3246,6 +3276,8 @@ function createStroke(input) {
3246
3276
  width: input.width ?? 2,
3247
3277
  opacity: input.opacity ?? 1
3248
3278
  };
3279
+ if (input.blendMode) result.blendMode = input.blendMode;
3280
+ return result;
3249
3281
  }
3250
3282
  function createNote(input) {
3251
3283
  return {
@@ -3310,7 +3342,7 @@ function createHtmlElement(input) {
3310
3342
  return el;
3311
3343
  }
3312
3344
  function createShape(input) {
3313
- return {
3345
+ const result = {
3314
3346
  id: createId("shape"),
3315
3347
  type: "shape",
3316
3348
  position: input.position,
@@ -3323,6 +3355,8 @@ function createShape(input) {
3323
3355
  strokeWidth: input.strokeWidth ?? 2,
3324
3356
  fillColor: input.fillColor ?? "none"
3325
3357
  };
3358
+ if (input.flip) result.flip = input.flip;
3359
+ return result;
3326
3360
  }
3327
3361
  function createGrid(input) {
3328
3362
  return {
@@ -6153,7 +6187,7 @@ var DEFAULT_MIN_POINT_DISTANCE = 3;
6153
6187
  var DEFAULT_PROGRESSIVE_THRESHOLD = 200;
6154
6188
  var PROGRESSIVE_HOT_ZONE = 30;
6155
6189
  var PencilTool = class {
6156
- name = "pencil";
6190
+ name;
6157
6191
  drawing = false;
6158
6192
  points = [];
6159
6193
  color;
@@ -6162,14 +6196,19 @@ var PencilTool = class {
6162
6196
  minPointDistance;
6163
6197
  progressiveThreshold;
6164
6198
  nextSimplifyAt;
6199
+ opacity;
6200
+ blendMode;
6165
6201
  optionListeners = /* @__PURE__ */ new Set();
6166
6202
  constructor(options = {}) {
6203
+ this.name = options.name ?? "pencil";
6167
6204
  this.color = options.color ?? "#000000";
6168
6205
  this.width = options.width ?? 2;
6169
6206
  this.smoothing = options.smoothing ?? DEFAULT_SMOOTHING;
6170
6207
  this.minPointDistance = options.minPointDistance ?? DEFAULT_MIN_POINT_DISTANCE;
6171
6208
  this.progressiveThreshold = options.progressiveSimplifyThreshold ?? DEFAULT_PROGRESSIVE_THRESHOLD;
6172
6209
  this.nextSimplifyAt = this.progressiveThreshold;
6210
+ this.opacity = options.opacity ?? 1;
6211
+ this.blendMode = options.blendMode;
6173
6212
  }
6174
6213
  onActivate(ctx) {
6175
6214
  ctx.setCursor?.("crosshair");
@@ -6183,7 +6222,9 @@ var PencilTool = class {
6183
6222
  width: this.width,
6184
6223
  smoothing: this.smoothing,
6185
6224
  minPointDistance: this.minPointDistance,
6186
- progressiveSimplifyThreshold: this.progressiveThreshold
6225
+ progressiveSimplifyThreshold: this.progressiveThreshold,
6226
+ opacity: this.opacity,
6227
+ blendMode: this.blendMode
6187
6228
  };
6188
6229
  }
6189
6230
  onOptionsChange(listener) {
@@ -6197,6 +6238,8 @@ var PencilTool = class {
6197
6238
  if (options.minPointDistance !== void 0) this.minPointDistance = options.minPointDistance;
6198
6239
  if (options.progressiveSimplifyThreshold !== void 0)
6199
6240
  this.progressiveThreshold = options.progressiveSimplifyThreshold;
6241
+ if (options.opacity !== void 0) this.opacity = options.opacity;
6242
+ if (options.blendMode !== void 0) this.blendMode = options.blendMode;
6200
6243
  this.notifyOptionsChange();
6201
6244
  }
6202
6245
  onPointerDown(state, ctx) {
@@ -6238,7 +6281,9 @@ var PencilTool = class {
6238
6281
  points: simplified,
6239
6282
  color: this.color,
6240
6283
  width: this.width,
6241
- layerId: ctx.activeLayerId ?? ""
6284
+ layerId: ctx.activeLayerId ?? "",
6285
+ opacity: this.opacity,
6286
+ blendMode: this.blendMode
6242
6287
  });
6243
6288
  ctx.store.add(stroke);
6244
6289
  computeStrokeSegments(stroke);
@@ -6254,7 +6299,8 @@ var PencilTool = class {
6254
6299
  ctx.strokeStyle = this.color;
6255
6300
  ctx.lineCap = "round";
6256
6301
  ctx.lineJoin = "round";
6257
- ctx.globalAlpha = 0.8;
6302
+ ctx.globalAlpha = this.blendMode ? this.opacity : 0.8;
6303
+ if (this.blendMode) ctx.globalCompositeOperation = this.blendMode;
6258
6304
  const segments = smoothToSegments(this.points);
6259
6305
  for (const seg of segments) {
6260
6306
  const w = (pressureToWidth(seg.start.pressure, this.width) + pressureToWidth(seg.end.pressure, this.width)) / 2;
@@ -6643,6 +6689,12 @@ var SelectTool = class {
6643
6689
  ctx.requestRender();
6644
6690
  return;
6645
6691
  }
6692
+ const lineHit = this.hitTestLineHandles(world, ctx);
6693
+ if (lineHit) {
6694
+ this.mode = { type: "line-handle", elementId: lineHit.elementId, fixed: lineHit.fixed };
6695
+ ctx.requestRender();
6696
+ return;
6697
+ }
6646
6698
  const templateResizeHit = this.hitTestTemplateResizeHandle(world, ctx);
6647
6699
  if (templateResizeHit) {
6648
6700
  this.mode = { type: "resizing-template", elementId: templateResizeHit };
@@ -6698,6 +6750,15 @@ var SelectTool = class {
6698
6750
  applyArrowHandleDrag(this.mode.handle, this.mode.elementId, world, ctx);
6699
6751
  return;
6700
6752
  }
6753
+ if (this.mode.type === "line-handle") {
6754
+ ctx.setCursor?.("grabbing");
6755
+ const el = ctx.store.getById(this.mode.elementId);
6756
+ if (el && el.type === "shape") {
6757
+ ctx.store.update(el.id, lineFromEndpoints(this.mode.fixed, world));
6758
+ }
6759
+ ctx.requestRender();
6760
+ return;
6761
+ }
6701
6762
  if (this.mode.type === "resizing-template") {
6702
6763
  ctx.setCursor?.("nwse-resize");
6703
6764
  this.handleTemplateResize(world, ctx);
@@ -6866,6 +6927,10 @@ var SelectTool = class {
6866
6927
  ctx.setCursor?.(getArrowHandleCursor(arrowHit.handle, false));
6867
6928
  return null;
6868
6929
  }
6930
+ if (this.hitTestLineHandles(world, ctx)) {
6931
+ ctx.setCursor?.("grab");
6932
+ return null;
6933
+ }
6869
6934
  const templateResizeHit = this.hitTestTemplateResizeHandle(world, ctx);
6870
6935
  if (templateResizeHit) {
6871
6936
  ctx.setCursor?.("nwse-resize");
@@ -6953,6 +7018,7 @@ var SelectTool = class {
6953
7018
  for (const id of this._selectedIds) {
6954
7019
  const el = ctx.store.getById(id);
6955
7020
  if (!el || !("size" in el)) continue;
7021
+ if (el.type === "shape" && el.shape === "line") continue;
6956
7022
  const bounds = getElementBounds(el);
6957
7023
  if (!bounds) continue;
6958
7024
  const corners = this.getHandlePositions(bounds);
@@ -6964,6 +7030,20 @@ var SelectTool = class {
6964
7030
  }
6965
7031
  return null;
6966
7032
  }
7033
+ hitTestLineHandles(world, ctx) {
7034
+ if (this._selectedIds.length === 0) return null;
7035
+ const zoom = ctx.camera.zoom;
7036
+ const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
7037
+ const r2 = r * r;
7038
+ for (const id of this._selectedIds) {
7039
+ const el = ctx.store.getById(id);
7040
+ if (!el || el.type !== "shape" || el.shape !== "line") continue;
7041
+ const [a, b] = lineEndpoints(el);
7042
+ if ((world.x - a.x) ** 2 + (world.y - a.y) ** 2 <= r2) return { elementId: id, fixed: b };
7043
+ if ((world.x - b.x) ** 2 + (world.y - b.y) ** 2 <= r2) return { elementId: id, fixed: a };
7044
+ }
7045
+ return null;
7046
+ }
6967
7047
  getHandlePositions(bounds) {
6968
7048
  return [
6969
7049
  ["nw", { x: bounds.x, y: bounds.y }],
@@ -7001,6 +7081,19 @@ var SelectTool = class {
7001
7081
  this.renderBindingHighlights(canvasCtx, el, zoom);
7002
7082
  continue;
7003
7083
  }
7084
+ if (el.type === "shape" && el.shape === "line") {
7085
+ canvasCtx.setLineDash([]);
7086
+ canvasCtx.fillStyle = "#ffffff";
7087
+ const r = handleWorldSize / 2;
7088
+ for (const p of lineEndpoints(el)) {
7089
+ canvasCtx.beginPath();
7090
+ canvasCtx.arc(p.x, p.y, r, 0, Math.PI * 2);
7091
+ canvasCtx.fill();
7092
+ canvasCtx.stroke();
7093
+ }
7094
+ canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7095
+ continue;
7096
+ }
7004
7097
  const bounds = getElementBounds(el);
7005
7098
  if (!bounds) continue;
7006
7099
  const pad = SELECTION_PAD / zoom;
@@ -7144,6 +7237,11 @@ var SelectTool = class {
7144
7237
  }
7145
7238
  isInsideBounds(point, el) {
7146
7239
  if (el.type === "grid") return false;
7240
+ if (el.type === "shape" && el.shape === "line") {
7241
+ const [a, b] = lineEndpoints(el);
7242
+ const threshold = Math.max(el.strokeWidth / 2, 6);
7243
+ return distSqToSegment(point, a, b) <= threshold * threshold;
7244
+ }
7147
7245
  if ("size" in el) {
7148
7246
  const s = el.size;
7149
7247
  return point.x >= el.position.x && point.x <= el.position.x + s.w && point.y >= el.position.y && point.y <= el.position.y + s.h;
@@ -7457,6 +7555,15 @@ var ImageTool = class {
7457
7555
  };
7458
7556
 
7459
7557
  // src/tools/shape-tool.ts
7558
+ function snapTo45(start, end) {
7559
+ const dx = end.x - start.x;
7560
+ const dy = end.y - start.y;
7561
+ const len = Math.hypot(dx, dy);
7562
+ if (len === 0) return { ...end };
7563
+ const step = Math.PI / 4;
7564
+ const angle = Math.round(Math.atan2(dy, dx) / step) * step;
7565
+ return { x: start.x + Math.cos(angle) * len, y: start.y + Math.sin(angle) * len };
7566
+ }
7460
7567
  var ShapeTool = class {
7461
7568
  name = "shape";
7462
7569
  drawing = false;
@@ -7514,13 +7621,17 @@ var ShapeTool = class {
7514
7621
  onPointerMove(state, ctx) {
7515
7622
  if (!this.drawing) return;
7516
7623
  this.end = this.snap(ctx.camera.screenToWorld({ x: state.x, y: state.y }), ctx);
7624
+ if (this.shape === "line" && this.shiftHeld) {
7625
+ this.end = snapTo45(this.start, this.end);
7626
+ }
7517
7627
  ctx.requestRender();
7518
7628
  }
7519
7629
  onPointerUp(_state, ctx) {
7520
7630
  if (!this.drawing) return;
7521
7631
  this.drawing = false;
7522
7632
  const { position, size } = this.computeRect();
7523
- if (size.w === 0 || size.h === 0) return;
7633
+ const isLine = this.shape === "line";
7634
+ if (isLine ? size.w === 0 && size.h === 0 : size.w === 0 || size.h === 0) return;
7524
7635
  const shape = createShape({
7525
7636
  position,
7526
7637
  size,
@@ -7528,6 +7639,7 @@ var ShapeTool = class {
7528
7639
  strokeColor: this.strokeColor,
7529
7640
  strokeWidth: this.strokeWidth,
7530
7641
  fillColor: this.fillColor,
7642
+ ...isLine ? { flip: this.end.x > this.start.x !== this.end.y > this.start.y } : {},
7531
7643
  layerId: ctx.activeLayerId ?? ""
7532
7644
  });
7533
7645
  ctx.store.add(shape);
@@ -7561,6 +7673,13 @@ var ShapeTool = class {
7561
7673
  ctx.stroke();
7562
7674
  break;
7563
7675
  }
7676
+ case "line":
7677
+ ctx.lineCap = "round";
7678
+ ctx.beginPath();
7679
+ ctx.moveTo(this.start.x, this.start.y);
7680
+ ctx.lineTo(this.end.x, this.end.y);
7681
+ ctx.stroke();
7682
+ break;
7564
7683
  }
7565
7684
  ctx.restore();
7566
7685
  }
@@ -7569,7 +7688,7 @@ var ShapeTool = class {
7569
7688
  let y = Math.min(this.start.y, this.end.y);
7570
7689
  let w = Math.abs(this.end.x - this.start.x);
7571
7690
  let h = Math.abs(this.end.y - this.start.y);
7572
- if (this.shiftHeld) {
7691
+ if (this.shiftHeld && this.shape !== "line") {
7573
7692
  const side = Math.max(w, h);
7574
7693
  w = side;
7575
7694
  h = side;
@@ -7985,7 +8104,7 @@ var TemplateTool = class {
7985
8104
  };
7986
8105
 
7987
8106
  // src/index.ts
7988
- var VERSION = "0.29.0";
8107
+ var VERSION = "0.31.0";
7989
8108
  // Annotate the CommonJS export names for ESM import in node:
7990
8109
  0 && (module.exports = {
7991
8110
  ArrowTool,