@fieldnotes/core 0.2.0 → 0.2.2

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
@@ -152,6 +152,13 @@ function migrateElement(obj) {
152
152
  if (obj["type"] === "arrow" && typeof obj["bend"] !== "number") {
153
153
  obj["bend"] = 0;
154
154
  }
155
+ if (obj["type"] === "stroke" && Array.isArray(obj["points"])) {
156
+ for (const pt of obj["points"]) {
157
+ if (typeof pt["pressure"] !== "number") {
158
+ pt["pressure"] = 0.5;
159
+ }
160
+ }
161
+ }
155
162
  }
156
163
 
157
164
  // src/core/auto-save.ts
@@ -407,24 +414,8 @@ var InputHandler = class {
407
414
  y: e.clientY - rect.top
408
415
  });
409
416
  };
410
- isInteractiveHtmlContent(e) {
411
- const target = e.target;
412
- if (!target) return false;
413
- const node = target.closest("[data-element-id]");
414
- if (!node) return false;
415
- const elementId = node.dataset["elementId"];
416
- if (!elementId) return false;
417
- const store = this.toolContext?.store;
418
- if (!store) return false;
419
- const element = store.getById(elementId);
420
- if (!element || element.type !== "html") return false;
421
- return true;
422
- }
423
417
  onPointerDown = (e) => {
424
418
  this.activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
425
- if (this.isInteractiveHtmlContent(e)) {
426
- return;
427
- }
428
419
  this.element.setPointerCapture?.(e.pointerId);
429
420
  if (this.activePointers.size === 2) {
430
421
  this.startPinch();
@@ -750,6 +741,79 @@ function isNearLine(point, a, b, threshold) {
750
741
  return Math.hypot(point.x - projX, point.y - projY) <= threshold;
751
742
  }
752
743
 
744
+ // src/elements/stroke-smoothing.ts
745
+ var MIN_PRESSURE_SCALE = 0.2;
746
+ function pressureToWidth(pressure, baseWidth) {
747
+ return baseWidth * (MIN_PRESSURE_SCALE + (1 - MIN_PRESSURE_SCALE) * pressure);
748
+ }
749
+ function simplifyPoints(points, tolerance) {
750
+ if (points.length <= 2) return points.slice();
751
+ return rdp(points, 0, points.length - 1, tolerance);
752
+ }
753
+ function rdp(points, start, end, tolerance) {
754
+ const first = points[start];
755
+ const last = points[end];
756
+ if (!first || !last) return [];
757
+ if (end - start <= 1) return [first, last];
758
+ let maxDist = 0;
759
+ let maxIndex = start;
760
+ for (let i = start + 1; i < end; i++) {
761
+ const pt = points[i];
762
+ if (!pt) continue;
763
+ const dist = perpendicularDistance(pt, first, last);
764
+ if (dist > maxDist) {
765
+ maxDist = dist;
766
+ maxIndex = i;
767
+ }
768
+ }
769
+ if (maxDist <= tolerance) return [first, last];
770
+ const left = rdp(points, start, maxIndex, tolerance);
771
+ const right = rdp(points, maxIndex, end, tolerance);
772
+ return left.concat(right.slice(1));
773
+ }
774
+ function perpendicularDistance(pt, lineStart, lineEnd) {
775
+ const dx = lineEnd.x - lineStart.x;
776
+ const dy = lineEnd.y - lineStart.y;
777
+ const lenSq = dx * dx + dy * dy;
778
+ if (lenSq === 0) {
779
+ const ex = pt.x - lineStart.x;
780
+ const ey = pt.y - lineStart.y;
781
+ return Math.sqrt(ex * ex + ey * ey);
782
+ }
783
+ const num = Math.abs(dy * pt.x - dx * pt.y + lineEnd.x * lineStart.y - lineEnd.y * lineStart.x);
784
+ return num / Math.sqrt(lenSq);
785
+ }
786
+ function smoothToSegments(points) {
787
+ if (points.length < 2) return [];
788
+ if (points.length === 2) {
789
+ const p0 = points[0];
790
+ const p1 = points[1];
791
+ if (!p0 || !p1) return [];
792
+ const mx = (p0.x + p1.x) / 2;
793
+ const my = (p0.y + p1.y) / 2;
794
+ return [{ start: p0, cp1: { x: mx, y: my }, cp2: { x: mx, y: my }, end: p1 }];
795
+ }
796
+ const segments = [];
797
+ const n = points.length;
798
+ for (let i = 0; i < n - 1; i++) {
799
+ const p0 = points[Math.max(0, i - 1)];
800
+ const p1 = points[i];
801
+ const p2 = points[i + 1];
802
+ const p3 = points[Math.min(n - 1, i + 2)];
803
+ if (!p0 || !p1 || !p2 || !p3) continue;
804
+ const cp1 = {
805
+ x: p1.x + (p2.x - p0.x) / 6,
806
+ y: p1.y + (p2.y - p0.y) / 6
807
+ };
808
+ const cp2 = {
809
+ x: p2.x - (p3.x - p1.x) / 6,
810
+ y: p2.y - (p3.y - p1.y) / 6
811
+ };
812
+ segments.push({ start: p1, cp1, cp2, end: p2 });
813
+ }
814
+ return segments;
815
+ }
816
+
753
817
  // src/elements/element-renderer.ts
754
818
  var DOM_ELEMENT_TYPES = /* @__PURE__ */ new Set(["note", "image", "html"]);
755
819
  var ARROWHEAD_LENGTH = 12;
@@ -773,22 +837,18 @@ var ElementRenderer = class {
773
837
  ctx.save();
774
838
  ctx.translate(stroke.position.x, stroke.position.y);
775
839
  ctx.strokeStyle = stroke.color;
776
- ctx.lineWidth = stroke.width;
777
840
  ctx.lineCap = "round";
778
841
  ctx.lineJoin = "round";
779
842
  ctx.globalAlpha = stroke.opacity;
780
- ctx.beginPath();
781
- const first = stroke.points[0];
782
- if (first) {
783
- ctx.moveTo(first.x, first.y);
784
- }
785
- for (let i = 1; i < stroke.points.length; i++) {
786
- const pt = stroke.points[i];
787
- if (pt) {
788
- ctx.lineTo(pt.x, pt.y);
789
- }
843
+ const segments = smoothToSegments(stroke.points);
844
+ for (const seg of segments) {
845
+ const w = (pressureToWidth(seg.start.pressure, stroke.width) + pressureToWidth(seg.end.pressure, stroke.width)) / 2;
846
+ ctx.lineWidth = w;
847
+ ctx.beginPath();
848
+ ctx.moveTo(seg.start.x, seg.start.y);
849
+ ctx.bezierCurveTo(seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.end.x, seg.end.y);
850
+ ctx.stroke();
790
851
  }
791
- ctx.stroke();
792
852
  ctx.restore();
793
853
  }
794
854
  renderArrow(ctx, arrow) {
@@ -931,6 +991,9 @@ var ToolManager = class {
931
991
  register(tool) {
932
992
  this.tools.set(tool.name, tool);
933
993
  }
994
+ getTool(name) {
995
+ return this.tools.get(name);
996
+ }
934
997
  setTool(name, ctx) {
935
998
  const tool = this.tools.get(name);
936
999
  if (!tool) return;
@@ -1290,6 +1353,7 @@ var Viewport = class {
1290
1353
  needsRender = true;
1291
1354
  domNodes = /* @__PURE__ */ new Map();
1292
1355
  htmlContent = /* @__PURE__ */ new Map();
1356
+ interactingElementId = null;
1293
1357
  get ctx() {
1294
1358
  return this.canvasEl.getContext("2d");
1295
1359
  }
@@ -1335,6 +1399,7 @@ var Viewport = class {
1335
1399
  this.store.add(image);
1336
1400
  this.historyRecorder.commit();
1337
1401
  this.requestRender();
1402
+ return image.id;
1338
1403
  }
1339
1404
  addHtmlElement(dom, position, size = { w: 200, h: 150 }) {
1340
1405
  const el = createHtmlElement({ position, size });
@@ -1347,6 +1412,7 @@ var Viewport = class {
1347
1412
  }
1348
1413
  destroy() {
1349
1414
  cancelAnimationFrame(this.animFrameId);
1415
+ this.stopInteracting();
1350
1416
  this.noteEditor.destroy(this.store);
1351
1417
  this.historyRecorder.destroy();
1352
1418
  this.wrapper.removeEventListener("dblclick", this.onDblClick);
@@ -1404,11 +1470,74 @@ var Viewport = class {
1404
1470
  }
1405
1471
  onDblClick = (e) => {
1406
1472
  const el = document.elementFromPoint(e.clientX, e.clientY);
1407
- if (!el) return;
1408
- const nodeEl = el.closest("[data-element-id]");
1409
- if (!nodeEl) return;
1410
- const elementId = nodeEl.dataset["elementId"];
1411
- if (elementId) this.startEditingNote(elementId);
1473
+ const nodeEl = el?.closest("[data-element-id]");
1474
+ if (nodeEl) {
1475
+ const elementId = nodeEl.dataset["elementId"];
1476
+ if (elementId) {
1477
+ const element = this.store.getById(elementId);
1478
+ if (element?.type === "note") {
1479
+ this.startEditingNote(elementId);
1480
+ return;
1481
+ }
1482
+ }
1483
+ }
1484
+ const rect = this.wrapper.getBoundingClientRect();
1485
+ const screen = { x: e.clientX - rect.left, y: e.clientY - rect.top };
1486
+ const world = this.camera.screenToWorld(screen);
1487
+ const hit = this.hitTestWorld(world);
1488
+ if (hit?.type === "html") {
1489
+ this.startInteracting(hit.id);
1490
+ }
1491
+ };
1492
+ hitTestWorld(world) {
1493
+ const elements = this.store.getAll().reverse();
1494
+ for (const el of elements) {
1495
+ if (!("size" in el)) continue;
1496
+ const { x, y } = el.position;
1497
+ const { w, h } = el.size;
1498
+ if (world.x >= x && world.x <= x + w && world.y >= y && world.y <= y + h) {
1499
+ return el;
1500
+ }
1501
+ }
1502
+ return null;
1503
+ }
1504
+ startInteracting(id) {
1505
+ this.stopInteracting();
1506
+ const node = this.domNodes.get(id);
1507
+ if (!node) return;
1508
+ this.interactingElementId = id;
1509
+ node.style.pointerEvents = "auto";
1510
+ node.addEventListener("pointerdown", this.onInteractNodePointerDown);
1511
+ window.addEventListener("keydown", this.onInteractKeyDown);
1512
+ window.addEventListener("pointerdown", this.onInteractPointerDown);
1513
+ }
1514
+ stopInteracting() {
1515
+ if (!this.interactingElementId) return;
1516
+ const node = this.domNodes.get(this.interactingElementId);
1517
+ if (node) {
1518
+ node.style.pointerEvents = "none";
1519
+ node.removeEventListener("pointerdown", this.onInteractNodePointerDown);
1520
+ }
1521
+ this.interactingElementId = null;
1522
+ window.removeEventListener("keydown", this.onInteractKeyDown);
1523
+ window.removeEventListener("pointerdown", this.onInteractPointerDown);
1524
+ }
1525
+ onInteractNodePointerDown = (e) => {
1526
+ e.stopPropagation();
1527
+ };
1528
+ onInteractKeyDown = (e) => {
1529
+ if (e.key === "Escape") {
1530
+ this.stopInteracting();
1531
+ }
1532
+ };
1533
+ onInteractPointerDown = (e) => {
1534
+ if (!this.interactingElementId) return;
1535
+ const target = e.target;
1536
+ if (!target) return;
1537
+ const node = this.domNodes.get(this.interactingElementId);
1538
+ if (node && !node.contains(target)) {
1539
+ this.stopInteracting();
1540
+ }
1412
1541
  };
1413
1542
  onDragOver = (e) => {
1414
1543
  e.preventDefault();
@@ -1506,7 +1635,8 @@ var Viewport = class {
1506
1635
  if (content) {
1507
1636
  node.dataset["initialized"] = "true";
1508
1637
  Object.assign(node.style, {
1509
- overflow: "hidden"
1638
+ overflow: "hidden",
1639
+ pointerEvents: "none"
1510
1640
  });
1511
1641
  node.appendChild(content);
1512
1642
  }
@@ -1609,15 +1739,19 @@ var HandTool = class {
1609
1739
 
1610
1740
  // src/tools/pencil-tool.ts
1611
1741
  var MIN_POINTS_FOR_STROKE = 2;
1742
+ var DEFAULT_SMOOTHING = 1.5;
1743
+ var DEFAULT_PRESSURE = 0.5;
1612
1744
  var PencilTool = class {
1613
1745
  name = "pencil";
1614
1746
  drawing = false;
1615
1747
  points = [];
1616
1748
  color;
1617
1749
  width;
1750
+ smoothing;
1618
1751
  constructor(options = {}) {
1619
1752
  this.color = options.color ?? "#000000";
1620
1753
  this.width = options.width ?? 2;
1754
+ this.smoothing = options.smoothing ?? DEFAULT_SMOOTHING;
1621
1755
  }
1622
1756
  onActivate(ctx) {
1623
1757
  ctx.setCursor?.("crosshair");
@@ -1628,16 +1762,19 @@ var PencilTool = class {
1628
1762
  setOptions(options) {
1629
1763
  if (options.color !== void 0) this.color = options.color;
1630
1764
  if (options.width !== void 0) this.width = options.width;
1765
+ if (options.smoothing !== void 0) this.smoothing = options.smoothing;
1631
1766
  }
1632
1767
  onPointerDown(state, ctx) {
1633
1768
  this.drawing = true;
1634
1769
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
1635
- this.points = [world];
1770
+ const pressure = state.pressure === 0 ? DEFAULT_PRESSURE : state.pressure;
1771
+ this.points = [{ x: world.x, y: world.y, pressure }];
1636
1772
  }
1637
1773
  onPointerMove(state, ctx) {
1638
1774
  if (!this.drawing) return;
1639
1775
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
1640
- this.points.push(world);
1776
+ const pressure = state.pressure === 0 ? DEFAULT_PRESSURE : state.pressure;
1777
+ this.points.push({ x: world.x, y: world.y, pressure });
1641
1778
  ctx.requestRender();
1642
1779
  }
1643
1780
  onPointerUp(_state, ctx) {
@@ -1647,8 +1784,9 @@ var PencilTool = class {
1647
1784
  this.points = [];
1648
1785
  return;
1649
1786
  }
1787
+ const simplified = simplifyPoints(this.points, this.smoothing);
1650
1788
  const stroke = createStroke({
1651
- points: this.points,
1789
+ points: simplified,
1652
1790
  color: this.color,
1653
1791
  width: this.width
1654
1792
  });
@@ -1660,19 +1798,18 @@ var PencilTool = class {
1660
1798
  if (!this.drawing || this.points.length < 2) return;
1661
1799
  ctx.save();
1662
1800
  ctx.strokeStyle = this.color;
1663
- ctx.lineWidth = this.width;
1664
1801
  ctx.lineCap = "round";
1665
1802
  ctx.lineJoin = "round";
1666
1803
  ctx.globalAlpha = 0.8;
1667
- ctx.beginPath();
1668
- const first = this.points[0];
1669
- if (!first) return;
1670
- ctx.moveTo(first.x, first.y);
1671
- for (let i = 1; i < this.points.length; i++) {
1672
- const p = this.points[i];
1673
- if (p) ctx.lineTo(p.x, p.y);
1804
+ const segments = smoothToSegments(this.points);
1805
+ for (const seg of segments) {
1806
+ const w = (pressureToWidth(seg.start.pressure, this.width) + pressureToWidth(seg.end.pressure, this.width)) / 2;
1807
+ ctx.lineWidth = w;
1808
+ ctx.beginPath();
1809
+ ctx.moveTo(seg.start.x, seg.start.y);
1810
+ ctx.bezierCurveTo(seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.end.x, seg.end.y);
1811
+ ctx.stroke();
1674
1812
  }
1675
- ctx.stroke();
1676
1813
  ctx.restore();
1677
1814
  }
1678
1815
  };
@@ -1960,7 +2097,7 @@ var SelectTool = class {
1960
2097
  handleResize(world, ctx) {
1961
2098
  if (this.mode.type !== "resizing") return;
1962
2099
  const el = ctx.store.getById(this.mode.elementId);
1963
- if (!el || !("size" in el)) return;
2100
+ if (!el || !("size" in el) || el.locked) return;
1964
2101
  const { handle } = this.mode;
1965
2102
  const dx = world.x - this.lastWorld.x;
1966
2103
  const dy = world.y - this.lastWorld.y;
@@ -2167,6 +2304,10 @@ var ArrowTool = class {
2167
2304
  this.color = options.color ?? "#000000";
2168
2305
  this.width = options.width ?? 2;
2169
2306
  }
2307
+ setOptions(options) {
2308
+ if (options.color !== void 0) this.color = options.color;
2309
+ if (options.width !== void 0) this.width = options.width;
2310
+ }
2170
2311
  onPointerDown(state, ctx) {
2171
2312
  this.drawing = true;
2172
2313
  this.start = ctx.camera.screenToWorld({ x: state.x, y: state.y });
@@ -2231,6 +2372,10 @@ var NoteTool = class {
2231
2372
  this.backgroundColor = options.backgroundColor ?? "#ffeb3b";
2232
2373
  this.size = options.size ?? { w: 200, h: 100 };
2233
2374
  }
2375
+ setOptions(options) {
2376
+ if (options.backgroundColor !== void 0) this.backgroundColor = options.backgroundColor;
2377
+ if (options.size !== void 0) this.size = options.size;
2378
+ }
2234
2379
  onPointerDown(_state, _ctx) {
2235
2380
  }
2236
2381
  onPointerMove(_state, _ctx) {
@@ -2280,7 +2425,7 @@ var ImageTool = class {
2280
2425
  };
2281
2426
 
2282
2427
  // src/index.ts
2283
- var VERSION = "0.1.2";
2428
+ var VERSION = "0.2.2";
2284
2429
  // Annotate the CommonJS export names for ESM import in node:
2285
2430
  0 && (module.exports = {
2286
2431
  AddElementCommand,