@fieldnotes/core 0.28.0 → 0.30.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,19 @@ function getArrowRenderGeometry(arrow) {
2276
2276
  return geometry;
2277
2277
  }
2278
2278
 
2279
+ // src/elements/shape-geometry.ts
2280
+ function lineEndpoints(shape) {
2281
+ const { x, y } = shape.position;
2282
+ const { w, h } = shape.size;
2283
+ return shape.flip ? [
2284
+ { x, y: y + h },
2285
+ { x: x + w, y }
2286
+ ] : [
2287
+ { x, y },
2288
+ { x: x + w, y: y + h }
2289
+ ];
2290
+ }
2291
+
2279
2292
  // src/elements/arrow-binding.ts
2280
2293
  var BINDABLE_TYPES = /* @__PURE__ */ new Set(["note", "text", "image", "html", "shape"]);
2281
2294
  function isBindable(element) {
@@ -2800,6 +2813,7 @@ var ElementRenderer = class {
2800
2813
  renderStroke(ctx, stroke) {
2801
2814
  if (stroke.points.length < 2) return;
2802
2815
  ctx.save();
2816
+ if (stroke.blendMode) ctx.globalCompositeOperation = stroke.blendMode;
2803
2817
  ctx.translate(stroke.position.x, stroke.position.y);
2804
2818
  ctx.strokeStyle = stroke.color;
2805
2819
  ctx.lineCap = "round";
@@ -2922,7 +2936,7 @@ var ElementRenderer = class {
2922
2936
  }
2923
2937
  renderShape(ctx, shape) {
2924
2938
  ctx.save();
2925
- if (shape.fillColor !== "none") {
2939
+ if (shape.fillColor !== "none" && shape.shape !== "line") {
2926
2940
  ctx.fillStyle = shape.fillColor;
2927
2941
  this.fillShapePath(ctx, shape);
2928
2942
  }
@@ -2961,6 +2975,15 @@ var ElementRenderer = class {
2961
2975
  ctx.stroke();
2962
2976
  break;
2963
2977
  }
2978
+ case "line": {
2979
+ const [a, b] = lineEndpoints(shape);
2980
+ ctx.lineCap = "round";
2981
+ ctx.beginPath();
2982
+ ctx.moveTo(a.x, a.y);
2983
+ ctx.lineTo(b.x, b.y);
2984
+ ctx.stroke();
2985
+ break;
2986
+ }
2964
2987
  }
2965
2988
  }
2966
2989
  renderGrid(ctx, grid) {
@@ -3234,7 +3257,7 @@ var ElementRenderer = class {
3234
3257
  // src/elements/element-factory.ts
3235
3258
  var DEFAULT_NOTE_FONT_SIZE = 18;
3236
3259
  function createStroke(input) {
3237
- return {
3260
+ const result = {
3238
3261
  id: createId("stroke"),
3239
3262
  type: "stroke",
3240
3263
  position: input.position ?? { x: 0, y: 0 },
@@ -3246,6 +3269,8 @@ function createStroke(input) {
3246
3269
  width: input.width ?? 2,
3247
3270
  opacity: input.opacity ?? 1
3248
3271
  };
3272
+ if (input.blendMode) result.blendMode = input.blendMode;
3273
+ return result;
3249
3274
  }
3250
3275
  function createNote(input) {
3251
3276
  return {
@@ -3310,7 +3335,7 @@ function createHtmlElement(input) {
3310
3335
  return el;
3311
3336
  }
3312
3337
  function createShape(input) {
3313
- return {
3338
+ const result = {
3314
3339
  id: createId("shape"),
3315
3340
  type: "shape",
3316
3341
  position: input.position,
@@ -3323,6 +3348,8 @@ function createShape(input) {
3323
3348
  strokeWidth: input.strokeWidth ?? 2,
3324
3349
  fillColor: input.fillColor ?? "none"
3325
3350
  };
3351
+ if (input.flip) result.flip = input.flip;
3352
+ return result;
3326
3353
  }
3327
3354
  function createGrid(input) {
3328
3355
  return {
@@ -6153,7 +6180,7 @@ var DEFAULT_MIN_POINT_DISTANCE = 3;
6153
6180
  var DEFAULT_PROGRESSIVE_THRESHOLD = 200;
6154
6181
  var PROGRESSIVE_HOT_ZONE = 30;
6155
6182
  var PencilTool = class {
6156
- name = "pencil";
6183
+ name;
6157
6184
  drawing = false;
6158
6185
  points = [];
6159
6186
  color;
@@ -6162,14 +6189,19 @@ var PencilTool = class {
6162
6189
  minPointDistance;
6163
6190
  progressiveThreshold;
6164
6191
  nextSimplifyAt;
6192
+ opacity;
6193
+ blendMode;
6165
6194
  optionListeners = /* @__PURE__ */ new Set();
6166
6195
  constructor(options = {}) {
6196
+ this.name = options.name ?? "pencil";
6167
6197
  this.color = options.color ?? "#000000";
6168
6198
  this.width = options.width ?? 2;
6169
6199
  this.smoothing = options.smoothing ?? DEFAULT_SMOOTHING;
6170
6200
  this.minPointDistance = options.minPointDistance ?? DEFAULT_MIN_POINT_DISTANCE;
6171
6201
  this.progressiveThreshold = options.progressiveSimplifyThreshold ?? DEFAULT_PROGRESSIVE_THRESHOLD;
6172
6202
  this.nextSimplifyAt = this.progressiveThreshold;
6203
+ this.opacity = options.opacity ?? 1;
6204
+ this.blendMode = options.blendMode;
6173
6205
  }
6174
6206
  onActivate(ctx) {
6175
6207
  ctx.setCursor?.("crosshair");
@@ -6183,7 +6215,9 @@ var PencilTool = class {
6183
6215
  width: this.width,
6184
6216
  smoothing: this.smoothing,
6185
6217
  minPointDistance: this.minPointDistance,
6186
- progressiveSimplifyThreshold: this.progressiveThreshold
6218
+ progressiveSimplifyThreshold: this.progressiveThreshold,
6219
+ opacity: this.opacity,
6220
+ blendMode: this.blendMode
6187
6221
  };
6188
6222
  }
6189
6223
  onOptionsChange(listener) {
@@ -6197,6 +6231,8 @@ var PencilTool = class {
6197
6231
  if (options.minPointDistance !== void 0) this.minPointDistance = options.minPointDistance;
6198
6232
  if (options.progressiveSimplifyThreshold !== void 0)
6199
6233
  this.progressiveThreshold = options.progressiveSimplifyThreshold;
6234
+ if (options.opacity !== void 0) this.opacity = options.opacity;
6235
+ if (options.blendMode !== void 0) this.blendMode = options.blendMode;
6200
6236
  this.notifyOptionsChange();
6201
6237
  }
6202
6238
  onPointerDown(state, ctx) {
@@ -6238,7 +6274,9 @@ var PencilTool = class {
6238
6274
  points: simplified,
6239
6275
  color: this.color,
6240
6276
  width: this.width,
6241
- layerId: ctx.activeLayerId ?? ""
6277
+ layerId: ctx.activeLayerId ?? "",
6278
+ opacity: this.opacity,
6279
+ blendMode: this.blendMode
6242
6280
  });
6243
6281
  ctx.store.add(stroke);
6244
6282
  computeStrokeSegments(stroke);
@@ -6254,7 +6292,8 @@ var PencilTool = class {
6254
6292
  ctx.strokeStyle = this.color;
6255
6293
  ctx.lineCap = "round";
6256
6294
  ctx.lineJoin = "round";
6257
- ctx.globalAlpha = 0.8;
6295
+ ctx.globalAlpha = this.blendMode ? this.opacity : 0.8;
6296
+ if (this.blendMode) ctx.globalCompositeOperation = this.blendMode;
6258
6297
  const segments = smoothToSegments(this.points);
6259
6298
  for (const seg of segments) {
6260
6299
  const w = (pressureToWidth(seg.start.pressure, this.width) + pressureToWidth(seg.end.pressure, this.width)) / 2;
@@ -6291,6 +6330,79 @@ function hitTestStroke(stroke, point, radius) {
6291
6330
  return false;
6292
6331
  }
6293
6332
 
6333
+ // src/elements/stroke-erase.ts
6334
+ function lerp(a, b, t) {
6335
+ return {
6336
+ x: a.x + (b.x - a.x) * t,
6337
+ y: a.y + (b.y - a.y) * t,
6338
+ pressure: a.pressure + (b.pressure - a.pressure) * t
6339
+ };
6340
+ }
6341
+ function erasePoints(points, eraser, radius) {
6342
+ const r2 = radius * radius;
6343
+ if (points.length < 2) {
6344
+ const p = points[0];
6345
+ if (p && (p.x - eraser.x) ** 2 + (p.y - eraser.y) ** 2 <= r2) return [];
6346
+ return null;
6347
+ }
6348
+ const runs = [];
6349
+ let current = [];
6350
+ let erased = false;
6351
+ const flush = () => {
6352
+ if (current.length >= 2) runs.push(current);
6353
+ current = [];
6354
+ };
6355
+ for (let i = 0; i < points.length - 1; i++) {
6356
+ const a = points[i];
6357
+ const b = points[i + 1];
6358
+ if (!a || !b) continue;
6359
+ const dx = b.x - a.x;
6360
+ const dy = b.y - a.y;
6361
+ const fx = a.x - eraser.x;
6362
+ const fy = a.y - eraser.y;
6363
+ const A = dx * dx + dy * dy;
6364
+ const B = 2 * (fx * dx + fy * dy);
6365
+ const C = fx * fx + fy * fy - r2;
6366
+ let tLo = 1;
6367
+ let tHi = 0;
6368
+ if (A === 0) {
6369
+ if (C <= 0) {
6370
+ tLo = 0;
6371
+ tHi = 1;
6372
+ }
6373
+ } else {
6374
+ const disc = B * B - 4 * A * C;
6375
+ if (disc >= 0) {
6376
+ const sq = Math.sqrt(disc);
6377
+ const lo = Math.max(0, (-B - sq) / (2 * A));
6378
+ const hi = Math.min(1, (-B + sq) / (2 * A));
6379
+ if (lo < hi) {
6380
+ tLo = lo;
6381
+ tHi = hi;
6382
+ }
6383
+ }
6384
+ }
6385
+ if (tLo > tHi) {
6386
+ if (current.length === 0) current.push(a);
6387
+ current.push(b);
6388
+ continue;
6389
+ }
6390
+ erased = true;
6391
+ if (tLo > 0) {
6392
+ if (current.length === 0) current.push(a);
6393
+ current.push(lerp(a, b, tLo));
6394
+ flush();
6395
+ } else {
6396
+ flush();
6397
+ }
6398
+ if (tHi < 1) {
6399
+ current = [lerp(a, b, tHi), b];
6400
+ }
6401
+ }
6402
+ flush();
6403
+ return erased ? runs : null;
6404
+ }
6405
+
6294
6406
  // src/tools/eraser-tool.ts
6295
6407
  var DEFAULT_RADIUS = 20;
6296
6408
  function makeEraserCursor(radius) {
@@ -6303,12 +6415,21 @@ var EraserTool = class {
6303
6415
  erasing = false;
6304
6416
  radius;
6305
6417
  cursor;
6418
+ mode;
6306
6419
  constructor(options = {}) {
6307
6420
  this.radius = options.radius ?? DEFAULT_RADIUS;
6308
6421
  this.cursor = makeEraserCursor(this.radius);
6422
+ this.mode = options.mode ?? "partial";
6309
6423
  }
6310
6424
  getOptions() {
6311
- return { radius: this.radius };
6425
+ return { radius: this.radius, mode: this.mode };
6426
+ }
6427
+ setOptions(options) {
6428
+ if (options.mode !== void 0) this.mode = options.mode;
6429
+ if (options.radius !== void 0) {
6430
+ this.radius = options.radius;
6431
+ this.cursor = makeEraserCursor(this.radius);
6432
+ }
6312
6433
  }
6313
6434
  onActivate(ctx) {
6314
6435
  ctx.setCursor?.(this.cursor);
@@ -6341,10 +6462,30 @@ var EraserTool = class {
6341
6462
  if (el.type !== "stroke") continue;
6342
6463
  if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
6343
6464
  if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
6344
- if (this.strokeIntersects(el, world)) {
6465
+ if (!this.strokeIntersects(el, world)) continue;
6466
+ if (this.mode === "stroke") {
6345
6467
  ctx.store.remove(el.id);
6346
6468
  erased = true;
6469
+ continue;
6347
6470
  }
6471
+ const localEraser = { x: world.x - el.position.x, y: world.y - el.position.y };
6472
+ const runs = erasePoints(el.points, localEraser, this.radius);
6473
+ if (runs === null) continue;
6474
+ ctx.store.remove(el.id);
6475
+ for (const run of runs) {
6476
+ ctx.store.add(
6477
+ createStroke({
6478
+ points: run,
6479
+ color: el.color,
6480
+ width: el.width,
6481
+ opacity: el.opacity,
6482
+ layerId: el.layerId,
6483
+ zIndex: el.zIndex,
6484
+ position: el.position
6485
+ })
6486
+ );
6487
+ }
6488
+ erased = true;
6348
6489
  }
6349
6490
  if (erased) ctx.requestRender();
6350
6491
  }
@@ -7042,6 +7183,11 @@ var SelectTool = class {
7042
7183
  }
7043
7184
  isInsideBounds(point, el) {
7044
7185
  if (el.type === "grid") return false;
7186
+ if (el.type === "shape" && el.shape === "line") {
7187
+ const [a, b] = lineEndpoints(el);
7188
+ const threshold = Math.max(el.strokeWidth / 2, 6);
7189
+ return distSqToSegment(point, a, b) <= threshold * threshold;
7190
+ }
7045
7191
  if ("size" in el) {
7046
7192
  const s = el.size;
7047
7193
  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;
@@ -7355,6 +7501,15 @@ var ImageTool = class {
7355
7501
  };
7356
7502
 
7357
7503
  // src/tools/shape-tool.ts
7504
+ function snapTo45(start, end) {
7505
+ const dx = end.x - start.x;
7506
+ const dy = end.y - start.y;
7507
+ const len = Math.hypot(dx, dy);
7508
+ if (len === 0) return { ...end };
7509
+ const step = Math.PI / 4;
7510
+ const angle = Math.round(Math.atan2(dy, dx) / step) * step;
7511
+ return { x: start.x + Math.cos(angle) * len, y: start.y + Math.sin(angle) * len };
7512
+ }
7358
7513
  var ShapeTool = class {
7359
7514
  name = "shape";
7360
7515
  drawing = false;
@@ -7412,13 +7567,17 @@ var ShapeTool = class {
7412
7567
  onPointerMove(state, ctx) {
7413
7568
  if (!this.drawing) return;
7414
7569
  this.end = this.snap(ctx.camera.screenToWorld({ x: state.x, y: state.y }), ctx);
7570
+ if (this.shape === "line" && this.shiftHeld) {
7571
+ this.end = snapTo45(this.start, this.end);
7572
+ }
7415
7573
  ctx.requestRender();
7416
7574
  }
7417
7575
  onPointerUp(_state, ctx) {
7418
7576
  if (!this.drawing) return;
7419
7577
  this.drawing = false;
7420
7578
  const { position, size } = this.computeRect();
7421
- if (size.w === 0 || size.h === 0) return;
7579
+ const isLine = this.shape === "line";
7580
+ if (isLine ? size.w === 0 && size.h === 0 : size.w === 0 || size.h === 0) return;
7422
7581
  const shape = createShape({
7423
7582
  position,
7424
7583
  size,
@@ -7426,6 +7585,7 @@ var ShapeTool = class {
7426
7585
  strokeColor: this.strokeColor,
7427
7586
  strokeWidth: this.strokeWidth,
7428
7587
  fillColor: this.fillColor,
7588
+ ...isLine ? { flip: this.end.x > this.start.x !== this.end.y > this.start.y } : {},
7429
7589
  layerId: ctx.activeLayerId ?? ""
7430
7590
  });
7431
7591
  ctx.store.add(shape);
@@ -7459,6 +7619,13 @@ var ShapeTool = class {
7459
7619
  ctx.stroke();
7460
7620
  break;
7461
7621
  }
7622
+ case "line":
7623
+ ctx.lineCap = "round";
7624
+ ctx.beginPath();
7625
+ ctx.moveTo(this.start.x, this.start.y);
7626
+ ctx.lineTo(this.end.x, this.end.y);
7627
+ ctx.stroke();
7628
+ break;
7462
7629
  }
7463
7630
  ctx.restore();
7464
7631
  }
@@ -7467,7 +7634,7 @@ var ShapeTool = class {
7467
7634
  let y = Math.min(this.start.y, this.end.y);
7468
7635
  let w = Math.abs(this.end.x - this.start.x);
7469
7636
  let h = Math.abs(this.end.y - this.start.y);
7470
- if (this.shiftHeld) {
7637
+ if (this.shiftHeld && this.shape !== "line") {
7471
7638
  const side = Math.max(w, h);
7472
7639
  w = side;
7473
7640
  h = side;
@@ -7883,7 +8050,7 @@ var TemplateTool = class {
7883
8050
  };
7884
8051
 
7885
8052
  // src/index.ts
7886
- var VERSION = "0.28.0";
8053
+ var VERSION = "0.30.0";
7887
8054
  // Annotate the CommonJS export names for ESM import in node:
7888
8055
  0 && (module.exports = {
7889
8056
  ArrowTool,