@fieldnotes/core 0.46.0 → 0.47.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
@@ -698,8 +698,20 @@ var InputFilter = class _InputFilter {
698
698
 
699
699
  // src/elements/create-id.ts
700
700
  var counter = 0;
701
+ function randomClientComponent() {
702
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
703
+ const bytes = new Uint8Array(8);
704
+ crypto.getRandomValues(bytes);
705
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
706
+ }
707
+ return Math.random().toString(36).slice(2, 14);
708
+ }
709
+ var CLIENT = randomClientComponent();
710
+ function formatId(prefix, time, counter2, client) {
711
+ return `${prefix}_${time}_${counter2}_${client}`;
712
+ }
701
713
  function createId(prefix) {
702
- return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
714
+ return formatId(prefix, Date.now().toString(36), (counter++).toString(36), CLIENT);
703
715
  }
704
716
 
705
717
  // src/core/geometry.ts
@@ -3312,6 +3324,48 @@ function drawHexPath(ctx, cx, cy, cellSize, orientation) {
3312
3324
  ctx.closePath();
3313
3325
  }
3314
3326
 
3327
+ // src/elements/renderers/template-measure.ts
3328
+ function renderTemplateFeetLabel(ctx, p) {
3329
+ if (p.feet <= 0) return;
3330
+ const directional = p.templateShape === "cone" || p.templateShape === "line";
3331
+ const dir = directional ? p.angle : 0;
3332
+ const length = p.templateShape === "square" ? p.radius / 2 : p.radius;
3333
+ const cos = Math.cos(dir);
3334
+ const sin = Math.sin(dir);
3335
+ const start = p.position;
3336
+ const end = { x: start.x + length * cos, y: start.y + length * sin };
3337
+ const midX = start.x + length / 2 * cos;
3338
+ const midY = start.y + length / 2 * sin;
3339
+ ctx.save();
3340
+ ctx.globalAlpha = 1;
3341
+ ctx.beginPath();
3342
+ ctx.setLineDash([4, 4]);
3343
+ ctx.strokeStyle = p.color;
3344
+ ctx.lineWidth = 1.5;
3345
+ ctx.moveTo(start.x, start.y);
3346
+ ctx.lineTo(end.x, end.y);
3347
+ ctx.stroke();
3348
+ ctx.setLineDash([]);
3349
+ const label = `${Math.round(p.feet)} ft`;
3350
+ const fontSize = Math.max(10, Math.min(14, p.radius * 0.15));
3351
+ ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
3352
+ ctx.textAlign = "center";
3353
+ ctx.textBaseline = "bottom";
3354
+ const textY = midY - 4;
3355
+ const metrics = ctx.measureText(label);
3356
+ const padX = 4;
3357
+ const padY = 2;
3358
+ const textW = metrics.width + padX * 2;
3359
+ const textH = fontSize + padY * 2;
3360
+ ctx.fillStyle = "rgba(255, 255, 255, 0.85)";
3361
+ ctx.beginPath();
3362
+ ctx.roundRect(midX - textW / 2, textY - textH, textW, textH, 3);
3363
+ ctx.fill();
3364
+ ctx.fillStyle = p.color;
3365
+ ctx.fillText(label, midX, textY - padY);
3366
+ ctx.restore();
3367
+ }
3368
+
3315
3369
  // src/elements/renderers/template-renderer.ts
3316
3370
  function renderTemplate(ctx, template, store) {
3317
3371
  const grid = store?.getElementsByType("grid")[0];
@@ -3335,9 +3389,6 @@ function renderGeometricTemplate(ctx, template) {
3335
3389
  ctx.arc(cx, cy, r, 0, Math.PI * 2);
3336
3390
  ctx.fill();
3337
3391
  ctx.stroke();
3338
- if (template.radiusFeet != null && template.radiusFeet > 0) {
3339
- renderRadiusMarker(ctx, cx, cy, r, template.radiusFeet);
3340
- }
3341
3392
  break;
3342
3393
  case "square":
3343
3394
  ctx.fillRect(cx - r / 2, cy - r / 2, r, r);
@@ -3370,6 +3421,16 @@ function renderGeometricTemplate(ctx, template) {
3370
3421
  break;
3371
3422
  }
3372
3423
  }
3424
+ if (template.radiusFeet != null && template.radiusFeet > 0) {
3425
+ renderTemplateFeetLabel(ctx, {
3426
+ position: template.position,
3427
+ radius: template.radius,
3428
+ angle: template.angle,
3429
+ templateShape: template.templateShape,
3430
+ feet: template.radiusFeet,
3431
+ color: template.strokeColor
3432
+ });
3433
+ }
3373
3434
  ctx.restore();
3374
3435
  }
