@xom11/whiteboard 0.25.0 → 0.28.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.
Files changed (93) hide show
  1. package/dist/{ExcalidrawWithMenus-WENZRYYE.mjs → ExcalidrawWithMenus-2QPPTXJM.mjs} +3 -2
  2. package/dist/ExcalidrawWithMenus-2QPPTXJM.mjs.map +1 -0
  3. package/dist/ai.d.mts +3035 -108
  4. package/dist/ai.d.ts +3035 -108
  5. package/dist/ai.js +6780 -788
  6. package/dist/ai.js.map +1 -1
  7. package/dist/ai.mjs +5140 -577
  8. package/dist/ai.mjs.map +1 -1
  9. package/dist/catalog.json +5 -5
  10. package/dist/{chunk-NDEZJKNY.mjs → chunk-5JM35CXV.mjs} +4 -4
  11. package/dist/{chunk-NDEZJKNY.mjs.map → chunk-5JM35CXV.mjs.map} +1 -1
  12. package/dist/{chunk-VNCCIV6O.mjs → chunk-AJAHD35N.mjs} +779 -9
  13. package/dist/chunk-AJAHD35N.mjs.map +1 -0
  14. package/dist/{chunk-M42TGYT6.mjs → chunk-BNBOIDO5.mjs} +3 -3
  15. package/dist/{chunk-M42TGYT6.mjs.map → chunk-BNBOIDO5.mjs.map} +1 -1
  16. package/dist/{chunk-ONBCUWVI.mjs → chunk-BU5KLO3P.mjs} +3 -3
  17. package/dist/{chunk-ONBCUWVI.mjs.map → chunk-BU5KLO3P.mjs.map} +1 -1
  18. package/dist/{chunk-CJBLJUWG.mjs → chunk-CXHNVYMD.mjs} +4 -4
  19. package/dist/{chunk-CJBLJUWG.mjs.map → chunk-CXHNVYMD.mjs.map} +1 -1
  20. package/dist/chunk-H22OZYTW.mjs +265 -0
  21. package/dist/chunk-H22OZYTW.mjs.map +1 -0
  22. package/dist/chunk-J5LGTIGS.mjs +10 -0
  23. package/dist/chunk-J5LGTIGS.mjs.map +1 -0
  24. package/dist/{chunk-TB4CL25L.mjs → chunk-OQIQNKPQ.mjs} +206 -66
  25. package/dist/chunk-OQIQNKPQ.mjs.map +1 -0
  26. package/dist/{chunk-SGFJLHHG.mjs → chunk-PPKHCRRE.mjs} +3 -3
  27. package/dist/{chunk-SGFJLHHG.mjs.map → chunk-PPKHCRRE.mjs.map} +1 -1
  28. package/dist/{chunk-YSJOVBCD.mjs → chunk-QCZVFEN4.mjs} +4 -4
  29. package/dist/{chunk-YSJOVBCD.mjs.map → chunk-QCZVFEN4.mjs.map} +1 -1
  30. package/dist/{chunk-ESVPQWHX.mjs → chunk-QRUAEXLR.mjs} +5 -5
  31. package/dist/{chunk-ESVPQWHX.mjs.map → chunk-QRUAEXLR.mjs.map} +1 -1
  32. package/dist/{chunk-AYSFWUPK.mjs → chunk-SZDAS7LK.mjs} +79 -2
  33. package/dist/chunk-SZDAS7LK.mjs.map +1 -0
  34. package/dist/chunk-T3SOHYB2.mjs +851 -0
  35. package/dist/chunk-T3SOHYB2.mjs.map +1 -0
  36. package/dist/{chunk-I24QOHPU.mjs → chunk-V3YJ6JFL.mjs} +3 -3
  37. package/dist/{chunk-I24QOHPU.mjs.map → chunk-V3YJ6JFL.mjs.map} +1 -1
  38. package/dist/{chunk-REIJZDVZ.mjs → chunk-ZTQBUKLJ.mjs} +960 -196
  39. package/dist/chunk-ZTQBUKLJ.mjs.map +1 -0
  40. package/dist/geometry-2d.d.mts +1 -1
  41. package/dist/geometry-2d.d.ts +1 -1
  42. package/dist/geometry-2d.js +5521 -1384
  43. package/dist/geometry-2d.js.map +1 -1
  44. package/dist/geometry-2d.mjs +5 -4
  45. package/dist/geometry-3d.d.mts +1 -1
  46. package/dist/geometry-3d.d.ts +1 -1
  47. package/dist/geometry-3d.js +1351 -252
  48. package/dist/geometry-3d.js.map +1 -1
  49. package/dist/geometry-3d.mjs +4 -3
  50. package/dist/graph-2d.d.mts +1 -1
  51. package/dist/graph-2d.d.ts +1 -1
  52. package/dist/graph-2d.js +1517 -341
  53. package/dist/graph-2d.js.map +1 -1
  54. package/dist/graph-2d.mjs +7 -6
  55. package/dist/handleExtractProblem-C-U5KluK.d.mts +158 -0
  56. package/dist/handleExtractProblem-C-U5KluK.d.ts +158 -0
  57. package/dist/{host-A64ITWVX.mjs → host-2ISGVO7O.mjs} +6 -5
  58. package/dist/host-2ISGVO7O.mjs.map +1 -0
  59. package/dist/{host-L7FMFZUW.mjs → host-4P766V4J.mjs} +1363 -463
  60. package/dist/host-4P766V4J.mjs.map +1 -0
  61. package/dist/{host-QK53UYMD.mjs → host-HOSJHQ5H.mjs} +10 -9
  62. package/dist/host-HOSJHQ5H.mjs.map +1 -0
  63. package/dist/{host-QS2EOTRJ.mjs → host-ZQCDAT6O.mjs} +3 -2
  64. package/dist/host-ZQCDAT6O.mjs.map +1 -0
  65. package/dist/index.d.mts +10 -4
  66. package/dist/index.d.ts +10 -4
  67. package/dist/index.js +5746 -1603
  68. package/dist/index.js.map +1 -1
  69. package/dist/index.mjs +26 -22
  70. package/dist/index.mjs.map +1 -1
  71. package/dist/latex.d.mts +1 -1
  72. package/dist/latex.d.ts +1 -1
  73. package/dist/latex.mjs +2 -1
  74. package/dist/render-ZX2O2IK7.mjs +10 -0
  75. package/dist/{render-3WTY7NZB.mjs.map → render-ZX2O2IK7.mjs.map} +1 -1
  76. package/dist/serialize-N4G6RFBB.mjs +9 -0
  77. package/dist/{serialize-SRJVKYUG.mjs.map → serialize-N4G6RFBB.mjs.map} +1 -1
  78. package/dist/{types-DWRyCa2m.d.ts → types-BHYC2Fiw.d.mts} +130 -1
  79. package/dist/{types-DWRyCa2m.d.mts → types-BHYC2Fiw.d.ts} +130 -1
  80. package/package.json +10 -1
  81. package/dist/ExcalidrawWithMenus-WENZRYYE.mjs.map +0 -1
  82. package/dist/chunk-AYSFWUPK.mjs.map +0 -1
  83. package/dist/chunk-REIJZDVZ.mjs.map +0 -1
  84. package/dist/chunk-TB4CL25L.mjs.map +0 -1
  85. package/dist/chunk-VNCCIV6O.mjs.map +0 -1
  86. package/dist/chunk-VRHWDZ66.mjs +0 -96
  87. package/dist/chunk-VRHWDZ66.mjs.map +0 -1
  88. package/dist/host-A64ITWVX.mjs.map +0 -1
  89. package/dist/host-L7FMFZUW.mjs.map +0 -1
  90. package/dist/host-QK53UYMD.mjs.map +0 -1
  91. package/dist/host-QS2EOTRJ.mjs.map +0 -1
  92. package/dist/render-3WTY7NZB.mjs +0 -9
  93. package/dist/serialize-SRJVKYUG.mjs +0 -8
package/dist/graph-2d.js CHANGED
@@ -2,7 +2,7 @@
2
2
  'use strict';
3
3
 
4
4
  var immer = require('immer');
5
- var React = require('react');
5
+ var React5 = require('react');
6
6
  var jsxRuntime = require('react/jsx-runtime');
7
7
  var reactDom = require('react-dom');
8
8
 
@@ -24,7 +24,7 @@ function _interopNamespace(e) {
24
24
  return Object.freeze(n);
25
25
  }
26
26
 
27
- var React__namespace = /*#__PURE__*/_interopNamespace(React);
27
+ var React5__namespace = /*#__PURE__*/_interopNamespace(React5);
28
28
 
29
29
  var __defProp = Object.defineProperty;
30
30
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -466,10 +466,87 @@ var init_JxgRenderer = __esm({
466
466
  const el = def22.render(obj, this.ctx());
467
467
  this.elements.set(obj.id, el);
468
468
  this.attachFreePointDragSync(obj, el);
469
+ this.attachGliderDragSync(obj, el);
469
470
  } catch (err) {
470
471
  console.warn(`[scene/render/2d] kh\xF4ng render \u0111\u01B0\u1EE3c ${obj.kind} id="${obj.id}":`, err);
471
472
  }
472
473
  }
