@fieldnotes/core 0.20.0 → 0.21.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
@@ -2719,6 +2719,7 @@ var ElementRenderer = class {
2719
2719
  store = null;
2720
2720
  imageCache = /* @__PURE__ */ new Map();
2721
2721
  onImageLoad = null;
2722
+ onImageError = null;
2722
2723
  camera = null;
2723
2724
  canvasSize = null;
2724
2725
  hexTileCache = null;
@@ -2729,6 +2730,9 @@ var ElementRenderer = class {
2729
2730
  setOnImageLoad(callback) {
2730
2731
  this.onImageLoad = callback;
2731
2732
  }
2733
+ setOnImageError(callback) {
2734
+ this.onImageError = callback;
2735
+ }
2732
2736
  setCamera(camera) {
2733
2737
  this.camera = camera;
2734
2738
  }
@@ -3087,6 +3091,10 @@ var ElementRenderer = class {
3087
3091
  ctx.restore();
3088
3092
  }
3089
3093
  renderImage(ctx, image) {
3094
+ if (this.imageCache.get(image.src) === "failed") {
3095
+ this.renderImagePlaceholder(ctx, image);
3096
+ return;
3097
+ }
3090
3098
  const img = this.getImage(image.src);
3091
3099
  if (!img) return;
3092
3100
  ctx.drawImage(
@@ -3097,6 +3105,27 @@ var ElementRenderer = class {
3097
3105
  image.size.h
3098
3106
  );
3099
3107
  }
3108
+ renderImagePlaceholder(ctx, image) {
3109
+ const { x, y } = image.position;
3110
+ const { w, h } = image.size;
3111
+ ctx.save();
3112
+ ctx.fillStyle = "#eeeeee";
3113
+ ctx.fillRect(x, y, w, h);
3114
+ ctx.strokeStyle = "#bdbdbd";
3115
+ ctx.lineWidth = 1;
3116
+ ctx.strokeRect(x, y, w, h);
3117
+ const glyph = Math.min(24, w / 2, h / 2);
3118
+ const cx = x + w / 2;
3119
+ const cy = y + h / 2;
3120
+ ctx.strokeStyle = "#9e9e9e";
3121
+ ctx.lineWidth = 2;
3122
+ ctx.beginPath();
3123
+ ctx.arc(cx, cy, glyph / 2, 0, Math.PI * 2);
3124
+ ctx.moveTo(cx - glyph / 2, cy + glyph / 2);
3125
+ ctx.lineTo(cx + glyph / 2, cy - glyph / 2);
3126
+ ctx.stroke();
3127
+ ctx.restore();
3128
+ }
3100
3129
  getHexTile(cellSize, orientation, strokeColor, strokeWidth, opacity, scale) {
3101
3130
  const key = `${cellSize}:${orientation}:${strokeColor}:${strokeWidth}:${opacity}:${scale}`;
3102
3131
  if (this.hexTileCacheKey === key && this.hexTileCache) {
@@ -3112,6 +3141,7 @@ var ElementRenderer = class {
3112
3141
  getImage(src) {
3113
3142
  const cached = this.imageCache.get(src);
3114
3143
  if (cached) {
3144
+ if (cached === "failed") return null;
3115
3145
  if (cached instanceof HTMLImageElement) return cached.complete ? cached : null;
3116
3146
  return cached;
3117
3147
  }
@@ -3128,6 +3158,11 @@ var ElementRenderer = class {
3128
3158
  });
3129
3159
  }
3130
3160
  };
3161
+ img.onerror = () => {
3162
+ this.imageCache.set(src, "failed");
3163
+ this.onImageError?.(src);
3164
+ this.onImageLoad?.();
3165
+ };
3131
3166
  return null;
3132
3167
  }
3133
3168
  };
@@ -5039,6 +5074,17 @@ var Viewport = class {
5039
5074
  this.renderLoop.markAllLayersDirty();
5040
5075
  this.requestRender();
5041
5076
  });
5077
+ this.renderer.setOnImageError((src) => {
5078
+ const elementIds = [];
5079
+ for (const el of this.store.getAll()) {
5080
+ if (el.type === "image" && el.src === src) elementIds.push(el.id);
5081
+ }
5082
+ if (options.onImageError) {
5083
+ options.onImageError({ src, elementIds });
5084
+ } else {
5085
+ console.warn(`[fieldnotes] image failed to load: ${src}`);
5086
+ }
5087
+ });
5042
5088
  this.noteEditor = new NoteEditor({
5043
5089
  fontSizePresets: options.fontSizePresets,
5044
5090
  toolbar: options.toolbar,
@@ -5743,6 +5789,43 @@ var PencilTool = class {
5743
5789
  }
5744
5790
  };
5745
5791
 
5792
+ // src/elements/stroke-hit.ts
5793
+ function distSqToSegment(p, a, b) {
5794
+ const abx = b.x - a.x;
5795
+ const aby = b.y - a.y;
5796
+ const apx = p.x - a.x;
5797
+ const apy = p.y - a.y;
5798
+ const lenSq = abx * abx + aby * aby;
5799
+ if (lenSq === 0) {
5800
+ return apx * apx + apy * apy;
5801
+ }
5802
+ const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / lenSq));
5803
+ const dx = p.x - (a.x + t * abx);
5804
+ const dy = p.y - (a.y + t * aby);
5805
+ return dx * dx + dy * dy;
5806
+ }
5807
+ function hitTestStroke(stroke, point, radius) {
5808
+ const bounds = getElementBounds(stroke);
5809
+ if (!bounds) return false;
5810
+ if (point.x < bounds.x - radius || point.x > bounds.x + bounds.w + radius || point.y < bounds.y - radius || point.y > bounds.y + bounds.h + radius) {
5811
+ return false;
5812
+ }
5813
+ const radiusSq = radius * radius;
5814
+ const local = { x: point.x - stroke.position.x, y: point.y - stroke.position.y };
5815
+ const { segments } = getStrokeRenderData(stroke);
5816
+ if (segments.length === 0) {
5817
+ const p = stroke.points[0];
5818
+ if (!p) return false;
5819
+ const dx = p.x - local.x;
5820
+ const dy = p.y - local.y;
5821
+ return dx * dx + dy * dy <= radiusSq;
5822
+ }
5823
+ for (const seg of segments) {
5824
+ if (distSqToSegment(local, seg.start, seg.end) <= radiusSq) return true;
5825
+ }
5826
+ return false;
5827
+ }
5828
+
5746
5829
  // src/tools/eraser-tool.ts
