@fieldnotes/core 0.2.0 → 0.2.1

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,69 @@ 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
+ window.addEventListener("keydown", this.onInteractKeyDown);
1511
+ window.addEventListener("pointerdown", this.onInteractPointerDown);
1512
+ }
1513
+ stopInteracting() {
1514
+ if (!this.interactingElementId) return;
1515
+ const node = this.domNodes.get(this.interactingElementId);
1516
+ if (node) {
1517
+ node.style.pointerEvents = "none";
1518
+ }
1519
+ this.interactingElementId = null;
1520
+ window.removeEventListener("keydown", this.onInteractKeyDown);
1521
+ window.removeEventListener("pointerdown", this.onInteractPointerDown);
1522
+ }
1523
+ onInteractKeyDown = (e) => {
1524
+ if (e.key === "Escape") {
1525
+ this.stopInteracting();
1526
+ }
1527
+ };
1528
+ onInteractPointerDown = (e) => {
1529
+ if (!this.interactingElementId) return;
1530
+ const target = e.target;
1531
+ if (!target) return;
1532
+ const node = this.domNodes.get(this.interactingElementId);
1533
+ if (node && !node.contains(target)) {
1534
+ this.stopInteracting();
1535
+ }
1412
1536
  };
1413
1537
  onDragOver = (e) => {
1414
1538
  e.preventDefault();
@@ -1506,7 +1630,8 @@ var Viewport = class {
1506
1630
  if (content) {
1507
1631
  node.dataset["initialized"] = "true";
1508
1632
  Object.assign(node.style, {
1509
- overflow: "hidden"
1633
+ overflow: "hidden",
1634
+ pointerEvents: "none"
1510
1635
  });
1511
1636
  node.appendChild(content);
1512
1637
  }
@@ -1609,15 +1734,19 @@ var HandTool = class {
1609
1734
 
1610
1735
  // src/tools/pencil-tool.ts
1611
1736
  var MIN_POINTS_FOR_STROKE = 2;
1737
+ var DEFAULT_SMOOTHING = 1.5;
1738
+ var DEFAULT_PRESSURE = 0.5;
1612
1739
  var PencilTool = class {
1613
1740
  name = "pencil";
1614
1741
  drawing = false;
1615
1742
  points = [];
1616
1743
  color;
1617
1744
  width;
1745
+ smoothing;
1618
1746
  constructor(options = {}) {
1619
1747
  this.color = options.color ?? "#000000";
1620
1748
  this.width = options.width ?? 2;
1749
+ this.smoothing = options.smoothing ?? DEFAULT_SMOOTHING;
1621
1750
  }
1622
1751
  onActivate(ctx) {
1623
1752
  ctx.setCursor?.("crosshair");
@@ -1628,16 +1757,19 @@ var PencilTool = class {
1628
1757
  setOptions(options) {
1629
1758
  if (options.color !== void 0) this.color = options.color;
1630
1759
  if (options.width !== void 0) this.width = options.width;
1760
+ if (options.smoothing !== void 0) this.smoothing = options.smoothing;
1631
1761
  }
1632
1762
  onPointerDown(state, ctx) {
1633
1763
  this.drawing = true;
1634
1764
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
1635
- this.points = [world];
1765
+ const pressure = state.pressure === 0 ? DEFAULT_PRESSURE : state.pressure;
1766
+ this.points = [{ x: world.x, y: world.y, pressure }];
1636
1767
  }
1637
1768
  onPointerMove(state, ctx) {
1638
1769
  if (!this.drawing) return;
1639
1770
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
1640
- this.points.push(world);
1771
+ const pressure = state.pressure === 0 ? DEFAULT_PRESSURE : state.pressure;
1772
+ this.points.push({ x: world.x, y: world.y, pressure });
1641
1773
  ctx.requestRender();
1642
1774
  }
1643
1775
  onPointerUp(_state, ctx) {
@@ -1647,8 +1779,9 @@ var PencilTool = class {
1647
1779
  this.points = [];
1648
1780
  return;
1649
1781
  }
1782
+ const simplified = simplifyPoints(this.points, this.smoothing);
1650
1783
  const stroke = createStroke({
1651
- points: this.points,
1784
+ points: simplified,
1652
1785
  color: this.color,
1653
1786
  width: this.width
1654
1787
  });
@@ -1660,19 +1793,18 @@ var PencilTool = class {
1660
1793
  if (!this.drawing || this.points.length < 2) return;
1661
1794
  ctx.save();
1662
1795
  ctx.strokeStyle = this.color;
1663
- ctx.lineWidth = this.width;
1664
1796
  ctx.lineCap = "round";
1665
1797
  ctx.lineJoin = "round";
1666
1798
  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);
1799
+ const segments = smoothToSegments(this.points);
1800
+ for (const seg of segments) {
1801
+ const w = (pressureToWidth(seg.start.pressure, this.width) + pressureToWidth(seg.end.pressure, this.width)) / 2;
1802
+ ctx.lineWidth = w;
1803
+ ctx.beginPath();
1804
+ ctx.moveTo(seg.start.x, seg.start.y);
1805
+ ctx.bezierCurveTo(seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.end.x, seg.end.y);
1806
+ ctx.stroke();
1674
1807
  }
1675
- ctx.stroke();
1676
1808
  ctx.restore();
1677
1809
  }
1678
1810
  };
@@ -1960,7 +2092,7 @@ var SelectTool = class {
1960
2092
  handleResize(world, ctx) {
1961
2093
  if (this.mode.type !== "resizing") return;
1962
2094
  const el = ctx.store.getById(this.mode.elementId);
1963
- if (!el || !("size" in el)) return;
2095
+ if (!el || !("size" in el) || el.locked) return;
1964
2096
  const { handle } = this.mode;
1965
2097
  const dx = world.x - this.lastWorld.x;
1966
2098
  const dy = world.y - this.lastWorld.y;
@@ -2167,6 +2299,10 @@ var ArrowTool = class {
2167
2299
  this.color = options.color ?? "#000000";
2168
2300
  this.width = options.width ?? 2;
2169
2301
  }
2302
+ setOptions(options) {
2303
+ if (options.color !== void 0) this.color = options.color;
2304
+ if (options.width !== void 0) this.width = options.width;
2305
+ }
2170
2306
  onPointerDown(state, ctx) {
2171
2307
  this.drawing = true;
2172
2308
  this.start = ctx.camera.screenToWorld({ x: state.x, y: state.y });
@@ -2231,6 +2367,10 @@ var NoteTool = class {
2231
2367
  this.backgroundColor = options.backgroundColor ?? "#ffeb3b";
2232
2368
  this.size = options.size ?? { w: 200, h: 100 };
2233
2369
  }
2370
+ setOptions(options) {
2371
+ if (options.backgroundColor !== void 0) this.backgroundColor = options.backgroundColor;
2372
+ if (options.size !== void 0) this.size = options.size;
2373
+ }
2234
2374
  onPointerDown(_state, _ctx) {
2235
2375
  }
2236
2376
  onPointerMove(_state, _ctx) {
@@ -2280,7 +2420,7 @@ var ImageTool = class {
2280
2420
  };
2281
2421
 
2282
2422
  // src/index.ts
2283
- var VERSION = "0.1.2";
2423
+ var VERSION = "0.2.1";
2284
2424
  // Annotate the CommonJS export names for ESM import in node:
2285
2425
  0 && (module.exports = {
2286
2426
  AddElementCommand,