474
+ /**
475
+ * Sync vị trí glider của 3 constraint mới (onPerpendicular / onPerpBisector /
476
+ * onCircleAroundPoint) — kéo → recompute t/theta dựa trên ref points + vị trí
477
+ * glider hiện tại → UPDATE_ATTRS. Không đụng onLine/onSegment/onCircle hiện
478
+ * tại (giữ behavior cũ).
479
+ */
480
+ attachGliderDragSync(obj, el) {
481
+ if (obj.kind !== "point") return;
482
+ const c = obj.attrs.constraint;
483
+ if (!c) return;
484
+ if (c.kind !== "onPerpendicular" && c.kind !== "onPerpBisector" && c.kind !== "onCircleAroundPoint") {
485
+ return;
486
+ }
487
+ const point = el;
488
+ if (typeof point.on !== "function") return;
489
+ const sceneId = obj.id;
490
+ point.on("up", () => {
491
+ if (this.disposed) return;
492
+ const cur = this.store.getState().objects[sceneId];
493
+ if (!cur) return;
494
+ const curC = cur.attrs.constraint;
495
+ if (!curC) return;
496
+ if (typeof point.X !== "function" || typeof point.Y !== "function") return;
497
+ const x = point.X();
498
+ const y = point.Y();
499
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return;
500
+ if (curC.kind === "onPerpendicular") {
501
+ const T = this.elements.get(curC.through);
502
+ const A = this.elements.get(curC.perpToA);
503
+ const B = this.elements.get(curC.perpToB);
504
+ if (!T || !A || !B) return;
505
+ const dx = B.X() - A.X();
506
+ const dy = B.Y() - A.Y();
507
+ const len = Math.hypot(dx, dy) || 1;
508
+ const ux = -dy / len;
509
+ const uy = dx / len;
510
+ const newT = (x - T.X()) * ux + (y - T.Y()) * uy;
511
+ if (Math.abs(newT - curC.t) < 1e-9) return;
512
+ this.store.dispatch({
513
+ type: "UPDATE_ATTRS",
514
+ payload: { id: sceneId, patch: { constraint: { ...curC, t: newT } } }
515
+ });
516
+ return;
517
+ }
518
+ if (curC.kind === "onPerpBisector") {
519
+ const A = this.elements.get(curC.p1);
520
+ const B = this.elements.get(curC.p2);
521
+ if (!A || !B) return;
522
+ const Mx = (A.X() + B.X()) / 2;
523
+ const My = (A.Y() + B.Y()) / 2;
524
+ const dx = B.X() - A.X();
525
+ const dy = B.Y() - A.Y();
526
+ const len = Math.hypot(dx, dy) || 1;
527
+ const ux = -dy / len;
528
+ const uy = dx / len;
529
+ const newT = (x - Mx) * ux + (y - My) * uy;
530
+ if (Math.abs(newT - curC.t) < 1e-9) return;
531
+ this.store.dispatch({
532
+ type: "UPDATE_ATTRS",
533
+ payload: { id: sceneId, patch: { constraint: { ...curC, t: newT } } }
534
+ });
535
+ return;
536
+ }
537
+ if (curC.kind === "onCircleAroundPoint") {
538
+ const C = this.elements.get(curC.center);
539
+ if (!C) return;
540
+ const newTheta = Math.atan2(y - C.Y(), x - C.X());
541
+ if (Math.abs(newTheta - curC.theta) < 1e-9) return;
542
+ this.store.dispatch({
543
+ type: "UPDATE_ATTRS",
544
+ payload: { id: sceneId, patch: { constraint: { ...curC, theta: newTheta } } }
545
+ });
546
+ return;
547
+ }
548
+ });
549
+ }
473
550
  /**
474
551
  * Đồng bộ toạ độ live của free point về scene.constraint khi user kéo bằng
475
552
  * tay (Move tool / mobile drag). JSXGraph mutate obj.X()/Y() ngay nhưng
@@ -951,9 +1028,9 @@ var init_selectors = __esm({
951
1028
  });
952
1029
  function useEditorState(opts) {
953
1030
  const { store, initialState, onHistoryChange, bindKeyboardShortcuts = true } = opts;
954
- const onHistoryChangeRef = React__namespace.useRef(onHistoryChange);
1031
+ const onHistoryChangeRef = React5__namespace.useRef(onHistoryChange);
955
1032
  onHistoryChangeRef.current = onHistoryChange;
956
- React__namespace.useEffect(() => {
1033
+ React5__namespace.useEffect(() => {
957
1034
  if (initialState?.state) {
958
1035
  const loaded = initialState.state;
959
1036
  store.withoutHistory(() => {
@@ -961,14 +1038,14 @@ function useEditorState(opts) {
961
1038
  });
962
1039
  }
963
1040
  }, []);
964
- React__namespace.useEffect(() => {
1041
+ React5__namespace.useEffect(() => {
965
1042
  onHistoryChangeRef.current?.(store.canUndo(), store.canRedo());
966
1043
  const unsub = store.subscribe(() => {
967
1044
  onHistoryChangeRef.current?.(store.canUndo(), store.canRedo());
968
1045
  });
969
1046
  return unsub;
970
1047
  }, [store]);
971
- React__namespace.useEffect(() => {
1048
+ React5__namespace.useEffect(() => {
972
1049
  if (!bindKeyboardShortcuts) return;
973
1050
  const onKey = (e) => {
974
1051
  const ae = document.activeElement;
@@ -1554,6 +1631,31 @@ function constraintRefs2D(c) {
1554
1631
  return [c.vertices[0], c.vertices[1], c.vertices[2]];
1555
1632
  case "orthocenter":
1556
1633
  return [c.vertices[0], c.vertices[1], c.vertices[2]];
1634
+ case "onPerpendicular":
1635
+ return [c.through, c.perpToA, c.perpToB];
1636
+ case "onPerpBisector":
1637
+ return [c.p1, c.p2];
1638
+ case "onCircleAroundPoint":
1639
+ return [c.center, c.radiusPoint];
1640
+ case "tangentPointExt":
1641
+ return [c.from, c.circle];
1642
+ case "circleIntersection":
1643
+ return [c.c1, c.c2];
1644
+ case "circleSecondIntersection":
1645
+ return [c.c1, c.c2, c.exclude];
1646
+ case "secondIntersection":
1647
+ return [c.line, c.circle, c.other];
1648
+ case "tangencyPoint":
1649
+ return [c.circle, c.onLine];
1650
+ case "arcMidpoint":
1651
+ return [c.circle, c.a, c.b, c.notContaining];
1652
+ case "pointAtDistance": {
1653
+ const d = c.distance;
1654
+ const extra = d.kind === "circleRadius" ? [d.circle] : d.kind === "segmentLength" ? [d.p1, d.p2] : [];
1655
+ return [c.from, c.through, ...extra];
1656
+ }
1657
+ case "excenter":
1658
+ return [c.vertices[0], c.vertices[1], c.vertices[2]];
1557
1659
  default:
1558
1660
  return [];
1559
1661
  }
@@ -1563,7 +1665,412 @@ var init_d_constraint2 = __esm({
1563
1665
  }
1564
1666
  });
1565
1667
 
1566
- // src/core/scene/kinds/point.ts
1668
+ // src/core/scene/kinds/point-constraints/_types.ts
1669
+ function definePointConstraint(m) {
1670
+ return m;
1671
+ }
1672
+ var init_types3 = __esm({
1673
+ "src/core/scene/kinds/point-constraints/_types.ts"() {
1674
+ }
1675
+ });
1676
+
1677
+ // src/core/scene/kinds/point-constraints/free.ts
1678
+ var freeConstraint;
1679
+ var init_free = __esm({
1680
+ "src/core/scene/kinds/point-constraints/free.ts"() {
1681
+ init_types3();
1682
+ freeConstraint = definePointConstraint({
1683
+ kind: "free",
1684
+ describe: (obj) => `\u0110i\u1EC3m ${obj.label}`,
1685
+ render: (obj, ctx, c, opts) => {
1686
+ const board = ctx.jxg;
1687
+ return board.create("point", [c.x, c.y], opts);
1688
+ }
1689
+ });
1690
+ }
1691
+ });
1692
+
1693
+ // src/core/scene/kinds/point-constraints/onAxis.ts
1694
+ var onAxisConstraint;
1695
+ var init_onAxis = __esm({
1696
+ "src/core/scene/kinds/point-constraints/onAxis.ts"() {
1697
+ init_types3();
1698
+ onAxisConstraint = definePointConstraint({
1699
+ kind: "onAxis",
1700
+ describe: (obj, _state, c) => `${obj.label} tr\xEAn tr\u1EE5c ${c.axis}`,
1701
+ render: (obj, ctx, c, opts) => {
1702
+ const board = ctx.jxg;
1703
+ const coords = c.axis === "x" ? [c.t, 0] : [0, c.t];
1704
+ return board.create("point", coords, opts);
1705
+ }
1706
+ });
1707
+ }
1708
+ });
1709
+
1710
+ // src/core/scene/kinds/point-constraints/midpoint.ts
1711
+ var midpointConstraint;
1712
+ var init_midpoint = __esm({
1713
+ "src/core/scene/kinds/point-constraints/midpoint.ts"() {
1714
+ init_types3();
1715
+ midpointConstraint = definePointConstraint({
1716
+ kind: "midpoint",
1717
+ describe: (obj, state, c) => {
1718
+ const l1 = state?.objects[c.p1]?.label ?? c.p1;
1719
+ const l2 = state?.objects[c.p2]?.label ?? c.p2;
1720
+ return `${obj.label} = trung \u0111i\u1EC3m ${l1}${l2}`;
1721
+ },
1722
+ render: (obj, ctx, c, opts) => {
1723
+ const board = ctx.jxg;
1724
+ const p1 = ctx.resolveRef(c.p1);
1725
+ const p2 = ctx.resolveRef(c.p2);
1726
+ return board.create("midpoint", [p1, p2], opts);
1727
+ }
1728
+ });
1729
+ }
1730
+ });
1731
+
1732
+ // src/core/scene/kinds/point-constraints/perpFoot.ts
1733
+ var perpFootConstraint;
1734
+ var init_perpFoot = __esm({
1735
+ "src/core/scene/kinds/point-constraints/perpFoot.ts"() {
1736
+ init_types3();
1737
+ perpFootConstraint = definePointConstraint({
1738
+ kind: "perpFoot",
1739
+ validate: (c) => {
1740
+ if (!c.from || !c.onLine) {
1741
+ throw new Error("point.perpFoot: from v\xE0 onLine b\u1EAFt bu\u1ED9c");
1742
+ }
1743
+ },
1744
+ describe: (obj, state, c) => {
1745
+ const fromLabel = state?.objects[c.from]?.label ?? c.from;
1746
+ const lineLabel = state?.objects[c.onLine]?.label ?? c.onLine;
1747
+ return `${obj.label} = ch\xE2n \u27C2 t\u1EEB ${fromLabel} xu\u1ED1ng ${lineLabel}`;
1748
+ },
1749
+ render: (obj, ctx, c, opts) => {
1750
+ const board = ctx.jxg;
1751
+ const from = ctx.resolveRef(c.from);
1752
+ const onLine = ctx.resolveRef(c.onLine);
1753
+ return board.create("perpendicularpoint", [onLine, from], opts);
1754
+ }
1755
+ });
1756
+ }
1757
+ });
1758
+
1759
+ // src/core/scene/kinds/point-constraints/circumcenter.ts
1760
+ var circumcenterConstraint;
1761
+ var init_circumcenter = __esm({
1762
+ "src/core/scene/kinds/point-constraints/circumcenter.ts"() {
1763
+ init_types3();
1764
+ circumcenterConstraint = definePointConstraint({
1765
+ kind: "circumcenter",
1766
+ validate: (c) => {
1767
+ if (!Array.isArray(c.vertices) || c.vertices.length !== 3) {
1768
+ throw new Error("point.circumcenter: vertices ph\u1EA3i l\xE0 tuple 3 id");
1769
+ }
1770
+ if (!c.vertices[0] || !c.vertices[1] || !c.vertices[2]) {
1771
+ throw new Error("point.circumcenter: 3 vertex id ph\u1EA3i non-empty");
1772
+ }
1773
+ },
1774
+ describe: (obj, state, c) => {
1775
+ const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
1776
+ return `${obj.label} = t\xE2m ngo\u1EA1i ti\u1EBFp \u0394${labels}`;
1777
+ },
1778
+ render: (obj, ctx, c, opts) => {
1779
+ const board = ctx.jxg;
1780
+ const a = ctx.resolveRef(c.vertices[0]);
1781
+ const b = ctx.resolveRef(c.vertices[1]);
1782
+ const c3 = ctx.resolveRef(c.vertices[2]);
1783
+ return board.create("circumcenter", [a, b, c3], opts);
1784
+ }
1785
+ });
1786
+ }
1787
+ });
1788
+
1789
+ // src/core/scene/kinds/point-constraints/incenter.ts
1790
+ var incenterConstraint;
1791
+ var init_incenter = __esm({
1792
+ "src/core/scene/kinds/point-constraints/incenter.ts"() {
1793
+ init_types3();
1794
+ incenterConstraint = definePointConstraint({
1795
+ kind: "incenter",
1796
+ validate: (c) => {
1797
+ if (!Array.isArray(c.vertices) || c.vertices.length !== 3) {
1798
+ throw new Error("point.incenter: vertices ph\u1EA3i l\xE0 tuple 3 id");
1799
+ }
1800
+ if (!c.vertices[0] || !c.vertices[1] || !c.vertices[2]) {
1801
+ throw new Error("point.incenter: 3 vertex id ph\u1EA3i non-empty");
1802
+ }
1803
+ },
1804
+ describe: (obj, state, c) => {
1805
+ const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
1806
+ return `${obj.label} = t\xE2m n\u1ED9i ti\u1EBFp \u0394${labels}`;
1807
+ },
1808
+ render: (obj, ctx, c, opts) => {
1809
+ const board = ctx.jxg;
1810
+ const a = ctx.resolveRef(c.vertices[0]);
1811
+ const b = ctx.resolveRef(c.vertices[1]);
1812
+ const c3 = ctx.resolveRef(c.vertices[2]);
1813
+ return board.create("incenter", [a, b, c3], opts);
1814
+ }
1815
+ });
1816
+ }
1817
+ });
1818
+
1819
+ // src/core/scene/kinds/point-constraints/onLine.ts
1820
+ var onLineConstraint;
1821
+ var init_onLine = __esm({
1822
+ "src/core/scene/kinds/point-constraints/onLine.ts"() {
1823
+ init_types3();
1824
+ onLineConstraint = definePointConstraint({
1825
+ kind: "onLine",
1826
+ describe: (obj, state, c) => `${obj.label} tr\xEAn \u0111\u01B0\u1EDDng ${state?.objects[c.lineId]?.label ?? c.lineId}`,
1827
+ render: (obj, ctx, c, opts) => {
1828
+ const board = ctx.jxg;
1829
+ const line = ctx.resolveRef(c.lineId);
1830
+ const p1 = line.point1;
1831
+ const p2 = line.point2;
1832
+ const sx = p1 && p2 ? p1.X() + c.t * (p2.X() - p1.X()) : c.t;
1833
+ const sy = p1 && p2 ? p1.Y() + c.t * (p2.Y() - p1.Y()) : c.t;
1834
+ return board.create("glider", [sx, sy, line], opts);
1835
+ }
1836
+ });
1837
+ }
1838
+ });
1839
+
1840
+ // src/core/scene/kinds/point-constraints/onSegment.ts
1841
+ var onSegmentConstraint;
1842
+ var init_onSegment = __esm({
1843
+ "src/core/scene/kinds/point-constraints/onSegment.ts"() {
1844
+ init_types3();
1845
+ onSegmentConstraint = definePointConstraint({
1846
+ kind: "onSegment",
1847
+ describe: (obj, state, c) => `${obj.label} tr\xEAn \u0111o\u1EA1n ${state?.objects[c.segmentId]?.label ?? c.segmentId}`,
1848
+ render: (obj, ctx, c, opts) => {
1849
+ const board = ctx.jxg;
1850
+ const seg = ctx.resolveRef(c.segmentId);
1851
+ const p1 = seg.point1;
1852
+ const p2 = seg.point2;
1853
+ const sx = p1 && p2 ? p1.X() + c.t * (p2.X() - p1.X()) : c.t;
1854
+ const sy = p1 && p2 ? p1.Y() + c.t * (p2.Y() - p1.Y()) : c.t;
1855
+ return board.create("glider", [sx, sy, seg], opts);
1856
+ }
1857
+ });
1858
+ }
1859
+ });
1860
+
1861
+ // src/core/scene/kinds/point-constraints/onCircle.ts
1862
+ var onCircleConstraint;
1863
+ var init_onCircle = __esm({
1864
+ "src/core/scene/kinds/point-constraints/onCircle.ts"() {
1865
+ init_types3();
1866
+ onCircleConstraint = definePointConstraint({
1867
+ kind: "onCircle",
1868
+ describe: (obj, state, c) => `${obj.label} tr\xEAn \u0111\u01B0\u1EDDng tr\xF2n ${state?.objects[c.circleId]?.label ?? c.circleId}`,
1869
+ render: (obj, ctx, c, opts) => {
1870
+ const board = ctx.jxg;
1871
+ const circle = ctx.resolveRef(c.circleId);
1872
+ const O = circle.center ?? circle.midpoint;
1873
+ const ox = O ? O.X() : 0;
1874
+ const oy = O ? O.Y() : 0;
1875
+ return board.create("glider", [ox + Math.cos(c.theta), oy + Math.sin(c.theta), circle], opts);
1876
+ }
1877
+ });
1878
+ }
1879
+ });
1880
+
1881
+ // src/core/scene/kinds/point-constraints/onPolygon.ts
1882
+ var onPolygonConstraint;
1883
+ var init_onPolygon = __esm({
1884
+ "src/core/scene/kinds/point-constraints/onPolygon.ts"() {
1885
+ init_types3();
1886
+ onPolygonConstraint = definePointConstraint({
1887
+ kind: "onPolygon",
1888
+ describe: (obj, state, c) => `${obj.label} tr\xEAn \u0111a gi\xE1c ${state?.objects[c.polygonId]?.label ?? c.polygonId}`,
1889
+ render: (obj, ctx, c, opts) => {
1890
+ const board = ctx.jxg;
1891
+ const poly = ctx.resolveRef(c.polygonId);
1892
+ return board.create("glider", [c.u, c.v, poly], opts);
1893
+ }
1894
+ });
1895
+ }
1896
+ });
1897
+
1898
+ // src/core/scene/kinds/point-constraints/centroid.ts
1899
+ var centroidConstraint;
1900
+ var init_centroid = __esm({
1901
+ "src/core/scene/kinds/point-constraints/centroid.ts"() {
1902
+ init_types3();
1903
+ centroidConstraint = definePointConstraint({
1904
+ kind: "centroid",
1905
+ validate: (c) => {
1906
+ if (!Array.isArray(c.vertices) || c.vertices.length !== 3) {
1907
+ throw new Error("point.centroid: vertices ph\u1EA3i l\xE0 tuple 3 id");
1908
+ }
1909
+ if (!c.vertices[0] || !c.vertices[1] || !c.vertices[2]) {
1910
+ throw new Error("point.centroid: 3 vertex id ph\u1EA3i non-empty");
1911
+ }
1912
+ },
1913
+ describe: (obj, state, c) => {
1914
+ const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
1915
+ return `${obj.label} = tr\u1ECDng t\xE2m \u0394${labels}`;
1916
+ },
1917
+ render: (obj, ctx, c, opts) => {
1918
+ const board = ctx.jxg;
1919
+ const a = ctx.resolveRef(c.vertices[0]);
1920
+ const b = ctx.resolveRef(c.vertices[1]);
1921
+ const c3 = ctx.resolveRef(c.vertices[2]);
1922
+ return board.create("point", [
1923
+ () => (a.X() + b.X() + c3.X()) / 3,
1924
+ () => (a.Y() + b.Y() + c3.Y()) / 3
1925
+ ], opts);
1926
+ }
1927
+ });
1928
+ }
1929
+ });
1930
+
1931
+ // src/core/scene/kinds/pointConstructions.ts
1932
+ function dist(p, q) {
1933
+ return Math.hypot(p[0] - q[0], p[1] - q[1]);
1934
+ }
1935
+ function sideOf(a, b, p) {
1936
+ return (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
1937
+ }
1938
+ function arcMidpoint(center, radius, a, b, notContaining) {
1939
+ const mcx = (a[0] + b[0]) / 2;
1940
+ const mcy = (a[1] + b[1]) / 2;
1941
+ let ux = mcx - center[0];
1942
+ let uy = mcy - center[1];
1943
+ let len = Math.hypot(ux, uy);
1944
+ if (len < 1e-9) {
1945
+ ux = -(b[1] - a[1]);
1946
+ uy = b[0] - a[0];
1947
+ len = Math.hypot(ux, uy) || 1;
1948
+ }
1949
+ ux /= len;
1950
+ uy /= len;
1951
+ const cand1 = [center[0] + radius * ux, center[1] + radius * uy];
1952
+ const cand2 = [center[0] - radius * ux, center[1] - radius * uy];
1953
+ const sN = sideOf(a, b, notContaining);
1954
+ if (Math.abs(sN) < 1e-9) {
1955
+ return dist(cand1, notContaining) >= dist(cand2, notContaining) ? cand1 : cand2;
1956
+ }
1957
+ const s1 = sideOf(a, b, cand1);
1958
+ return s1 * sN < 0 ? cand1 : cand2;
1959
+ }
1960
+ function excenter(vertices, oppositeIndex) {
1961
+ const [A, B, C] = vertices;
1962
+ const a = dist(B, C);
1963
+ const b = dist(C, A);
1964
+ const c = dist(A, B);
1965
+ const w = [a, b, c];
1966
+ w[oppositeIndex] = -w[oppositeIndex];
1967
+ const sum = w[0] + w[1] + w[2];
1968
+ if (Math.abs(sum) < 1e-9) return A;
1969
+ return [
1970
+ (w[0] * A[0] + w[1] * B[0] + w[2] * C[0]) / sum,
1971
+ (w[0] * A[1] + w[1] * B[1] + w[2] * C[1]) / sum
1972
+ ];
1973
+ }
1974
+ function pointAtDistanceCoord(from, through, d) {
1975
+ const dx = through[0] - from[0];
1976
+ const dy = through[1] - from[1];
1977
+ const len = Math.hypot(dx, dy) || 1;
1978
+ return [through[0] + d * dx / len, through[1] + d * dy / len];
1979
+ }
1980
+ function radicalAxisFoot(o1, r1, o2, r2) {
1981
+ const dx = o2[0] - o1[0], dy = o2[1] - o1[1];
1982
+ const d2 = dx * dx + dy * dy;
1983
+ if (d2 < 1e-12) return o1;
1984
+ const t = (d2 + r1 * r1 - r2 * r2) / (2 * d2);
1985
+ return [o1[0] + t * dx, o1[1] + t * dy];
1986
+ }
1987
+ var init_pointConstructions = __esm({
1988
+ "src/core/scene/kinds/pointConstructions.ts"() {
1989
+ }
1990
+ });
1991
+
1992
+ // src/core/scene/kinds/point-constraints/arcMidpoint.ts
1993
+ var arcMidpointConstraint;
1994
+ var init_arcMidpoint = __esm({
1995
+ "src/core/scene/kinds/point-constraints/arcMidpoint.ts"() {
1996
+ init_pointConstructions();
1997
+ init_types3();
1998
+ arcMidpointConstraint = definePointConstraint({
1999
+ kind: "arcMidpoint",
2000
+ validate: (c) => {
2001
+ if (!c.circle || !c.a || !c.b || !c.notContaining) {
2002
+ throw new Error("point.arcMidpoint: circle, a, b, notContaining b\u1EAFt bu\u1ED9c");
2003
+ }
2004
+ },
2005
+ describe: (obj, state, c) => {
2006
+ const al = state?.objects[c.a]?.label ?? c.a;
2007
+ const bl = state?.objects[c.b]?.label ?? c.b;
2008
+ const nl = state?.objects[c.notContaining]?.label ?? c.notContaining;
2009
+ return `${obj.label} = trung \u0111i\u1EC3m cung ${al}${bl} (kh\xF4ng ch\u1EE9a ${nl})`;
2010
+ },
2011
+ render: (obj, ctx, c, opts) => {
2012
+ const board = ctx.jxg;
2013
+ const circle = ctx.resolveRef(c.circle);
2014
+ const A = ctx.resolveRef(c.a);
2015
+ const B = ctx.resolveRef(c.b);
2016
+ const N = ctx.resolveRef(c.notContaining);
2017
+ const O = circle?.center ?? circle?.midpoint ?? circle;
2018
+ const am = () => arcMidpoint(
2019
+ [O.X(), O.Y()],
2020
+ circle.Radius(),
2021
+ [A.X(), A.Y()],
2022
+ [B.X(), B.Y()],
2023
+ [N.X(), N.Y()]
2024
+ );
2025
+ return board.create("point", [() => am()[0], () => am()[1]], opts);
2026
+ }
2027
+ });
2028
+ }
2029
+ });
2030
+
2031
+ // src/core/scene/kinds/point-constraints/excenter.ts
2032
+ var excenterConstraint;
2033
+ var init_excenter = __esm({
2034
+ "src/core/scene/kinds/point-constraints/excenter.ts"() {
2035
+ init_pointConstructions();
2036
+ init_types3();
2037
+ excenterConstraint = definePointConstraint({
2038
+ kind: "excenter",
2039
+ validate: (c) => {
2040
+ if (!Array.isArray(c.vertices) || c.vertices.length !== 3) {
2041
+ throw new Error("point.excenter: vertices ph\u1EA3i l\xE0 tuple 3 id");
2042
+ }
2043
+ if (!c.opposite) throw new Error("point.excenter: opposite b\u1EAFt bu\u1ED9c");
2044
+ if (!c.vertices[0] || !c.vertices[1] || !c.vertices[2]) {
2045
+ throw new Error("point.excenter: 3 vertex id ph\u1EA3i non-empty");
2046
+ }
2047
+ if (!c.vertices.includes(c.opposite)) {
2048
+ throw new Error("point.excenter: opposite ph\u1EA3i l\xE0 m\u1ED9t trong vertices");
2049
+ }
2050
+ },
2051
+ describe: (obj, state, c) => {
2052
+ const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
2053
+ const opp = state?.objects[c.opposite]?.label ?? c.opposite;
2054
+ return `${obj.label} = t\xE2m b\xE0ng ti\u1EBFp \u0394${labels} \u0111\u1ED1i di\u1EC7n ${opp}`;
2055
+ },
2056
+ render: (obj, ctx, c, opts) => {
2057
+ const board = ctx.jxg;
2058
+ const a = ctx.resolveRef(c.vertices[0]);
2059
+ const b = ctx.resolveRef(c.vertices[1]);
2060
+ const c3 = ctx.resolveRef(c.vertices[2]);
2061
+ const oppIdx = c.vertices.indexOf(c.opposite);
2062
+ const idx = oppIdx < 0 ? 0 : oppIdx;
2063
+ const ex = () => excenter(
2064
+ [[a.X(), a.Y()], [b.X(), b.Y()], [c3.X(), c3.Y()]],
2065
+ idx
2066
+ );
2067
+ return board.create("point", [() => ex()[0], () => ex()[1]], opts);
2068
+ }
2069
+ });
2070
+ }
2071
+ });
2072
+
2073
+ // src/core/scene/kinds/point-constraints/shared.ts
1567
2074
  function buildJxgTransforms(board, ctx, t) {
1568
2075
  switch (t.kind) {
1569
2076
  case "translate":
@@ -1590,11 +2097,392 @@ function buildJxgTransforms(board, ctx, t) {
1590
2097
  }
1591
2098
  }
1592
2099
  }
2100
+ function makeDistanceFn(ctx, d) {
2101
+ const scale = d.scale ?? 1;
2102
+ const offset = d.offset ?? 0;
2103
+ if (d.kind === "literal") {
2104
+ const v = d.value;
2105
+ return () => scale * v + offset;
2106
+ }
2107
+ if (d.kind === "segmentLength") {
2108
+ const p = ctx.resolveRef(d.p1);
2109
+ const q = ctx.resolveRef(d.p2);
2110
+ return () => scale * Math.hypot(p.X() - q.X(), p.Y() - q.Y()) + offset;
2111
+ }
2112
+ const circle = ctx.resolveRef(d.circle);
2113
+ return () => scale * circle.Radius() + offset;
2114
+ }
2115
+ function buildPointOpts(obj) {
2116
+ return {
2117
+ name: obj.label,
2118
+ withLabel: obj.attrs.showLabel ?? true,
2119
+ visible: obj.visible,
2120
+ fixed: obj.locked,
2121
+ strokeColor: obj.attrs.color ?? "#1e40af",
2122
+ fillColor: obj.attrs.color ?? "#1e40af",
2123
+ face: obj.attrs.face ?? "o",
2124
+ size: obj.attrs.size ?? 4
2125
+ };
2126
+ }
2127
+ var init_shared = __esm({
2128
+ "src/core/scene/kinds/point-constraints/shared.ts"() {
2129
+ }
2130
+ });
2131
+
2132
+ // src/core/scene/kinds/point-constraints/pointAtDistance.ts
2133
+ var pointAtDistanceConstraint;
2134
+ var init_pointAtDistance = __esm({
2135
+ "src/core/scene/kinds/point-constraints/pointAtDistance.ts"() {
2136
+ init_pointConstructions();
2137
+ init_types3();
2138
+ init_shared();
2139
+ pointAtDistanceConstraint = definePointConstraint({
2140
+ kind: "pointAtDistance",
2141
+ describe: (obj, state, c) => {
2142
+ const fromL = state?.objects[c.from]?.label ?? c.from;
2143
+ const thrL = state?.objects[c.through]?.label ?? c.through;
2144
+ const d = c.distance;
2145
+ const dLabel = d.kind === "literal" ? `${d.value}` : d.kind === "segmentLength" ? `${state?.objects[d.p1]?.label ?? d.p1}${state?.objects[d.p2]?.label ?? d.p2}` : `b\xE1n k\xEDnh (${state?.objects[d.circle]?.label ?? d.circle})`;
2146
+ return `${obj.label} = tr\xEAn tia ${fromL}${thrL} k\xE9o d\xE0i, c\xE1ch ${thrL} kho\u1EA3ng ${dLabel}`;
2147
+ },
2148
+ render: (obj, ctx, c, opts) => {
2149
+ const board = ctx.jxg;
2150
+ const A = ctx.resolveRef(c.from);
2151
+ const B = ctx.resolveRef(c.through);
2152
+ const dFn = makeDistanceFn(ctx, c.distance);
2153
+ const pc = () => pointAtDistanceCoord([A.X(), A.Y()], [B.X(), B.Y()], dFn());
2154
+ return board.create("point", [() => pc()[0], () => pc()[1]], opts);
2155
+ }
2156
+ });
2157
+ }
2158
+ });
2159
+
2160
+ // src/core/scene/kinds/point-constraints/circleIntersection.ts
2161
+ var circleIntersectionConstraint;
2162
+ var init_circleIntersection = __esm({
2163
+ "src/core/scene/kinds/point-constraints/circleIntersection.ts"() {
2164
+ init_types3();
2165
+ circleIntersectionConstraint = definePointConstraint({
2166
+ kind: "circleIntersection",
2167
+ // Không có describe-arm riêng trong point.ts → giữ fallback `Điểm ${label}`.
2168
+ describe: (obj) => `\u0110i\u1EC3m ${obj.label}`,
2169
+ render: (obj, ctx, c, opts) => {
2170
+ const board = ctx.jxg;
2171
+ const k1 = ctx.resolveRef(c.c1);
2172
+ const k2 = ctx.resolveRef(c.c2);
2173
+ return board.create("intersection", [k1, k2, c.which], opts);
2174
+ }
2175
+ });
2176
+ }
2177
+ });
2178
+
2179
+ // src/core/scene/kinds/point-constraints/circleSecondIntersection.ts
2180
+ var circleSecondIntersectionConstraint;
2181
+ var init_circleSecondIntersection = __esm({
2182
+ "src/core/scene/kinds/point-constraints/circleSecondIntersection.ts"() {
2183
+ init_types3();
2184
+ circleSecondIntersectionConstraint = definePointConstraint({
2185
+ kind: "circleSecondIntersection",
2186
+ // Không có describe-arm riêng trong point.ts → giữ fallback `Điểm ${label}`.
2187
+ describe: (obj) => `\u0110i\u1EC3m ${obj.label}`,
2188
+ render: (obj, ctx, c, opts) => {
2189
+ const board = ctx.jxg;
2190
+ const k1 = ctx.resolveRef(c.c1);
2191
+ const k2 = ctx.resolveRef(c.c2);
2192
+ const ex = ctx.resolveRef(c.exclude);
2193
+ return board.create("otherintersection", [k1, k2, ex], opts);
2194
+ }
2195
+ });
2196
+ }
2197
+ });
2198
+
2199
+ // src/core/scene/kinds/point-constraints/secondIntersection.ts
2200
+ var secondIntersectionConstraint;
2201
+ var init_secondIntersection = __esm({
2202
+ "src/core/scene/kinds/point-constraints/secondIntersection.ts"() {
2203
+ init_types3();
2204
+ secondIntersectionConstraint = definePointConstraint({
2205
+ kind: "secondIntersection",
2206
+ // Không có describe-arm riêng trong point.ts → giữ fallback `Điểm ${label}`.
2207
+ describe: (obj) => `\u0110i\u1EC3m ${obj.label}`,
2208
+ render: (obj, ctx, c, opts) => {
2209
+ const board = ctx.jxg;
2210
+ const line = ctx.resolveRef(c.line);
2211
+ const circle = ctx.resolveRef(c.circle);
2212
+ const other = ctx.resolveRef(c.other);
2213
+ return board.create("otherintersection", [circle, line, other], opts);
2214
+ }
2215
+ });
2216
+ }
2217
+ });
2218
+
2219
+ // src/core/scene/kinds/point-constraints/tangencyPoint.ts
2220
+ var tangencyPointConstraint;
2221
+ var init_tangencyPoint = __esm({
2222
+ "src/core/scene/kinds/point-constraints/tangencyPoint.ts"() {
2223
+ init_types3();
2224
+ tangencyPointConstraint = definePointConstraint({
2225
+ kind: "tangencyPoint",
2226
+ // Không có describe-arm riêng trong point.ts → giữ fallback `Điểm ${label}`.
2227
+ describe: (obj) => `\u0110i\u1EC3m ${obj.label}`,
2228
+ render: (obj, ctx, c, opts) => {
2229
+ const board = ctx.jxg;
2230
+ const circle = ctx.resolveRef(c.circle);
2231
+ const line = ctx.resolveRef(c.onLine);
2232
+ const O = circle?.center ?? circle?.midpoint ?? circle;
2233
+ return board.create("perpendicularpoint", [line, O], opts);
2234
+ }
2235
+ });
2236
+ }
2237
+ });
2238
+
2239
+ // src/core/scene/kinds/point-constraints/transformed.ts
2240
+ var transformedConstraint;
2241
+ var init_transformed = __esm({
2242
+ "src/core/scene/kinds/point-constraints/transformed.ts"() {
2243
+ init_types3();
2244
+ init_shared();
2245
+ transformedConstraint = definePointConstraint({
2246
+ kind: "transformed",
2247
+ describe: (obj, state, c) => {
2248
+ const t = c.transform;
2249
+ const labelRef = (id) => state?.objects[id]?.label ?? id;
2250
+ const op = t.kind === "translate" ? `t\u1ECBnh ti\u1EBFn (${t.dx.toFixed(2)}, ${t.dy.toFixed(2)})` : t.kind === "rotate" ? `quay ${(t.angleRad * 180 / Math.PI).toFixed(0)}\xB0 quanh ${labelRef(t.center)}` : t.kind === "reflectLine" ? `\u0111\u1ED1i x\u1EE9ng qua ${labelRef(t.line)}` : t.kind === "reflectPoint" ? `\u0111\u1ED1i x\u1EE9ng qua \u0111i\u1EC3m ${labelRef(t.center)}` : t.kind === "dilate" ? `v\u1ECB t\u1EF1 k=${t.k} quanh ${labelRef(t.center)}` : "";
2251
+ return `${obj.label} = \u1EA3nh c\u1EE7a ${labelRef(c.source)} (${op})`;
2252
+ },
2253
+ render: (obj, ctx, c, opts) => {
2254
+ const board = ctx.jxg;
2255
+ const src = ctx.resolveRef(c.source);
2256
+ const transforms = buildJxgTransforms(board, ctx, c.transform);
2257
+ const parent = transforms.length === 1 ? transforms[0] : transforms;
2258
+ const pt = board.create("point", [src, parent], opts);
2259
+ pt._helpers = transforms;
2260
+ return pt;
2261
+ }
2262
+ });
2263
+ }
2264
+ });
2265
+
2266
+ // src/core/scene/kinds/point-constraints/orthocenter.ts
2267
+ var orthocenterConstraint;
2268
+ var init_orthocenter = __esm({
2269
+ "src/core/scene/kinds/point-constraints/orthocenter.ts"() {
2270
+ init_types3();
2271
+ orthocenterConstraint = definePointConstraint({
2272
+ kind: "orthocenter",
2273
+ validate: (c) => {
2274
+ if (!Array.isArray(c.vertices) || c.vertices.length !== 3) {
2275
+ throw new Error("point.orthocenter: vertices ph\u1EA3i l\xE0 tuple 3 id");
2276
+ }
2277
+ if (!c.vertices[0] || !c.vertices[1] || !c.vertices[2]) {
2278
+ throw new Error("point.orthocenter: 3 vertex id ph\u1EA3i non-empty");
2279
+ }
2280
+ },
2281
+ describe: (obj, state, c) => {
2282
+ const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
2283
+ return `${obj.label} = tr\u1EF1c t\xE2m \u0394${labels}`;
2284
+ },
2285
+ render: (obj, ctx, c, opts) => {
2286
+ const board = ctx.jxg;
2287
+ const a = ctx.resolveRef(c.vertices[0]);
2288
+ const b = ctx.resolveRef(c.vertices[1]);
2289
+ const c3 = ctx.resolveRef(c.vertices[2]);
2290
+ const hide = { visible: false, withLabel: false, fixed: true, name: "" };
2291
+ const lineBC = board.create("line", [b, c3], hide);
2292
+ const altA = board.create("perpendicular", [lineBC, a], hide);
2293
+ const lineAC = board.create("line", [a, c3], hide);
2294
+ const altB = board.create("perpendicular", [lineAC, b], hide);
2295
+ const ortho = board.create("intersection", [altA, altB, 0], opts);
2296
+ ortho._helpers = [lineBC, altA, lineAC, altB];
2297
+ return ortho;
2298
+ }
2299
+ });
2300
+ }
2301
+ });
2302
+
2303
+ // src/core/scene/kinds/point-constraints/onPerpendicular.ts
2304
+ var onPerpendicularConstraint;
2305
+ var init_onPerpendicular = __esm({
2306
+ "src/core/scene/kinds/point-constraints/onPerpendicular.ts"() {
2307
+ init_types3();
2308
+ onPerpendicularConstraint = definePointConstraint({
2309
+ kind: "onPerpendicular",
2310
+ describe: (obj) => `\u0110i\u1EC3m ${obj.label}`,
2311
+ render: (obj, ctx, c, opts) => {
2312
+ const board = ctx.jxg;
2313
+ const T = ctx.resolveRef(c.through);
2314
+ const A = ctx.resolveRef(c.perpToA);
2315
+ const B = ctx.resolveRef(c.perpToB);
2316
+ const hide = { visible: false, withLabel: false, fixed: true, name: "" };
2317
+ const refLine = board.create("line", [A, B], hide);
2318
+ const perpLine = board.create("perpendicular", [refLine, T], hide);
2319
+ const dx = B.X() - A.X();
2320
+ const dy = B.Y() - A.Y();
2321
+ const len = Math.hypot(dx, dy) || 1;
2322
+ const ux = -dy / len;
2323
+ const uy = dx / len;
2324
+ const x0 = T.X() + c.t * ux;
2325
+ const y0 = T.Y() + c.t * uy;
2326
+ const gl = board.create("glider", [x0, y0, perpLine], opts);
2327
+ gl._helpers = [refLine, perpLine];
2328
+ return gl;
2329
+ }
2330
+ });
2331
+ }
2332
+ });
2333
+
2334
+ // src/core/scene/kinds/point-constraints/onPerpBisector.ts
2335
+ var onPerpBisectorConstraint;
2336
+ var init_onPerpBisector = __esm({
2337
+ "src/core/scene/kinds/point-constraints/onPerpBisector.ts"() {
2338
+ init_types3();
2339
+ onPerpBisectorConstraint = definePointConstraint({
2340
+ kind: "onPerpBisector",
2341
+ describe: (obj) => `\u0110i\u1EC3m ${obj.label}`,
2342
+ render: (obj, ctx, c, opts) => {
2343
+ const board = ctx.jxg;
2344
+ const A = ctx.resolveRef(c.p1);
2345
+ const B = ctx.resolveRef(c.p2);
2346
+ const hide = { visible: false, withLabel: false, fixed: true, name: "" };
2347
+ const refLine = board.create("line", [A, B], hide);
2348
+ const mid = board.create("midpoint", [A, B], hide);
2349
+ const bisLine = board.create("perpendicular", [refLine, mid], hide);
2350
+ const Mx = (A.X() + B.X()) / 2;
2351
+ const My = (A.Y() + B.Y()) / 2;
2352
+ const dx = B.X() - A.X();
2353
+ const dy = B.Y() - A.Y();
2354
+ const len = Math.hypot(dx, dy) || 1;
2355
+ const ux = -dy / len;
2356
+ const uy = dx / len;
2357
+ const x0 = Mx + c.t * ux;
2358
+ const y0 = My + c.t * uy;
2359
+ const gl = board.create("glider", [x0, y0, bisLine], opts);
2360
+ gl._helpers = [refLine, mid, bisLine];
2361
+ return gl;
2362
+ }
2363
+ });
2364
+ }
2365
+ });
2366
+
2367
+ // src/core/scene/kinds/point-constraints/onCircleAroundPoint.ts
2368
+ var onCircleAroundPointConstraint;
2369
+ var init_onCircleAroundPoint = __esm({
2370
+ "src/core/scene/kinds/point-constraints/onCircleAroundPoint.ts"() {
2371
+ init_types3();
2372
+ onCircleAroundPointConstraint = definePointConstraint({
2373
+ kind: "onCircleAroundPoint",
2374
+ describe: (obj) => `\u0110i\u1EC3m ${obj.label}`,
2375
+ render: (obj, ctx, c, opts) => {
2376
+ const board = ctx.jxg;
2377
+ const C = ctx.resolveRef(c.center);
2378
+ const R = ctx.resolveRef(c.radiusPoint);
2379
+ const hide = { visible: false, withLabel: false, fixed: true, name: "" };
2380
+ const auxCircle = board.create("circle", [C, R], hide);
2381
+ const r = Math.hypot(R.X() - C.X(), R.Y() - C.Y());
2382
+ const x0 = C.X() + r * Math.cos(c.theta);
2383
+ const y0 = C.Y() + r * Math.sin(c.theta);
2384
+ const gl = board.create("glider", [x0, y0, auxCircle], opts);
2385
+ gl._helpers = [auxCircle];
2386
+ return gl;
2387
+ }
2388
+ });
2389
+ }
2390
+ });
2391
+
2392
+ // src/core/scene/kinds/point-constraints/tangentPointExt.ts
2393
+ var tangentPointExtConstraint;
2394
+ var init_tangentPointExt = __esm({
2395
+ "src/core/scene/kinds/point-constraints/tangentPointExt.ts"() {
2396
+ init_types3();
2397
+ tangentPointExtConstraint = definePointConstraint({
2398
+ kind: "tangentPointExt",
2399
+ describe: (obj, state, c) => {
2400
+ const fromLabel = state?.objects[c.from]?.label ?? c.from;
2401
+ const circleLabel = state?.objects[c.circle]?.label ?? c.circle;
2402
+ return `${obj.label} = ti\u1EBFp \u0111i\u1EC3m c\u1EE7a (${circleLabel}) v\u1EDBi ti\u1EBFp tuy\u1EBFn t\u1EEB ${fromLabel}`;
2403
+ },
2404
+ render: (obj, ctx, c, opts) => {
2405
+ const board = ctx.jxg;
2406
+ const P = ctx.resolveRef(c.from);
2407
+ const K = ctx.resolveRef(c.circle);
2408
+ const O = K.center ?? K.midpoint;
2409
+ const hide = { visible: false, withLabel: false, fixed: true, name: "" };
2410
+ const mid = board.create("midpoint", [P, O], hide);
2411
+ const thales = board.create("circle", [mid, P], hide);
2412
+ const inter = board.create("intersection", [thales, K, c.which], opts);
2413
+ inter._helpers = [mid, thales];
2414
+ return inter;
2415
+ }
2416
+ });
2417
+ }
2418
+ });
2419
+
2420
+ // src/core/scene/kinds/point-constraints/registry.ts
2421
+ var ALL, POINT_CONSTRAINTS;
2422
+ var init_registry2 = __esm({
2423
+ "src/core/scene/kinds/point-constraints/registry.ts"() {
2424
+ init_free();
2425
+ init_onAxis();
2426
+ init_midpoint();
2427
+ init_perpFoot();
2428
+ init_circumcenter();
2429
+ init_incenter();
2430
+ init_onLine();
2431
+ init_onSegment();
2432
+ init_onCircle();
2433
+ init_onPolygon();
2434
+ init_centroid();
2435
+ init_arcMidpoint();
2436
+ init_excenter();
2437
+ init_pointAtDistance();
2438
+ init_circleIntersection();
2439
+ init_circleSecondIntersection();
2440
+ init_secondIntersection();
2441
+ init_tangencyPoint();
2442
+ init_transformed();
2443
+ init_orthocenter();
2444
+ init_onPerpendicular();
2445
+ init_onPerpBisector();
2446
+ init_onCircleAroundPoint();
2447
+ init_tangentPointExt();
2448
+ ALL = [
2449
+ freeConstraint,
2450
+ onAxisConstraint,
2451
+ midpointConstraint,
2452
+ perpFootConstraint,
2453
+ circumcenterConstraint,
2454
+ incenterConstraint,
2455
+ onLineConstraint,
2456
+ onSegmentConstraint,
2457
+ onCircleConstraint,
2458
+ onPolygonConstraint,
2459
+ centroidConstraint,
2460
+ arcMidpointConstraint,
2461
+ excenterConstraint,
2462
+ pointAtDistanceConstraint,
2463
+ circleIntersectionConstraint,
2464
+ circleSecondIntersectionConstraint,
2465
+ secondIntersectionConstraint,
2466
+ tangencyPointConstraint,
2467
+ transformedConstraint,
2468
+ orthocenterConstraint,
2469
+ onPerpendicularConstraint,
2470
+ onPerpBisectorConstraint,
2471
+ onCircleAroundPointConstraint,
2472
+ tangentPointExtConstraint
2473
+ ];
2474
+ POINT_CONSTRAINTS = new Map(ALL.map((m) => [m.kind, m]));
2475
+ }
2476
+ });
2477
+
2478
+ // src/core/scene/kinds/point.ts
1593
2479
  var def3;
1594
2480
  var init_point = __esm({
1595
2481
  "src/core/scene/kinds/point.ts"() {
1596
2482
  init_registry();
1597
2483
  init_d_constraint2();
2484
+ init_registry2();
2485
+ init_shared();
1598
2486
  def3 = {
1599
2487
  type: "point",
1600
2488
  schemaVersion: 1,
@@ -1604,43 +2492,7 @@ var init_point = __esm({
1604
2492
  throw new Error("point: constraint required");
1605
2493
  }
1606
2494
  const c = a.constraint;
1607
- if (c.kind === "perpFoot") {
1608
- if (!c.from || !c.onLine) {
1609
- throw new Error("point.perpFoot: from v\xE0 onLine b\u1EAFt bu\u1ED9c");
1610
- }
1611
- }
1612
- if (c.kind === "circumcenter") {
1613
- if (!Array.isArray(c.vertices) || c.vertices.length !== 3) {
1614
- throw new Error("point.circumcenter: vertices ph\u1EA3i l\xE0 tuple 3 id");
1615
- }
1616
- if (!c.vertices[0] || !c.vertices[1] || !c.vertices[2]) {
1617
- throw new Error("point.circumcenter: 3 vertex id ph\u1EA3i non-empty");
1618
- }
1619
- }
1620
- if (c.kind === "incenter") {
1621
- if (!Array.isArray(c.vertices) || c.vertices.length !== 3) {
1622
- throw new Error("point.incenter: vertices ph\u1EA3i l\xE0 tuple 3 id");
1623
- }
1624
- if (!c.vertices[0] || !c.vertices[1] || !c.vertices[2]) {
1625
- throw new Error("point.incenter: 3 vertex id ph\u1EA3i non-empty");
1626
- }
1627
- }
1628
- if (c.kind === "centroid") {
1629
- if (!Array.isArray(c.vertices) || c.vertices.length !== 3) {
1630
- throw new Error("point.centroid: vertices ph\u1EA3i l\xE0 tuple 3 id");
1631
- }
1632
- if (!c.vertices[0] || !c.vertices[1] || !c.vertices[2]) {
1633
- throw new Error("point.centroid: 3 vertex id ph\u1EA3i non-empty");
1634
- }
1635
- }
1636
- if (c.kind === "orthocenter") {
1637
- if (!Array.isArray(c.vertices) || c.vertices.length !== 3) {
1638
- throw new Error("point.orthocenter: vertices ph\u1EA3i l\xE0 tuple 3 id");
1639
- }
1640
- if (!c.vertices[0] || !c.vertices[1] || !c.vertices[2]) {
1641
- throw new Error("point.orthocenter: 3 vertex id ph\u1EA3i non-empty");
1642
- }
1643
- }
2495
+ POINT_CONSTRAINTS.get(c.kind)?.validate?.(c);
1644
2496
  },
1645
2497
  dependsOn: (a) => constraintRefs2D(a.constraint),
1646
2498
  measure: (obj) => {
@@ -1655,132 +2507,16 @@ var init_point = __esm({
1655
2507
  },
1656
2508
  describe: (obj, state) => {
1657
2509
  const c = obj.attrs.constraint;
1658
- if (c.kind === "free") return `\u0110i\u1EC3m ${obj.label}`;
1659
- if (c.kind === "onAxis") return `${obj.label} tr\xEAn tr\u1EE5c ${c.axis}`;
1660
- if (c.kind === "onLine") return `${obj.label} tr\xEAn \u0111\u01B0\u1EDDng ${state?.objects[c.lineId]?.label ?? c.lineId}`;
1661
- if (c.kind === "onSegment") return `${obj.label} tr\xEAn \u0111o\u1EA1n ${state?.objects[c.segmentId]?.label ?? c.segmentId}`;
1662
- if (c.kind === "onCircle") return `${obj.label} tr\xEAn \u0111\u01B0\u1EDDng tr\xF2n ${state?.objects[c.circleId]?.label ?? c.circleId}`;
1663
- if (c.kind === "onPolygon") return `${obj.label} tr\xEAn \u0111a gi\xE1c ${state?.objects[c.polygonId]?.label ?? c.polygonId}`;
1664
- if (c.kind === "midpoint") {
1665
- const l1 = state?.objects[c.p1]?.label ?? c.p1;
1666
- const l2 = state?.objects[c.p2]?.label ?? c.p2;
1667
- return `${obj.label} = trung \u0111i\u1EC3m ${l1}${l2}`;
1668
- }
1669
- if (c.kind === "transformed") {
1670
- const t = c.transform;
1671
- const labelRef = (id) => state?.objects[id]?.label ?? id;
1672
- const op = t.kind === "translate" ? `t\u1ECBnh ti\u1EBFn (${t.dx.toFixed(2)}, ${t.dy.toFixed(2)})` : t.kind === "rotate" ? `quay ${(t.angleRad * 180 / Math.PI).toFixed(0)}\xB0 quanh ${labelRef(t.center)}` : t.kind === "reflectLine" ? `\u0111\u1ED1i x\u1EE9ng qua ${labelRef(t.line)}` : t.kind === "reflectPoint" ? `\u0111\u1ED1i x\u1EE9ng qua \u0111i\u1EC3m ${labelRef(t.center)}` : t.kind === "dilate" ? `v\u1ECB t\u1EF1 k=${t.k} quanh ${labelRef(t.center)}` : "";
1673
- return `${obj.label} = \u1EA3nh c\u1EE7a ${labelRef(c.source)} (${op})`;
1674
- }
1675
- if (c.kind === "perpFoot") {
1676
- const fromLabel = state?.objects[c.from]?.label ?? c.from;
1677
- const lineLabel = state?.objects[c.onLine]?.label ?? c.onLine;
1678
- return `${obj.label} = ch\xE2n \u27C2 t\u1EEB ${fromLabel} xu\u1ED1ng ${lineLabel}`;
1679
- }
1680
- if (c.kind === "circumcenter") {
1681
- const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
1682
- return `${obj.label} = t\xE2m ngo\u1EA1i ti\u1EBFp \u0394${labels}`;
1683
- }
1684
- if (c.kind === "incenter") {
1685
- const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
1686
- return `${obj.label} = t\xE2m n\u1ED9i ti\u1EBFp \u0394${labels}`;
1687
- }
1688
- if (c.kind === "centroid") {
1689
- const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
1690
- return `${obj.label} = tr\u1ECDng t\xE2m \u0394${labels}`;
1691
- }
1692
- if (c.kind === "orthocenter") {
1693
- const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
1694
- return `${obj.label} = tr\u1EF1c t\xE2m \u0394${labels}`;
1695
- }
2510
+ const mod = POINT_CONSTRAINTS.get(c.kind);
2511
+ if (mod) return mod.describe(obj, state, c);
1696
2512
  return `\u0110i\u1EC3m ${obj.label}`;
1697
2513
  },
1698
2514
  render: (obj, ctx) => {
1699
2515
  const board = ctx.jxg;
1700
2516
  const c = obj.attrs.constraint;
1701
- const opts = {
1702
- name: obj.label,
1703
- withLabel: obj.attrs.showLabel ?? true,
1704
- visible: obj.visible,
1705
- fixed: obj.locked,
1706
- strokeColor: obj.attrs.color ?? "#1e40af",
1707
- fillColor: obj.attrs.color ?? "#1e40af",
1708
- face: obj.attrs.face ?? "o",
1709
- size: obj.attrs.size ?? 4
1710
- };
1711
- if (c.kind === "free") return board.create("point", [c.x, c.y], opts);
1712
- if (c.kind === "onAxis") {
1713
- const coords = c.axis === "x" ? [c.t, 0] : [0, c.t];
1714
- return board.create("point", coords, opts);
1715
- }
1716
- if (c.kind === "onLine") {
1717
- const line = ctx.resolveRef(c.lineId);
1718
- return board.create("glider", [c.t, c.t, line], opts);
1719
- }
1720
- if (c.kind === "onSegment") {
1721
- const seg = ctx.resolveRef(c.segmentId);
1722
- return board.create("glider", [c.t, c.t, seg], opts);
1723
- }
1724
- if (c.kind === "onCircle") {
1725
- const circle = ctx.resolveRef(c.circleId);
1726
- return board.create("glider", [Math.cos(c.theta), Math.sin(c.theta), circle], opts);
1727
- }
1728
- if (c.kind === "onPolygon") {
1729
- const poly = ctx.resolveRef(c.polygonId);
1730
- return board.create("glider", [c.u, c.v, poly], opts);
1731
- }
1732
- if (c.kind === "midpoint") {
1733
- const p1 = ctx.resolveRef(c.p1);
1734
- const p2 = ctx.resolveRef(c.p2);
1735
- return board.create("midpoint", [p1, p2], opts);
1736
- }
1737
- if (c.kind === "transformed") {
1738
- const src = ctx.resolveRef(c.source);
1739
- const transforms = buildJxgTransforms(board, ctx, c.transform);
1740
- const parent = transforms.length === 1 ? transforms[0] : transforms;
1741
- const pt = board.create("point", [src, parent], opts);
1742
- pt._helpers = transforms;
1743
- return pt;
1744
- }
1745
- if (c.kind === "perpFoot") {
1746
- const from = ctx.resolveRef(c.from);
1747
- const onLine = ctx.resolveRef(c.onLine);
1748
- return board.create("perpendicularpoint", [onLine, from], opts);
1749
- }
1750
- if (c.kind === "circumcenter") {
1751
- const a = ctx.resolveRef(c.vertices[0]);
1752
- const b = ctx.resolveRef(c.vertices[1]);
1753
- const c3 = ctx.resolveRef(c.vertices[2]);
1754
- return board.create("circumcenter", [a, b, c3], opts);
1755
- }
1756
- if (c.kind === "incenter") {
1757
- const a = ctx.resolveRef(c.vertices[0]);
1758
- const b = ctx.resolveRef(c.vertices[1]);
1759
- const c3 = ctx.resolveRef(c.vertices[2]);
1760
- return board.create("incenter", [a, b, c3], opts);
1761
- }
1762
- if (c.kind === "centroid") {
1763
- const a = ctx.resolveRef(c.vertices[0]);
1764
- const b = ctx.resolveRef(c.vertices[1]);
1765
- const c3 = ctx.resolveRef(c.vertices[2]);
1766
- return board.create("point", [
1767
- () => (a.X() + b.X() + c3.X()) / 3,
1768
- () => (a.Y() + b.Y() + c3.Y()) / 3
1769
- ], opts);
1770
- }
1771
- if (c.kind === "orthocenter") {
1772
- const a = ctx.resolveRef(c.vertices[0]);
1773
- const b = ctx.resolveRef(c.vertices[1]);
1774
- const c3 = ctx.resolveRef(c.vertices[2]);
1775
- const hide = { visible: false, withLabel: false, fixed: true, name: "" };
1776
- const lineBC = board.create("line", [b, c3], hide);
1777
- const altA = board.create("perpendicular", [lineBC, a], hide);
1778
- const lineAC = board.create("line", [a, c3], hide);
1779
- const altB = board.create("perpendicular", [lineAC, b], hide);
1780
- const ortho = board.create("intersection", [altA, altB, 0], opts);
1781
- ortho._helpers = [lineBC, altA, lineAC, altB];
1782
- return ortho;
1783
- }
2517
+ const opts = buildPointOpts(obj);
2518
+ const mod = POINT_CONSTRAINTS.get(c.kind);
2519
+ if (mod) return mod.render(obj, ctx, c, opts);
1784
2520
  return board.create("point", [0, 0], opts);
1785
2521
  },
1786
2522
  /**
@@ -1889,6 +2625,10 @@ function constructionRefs(c) {
1889
2625
  return [c.p1, c.vertex, c.p2];
1890
2626
  case "angleBisectorLines":
1891
2627
  return [stripBorderSuffix(c.line1), stripBorderSuffix(c.line2)];
2628
+ case "lineThrough":
2629
+ return [...c.points];
2630
+ case "radicalAxis":
2631
+ return [c.circle1, c.circle2];
1892
2632
  case "tangent":
1893
2633
  return [c.throughPoint, c.toCircle];
1894
2634
  }
@@ -1898,6 +2638,7 @@ var init_line = __esm({
1898
2638
  "src/core/scene/kinds/line.ts"() {
1899
2639
  init_registry();
1900
2640
  init_labelOf();
2641
+ init_pointConstructions();
1901
2642
  def5 = {
1902
2643
  type: "line",
1903
2644
  schemaVersion: 1,
@@ -1922,6 +2663,10 @@ var init_line = __esm({
1922
2663
  return `${obj.label}: ph\xE2n gi\xE1c g\xF3c ${L(c.p1)}${L(c.vertex)}${L(c.p2)}`;
1923
2664
  case "angleBisectorLines":
1924
2665
  return `${obj.label}: ph\xE2n gi\xE1c ${L(c.line1)} & ${L(c.line2)} (${c.branch === 0 ? "1" : "2"})`;
2666
+ case "lineThrough":
2667
+ return `${obj.label}: \u0111\u01B0\u1EDDng qua ${c.points.map(L).join("")}`;
2668
+ case "radicalAxis":
2669
+ return `${obj.label}: tr\u1EE5c \u0111\u1EB3ng ph\u01B0\u01A1ng ${L(c.circle1)} & ${L(c.circle2)}`;
1925
2670
  case "tangent":
1926
2671
  return `${obj.label}: ti\u1EBFp tuy\u1EBFn ${L(c.toCircle)} qua ${L(c.throughPoint)}`;
1927
2672
  }
@@ -2002,6 +2747,47 @@ var init_line = __esm({
2002
2747
  selected._helpers = [other];
2003
2748
  return selected;
2004
2749
  }
2750
+ case "lineThrough": {
2751
+ const pts = c.points.map((id) => ctx.resolveRef(id));
2752
+ let bi = 0, bj = 1, best = -1;
2753
+ for (let i = 0; i < pts.length; i++) {
2754
+ for (let j = i + 1; j < pts.length; j++) {
2755
+ const dx = pts[i].X() - pts[j].X();
2756
+ const dy = pts[i].Y() - pts[j].Y();
2757
+ const d = dx * dx + dy * dy;
2758
+ if (d > best) {
2759
+ best = d;
2760
+ bi = i;
2761
+ bj = j;
2762
+ }
2763
+ }
2764
+ }
2765
+ return board.create("line", [pts[bi], pts[bj]], {
2766
+ ...baseOpts,
2767
+ straightFirst: true,
2768
+ straightLast: true
2769
+ });
2770
+ }
2771
+ case "radicalAxis": {
2772
+ const k1 = ctx.resolveRef(c.circle1);
2773
+ const k2 = ctx.resolveRef(c.circle2);
2774
+ const o1 = () => [k1.center.X(), k1.center.Y()];
2775
+ const o2 = () => [k2.center.X(), k2.center.Y()];
2776
+ const foot = () => radicalAxisFoot(o1(), k1.Radius(), o2(), k2.Radius());
2777
+ const hide = { visible: false, withLabel: false, fixed: true, name: "" };
2778
+ const f1 = board.create("point", [() => foot()[0], () => foot()[1]], hide);
2779
+ const f2 = board.create("point", [
2780
+ () => foot()[0] - (o2()[1] - o1()[1]),
2781
+ () => foot()[1] + (o2()[0] - o1()[0])
2782
+ ], hide);
2783
+ const line = board.create("line", [f1, f2], {
2784
+ ...baseOpts,
2785
+ straightFirst: true,
2786
+ straightLast: true
2787
+ });
2788
+ line._helpers = [f1, f2];
2789
+ return line;
2790
+ }
2005
2791
  case "tangent": {
2006
2792
  const through = ctx.resolveRef(c.throughPoint);
2007
2793
  const toCircle = ctx.resolveRef(c.toCircle);
@@ -2120,10 +2906,29 @@ var init_vector = __esm({
2120
2906
  });
2121
2907
 
2122
2908
  // src/core/scene/kinds/circle.ts
2909
+ function asConstruction(a) {
2910
+ if (a.construction) return a.construction;
2911
+ const raw = a;
2912
+ if (raw.kind === "incircle" && raw.vertices) {
2913
+ return {
2914
+ kind: "incircle",
2915
+ p1: raw.vertices[0],
2916
+ p2: raw.vertices[1],
2917
+ p3: raw.vertices[2]
2918
+ };
2919
+ }
2920
+ return void 0;
2921
+ }
2123
2922
  function constructionRefs2(c) {
2124
2923
  switch (c.kind) {
2125
2924
  case "circumscribed":
2126
2925
  return [c.p1, c.p2, c.p3];
2926
+ case "incircle":
2927
+ return [c.p1, c.p2, c.p3];
2928
+ case "excircle":
2929
+ return [c.p1, c.p2, c.p3];
2930
+ case "diameter":
2931
+ return [c.p1, c.p2];
2127
2932
  }
2128
2933
  }
2129
2934
  var def8;
@@ -2131,19 +2936,33 @@ var init_circle = __esm({
2131
2936
  "src/core/scene/kinds/circle.ts"() {
2132
2937
  init_registry();
2133
2938
  init_labelOf();
2939
+ init_pointConstructions();
2134
2940
  def8 = {
2135
2941
  type: "circle",
2136
2942
  schemaVersion: 1,
2137
2943
  migrate: {},
2138
2944
  validate: (a) => {
2139
- if (a?.construction) return;
2945
+ if (asConstruction(a)) return;
2946
+ if (typeof a?.radius === "number") {
2947
+ if (!a.center) throw new Error("circle: center b\u1EAFt bu\u1ED9c khi d\xF9ng radius");
2948
+ if (!(a.radius > 0)) throw new Error("circle: radius ph\u1EA3i > 0");
2949
+ return;
2950
+ }
2140
2951
  if (!a?.center || !a?.surfacePoint) {
2141
- throw new Error("circle: center v\xE0 surfacePoint b\u1EAFt bu\u1ED9c (ho\u1EB7c construction)");
2952
+ throw new Error("circle: center v\xE0 surfacePoint b\u1EAFt bu\u1ED9c (ho\u1EB7c construction / radius)");
2142
2953
  }
2143
2954
  },
2144
- dependsOn: (a) => a.construction ? constructionRefs2(a.construction) : [a.center, a.surfacePoint],
2955
+ dependsOn: (a) => {
2956
+ const c = asConstruction(a);
2957
+ if (c) return constructionRefs2(c);
2958
+ if (typeof a.radius === "number") return [a.center];
2959
+ return [a.center, a.surfacePoint];
2960
+ },
2145
2961
  measure: (obj, state) => {
2146
- if (obj.attrs.construction) return null;
2962
+ if (asConstruction(obj.attrs)) return null;
2963
+ if (typeof obj.attrs.radius === "number") {
2964
+ return [{ label: "r", value: obj.attrs.radius }];
2965
+ }
2147
2966
  const center = obj.attrs.center ? state.objects[obj.attrs.center] : void 0;
2148
2967
  const surface = obj.attrs.surfacePoint ? state.objects[obj.attrs.surfacePoint] : void 0;
2149
2968
  if (!center || !surface) return null;
@@ -2156,17 +2975,31 @@ var init_circle = __esm({
2156
2975
  },
2157
2976
  describe: (obj, state) => {
2158
2977
  const L = (id) => labelOf(id, state);
2159
- const c = obj.attrs.construction;
2978
+ const c = asConstruction(obj.attrs);
2160
2979
  if (c?.kind === "circumscribed") {
2161
2980
  return `\u0110\u01B0\u1EDDng tr\xF2n \u0111i qua ${L(c.p1)}${L(c.p2)}${L(c.p3)}`;
2162
2981
  }
2982
+ if (c?.kind === "incircle") {
2983
+ return `\u0110\u01B0\u1EDDng tr\xF2n n\u1ED9i ti\u1EBFp \u0394${L(c.p1)}${L(c.p2)}${L(c.p3)}`;
2984
+ }
2985
+ if (c?.kind === "excircle") {
2986
+ return `\u0110\u01B0\u1EDDng tr\xF2n b\xE0ng ti\u1EBFp \u0394${L(c.p1)}${L(c.p2)}${L(c.p3)} \u0111\u1ED1i di\u1EC7n ${L(c.opposite)}`;
2987
+ }
2988
+ if (c?.kind === "diameter") {
2989
+ return `\u0110\u01B0\u1EDDng tr\xF2n \u0111\u01B0\u1EDDng k\xEDnh ${L(c.p1)}${L(c.p2)}`;
2990
+ }
2991
+ if (typeof obj.attrs.radius === "number") {
2992
+ return `\u0110\u01B0\u1EDDng tr\xF2n t\xE2m ${L(obj.attrs.center)} b\xE1n k\xEDnh ${obj.attrs.radius}`;
2993
+ }
2163
2994
  return `\u0110\u01B0\u1EDDng tr\xF2n t\xE2m ${L(obj.attrs.center)} b\xE1n k\xEDnh ${L(obj.attrs.center)}${L(obj.attrs.surfacePoint)}`;
2164
2995
  },
2165
2996
  render: (obj, ctx) => {
2166
2997
  const board = ctx.jxg;
2998
+ const isCenterLabel = (l) => /^[A-Z]['′]?\d*$/u.test(l);
2999
+ const isCenter = isCenterLabel(obj.label);
2167
3000
  const baseOpts = {
2168
3001
  name: obj.label,
2169
- withLabel: obj.attrs.showLabel ?? false,
3002
+ withLabel: isCenter ? obj.attrs.showLabel ?? false : true,
2170
3003
  strokeColor: obj.attrs.color ?? "#0f172a",
2171
3004
  strokeWidth: obj.attrs.width ?? 2,
2172
3005
  dash: obj.attrs.dash ?? 0,
@@ -2174,13 +3007,81 @@ var init_circle = __esm({
2174
3007
  visible: obj.visible,
2175
3008
  fixed: obj.locked
2176
3009
  };
2177
- const c = obj.attrs.construction;
3010
+ const c = asConstruction(obj.attrs);
2178
3011
  if (c?.kind === "circumscribed") {
2179
3012
  const p1 = ctx.resolveRef(c.p1);
2180
3013
  const p2 = ctx.resolveRef(c.p2);
2181
3014
  const p3 = ctx.resolveRef(c.p3);
3015
+ if (isCenter) {
3016
+ const center2 = board.create("circumcenter", [p1, p2, p3], {
3017
+ visible: obj.visible,
3018
+ withLabel: true,
3019
+ fixed: true,
3020
+ name: obj.label
3021
+ });
3022
+ const circ = board.create("circumcircle", [p1, p2, p3], { ...baseOpts, withLabel: false });
3023
+ circ.center = circ.center ?? center2;
3024
+ circ._helpers = [center2];
3025
+ return circ;
3026
+ }
2182
3027
  return board.create("circumcircle", [p1, p2, p3], baseOpts);
2183
3028
  }
3029
+ if (c?.kind === "incircle") {
3030
+ const p1 = ctx.resolveRef(c.p1);
3031
+ const p2 = ctx.resolveRef(c.p2);
3032
+ const p3 = ctx.resolveRef(c.p3);
3033
+ if (isCenter) {
3034
+ const center2 = board.create("incenter", [p1, p2, p3], {
3035
+ visible: obj.visible,
3036
+ withLabel: true,
3037
+ fixed: true,
3038
+ name: obj.label
3039
+ });
3040
+ const circ = board.create("incircle", [p1, p2, p3], { ...baseOpts, withLabel: false });
3041
+ circ.center = circ.center ?? center2;
3042
+ circ._helpers = [center2];
3043
+ return circ;
3044
+ }
3045
+ return board.create("incircle", [p1, p2, p3], baseOpts);
3046
+ }
3047
+ if (c?.kind === "excircle") {
3048
+ const P = [ctx.resolveRef(c.p1), ctx.resolveRef(c.p2), ctx.resolveRef(c.p3)];
3049
+ const ids = [c.p1, c.p2, c.p3];
3050
+ const oppIdx = Math.max(0, ids.indexOf(c.opposite));
3051
+ const verts = () => [
3052
+ [P[0].X(), P[0].Y()],
3053
+ [P[1].X(), P[1].Y()],
3054
+ [P[2].X(), P[2].Y()]
3055
+ ];
3056
+ const ctr = () => excenter(verts(), oppIdx);
3057
+ const radius = () => {
3058
+ const I = ctr();
3059
+ const others = [0, 1, 2].filter((i) => i !== oppIdx);
3060
+ const v = verts();
3061
+ const a = v[others[0]];
3062
+ const b = v[others[1]];
3063
+ const dx = b[0] - a[0];
3064
+ const dy = b[1] - a[1];
3065
+ const len = Math.hypot(dx, dy) || 1;
3066
+ return Math.abs((I[0] - a[0]) * dy - (I[1] - a[1]) * dx) / len;
3067
+ };
3068
+ const center2 = board.create("point", [() => ctr()[0], () => ctr()[1]], { visible: false, withLabel: false, fixed: true, name: "" });
3069
+ const circ = board.create("circle", [center2, () => radius()], baseOpts);
3070
+ circ._helpers = [center2];
3071
+ return circ;
3072
+ }
3073
+ if (c?.kind === "diameter") {
3074
+ const p1 = ctx.resolveRef(c.p1);
3075
+ const p2 = ctx.resolveRef(c.p2);
3076
+ const center2 = board.create("midpoint", [p1, p2], { visible: false, withLabel: false, fixed: true, name: "" });
3077
+ const circ = board.create("circle", [center2, p2], baseOpts);
3078
+ circ._helpers = [center2];
3079
+ return circ;
3080
+ }
3081
+ if (typeof obj.attrs.radius === "number") {
3082
+ const center2 = ctx.resolveRef(obj.attrs.center);
3083
+ return board.create("circle", [center2, obj.attrs.radius], baseOpts);
3084
+ }
2184
3085
  const center = ctx.resolveRef(obj.attrs.center);
2185
3086
  const surface = ctx.resolveRef(obj.attrs.surfacePoint);
2186
3087
  return board.create("circle", [center, surface], baseOpts);
@@ -2340,6 +3241,26 @@ function regularVertexLabels(p1Label, p2Label, n) {
2340
3241
  }
2341
3242
  return `${p1Label}${p2Label}\u2026`;
2342
3243
  }
3244
+ function specialShapeName(kind) {
3245
+ switch (kind) {
3246
+ case "square":
3247
+ return "H\xECnh vu\xF4ng";
3248
+ case "rectangle":
3249
+ return "H\xECnh ch\u1EEF nh\u1EADt";
3250
+ case "rhombus":
3251
+ return "H\xECnh thoi";
3252
+ case "parallelogram":
3253
+ return "H\xECnh b\xECnh h\xE0nh";
3254
+ case "isoTrapezoid":
3255
+ return "H\xECnh thang c\xE2n";
3256
+ case "isoTriangle":
3257
+ return "Tam gi\xE1c c\xE2n";
3258
+ case "rightTriangle":
3259
+ return "Tam gi\xE1c vu\xF4ng";
3260
+ case "regular":
3261
+ return "";
3262
+ }
3263
+ }
2343
3264
  var def11;
2344
3265
  var init_polygon = __esm({
2345
3266
  "src/core/scene/kinds/polygon.ts"() {
@@ -2351,13 +3272,27 @@ var init_polygon = __esm({
2351
3272
  migrate: {},
2352
3273
  validate: (a) => {
2353
3274
  if (a?.construction) {
2354
- if (a.construction.kind === "regular") {
2355
- if (!a.construction.p1 || !a.construction.p2) {
2356
- throw new Error("polygon (regular): p1 v\xE0 p2 b\u1EAFt bu\u1ED9c");
2357
- }
2358
- if (!Number.isFinite(a.construction.n) || a.construction.n < 3) {
2359
- throw new Error("polygon (regular): n \u2265 3");
2360
- }
3275
+ const c = a.construction;
3276
+ if (c.kind === "regular") {
3277
+ if (!c.p1 || !c.p2) throw new Error("polygon (regular): p1 v\xE0 p2 b\u1EAFt bu\u1ED9c");
3278
+ if (!Number.isFinite(c.n) || c.n < 3) throw new Error("polygon (regular): n \u2265 3");
3279
+ return;
3280
+ }
3281
+ if (c.kind === "square") {
3282
+ if (!c.p1 || !c.p2) throw new Error("polygon (square): p1 v\xE0 p2 b\u1EAFt bu\u1ED9c");
3283
+ return;
3284
+ }
3285
+ if (c.kind === "rectangle" || c.kind === "rhombus" || c.kind === "parallelogram" || c.kind === "isoTrapezoid") {
3286
+ if (!c.p1 || !c.p2 || !c.p3) throw new Error(`polygon (${c.kind}): p1, p2, p3 b\u1EAFt bu\u1ED9c`);
3287
+ return;
3288
+ }
3289
+ if (c.kind === "isoTriangle") {
3290
+ if (!c.base1 || !c.base2 || !c.apex) throw new Error("polygon (isoTriangle): base1, base2, apex b\u1EAFt bu\u1ED9c");
3291
+ return;
3292
+ }
3293
+ if (c.kind === "rightTriangle") {
3294
+ if (!c.rightAngle || !c.leg1End || !c.leg2End) throw new Error("polygon (rightTriangle): rightAngle, leg1End, leg2End b\u1EAFt bu\u1ED9c");
3295
+ return;
2361
3296
  }
2362
3297
  return;
2363
3298
  }
@@ -2366,14 +3301,51 @@ var init_polygon = __esm({
2366
3301
  }
2367
3302
  },
2368
3303
  dependsOn: (a) => {
2369
- if (a.construction?.kind === "regular") return [a.construction.p1, a.construction.p2];
2370
- return [...a.vertices ?? []];
3304
+ const c = a.construction;
3305
+ if (!c) return [...a.vertices ?? []];
3306
+ switch (c.kind) {
3307
+ case "regular":
3308
+ return [c.p1, c.p2];
3309
+ case "square":
3310
+ return [c.p1, c.p2];
3311
+ case "rectangle":
3312
+ case "rhombus":
3313
+ case "parallelogram":
3314
+ case "isoTrapezoid":
3315
+ return [c.p1, c.p2, c.p3];
3316
+ case "isoTriangle":
3317
+ return [c.base1, c.base2, c.apex];
3318
+ case "rightTriangle":
3319
+ return [c.rightAngle, c.leg1End, c.leg2End];
3320
+ }
2371
3321
  },
2372
3322
  describe: (obj, state) => {
2373
- if (obj.attrs.construction?.kind === "regular") {
2374
- const c = obj.attrs.construction;
2375
- const labels = regularVertexLabels(labelOf(c.p1, state), labelOf(c.p2, state), c.n);
2376
- return `${regularPolygonName(c.n)} ${labels}`;
3323
+ const c = obj.attrs.construction;
3324
+ if (c) {
3325
+ if (c.kind === "regular") {
3326
+ const labels2 = regularVertexLabels(labelOf(c.p1, state), labelOf(c.p2, state), c.n);
3327
+ return `${regularPolygonName(c.n)} ${labels2}`;
3328
+ }
3329
+ const name = specialShapeName(c.kind);
3330
+ let labels = [];
3331
+ switch (c.kind) {
3332
+ case "square":
3333
+ labels = [labelOf(c.p1, state), labelOf(c.p2, state)];
3334
+ break;
3335
+ case "rectangle":
3336
+ case "rhombus":
3337
+ case "parallelogram":
3338
+ case "isoTrapezoid":
3339
+ labels = [labelOf(c.p1, state), labelOf(c.p2, state), labelOf(c.p3, state)];
3340
+ break;
3341
+ case "isoTriangle":
3342
+ labels = [labelOf(c.apex, state), labelOf(c.base1, state), labelOf(c.base2, state)];
3343
+ break;
3344
+ case "rightTriangle":
3345
+ labels = [labelOf(c.rightAngle, state), labelOf(c.leg1End, state), labelOf(c.leg2End, state)];
3346
+ break;
3347
+ }
3348
+ return `${name} ${labels.join("")}`;
2377
3349
  }
2378
3350
  return `\u0110a gi\xE1c ${(obj.attrs.vertices ?? []).map((id) => labelOf(id, state)).join("")}`;
2379
3351
  },
@@ -2381,22 +3353,85 @@ var init_polygon = __esm({
2381
3353
  const board = ctx.jxg;
2382
3354
  const label = obj.label;
2383
3355
  const showValue = obj.attrs.showValue ?? false;
2384
- if (obj.attrs.construction?.kind === "regular") {
2385
- const c = obj.attrs.construction;
2386
- const p1 = ctx.resolveRef(c.p1);
2387
- const p2 = ctx.resolveRef(c.p2);
2388
- return board.create("regularpolygon", [p1, p2, c.n], {
2389
- name: label,
2390
- withLabel: obj.attrs.showLabel ?? false,
2391
- borders: {
2392
- strokeColor: obj.attrs.color ?? "#0f172a",
2393
- strokeWidth: obj.attrs.width ?? 2
2394
- },
2395
- fillColor: obj.attrs.color ?? "#60a5fa",
2396
- fillOpacity: obj.attrs.fillOpacity ?? 0.15,
2397
- visible: obj.visible,
2398
- fixed: obj.locked
3356
+ const cons = obj.attrs.construction;
3357
+ const commonAttrs = {
3358
+ name: label,
3359
+ withLabel: obj.attrs.showLabel ?? false,
3360
+ borders: {
3361
+ strokeColor: obj.attrs.color ?? "#0f172a",
3362
+ strokeWidth: obj.attrs.width ?? 2
3363
+ },
3364
+ fillColor: obj.attrs.color ?? "#60a5fa",
3365
+ fillOpacity: obj.attrs.fillOpacity ?? 0.15,
3366
+ visible: obj.visible,
3367
+ fixed: obj.locked
3368
+ };
3369
+ if (cons?.kind === "regular") {
3370
+ const p1 = ctx.resolveRef(cons.p1);
3371
+ const p2 = ctx.resolveRef(cons.p2);
3372
+ return board.create("regularpolygon", [p1, p2, cons.n], commonAttrs);
3373
+ }
3374
+ if (cons?.kind === "square") {
3375
+ const p1 = ctx.resolveRef(cons.p1);
3376
+ const p2 = ctx.resolveRef(cons.p2);
3377
+ return board.create("regularpolygon", [p1, p2, 4], commonAttrs);
3378
+ }
3379
+ if (cons?.kind === "rectangle" || cons?.kind === "rhombus" || cons?.kind === "parallelogram") {
3380
+ const A = ctx.resolveRef(cons.p1);
3381
+ const B = ctx.resolveRef(cons.p2);
3382
+ const C = ctx.resolveRef(cons.p3);
3383
+ const D = board.create(
3384
+ "point",
3385
+ [() => A.X() + C.X() - B.X(), () => A.Y() + C.Y() - B.Y()],
3386
+ { visible: false, withLabel: false, fixed: true, name: "" }
3387
+ );
3388
+ const poly2 = board.create("polygon", [A, B, C, D], commonAttrs);
3389
+ poly2._helpers = [D];
3390
+ return poly2;
3391
+ }
3392
+ if (cons?.kind === "isoTrapezoid") {
3393
+ const A = ctx.resolveRef(cons.p1);
3394
+ const B = ctx.resolveRef(cons.p2);
3395
+ const C = ctx.resolveRef(cons.p3);
3396
+ const Dx = () => {
3397
+ const Mx = (A.X() + B.X()) / 2;
3398
+ const My = (A.Y() + B.Y()) / 2;
3399
+ const ux = B.X() - A.X();
3400
+ const uy = B.Y() - A.Y();
3401
+ const len2 = ux * ux + uy * uy || 1;
3402
+ const proj = ((C.X() - Mx) * ux + (C.Y() - My) * uy) / len2;
3403
+ return C.X() - 2 * proj * ux;
3404
+ };
3405
+ const Dy = () => {
3406
+ const Mx = (A.X() + B.X()) / 2;
3407
+ const My = (A.Y() + B.Y()) / 2;
3408
+ const ux = B.X() - A.X();
3409
+ const uy = B.Y() - A.Y();
3410
+ const len2 = ux * ux + uy * uy || 1;
3411
+ const proj = ((C.X() - Mx) * ux + (C.Y() - My) * uy) / len2;
3412
+ return C.Y() - 2 * proj * uy;
3413
+ };
3414
+ const D = board.create("point", [Dx, Dy], {
3415
+ visible: false,
3416
+ withLabel: false,
3417
+ fixed: true,
3418
+ name: ""
2399
3419
  });
3420
+ const poly2 = board.create("polygon", [A, B, C, D], commonAttrs);
3421
+ poly2._helpers = [D];
3422
+ return poly2;
3423
+ }
3424
+ if (cons?.kind === "isoTriangle") {
3425
+ const Apex = ctx.resolveRef(cons.apex);
3426
+ const B1 = ctx.resolveRef(cons.base1);
3427
+ const B2 = ctx.resolveRef(cons.base2);
3428
+ return board.create("polygon", [Apex, B1, B2], commonAttrs);
3429
+ }
3430
+ if (cons?.kind === "rightTriangle") {
3431
+ const R = ctx.resolveRef(cons.rightAngle);
3432
+ const P = ctx.resolveRef(cons.leg1End);
3433
+ const Q = ctx.resolveRef(cons.leg2End);
3434
+ return board.create("polygon", [R, P, Q], commonAttrs);
2400
3435
  }
2401
3436
  const verts = (obj.attrs.vertices ?? []).map((id) => ctx.resolveRef(id));
2402
3437
  const poly = board.create("polygon", verts, {
@@ -3324,21 +4359,21 @@ var init_handlers = __esm({
3324
4359
  }
3325
4360
  });
3326
4361
  function useToolStateMachine(initial) {
3327
- const [tool, setToolState] = React.useState(initial);
3328
- const [pendingIds, setPendingIds] = React.useState([]);
3329
- const toolRef = React.useRef(initial);
3330
- const pendingIdsRef = React.useRef([]);
3331
- const setTool = React.useCallback((t) => {
4362
+ const [tool, setToolState] = React5.useState(initial);
4363
+ const [pendingIds, setPendingIds] = React5.useState([]);
4364
+ const toolRef = React5.useRef(initial);
4365
+ const pendingIdsRef = React5.useRef([]);
4366
+ const setTool = React5.useCallback((t) => {
3332
4367
  toolRef.current = t;
3333
4368
  pendingIdsRef.current = [];
3334
4369
  setToolState(t);
3335
4370
  setPendingIds([]);
3336
4371
  }, []);
3337
- const pushPending = React.useCallback((id) => {
4372
+ const pushPending = React5.useCallback((id) => {
3338
4373
  pendingIdsRef.current = [...pendingIdsRef.current, id];
3339
4374
  setPendingIds(pendingIdsRef.current);
3340
4375
  }, []);
3341
- const clearPending = React.useCallback(() => {
4376
+ const clearPending = React5.useCallback(() => {
3342
4377
  pendingIdsRef.current = [];
3343
4378
  setPendingIds([]);
3344
4379
  }, []);
@@ -3507,24 +4542,24 @@ var init_MiniBoard = __esm({
3507
4542
  init_safeJsx();
3508
4543
  init_attachJxgWheelZoom();
3509
4544
  init_initJxgBoard();
3510
- MiniBoard = React__namespace.default.forwardRef(
4545
+ MiniBoard = React5__namespace.default.forwardRef(
3511
4546
  function MiniBoard2({ store, selectedTool, showAxis, showGrid, isDark, onReady, onSelectionChange: _onSelectionChange }, ref) {
3512
- const isDarkRef = React.useRef(!!isDark);
4547
+ const isDarkRef = React5.useRef(!!isDark);
3513
4548
  isDarkRef.current = !!isDark;
3514
- const containerId = React.useId().replace(/:/g, "_") + "_graph_jxg";
3515
- const containerRef = React.useRef(null);
3516
- const boardRef = React.useRef(null);
3517
- const jxgRef = React.useRef(null);
3518
- const rendererRef = React.useRef(null);
4549
+ const containerId = React5.useId().replace(/:/g, "_") + "_graph_jxg";
4550
+ const containerRef = React5.useRef(null);
4551
+ const boardRef = React5.useRef(null);
4552
+ const jxgRef = React5.useRef(null);
4553
+ const rendererRef = React5.useRef(null);
3519
4554
  const toolSM = useToolStateMachine(selectedTool);
3520
- const showAxisRef = React.useRef(showAxis);
4555
+ const showAxisRef = React5.useRef(showAxis);
3521
4556
  showAxisRef.current = showAxis;
3522
- const showGridRef = React.useRef(showGrid);
4557
+ const showGridRef = React5.useRef(showGrid);
3523
4558
  showGridRef.current = showGrid;
3524
- React.useEffect(() => {
4559
+ React5.useEffect(() => {
3525
4560
  if (toolSM.toolRef.current !== selectedTool) toolSM.setTool(selectedTool);
3526
4561
  }, [selectedTool]);
3527
- React.useEffect(() => {
4562
+ React5.useEffect(() => {
3528
4563
  if (typeof window === "undefined" || !containerRef.current) return;
3529
4564
  let cancelled = false;
3530
4565
  let wheelCleanup = null;
@@ -3610,7 +4645,7 @@ var init_MiniBoard = __esm({
3610
4645
  boardRef.current = null;
3611
4646
  };
3612
4647
  }, [containerId]);
3613
- React.useImperativeHandle(
4648
+ React5.useImperativeHandle(
3614
4649
  ref,
3615
4650
  () => ({
3616
4651
  getState: () => store.getState(),
@@ -3656,27 +4691,27 @@ function reducer(state, action) {
3656
4691
  }
3657
4692
  }
3658
4693
  function ToastProvider({ children, maxVisible = 3 }) {
3659
- const [items, dispatch] = React.useReducer(reducer, []);
3660
- const timersRef = React.useRef(/* @__PURE__ */ new Map());
3661
- const itemsRef = React.useRef(items);
4694
+ const [items, dispatch] = React5.useReducer(reducer, []);
4695
+ const timersRef = React5.useRef(/* @__PURE__ */ new Map());
4696
+ const itemsRef = React5.useRef(items);
3662
4697
  itemsRef.current = items;