3375
3436
  function renderHexTemplate(ctx, template, cellSize, orientation) {
@@ -3416,44 +3477,18 @@ function renderHexTemplate(ctx, template, cellSize, orientation) {
3416
3477
  ctx.lineWidth = template.strokeWidth;
3417
3478
  ctx.stroke();
3418
3479
  }
3419
- if (template.templateShape === "circle" && template.radiusFeet != null && template.radiusFeet > 0) {
3420
- const r = template.radius;
3421
- renderRadiusMarker(ctx, center2.x, center2.y, r, template.radiusFeet);
3480
+ if (template.radiusFeet != null && template.radiusFeet > 0) {
3481
+ renderTemplateFeetLabel(ctx, {
3482
+ position: template.position,
3483
+ radius: template.radius,
3484
+ angle: template.angle,
3485
+ templateShape: template.templateShape,
3486
+ feet: template.radiusFeet,
3487
+ color: template.strokeColor
3488
+ });
3422
3489
  }
3423
3490
  ctx.restore();
3424
3491
  }
3425
- function renderRadiusMarker(ctx, cx, cy, r, feet) {
3426
- const markerColor = ctx.strokeStyle;
3427
- ctx.save();
3428
- ctx.globalAlpha = 1;
3429
- ctx.beginPath();
3430
- ctx.setLineDash([4, 4]);
3431
- ctx.strokeStyle = markerColor;
3432
- ctx.lineWidth = 1.5;
3433
- ctx.moveTo(cx, cy);
3434
- ctx.lineTo(cx + r, cy);
3435
- ctx.stroke();
3436
- ctx.setLineDash([]);
3437
- const label = `${Math.round(feet)} ft`;
3438
- const fontSize = Math.max(10, Math.min(14, r * 0.15));
3439
- ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
3440
- ctx.textAlign = "center";
3441
- ctx.textBaseline = "bottom";
3442
- const textX = cx + r / 2;
3443
- const textY = cy - 4;
3444
- const metrics = ctx.measureText(label);
3445
- const padX = 4;
3446
- const padY = 2;
3447
- const textW = metrics.width + padX * 2;
3448
- const textH = fontSize + padY * 2;
3449
- ctx.fillStyle = "rgba(255, 255, 255, 0.85)";
3450
- ctx.beginPath();
3451
- ctx.roundRect(textX - textW / 2, textY - textH, textW, textH, 3);
3452
- ctx.fill();
3453
- ctx.fillStyle = markerColor;
3454
- ctx.fillText(label, textX, textY - padY);
3455
- ctx.restore();
3456
- }
3457
3492
 
3458
3493
  // src/elements/grid-renderer.ts
3459
3494
  function getSquareGridLines(bounds, cellSize) {
@@ -8299,6 +8334,20 @@ function getOverlayLayout(el, zoom) {
8299
8334
  const rotateHandle = rotatePoint(topMid, center2, angle);
8300
8335
  return { center: center2, corners, rotateHandle, angle };
8301
8336
  }
8337
+ function templateAimKnob(el, zoom) {
8338
+ if (el.type !== "template") return null;
8339
+ if (el.templateShape !== "cone" && el.templateShape !== "line") return null;
8340
+ const gap = ROTATE_HANDLE_OFFSET / zoom;
8341
+ const dist = el.radius + gap;
8342
+ const origin = el.position;
8343
+ return {
8344
+ origin,
8345
+ knob: {
8346
+ x: origin.x + dist * Math.cos(el.angle),
8347
+ y: origin.y + dist * Math.sin(el.angle)
8348
+ }
8349
+ };
8350
+ }
8302
8351
  function getHandlePositions(bounds) {
8303
8352
  return [
8304
8353
  ["nw", { x: bounds.x, y: bounds.y }],
@@ -8447,6 +8496,22 @@ function renderSelectionBoxes(ctx, p) {
8447
8496
  handleWorldSize
8448
8497
  );
8449
8498
  ctx.setLineDash([4 / zoom, 4 / zoom]);
8499
+ if (p.selectedIds.length === 1 && (el.templateShape === "cone" || el.templateShape === "line")) {
8500
+ const aim = templateAimKnob(el, zoom);
8501
+ if (aim) {
8502
+ ctx.beginPath();
8503
+ ctx.moveTo(aim.origin.x, aim.origin.y);
8504
+ ctx.lineTo(aim.knob.x, aim.knob.y);
8505
+ ctx.stroke();
8506
+ ctx.setLineDash([]);
8507
+ ctx.fillStyle = "#ffffff";
8508
+ ctx.beginPath();
8509
+ ctx.arc(aim.knob.x, aim.knob.y, handleWorldSize / 2, 0, Math.PI * 2);
8510
+ ctx.fill();
8511
+ ctx.stroke();
8512
+ ctx.setLineDash([4 / zoom, 4 / zoom]);
8513
+ }
8514
+ }
8450
8515
  }
8451
8516
  if (p.selectedIds.length === 1 && ROTATABLE_TYPES.has(el.type)) {
8452
8517
  const stemStart = topMidpoint(layout);
@@ -8602,6 +8667,19 @@ function hitTestTemplateResizeHandle(world, ctx, selectedIds) {
8602
8667
  }
8603
8668
  return null;
8604
8669
  }
8670
+ function hitTestTemplateAimHandle(world, ctx, selectedIds) {
8671
+ if (selectedIds.length !== 1) return null;
8672
+ const id = selectedIds[0];
8673
+ if (!id) return null;
8674
+ const el = ctx.store.getById(id);
8675
+ if (!el || el.locked) return null;
8676
+ const knob = templateAimKnob(el, ctx.camera.zoom);
8677
+ if (!knob) return null;
8678
+ const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / ctx.camera.zoom;
8679
+ const dx = world.x - knob.knob.x;
8680
+ const dy = world.y - knob.knob.y;
8681
+ return dx * dx + dy * dy <= r * r ? { elementId: id } : null;
8682
+ }
8605
8683
  function findElementsInRect(marquee, ctx) {
8606
8684
  const candidates = ctx.store.queryRect(marquee);
8607
8685
  const ids = [];
@@ -8851,6 +8929,12 @@ var SelectTool = class {
8851
8929
  ctx.requestRender();
8852
8930
  return;
8853
8931
  }
8932
+ const aimHit = hitTestTemplateAimHandle(world, ctx, this._selectedIds);
8933
+ if (aimHit) {
8934
+ this.mode = { type: "aiming-template", elementId: aimHit.elementId };
8935
+ ctx.requestRender();
8936
+ return;
8937
+ }
8854
8938
  const rotateHit = hitTestRotateHandle(world, ctx, this._selectedIds);
8855
8939
  if (rotateHit) {
8856
8940
  const el = ctx.store.getById(rotateHit.elementId);
@@ -8932,6 +9016,19 @@ var SelectTool = class {
8932
9016
  this.handleTemplateResize(world, ctx);
8933
9017
  return;
8934
9018
  }
9019
+ if (this.mode.type === "aiming-template") {
9020
+ const el = ctx.store.getById(this.mode.elementId);
9021
+ if (el && el.type === "template" && !el.locked) {
9022
+ let a = Math.atan2(world.y - el.position.y, world.x - el.position.x);
9023
+ if (state.shiftKey) {
9024
+ const snap = ctx.gridType === "hex" ? Math.PI / 3 : ROTATE_SNAP;
9025
+ a = Math.round(a / snap) * snap;
9026
+ }
9027
+ ctx.store.update(this.mode.elementId, { angle: normalizeAngle(a) });
9028
+ ctx.requestRender();
9029
+ }
9030
+ return;
9031
+ }
8935
9032
  if (this.mode.type === "rotating") {
8936
9033
  const { elementId, center: center2, startPointerAngle, startRotation } = this.mode;
8937
9034
  const a = Math.atan2(world.y - center2.y, world.x - center2.x);
@@ -9138,6 +9235,10 @@ var SelectTool = class {
9138
9235
  ctx.setCursor?.("nwse-resize");
9139
9236
  return null;
9140
9237
  }
9238
+ if (hitTestTemplateAimHandle(world, ctx, this._selectedIds)) {
9239
+ ctx.setCursor?.("grab");
9240
+ return null;
9241
+ }
9141
9242
  if (hitTestRotateHandle(world, ctx, this._selectedIds)) {
9142
9243
  ctx.setCursor?.("grab");
9143
9244
  return null;
@@ -9807,6 +9908,7 @@ var TemplateTool = class {
9807
9908
  gridType;
9808
9909
  hexOrientation;
9809
9910
  snapEnabled = false;
9911
+ feetScaleUnit = 0;
9810
9912
  templateShape;
9811
9913
  fillColor;
9812
9914
  strokeColor;
@@ -9855,6 +9957,7 @@ var TemplateTool = class {
9855
9957
  this.gridType = ctx.gridType;
9856
9958
  this.hexOrientation = ctx.hexOrientation;
9857
9959
  this.snapEnabled = !!ctx.gridType || (ctx.snapToGrid ?? false);
9960
+ this.feetScaleUnit = ctx.gridSize && ctx.gridSize > 0 ? ctx.gridType === "hex" ? Math.sqrt(3) * ctx.gridSize : ctx.gridSize : 0;
9858
9961
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
9859
9962
  this.origin = this.snapToGrid(world, ctx);
9860
9963
  this.current = { ...this.origin };
@@ -9954,6 +10057,7 @@ var TemplateTool = class {
9954
10057
  break;
9955
10058
  }
9956
10059
  }
10060
+ this.drawFeetLabel(ctx, radius);
9957
10061
  ctx.restore();
9958
10062
  }
9959
10063
  renderHexOverlay(ctx, radius) {
@@ -10004,30 +10108,7 @@ var TemplateTool = class {
10004
10108
  ctx.lineWidth = this.strokeWidth;
10005
10109
  ctx.stroke();
10006
10110
  }
10007
- if (this.templateShape === "circle") {
10008
- const feet = radiusCells * this.feetPerCell;
10009
- if (feet > 0) {
10010
- ctx.globalAlpha = 1;
10011
- const label = `${Math.round(feet)} ft`;
10012
- const fontSize = Math.max(10, Math.min(14, radius * 0.15));
10013
- ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
10014
- ctx.textAlign = "center";
10015
- ctx.textBaseline = "bottom";
10016
- const textX = center2.x;
10017
- const textY = center2.y - 4;
10018
- const metrics = ctx.measureText(label);
10019
- const padX = 4;
10020
- const padY = 2;
10021
- const textW = metrics.width + padX * 2;
10022
- const textH = fontSize + padY * 2;
10023
- ctx.fillStyle = "rgba(255, 255, 255, 0.85)";
10024
- ctx.beginPath();
10025
- ctx.roundRect(textX - textW / 2, textY - textH, textW, textH, 3);
10026
- ctx.fill();
10027
- ctx.fillStyle = this.strokeColor;
10028
- ctx.fillText(label, textX, textY - padY);
10029
- }
10030
- }
10111
+ this.drawFeetLabel(ctx, radius);
10031
10112
  ctx.restore();
10032
10113
  }
10033
10114
  computeRadius() {
@@ -10045,6 +10126,17 @@ var TemplateTool = class {
10045
10126
  const dy = this.current.y - this.origin.y;
10046
10127
  return Math.atan2(dy, dx);
10047
10128
  }
10129
+ drawFeetLabel(ctx, radius) {
10130
+ const feet = this.feetScaleUnit > 0 ? radius / this.feetScaleUnit * this.feetPerCell : 0;
10131
+ renderTemplateFeetLabel(ctx, {
10132
+ position: this.origin,
10133
+ radius,
10134
+ angle: this.computeAngle(),
10135
+ templateShape: this.templateShape,
10136
+ feet,
10137
+ color: this.strokeColor
10138
+ });
10139
+ }
10048
10140
  snapToGrid(point, ctx) {
10049
10141
  if (!ctx.gridSize) return point;
10050
10142
  if (ctx.gridType === "hex" && ctx.hexOrientation) {
@@ -10174,7 +10266,7 @@ var LaserTool = class {
10174
10266
  };
10175
10267
 
10176
10268
  // src/index.ts
10177
- var VERSION = "0.46.0";
10269
+ var VERSION = "0.47.0";
10178
10270
  // Annotate the CommonJS export names for ESM import in node:
10179
10271
  0 && (module.exports = {
10180
10272
  ArrowTool,