5747
5830
  var DEFAULT_RADIUS = 20;
5748
5831
  function makeEraserCursor(radius) {
@@ -5801,12 +5884,7 @@ var EraserTool = class {
5801
5884
  if (erased) ctx.requestRender();
5802
5885
  }
5803
5886
  strokeIntersects(stroke, point) {
5804
- const radiusSq = this.radius * this.radius;
5805
- return stroke.points.some((p) => {
5806
- const dx = p.x + stroke.position.x - point.x;
5807
- const dy = p.y + stroke.position.y - point.y;
5808
- return dx * dx + dy * dy <= radiusSq;
5809
- });
5887
+ return hitTestStroke(stroke, point, this.radius);
5810
5888
  }
5811
5889
  };
5812
5890
 
@@ -6486,12 +6564,7 @@ var SelectTool = class {
6486
6564
  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;
6487
6565
  }
6488
6566
  if (el.type === "stroke") {
6489
- const HIT_RADIUS = 10;
6490
- return el.points.some((p) => {
6491
- const dx = p.x + el.position.x - point.x;
6492
- const dy = p.y + el.position.y - point.y;
6493
- return dx * dx + dy * dy <= HIT_RADIUS * HIT_RADIUS;
6494
- });
6567
+ return hitTestStroke(el, point, 10);
6495
6568
  }
6496
6569
  if (el.type === "arrow") {
6497
6570
  return isNearBezier(point, el.from, el.to, el.bend, 10);
@@ -7327,7 +7400,7 @@ var TemplateTool = class {
7327
7400
  };
7328
7401
 
7329
7402
  // src/index.ts
7330
- var VERSION = "0.20.0";
7403
+ var VERSION = "0.21.0";
7331
7404
  // Annotate the CommonJS export names for ESM import in node:
7332
7405
  0 && (module.exports = {
7333
7406
  AddElementCommand,