3663
- const clearTimer = React.useCallback((id) => {
4698
+ const clearTimer = React5.useCallback((id) => {
3664
4699
  const t = timersRef.current.get(id);
3665
4700
  if (t) {
3666
4701
  clearTimeout(t);
3667
4702
  timersRef.current.delete(id);
3668
4703
  }
3669
4704
  }, []);
3670
- const dismiss = React.useCallback((id) => {
4705
+ const dismiss = React5.useCallback((id) => {
3671
4706
  clearTimer(id);
3672
4707
  dispatch({ type: "DISMISS", id });
3673
4708
  }, [clearTimer]);
3674
- const scheduleAutoDismiss = React.useCallback((id, duration) => {
4709
+ const scheduleAutoDismiss = React5.useCallback((id, duration) => {
3675
4710
  if (duration <= 0) return;
3676
4711
  const t = setTimeout(() => dismiss(id), duration);
3677
4712
  timersRef.current.set(id, t);
3678
4713
  }, [dismiss]);
3679
- const showToast = React.useCallback((message, opts = {}) => {
4714
+ const showToast = React5.useCallback((message, opts = {}) => {
3680
4715
  const variant = opts.variant ?? "info";
3681
4716
  const duration = opts.duration ?? 3e3;
3682
4717
  const id = opts.id ?? `toast-${++autoIdCounter}`;
@@ -3690,17 +4725,17 @@ function ToastProvider({ children, maxVisible = 3 }) {
3690
4725
  }
3691
4726
  scheduleAutoDismiss(id, duration);
3692
4727
  }, [clearTimer, maxVisible, scheduleAutoDismiss]);
3693
- React.useEffect(() => () => {
4728
+ React5.useEffect(() => () => {
3694
4729
  timersRef.current.forEach((t) => clearTimeout(t));
3695
4730
  timersRef.current.clear();
3696
4731
  }, []);
3697
- const value = React.useMemo(() => ({ items, showToast, dismiss }), [items, showToast, dismiss]);
4732
+ const value = React5.useMemo(() => ({ items, showToast, dismiss }), [items, showToast, dismiss]);
3698
4733
  return /* @__PURE__ */ jsxRuntime.jsx(ToastContext.Provider, { value, children });
3699
4734
  }
3700
4735
  var ToastContext, autoIdCounter;
3701
4736
  var init_ToastProvider = __esm({
3702
4737
  "src/stamps/shared/Toast/ToastProvider.tsx"() {
3703
- ToastContext = React.createContext(null);
4738
+ ToastContext = React5.createContext(null);
3704
4739
  autoIdCounter = 0;
3705
4740
  }
3706
4741
  });
@@ -3758,7 +4793,7 @@ var init_Toast = __esm({
3758
4793
  }
3759
4794
  });
3760
4795
  function useToast() {
3761
- const ctx = React.useContext(ToastContext);
4796
+ const ctx = React5.useContext(ToastContext);
3762
4797
  if (!ctx) {
3763
4798
  throw new Error("useToast must be used inside <ToastProvider>");
3764
4799
  }
@@ -3803,7 +4838,7 @@ var init_EditorPanel = __esm({
3803
4838
  init_scene();
3804
4839
  init_constants();
3805
4840
  init_Toast2();
3806
- GraphEditorPanelInner = React.forwardRef(
4841
+ GraphEditorPanelInner = React5.forwardRef(
3807
4842
  function GraphEditorPanel({
3808
4843
  store,
3809
4844
  onInsert,
@@ -3822,23 +4857,23 @@ var init_EditorPanel = __esm({
3822
4857
  canUndo,
3823
4858
  canRedo
3824
4859
  }, ref) {
3825
- const miniRef = React.useRef(null);
3826
- const [ready, setReady] = React.useState(false);
3827
- const [hasContent, setHasContent] = React.useState(false);
3828
- const onSelectionChangeRef = React.useRef(onSelectionChange);
3829
- React.useEffect(() => {
4860
+ const miniRef = React5.useRef(null);
4861
+ const [ready, setReady] = React5.useState(false);
4862
+ const [hasContent, setHasContent] = React5.useState(false);
4863
+ const onSelectionChangeRef = React5.useRef(onSelectionChange);
4864
+ React5.useEffect(() => {
3830
4865
  onSelectionChangeRef.current = onSelectionChange;
3831
4866
  }, [onSelectionChange]);
3832
4867
  useEditorState({ store, onHistoryChange });
3833
- React.useEffect(() => {
4868
+ React5.useEffect(() => {
3834
4869
  const sync = () => setHasContent(Object.keys(store.getState().objects).length > 0);
3835
4870
  sync();
3836
4871
  return store.subscribe(sync);
3837
4872
  }, [store]);
3838
- const handleReady = React.useCallback(() => {
4873
+ const handleReady = React5.useCallback(() => {
3839
4874
  setReady(true);
3840
4875
  }, []);
3841
- const performInsert = React.useCallback(() => {
4876
+ const performInsert = React5.useCallback(() => {
3842
4877
  const h = miniRef.current;
3843
4878
  if (!h) return false;
3844
4879
  const state = h.getState();
@@ -3856,7 +4891,7 @@ var init_EditorPanel = __esm({
3856
4891
  })();
3857
4892
  return true;
3858
4893
  }, [isDark, onInsert]);
3859
- React.useImperativeHandle(ref, () => ({
4894
+ React5.useImperativeHandle(ref, () => ({
3860
4895
  insert: performInsert,
3861
4896
  hasContent: () => Object.keys(miniRef.current?.getState().objects ?? {}).length > 0,
3862
4897
  getStore: () => miniRef.current?.getStore() ?? null,
@@ -4014,13 +5049,27 @@ var init_EditorPanel = __esm({
4014
5049
  );
4015
5050
  }
4016
5051
  );
4017
- GraphEditorPanel2 = React.forwardRef(
5052
+ GraphEditorPanel2 = React5.forwardRef(
4018
5053
  function GraphEditorPanel3(props, ref) {
4019
5054
  return /* @__PURE__ */ jsxRuntime.jsx(ToastProvider, { children: /* @__PURE__ */ jsxRuntime.jsx(GraphEditorPanelInner, { ...props, ref }) });
4020
5055
  }
4021
5056
  );
4022
5057
  }
4023
5058
  });
5059
+ function clamp(n, min, max) {
5060
+ return Math.max(min, Math.min(max, n));
5061
+ }
5062
+ function readStoredWidth(key, fallback, min, max) {
5063
+ if (!key || typeof window === "undefined") return fallback;
5064
+ try {
5065
+ const raw = window.localStorage.getItem(key);
5066
+ if (!raw) return fallback;
5067
+ const n = parseInt(raw, 10);
5068
+ if (Number.isFinite(n)) return clamp(n, min, max);
5069
+ } catch {
5070
+ }
5071
+ return fallback;
5072
+ }
4024
5073
  function CloseIcon() {
4025
5074
  return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
4026
5075
  /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
@@ -4028,8 +5077,64 @@ function CloseIcon() {
4028
5077
  ] });
4029
5078
  }
4030
5079
  function LeftPanelShell(props) {
4031
- const { title, icon, onClose, isDark, tabs, activeTab, onTabChange, testId, children } = props;
5080
+ const {
5081
+ title,
5082
+ icon,
5083
+ onClose,
5084
+ isDark,
5085
+ tabs,
5086
+ activeTab,
5087
+ onTabChange,
5088
+ testId,
5089
+ resizable,
5090
+ widthStorageKey,
5091
+ defaultWidth,
5092
+ minWidth,
5093
+ maxWidth,
5094
+ children
5095
+ } = props;
4032
5096
  const showTabs = !!tabs && tabs.length >= 2;
5097
+ const min = minWidth ?? FALLBACK_MIN_WIDTH;
5098
+ const max = maxWidth ?? FALLBACK_MAX_WIDTH;
5099
+ const initial = clamp(defaultWidth ?? FALLBACK_DEFAULT_WIDTH, min, max);
5100
+ const [width, setWidth] = React5__namespace.useState(
5101
+ () => resizable ? readStoredWidth(widthStorageKey, initial, min, max) : initial
5102
+ );
5103
+ const widthRef = React5__namespace.useRef(width);
5104
+ widthRef.current = width;
5105
+ React5__namespace.useEffect(() => {
5106
+ if (!resizable || !widthStorageKey || typeof window === "undefined") return;
5107
+ try {
5108
+ window.localStorage.setItem(widthStorageKey, String(width));
5109
+ } catch {
5110
+ }
5111
+ }, [resizable, widthStorageKey, width]);
5112
+ const onResizeStart = React5__namespace.useCallback(
5113
+ (e) => {
5114
+ if (!resizable) return;
5115
+ e.preventDefault();
5116
+ const startX = e.clientX;
5117
+ const startW = widthRef.current;
5118
+ const onMove = (ev) => {
5119
+ setWidth(clamp(startW + (ev.clientX - startX), min, max));
5120
+ };
5121
+ const onUp = () => {
5122
+ window.removeEventListener("mousemove", onMove);
5123
+ window.removeEventListener("mouseup", onUp);
5124
+ document.body.style.cursor = "";
5125
+ document.body.style.userSelect = "";
5126
+ };
5127
+ window.addEventListener("mousemove", onMove);
5128
+ window.addEventListener("mouseup", onUp);
5129
+ document.body.style.cursor = "ew-resize";
5130
+ document.body.style.userSelect = "none";
5131
+ },
5132
+ [resizable, min, max]
5133
+ );
5134
+ const onResizeDoubleClick = React5__namespace.useCallback(() => {
5135
+ if (!resizable) return;
5136
+ setWidth(initial);
5137
+ }, [resizable, initial]);
4033
5138
  return /* @__PURE__ */ jsxRuntime.jsxs(
4034
5139
  "aside",
4035
5140
  {
@@ -4037,10 +5142,12 @@ function LeftPanelShell(props) {
4037
5142
  "aria-label": title,
4038
5143
  "data-testid": testId ?? "left-panel",
4039
5144
  "data-stamp-area": "true",
5145
+ style: resizable ? { width: `${width}px` } : void 0,
4040
5146
  className: [
4041
5147
  isDark ? "theme--dark " : "",
4042
- "absolute left-0 top-0 z-30 flex h-full w-60 flex-col border-r border-slate-200 bg-white shadow-md animate-in slide-in-from-left duration-200"
4043
- ].join(""),
5148
+ "absolute left-0 top-0 z-30 flex h-full flex-col border-r border-slate-200 bg-white shadow-md animate-in slide-in-from-left duration-200",
5149
+ resizable ? "" : "w-60"
5150
+ ].join(" "),
4044
5151
  children: [
4045
5152
  /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex items-center justify-between border-b border-slate-200 bg-gradient-to-r from-slate-50 to-white px-3 py-2", children: [
4046
5153
  /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold text-slate-800", children: [
@@ -4075,6 +5182,20 @@ function LeftPanelShell(props) {
4075
5182
  className: "min-h-0 flex-1 overflow-y-auto p-3 space-y-3",
4076
5183
  children
4077
5184
  }
5185
+ ),
5186
+ resizable && /* @__PURE__ */ jsxRuntime.jsx(
5187
+ "div",
5188
+ {
5189
+ role: "separator",
5190
+ "aria-orientation": "vertical",
5191
+ "aria-label": "K\xE9o \u0111\u1EC3 \u0111\u1ED5i r\u1ED9ng panel",
5192
+ "data-testid": "left-panel-resizer",
5193
+ onMouseDown: onResizeStart,
5194
+ onDoubleClick: onResizeDoubleClick,
5195
+ className: "group absolute right-0 top-0 z-40 h-full w-1.5 -mr-0.5 cursor-ew-resize select-none",
5196
+ title: "K\xE9o \u0111\u1EC3 \u0111\u1ED5i r\u1ED9ng (double-click \u0111\u1EC3 reset)",
5197
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pointer-events-none absolute inset-y-0 right-0 w-px bg-slate-200 transition group-hover:bg-emerald-400 group-hover:w-0.5 group-active:bg-emerald-500 group-active:w-0.5" })
5198
+ }
4078
5199
  )
4079
5200
  ]
4080
5201
  }
@@ -4104,9 +5225,13 @@ function Section(props) {
4104
5225
  props.children
4105
5226
  ] });
4106
5227
  }
5228
+ var FALLBACK_DEFAULT_WIDTH, FALLBACK_MIN_WIDTH, FALLBACK_MAX_WIDTH;
4107
5229
  var init_LeftPanelShell = __esm({
4108
5230
  "src/core/scene/ui/LeftPanelShell.tsx"() {
4109
5231
  "use client";
5232
+ FALLBACK_DEFAULT_WIDTH = 240;
5233
+ FALLBACK_MIN_WIDTH = 220;
5234
+ FALLBACK_MAX_WIDTH = 480;
4110
5235
  }
4111
5236
  });
4112
5237
 
@@ -4157,7 +5282,7 @@ var init_kindMeta = __esm({
4157
5282
  });
4158
5283
  function ObjectRowMenu(props) {
4159
5284
  const { locked, onToggleLocked, onRename, onChangeColor, onDelete } = props;
4160
- const [open, setOpen] = React__namespace.useState(false);
5285
+ const [open, setOpen] = React5__namespace.useState(false);
4161
5286
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative inline-block", children: [
4162
5287
  /* @__PURE__ */ jsxRuntime.jsx(
4163
5288
  "button",
@@ -4310,11 +5435,11 @@ var init_ObjectRow = __esm({
4310
5435
  });
4311
5436
  function ObjectListPanel(props) {
4312
5437
  const { store, selectedId, onSelect, renderRow } = props;
4313
- const subscribe = React__namespace.useCallback(
5438
+ const subscribe = React5__namespace.useCallback(
4314
5439
  (cb) => store.subscribe(() => cb()),
4315
5440
  [store]
4316
5441
  );
4317
- const state = React__namespace.useSyncExternalStore(subscribe, store.getState, store.getState);
5442
+ const state = React5__namespace.useSyncExternalStore(subscribe, store.getState, store.getState);
4318
5443
  const objects = listObjects(state);
4319
5444
  function handleSelect(id) {
4320
5445
  onSelect?.(id === selectedId ? null : id);
@@ -4345,7 +5470,7 @@ function ObjectListPanel(props) {
4345
5470
  if (renderRow) {
4346
5471
  const custom = renderRow(obj, { selected, onClick });
4347
5472
  if (custom != null) {
4348
- return /* @__PURE__ */ jsxRuntime.jsx(React__namespace.Fragment, { children: custom }, obj.id);
5473
+ return /* @__PURE__ */ jsxRuntime.jsx(React5__namespace.Fragment, { children: custom }, obj.id);
4349
5474
  }
4350
5475
  }
4351
5476
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -4458,29 +5583,29 @@ var init_AxisGridSection = __esm({
4458
5583
 
4459
5584
  // src/stamps/shared/StampLeftPanel/types.ts
4460
5585
  var TOOLTIP_DELAY_MS;
4461
- var init_types3 = __esm({
5586
+ var init_types4 = __esm({
4462
5587
  "src/stamps/shared/StampLeftPanel/types.ts"() {
4463
5588
  TOOLTIP_DELAY_MS = 400;
4464
5589
  }
4465
5590
  });
4466
5591
  function useToolHoverTooltip() {
4467
- const [hover, setHover] = React.useState(null);
4468
- const [portalReady, setPortalReady] = React.useState(false);
4469
- const hoverTimerRef = React.useRef(null);
4470
- React.useEffect(() => {
5592
+ const [hover, setHover] = React5.useState(null);
5593
+ const [portalReady, setPortalReady] = React5.useState(false);
5594
+ const hoverTimerRef = React5.useRef(null);
5595
+ React5.useEffect(() => {
4471
5596
  setPortalReady(true);
4472
5597
  return () => {
4473
5598
  if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
4474
5599
  };
4475
5600
  }, []);
4476
- const showHover = React.useCallback((el, t) => {
5601
+ const showHover = React5.useCallback((el, t) => {
4477
5602
  if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
4478
5603
  hoverTimerRef.current = setTimeout(() => {
4479
5604
  const r = el.getBoundingClientRect();
4480
5605
  setHover({ label: t.label, hint: t.hint, x: r.right, y: r.top + r.height / 2 });
4481
5606
  }, TOOLTIP_DELAY_MS);
4482
5607
  }, []);
4483
- const hideHover = React.useCallback(() => {
5608
+ const hideHover = React5.useCallback(() => {
4484
5609
  if (hoverTimerRef.current) {
4485
5610
  clearTimeout(hoverTimerRef.current);
4486
5611
  hoverTimerRef.current = null;
@@ -4491,27 +5616,118 @@ function useToolHoverTooltip() {
4491
5616
  }
4492
5617
  var init_useToolHoverTooltip = __esm({
4493
5618
  "src/stamps/shared/StampLeftPanel/useToolHoverTooltip.ts"() {
4494
- init_types3();
5619
+ init_types4();
4495
5620
  }
4496
5621
  });
5622
+ function normalize(s) {
5623
+ return s.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/đ/g, "d").replace(/Đ/g, "d");
5624
+ }
5625
+ function SearchIcon() {
5626
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
5627
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "11", cy: "11", r: "7" }),
5628
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "20", y1: "20", x2: "16.5", y2: "16.5" })
5629
+ ] });
5630
+ }
5631
+ function ClearIcon() {
5632
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
5633
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
5634
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
5635
+ ] });
5636
+ }
5637
+ function ToolResultList(props) {
5638
+ const { tools, activeTool, onToolChange } = props;
5639
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-0.5", "data-testid": "tool-result-list", children: tools.map((t) => {
5640
+ const active = activeTool === t.key;
5641
+ return /* @__PURE__ */ jsxRuntime.jsxs(
5642
+ "button",
5643
+ {
5644
+ type: "button",
5645
+ "data-tool": t.key,
5646
+ "aria-label": t.label,
5647
+ "aria-pressed": active,
5648
+ onClick: () => onToolChange(t.key),
5649
+ className: [
5650
+ "flex items-center gap-2 rounded-md px-2 py-1.5 text-left transition",
5651
+ active ? "bg-emerald-600 text-white" : "text-slate-700 hover:bg-slate-100"
5652
+ ].join(" "),
5653
+ children: [
5654
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-6 w-6 shrink-0 items-center justify-center", children: t.icon }),
5655
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "min-w-0", children: [
5656
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block truncate text-[12px] font-medium leading-tight", children: t.label }),
5657
+ t.hint && /* @__PURE__ */ jsxRuntime.jsx("span", { className: ["block truncate text-[10px] leading-tight", active ? "text-emerald-50" : "text-slate-400"].join(" "), children: t.hint })
5658
+ ] })
5659
+ ]
5660
+ },
5661
+ t.key
5662
+ );
5663
+ }) });
5664
+ }
4497
5665
  function ToolGrid(props) {
4498
5666
  const { tools, groupOrder, groupLabels, activeTool, onToolChange, chord } = props;
4499
5667
  const { hover, portalReady, showHover, hideHover } = useToolHoverTooltip();
4500
- const grouped = React.useMemo(() => {
5668
+ const [query, setQuery] = React5.useState("");
5669
+ const normalizedQuery = React5.useMemo(() => normalize(query.trim()), [query]);
5670
+ const filteredTools = React5.useMemo(() => {
5671
+ if (!normalizedQuery) return tools;
5672
+ return tools.filter((t) => {
5673
+ if (normalize(t.label).includes(normalizedQuery)) return true;
5674
+ if (t.hint && normalize(t.hint).includes(normalizedQuery)) return true;
5675
+ return false;
5676
+ });
5677
+ }, [tools, normalizedQuery]);
5678
+ const grouped = React5.useMemo(() => {
4501
5679
  var _a;
4502
5680
  const acc = {};
4503
- for (const t of tools) {
5681
+ for (const t of filteredTools) {
4504
5682
  (acc[_a = t.group] ?? (acc[_a] = [])).push(t);
4505
5683
  }
4506
5684
  return acc;
4507
- }, [tools]);
4508
- const groupKeys = React.useMemo(
4509
- () => groupOrder.filter((g) => grouped[g]),
5685
+ }, [filteredTools]);
5686
+ const groupKeys = React5.useMemo(
5687
+ () => groupOrder.filter((g) => grouped[g] && grouped[g].length > 0),
4510
5688
  [grouped, groupOrder]
4511
5689
  );
4512
- const activeGroupTools = chord?.activeGroup ? grouped[chord.activeGroup] ?? null : null;
5690
+ const noMatch = normalizedQuery !== "" && groupKeys.length === 0;
4513
5691
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4514
- groupKeys.map((group) => {
5692
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
5693
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 text-slate-400", children: /* @__PURE__ */ jsxRuntime.jsx(SearchIcon, {}) }),
5694
+ /* @__PURE__ */ jsxRuntime.jsx(
5695
+ "input",
5696
+ {
5697
+ type: "search",
5698
+ value: query,
5699
+ onChange: (e) => setQuery(e.target.value),
5700
+ placeholder: "T\xECm c\xF4ng c\u1EE5\u2026",
5701
+ "aria-label": "T\xECm c\xF4ng c\u1EE5",
5702
+ "data-testid": "tool-search-input",
5703
+ className: "w-full rounded-md border border-slate-200 bg-slate-50 py-1.5 pl-7 pr-7 text-[12px] text-slate-800 placeholder:text-slate-400 focus:border-emerald-400 focus:bg-white focus:outline-none focus:ring-1 focus:ring-emerald-300"
5704
+ }
5705
+ ),
5706
+ query && /* @__PURE__ */ jsxRuntime.jsx(
5707
+ "button",
5708
+ {
5709
+ type: "button",
5710
+ onClick: () => setQuery(""),
5711
+ "aria-label": "Xo\xE1 t\xECm ki\u1EBFm",
5712
+ "data-testid": "tool-search-clear",
5713
+ className: "absolute right-1.5 top-1/2 -translate-y-1/2 rounded p-0.5 text-slate-400 transition hover:bg-slate-200 hover:text-slate-700",
5714
+ children: /* @__PURE__ */ jsxRuntime.jsx(ClearIcon, {})
5715
+ }
5716
+ )
5717
+ ] }),
5718
+ noMatch && /* @__PURE__ */ jsxRuntime.jsxs(
5719
+ "div",
5720
+ {
5721
+ "data-testid": "tool-search-empty",
5722
+ className: "rounded-md border border-dashed border-slate-200 bg-slate-50 px-3 py-4 text-center text-[11px] text-slate-500",
5723
+ children: [
5724
+ "Kh\xF4ng c\xF3 c\xF4ng c\u1EE5 n\xE0o kh\u1EDBp \u201C",
5725
+ query.trim(),
5726
+ "\u201D."
5727
+ ]
5728
+ }
5729
+ ),
5730
+ normalizedQuery !== "" && !noMatch ? /* @__PURE__ */ jsxRuntime.jsx(ToolResultList, { tools: filteredTools, activeTool, onToolChange }) : groupKeys.map((group) => {
4515
5731
  const isChordActive = chord?.activeGroup === group;
4516
5732
  const dimmed = chord?.activeGroup != null && !isChordActive;
4517
5733
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -4525,23 +5741,10 @@ function ToolGrid(props) {
4525
5741
  dimmed ? "opacity-55" : "opacity-100"
4526
5742
  ].join(" "),
4527
5743
  children: [
4528
- /* @__PURE__ */ jsxRuntime.jsxs("h4", { className: "mb-1.5 flex items-center justify-between text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: [
4529
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: groupLabels[group] }),
4530
- chord && /* @__PURE__ */ jsxRuntime.jsx(
4531
- "span",
4532
- {
4533
- "data-testid": `chord-letter-${group}`,
4534
- className: [
4535
- "font-mono text-[10px] leading-none transition",
4536
- isChordActive ? "text-emerald-700 font-bold" : "text-slate-400"
4537
- ].join(" "),
4538
- children: chord.letterForGroup(group)
4539
- }
4540
- )
4541
- ] }),
4542
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-4 gap-1", children: grouped[group].map((t, i) => {
5744
+ /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: groupLabels[group] }),
5745
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-4 gap-1", children: grouped[group].map((t) => {
4543
5746
  const active = activeTool === t.key;
4544
- return /* @__PURE__ */ jsxRuntime.jsxs(
5747
+ return /* @__PURE__ */ jsxRuntime.jsx(
4545
5748
  "button",
4546
5749
  {
4547
5750
  type: "button",
@@ -4558,20 +5761,7 @@ function ToolGrid(props) {
4558
5761
  "relative flex h-10 items-center justify-center rounded-md transition",
4559
5762
  active ? "bg-emerald-600 text-white shadow-sm" : "text-slate-700 hover:bg-slate-100 hover:text-slate-900"
4560
5763
  ].join(" "),
4561
- children: [
4562
- t.icon,
4563
- chord && /* @__PURE__ */ jsxRuntime.jsx(
4564
- "span",
4565
- {
4566
- "data-testid": `chord-num-${t.key}`,
4567
- className: [
4568
- "pointer-events-none absolute bottom-0 right-0.5 font-mono text-[9px] leading-none transition",
4569
- active ? "text-white/70" : isChordActive ? "text-emerald-700 font-bold" : "text-slate-400"
4570
- ].join(" "),
4571
- children: i + 1
4572
- }
4573
- )
4574
- ]
5764
+ children: t.icon
4575
5765
  },
4576
5766
  t.key
4577
5767
  );
@@ -4581,22 +5771,6 @@ function ToolGrid(props) {
4581
5771
  group
4582
5772
  );
4583
5773
  }),
4584
- chord?.activeGroup && activeGroupTools && /* @__PURE__ */ jsxRuntime.jsxs(
4585
- "div",
4586
- {
4587
- "data-testid": "chord-hint",
4588
- className: "mt-1 rounded border border-emerald-200 bg-emerald-50/60 px-2 py-1 text-[11px] leading-snug text-slate-600",
4589
- children: [
4590
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono font-semibold text-emerald-700", children: chord.letterForGroup(chord.activeGroup) }),
4591
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mx-1 text-slate-400", children: "\u2192" }),
4592
- activeGroupTools.map((t, i) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "mr-2 inline-block", children: [
4593
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono font-semibold text-emerald-700", children: i + 1 }),
4594
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-1", children: t.label })
4595
- ] }, t.key)),
4596
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-slate-400", children: "Esc hu\u1EF7" })
4597
- ]
4598
- }
4599
- ),
4600
5774
  portalReady && hover && typeof document !== "undefined" ? reactDom.createPortal(
4601
5775
  /* @__PURE__ */ jsxRuntime.jsxs(
4602
5776
  "div",
@@ -4643,9 +5817,9 @@ function StampLeftPanelDesktop(props) {
4643
5817
  objects,
4644
5818
  tabs
4645
5819
  } = props;
4646
- const [tab, setTab] = React.useState("tools");
5820
+ const [tab, setTab] = React5.useState("tools");
4647
5821
  const hasObjects = !!objects;
4648
- React.useEffect(() => {
5822
+ React5.useEffect(() => {
4649
5823
  if (!hasObjects && tab === "objects") setTab("tools");
4650
5824
  }, [hasObjects, tab]);
4651
5825
  const tabSpecs = hasObjects ? [
@@ -4663,6 +5837,8 @@ function StampLeftPanelDesktop(props) {
4663
5837
  tabs: tabSpecs,
4664
5838
  activeTab: hasObjects ? tab : void 0,
4665
5839
  onTabChange: hasObjects ? setTab : void 0,
5840
+ resizable: true,
5841
+ widthStorageKey: "xom11.stamp-left-panel.width",
4666
5842
  children: !hasObjects || tab === "tools" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4667
5843
  /* @__PURE__ */ jsxRuntime.jsx(AxisGridSection, { view, history }),
4668
5844
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4724,9 +5900,9 @@ function MobileToolDrawer({
4724
5900
  testId,
4725
5901
  objectsTab
4726
5902
  }) {
4727
- const [mobileTab, setMobileTab] = React__namespace.default.useState("tools");
4728
- const prevOpenRef = React__namespace.default.useRef(drawerOpen);
4729
- React__namespace.default.useEffect(() => {
5903
+ const [mobileTab, setMobileTab] = React5__namespace.default.useState("tools");
5904
+ const prevOpenRef = React5__namespace.default.useRef(drawerOpen);
5905
+ React5__namespace.default.useEffect(() => {
4730
5906
  if (!prevOpenRef.current && drawerOpen) setMobileTab("tools");
4731
5907
  prevOpenRef.current = drawerOpen;
4732
5908
  }, [drawerOpen]);
@@ -4932,7 +6108,7 @@ function StampLeftPanelMobile(props) {
4932
6108
  drawerOpen,
4933
6109
  onDrawerClose
4934
6110
  } = props;
4935
- const groups = React.useMemo(() => {
6111
+ const groups = React5.useMemo(() => {
4936
6112
  const acc = /* @__PURE__ */ new Map();
4937
6113
  for (const t of tools) {
4938
6114
  if (!acc.has(t.group)) acc.set(t.group, []);
@@ -5122,11 +6298,11 @@ function readMatch(query) {
5122
6298
  }
5123
6299
  }
5124
6300
  function useIsMobile() {
5125
- const [state, setState] = React.useState(() => ({
6301
+ const [state, setState] = React5.useState(() => ({
5126
6302
  isMobile: readMatch(MOBILE_QUERY),
5127
6303
  isTouchOnly: readMatch(NO_HOVER_QUERY)
5128
6304
  }));
5129
- React.useEffect(() => {
6305
+ React5.useEffect(() => {
5130
6306
  if (typeof window === "undefined" || !window.matchMedia) return;
5131
6307
  const mql = window.matchMedia(MOBILE_QUERY);
5132
6308
  const tql = window.matchMedia(NO_HOVER_QUERY);
@@ -5152,7 +6328,7 @@ var init_useIsMobile = __esm({
5152
6328
  }
5153
6329
  });
5154
6330
  function useStampStore(domain, editingElement, parseInitial) {
5155
- const ref = React.useRef(null);
6331
+ const ref = React5.useRef(null);
5156
6332
  if (!ref.current) {
5157
6333
  const initial = editingElement?.customData ? parseInitial(editingElement.customData) ?? createEmptyState(domain) : createEmptyState(domain);
5158
6334
  ref.current = createStore(initial);
@@ -5251,9 +6427,9 @@ var init_tools = __esm({
5251
6427
  }
5252
6428
  });
5253
6429
  function FunctionRow({ obj, store, selected, onClick }) {
5254
- const [local, setLocal] = React.useState(obj.attrs.expression);
5255
- const [error, setError] = React.useState(null);
5256
- React.useEffect(() => {
6430
+ const [local, setLocal] = React5.useState(obj.attrs.expression);
6431
+ const [error, setError] = React5.useState(null);
6432
+ React5.useEffect(() => {
5257
6433
  setLocal(obj.attrs.expression);
5258
6434
  setError(null);
5259
6435
  }, [obj.attrs.expression]);
@@ -5474,35 +6650,35 @@ var init_host = __esm({
5474
6650
  /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 3 L3 21 L21 21" }),
5475
6651
  /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 14 Q9 8 12 10 Q15 12 18 6" })
5476
6652
  ] });
5477
- Graph2DStampHost = React.forwardRef(
6653
+ Graph2DStampHost = React5.forwardRef(
5478
6654
  function Graph2DStampHost2({ api, editingElement, onClose, isDark }, ref) {
5479
- const panelRef = React.useRef(null);
6655
+ const panelRef = React5.useRef(null);
5480
6656
  const { isMobile } = useIsMobile();
5481
- const [drawerOpen, setDrawerOpen] = React.useState(false);
6657
+ const [drawerOpen, setDrawerOpen] = React5.useState(false);
5482
6658
  const sceneStore = useStampStore("graph2d", editingElement, parseInitialState);
5483
- const [selectedObjectId, setSelectedObjectId] = React.useState(void 0);
6659
+ const [selectedObjectId, setSelectedObjectId] = React5.useState(void 0);
5484
6660
  const initialMeta = sceneStore.getState().meta;
5485
6661
  const initialView = initialMeta.domain === "graph2d" ? initialMeta.view : null;
5486
- const [selectedTool, setSelectedTool] = React.useState("move");
5487
- const [showAxis, setShowAxisState] = React.useState(initialView?.showAxis ?? true);
5488
- const [showGrid, setShowGridState] = React.useState(initialView?.showGrid ?? true);
5489
- const [canUndo, setCanUndo] = React.useState(false);
5490
- const [canRedo, setCanRedo] = React.useState(false);
5491
- const handleHistoryChange = React.useCallback((u, r) => {
6662
+ const [selectedTool, setSelectedTool] = React5.useState("move");
6663
+ const [showAxis, setShowAxisState] = React5.useState(initialView?.showAxis ?? true);
6664
+ const [showGrid, setShowGridState] = React5.useState(initialView?.showGrid ?? true);
6665
+ const [canUndo, setCanUndo] = React5.useState(false);
6666
+ const [canRedo, setCanRedo] = React5.useState(false);
6667
+ const handleHistoryChange = React5.useCallback((u, r) => {
5492
6668
  setCanUndo(u);
5493
6669
  setCanRedo(r);
5494
6670
  }, []);
5495
- const handleUndo = React.useCallback(() => sceneStore.undo(), [sceneStore]);
5496
- const handleRedo = React.useCallback(() => sceneStore.redo(), [sceneStore]);
5497
- const handleShowAxisChange = React.useCallback((b) => {
6671
+ const handleUndo = React5.useCallback(() => sceneStore.undo(), [sceneStore]);
6672
+ const handleRedo = React5.useCallback(() => sceneStore.redo(), [sceneStore]);
6673
+ const handleShowAxisChange = React5.useCallback((b) => {
5498
6674
  setShowAxisState(b);
5499
6675
  sceneStore.dispatch({ type: "UPDATE_VIEW", payload: { patch: { showAxis: b } } });
5500
6676
  }, [sceneStore]);
5501
- const handleShowGridChange = React.useCallback((b) => {
6677
+ const handleShowGridChange = React5.useCallback((b) => {
5502
6678
  setShowGridState(b);
5503
6679
  sceneStore.dispatch({ type: "UPDATE_VIEW", payload: { patch: { showGrid: b } } });
5504
6680
  }, [sceneStore]);
5505
- const handleAddFunction = React.useCallback(() => {
6681
+ const handleAddFunction = React5.useCallback(() => {
5506
6682
  const existing = Object.values(sceneStore.getState().objects).filter((o) => o.kind === "function2d");
5507
6683
  const id = `f${existing.length + 1}`;
5508
6684
  sceneStore.dispatch({
@@ -5521,7 +6697,7 @@ var init_host = __esm({
5521
6697
  }
5522
6698
  });
5523
6699
  }, [sceneStore]);
5524
- const handleAddParameter = React.useCallback(() => {
6700
+ const handleAddParameter = React5.useCallback(() => {
5525
6701
  const existing = Object.values(sceneStore.getState().objects).filter((o) => o.kind === "parameter");
5526
6702
  const labels = "abcdefghijklmnopqrstuvwxyz";
5527
6703
  const usedLabels = new Set(existing.map((o) => o.label));
@@ -5549,7 +6725,7 @@ var init_host = __esm({
5549
6725
  }
5550
6726
  });
5551
6727
  }, [sceneStore]);
5552
- const handleInsert = React.useCallback(
6728
+ const handleInsert = React5.useCallback(
5553
6729
  async (jsonState, svgString) => {
5554
6730
  if (!api) return;
5555
6731
  try {
@@ -5569,7 +6745,7 @@ var init_host = __esm({
5569
6745
  },
5570
6746
  [api, editingElement?.id, onClose]
5571
6747
  );
5572
- React.useImperativeHandle(
6748
+ React5.useImperativeHandle(
5573
6749
  ref,
5574
6750
  () => ({
5575
6751
  tryInsert: () => panelRef.current?.insert() ?? false,
@@ -5577,7 +6753,7 @@ var init_host = __esm({
5577
6753
  }),
5578
6754
  []
5579
6755
  );
5580
- const renderRow = React.useMemo(() => makeRenderRow(sceneStore), [sceneStore]);
6756
+ const renderRow = React5.useMemo(() => makeRenderRow(sceneStore), [sceneStore]);
5581
6757
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5582
6758
  /* @__PURE__ */ jsxRuntime.jsx(
5583
6759
  StampLeftPanel,
@@ -5655,7 +6831,7 @@ init_render();
5655
6831
  init_types();
5656
6832
  init_serialize();
5657
6833
  init_svgToStampFile();
5658
- var Graph2DStampHost3 = React.lazy(
6834
+ var Graph2DStampHost3 = React5.lazy(
5659
6835
  () => Promise.resolve().then(() => (init_host(), host_exports)).then((m) => ({ default: m.Graph2DStampHost }))
5660
6836
  );
5661
6837
  var Graph2DIcon = /* @__PURE__ */ jsxRuntime.jsxs(