@xom11/whiteboard 0.24.2 → 0.27.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 (103) hide show
  1. package/README.md +84 -11
  2. package/dist/{ExcalidrawWithMenus-WENZRYYE.mjs → ExcalidrawWithMenus-2QPPTXJM.mjs} +3 -2
  3. package/dist/ExcalidrawWithMenus-2QPPTXJM.mjs.map +1 -0
  4. package/dist/ai.d.mts +3217 -434
  5. package/dist/ai.d.ts +3217 -434
  6. package/dist/ai.js +7679 -598
  7. package/dist/ai.js.map +1 -1
  8. package/dist/ai.mjs +5707 -679
  9. package/dist/ai.mjs.map +1 -1
  10. package/dist/catalog.json +5 -5
  11. package/dist/{chunk-7WQXXEVR.mjs → chunk-4ETJ4CDY.mjs} +5 -5
  12. package/dist/{chunk-7WQXXEVR.mjs.map → chunk-4ETJ4CDY.mjs.map} +1 -1
  13. package/dist/chunk-AJAHD35N.mjs +1708 -0
  14. package/dist/chunk-AJAHD35N.mjs.map +1 -0
  15. package/dist/chunk-AYJPOHCI.mjs +265 -0
  16. package/dist/chunk-AYJPOHCI.mjs.map +1 -0
  17. package/dist/chunk-B4NJJZFR.mjs +18 -0
  18. package/dist/chunk-B4NJJZFR.mjs.map +1 -0
  19. package/dist/{chunk-AZIARTGX.mjs → chunk-BNBOIDO5.mjs} +3 -3
  20. package/dist/{chunk-AZIARTGX.mjs.map → chunk-BNBOIDO5.mjs.map} +1 -1
  21. package/dist/{chunk-LVNCYP4J.mjs → chunk-CXHNVYMD.mjs} +5 -5
  22. package/dist/{chunk-LVNCYP4J.mjs.map → chunk-CXHNVYMD.mjs.map} +1 -1
  23. package/dist/{chunk-45CGKJ7S.mjs → chunk-D5JLJ3PT.mjs} +4 -4
  24. package/dist/{chunk-45CGKJ7S.mjs.map → chunk-D5JLJ3PT.mjs.map} +1 -1
  25. package/dist/{chunk-WM2VDYQA.mjs → chunk-D5LWSN2Y.mjs} +944 -196
  26. package/dist/chunk-D5LWSN2Y.mjs.map +1 -0
  27. package/dist/{chunk-KRC2XOIG.mjs → chunk-HLAOGXEK.mjs} +3 -3
  28. package/dist/{chunk-KRC2XOIG.mjs.map → chunk-HLAOGXEK.mjs.map} +1 -1
  29. package/dist/{chunk-2WF6KIGF.mjs → chunk-I3L56GVH.mjs} +212 -71
  30. package/dist/chunk-I3L56GVH.mjs.map +1 -0
  31. package/dist/{chunk-ZBJBQKJ2.mjs → chunk-IHUFOV7L.mjs} +4 -19
  32. package/dist/chunk-IHUFOV7L.mjs.map +1 -0
  33. package/dist/chunk-J5LGTIGS.mjs +10 -0
  34. package/dist/chunk-J5LGTIGS.mjs.map +1 -0
  35. package/dist/{chunk-BEZSQKPY.mjs → chunk-KYMBUTPO.mjs} +5 -4
  36. package/dist/chunk-KYMBUTPO.mjs.map +1 -0
  37. package/dist/{chunk-4DS3MKID.mjs → chunk-KZGPSTZI.mjs} +4 -4
  38. package/dist/{chunk-4DS3MKID.mjs.map → chunk-KZGPSTZI.mjs.map} +1 -1
  39. package/dist/{chunk-SGFJLHHG.mjs → chunk-PPKHCRRE.mjs} +3 -3
  40. package/dist/{chunk-SGFJLHHG.mjs.map → chunk-PPKHCRRE.mjs.map} +1 -1
  41. package/dist/{chunk-BKSXPNPQ.mjs → chunk-SZDAS7LK.mjs} +81 -3
  42. package/dist/chunk-SZDAS7LK.mjs.map +1 -0
  43. package/dist/chunk-T3SOHYB2.mjs +851 -0
  44. package/dist/chunk-T3SOHYB2.mjs.map +1 -0
  45. package/dist/geometry-2d.d.mts +2 -2
  46. package/dist/geometry-2d.d.ts +2 -2
  47. package/dist/geometry-2d.js +6288 -901
  48. package/dist/geometry-2d.js.map +1 -1
  49. package/dist/geometry-2d.mjs +7 -5
  50. package/dist/geometry-3d.d.mts +2 -2
  51. package/dist/geometry-3d.d.ts +2 -2
  52. package/dist/geometry-3d.js +1335 -253
  53. package/dist/geometry-3d.js.map +1 -1
  54. package/dist/geometry-3d.mjs +6 -4
  55. package/dist/graph-2d.d.mts +2 -2
  56. package/dist/graph-2d.d.ts +2 -2
  57. package/dist/graph-2d.js +1501 -342
  58. package/dist/graph-2d.js.map +1 -1
  59. package/dist/graph-2d.mjs +9 -7
  60. package/dist/handleExtractProblem-C-U5KluK.d.mts +158 -0
  61. package/dist/handleExtractProblem-C-U5KluK.d.ts +158 -0
  62. package/dist/{host-EPZCNFLH.mjs → host-HAYCJJ2T.mjs} +1390 -376
  63. package/dist/host-HAYCJJ2T.mjs.map +1 -0
  64. package/dist/{host-LKCMYEAV.mjs → host-LTJHAY5A.mjs} +12 -10
  65. package/dist/host-LTJHAY5A.mjs.map +1 -0
  66. package/dist/{host-ZIQ77W33.mjs → host-M26FS244.mjs} +8 -6
  67. package/dist/host-M26FS244.mjs.map +1 -0
  68. package/dist/{host-QS2EOTRJ.mjs → host-ZQCDAT6O.mjs} +3 -2
  69. package/dist/host-ZQCDAT6O.mjs.map +1 -0
  70. package/dist/index.d.mts +4 -3
  71. package/dist/index.d.ts +4 -3
  72. package/dist/index.js +6493 -1102
  73. package/dist/index.js.map +1 -1
  74. package/dist/index.mjs +24 -21
  75. package/dist/index.mjs.map +1 -1
  76. package/dist/latex.d.mts +2 -2
  77. package/dist/latex.d.ts +2 -2
  78. package/dist/latex.mjs +2 -1
  79. package/dist/render-ZX2O2IK7.mjs +10 -0
  80. package/dist/{render-SA4JTOW3.mjs.map → render-ZX2O2IK7.mjs.map} +1 -1
  81. package/dist/serialize-C3LSUMSA.mjs +9 -0
  82. package/dist/{serialize-JAVOU22E.mjs.map → serialize-C3LSUMSA.mjs.map} +1 -1
  83. package/dist/types-zc_Pa0mp.d.mts +418 -0
  84. package/dist/types-zc_Pa0mp.d.ts +418 -0
  85. package/package.json +10 -1
  86. package/dist/ExcalidrawWithMenus-WENZRYYE.mjs.map +0 -1
  87. package/dist/chunk-2WF6KIGF.mjs.map +0 -1
  88. package/dist/chunk-BEZSQKPY.mjs.map +0 -1
  89. package/dist/chunk-BKSXPNPQ.mjs.map +0 -1
  90. package/dist/chunk-CGZZO4BX.mjs +0 -96
  91. package/dist/chunk-CGZZO4BX.mjs.map +0 -1
  92. package/dist/chunk-WM2VDYQA.mjs.map +0 -1
  93. package/dist/chunk-ZBJBQKJ2.mjs.map +0 -1
  94. package/dist/host-EPZCNFLH.mjs.map +0 -1
  95. package/dist/host-LKCMYEAV.mjs.map +0 -1
  96. package/dist/host-QS2EOTRJ.mjs.map +0 -1
  97. package/dist/host-ZIQ77W33.mjs.map +0 -1
  98. package/dist/render-SA4JTOW3.mjs +0 -8
  99. package/dist/serialize-JAVOU22E.mjs +0 -7
  100. package/dist/types-Crbefnfe.d.ts +0 -128
  101. package/dist/types-DxlMPh-6.d.mts +0 -49
  102. package/dist/types-DxlMPh-6.d.ts +0 -49
  103. package/dist/types-vtvyKGAA.d.mts +0 -128
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,10 +2975,22 @@ 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) => {
@@ -2174,13 +3005,66 @@ var init_circle = __esm({
2174
3005
  visible: obj.visible,
2175
3006
  fixed: obj.locked
2176
3007
  };
2177
- const c = obj.attrs.construction;
3008
+ const c = asConstruction(obj.attrs);
2178
3009
  if (c?.kind === "circumscribed") {
2179
3010
  const p1 = ctx.resolveRef(c.p1);
2180
3011
  const p2 = ctx.resolveRef(c.p2);
2181
3012
  const p3 = ctx.resolveRef(c.p3);
2182
3013
  return board.create("circumcircle", [p1, p2, p3], baseOpts);
2183
3014
  }
3015
+ if (c?.kind === "incircle") {
3016
+ const p1 = ctx.resolveRef(c.p1);
3017
+ const p2 = ctx.resolveRef(c.p2);
3018
+ const p3 = ctx.resolveRef(c.p3);
3019
+ const center2 = board.create("incenter", [p1, p2, p3], {
3020
+ visible: obj.visible,
3021
+ withLabel: true,
3022
+ fixed: true,
3023
+ name: obj.label
3024
+ });
3025
+ const circ = board.create("incircle", [p1, p2, p3], baseOpts);
3026
+ circ.center = circ.center ?? center2;
3027
+ circ._helpers = [center2];
3028
+ return circ;
3029
+ }
3030
+ if (c?.kind === "excircle") {
3031
+ const P = [ctx.resolveRef(c.p1), ctx.resolveRef(c.p2), ctx.resolveRef(c.p3)];
3032
+ const ids = [c.p1, c.p2, c.p3];
3033
+ const oppIdx = Math.max(0, ids.indexOf(c.opposite));
3034
+ const verts = () => [
3035
+ [P[0].X(), P[0].Y()],
3036
+ [P[1].X(), P[1].Y()],
3037
+ [P[2].X(), P[2].Y()]
3038
+ ];
3039
+ const ctr = () => excenter(verts(), oppIdx);
3040
+ const radius = () => {
3041
+ const I = ctr();
3042
+ const others = [0, 1, 2].filter((i) => i !== oppIdx);
3043
+ const v = verts();
3044
+ const a = v[others[0]];
3045
+ const b = v[others[1]];
3046
+ const dx = b[0] - a[0];
3047
+ const dy = b[1] - a[1];
3048
+ const len = Math.hypot(dx, dy) || 1;
3049
+ return Math.abs((I[0] - a[0]) * dy - (I[1] - a[1]) * dx) / len;
3050
+ };
3051
+ const center2 = board.create("point", [() => ctr()[0], () => ctr()[1]], { visible: false, withLabel: false, fixed: true, name: "" });
3052
+ const circ = board.create("circle", [center2, () => radius()], baseOpts);
3053
+ circ._helpers = [center2];
3054
+ return circ;
3055
+ }
3056
+ if (c?.kind === "diameter") {
3057
+ const p1 = ctx.resolveRef(c.p1);
3058
+ const p2 = ctx.resolveRef(c.p2);
3059
+ const center2 = board.create("midpoint", [p1, p2], { visible: false, withLabel: false, fixed: true, name: "" });
3060
+ const circ = board.create("circle", [center2, p2], baseOpts);
3061
+ circ._helpers = [center2];
3062
+ return circ;
3063
+ }
3064
+ if (typeof obj.attrs.radius === "number") {
3065
+ const center2 = ctx.resolveRef(obj.attrs.center);
3066
+ return board.create("circle", [center2, obj.attrs.radius], baseOpts);
3067
+ }
2184
3068
  const center = ctx.resolveRef(obj.attrs.center);
2185
3069
  const surface = ctx.resolveRef(obj.attrs.surfacePoint);
2186
3070
  return board.create("circle", [center, surface], baseOpts);
@@ -2340,6 +3224,26 @@ function regularVertexLabels(p1Label, p2Label, n) {
2340
3224
  }
2341
3225
  return `${p1Label}${p2Label}\u2026`;
2342
3226
  }
3227
+ function specialShapeName(kind) {
3228
+ switch (kind) {
3229
+ case "square":
3230
+ return "H\xECnh vu\xF4ng";
3231
+ case "rectangle":
3232
+ return "H\xECnh ch\u1EEF nh\u1EADt";
3233
+ case "rhombus":
3234
+ return "H\xECnh thoi";
3235
+ case "parallelogram":
3236
+ return "H\xECnh b\xECnh h\xE0nh";
3237
+ case "isoTrapezoid":
3238
+ return "H\xECnh thang c\xE2n";
3239
+ case "isoTriangle":
3240
+ return "Tam gi\xE1c c\xE2n";
3241
+ case "rightTriangle":
3242
+ return "Tam gi\xE1c vu\xF4ng";
3243
+ case "regular":
3244
+ return "";
3245
+ }
3246
+ }
2343
3247
  var def11;
2344
3248
  var init_polygon = __esm({
2345
3249
  "src/core/scene/kinds/polygon.ts"() {
@@ -2351,13 +3255,27 @@ var init_polygon = __esm({
2351
3255
  migrate: {},
2352
3256
  validate: (a) => {
2353
3257
  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
- }
3258
+ const c = a.construction;
3259
+ if (c.kind === "regular") {
3260
+ if (!c.p1 || !c.p2) throw new Error("polygon (regular): p1 v\xE0 p2 b\u1EAFt bu\u1ED9c");
3261
+ if (!Number.isFinite(c.n) || c.n < 3) throw new Error("polygon (regular): n \u2265 3");
3262
+ return;
3263
+ }
3264
+ if (c.kind === "square") {
3265
+ if (!c.p1 || !c.p2) throw new Error("polygon (square): p1 v\xE0 p2 b\u1EAFt bu\u1ED9c");
3266
+ return;
3267
+ }
3268
+ if (c.kind === "rectangle" || c.kind === "rhombus" || c.kind === "parallelogram" || c.kind === "isoTrapezoid") {
3269
+ if (!c.p1 || !c.p2 || !c.p3) throw new Error(`polygon (${c.kind}): p1, p2, p3 b\u1EAFt bu\u1ED9c`);
3270
+ return;
3271
+ }
3272
+ if (c.kind === "isoTriangle") {
3273
+ if (!c.base1 || !c.base2 || !c.apex) throw new Error("polygon (isoTriangle): base1, base2, apex b\u1EAFt bu\u1ED9c");
3274
+ return;
3275
+ }
3276
+ if (c.kind === "rightTriangle") {
3277
+ if (!c.rightAngle || !c.leg1End || !c.leg2End) throw new Error("polygon (rightTriangle): rightAngle, leg1End, leg2End b\u1EAFt bu\u1ED9c");
3278
+ return;
2361
3279
  }
2362
3280
  return;
2363
3281
  }
@@ -2366,14 +3284,51 @@ var init_polygon = __esm({
2366
3284
  }
2367
3285
  },
2368
3286
  dependsOn: (a) => {
2369
- if (a.construction?.kind === "regular") return [a.construction.p1, a.construction.p2];
2370
- return [...a.vertices ?? []];
3287
+ const c = a.construction;
3288
+ if (!c) return [...a.vertices ?? []];
3289
+ switch (c.kind) {
3290
+ case "regular":
3291
+ return [c.p1, c.p2];
3292
+ case "square":
3293
+ return [c.p1, c.p2];
3294
+ case "rectangle":
3295
+ case "rhombus":
3296
+ case "parallelogram":
3297
+ case "isoTrapezoid":
3298
+ return [c.p1, c.p2, c.p3];
3299
+ case "isoTriangle":
3300
+ return [c.base1, c.base2, c.apex];
3301
+ case "rightTriangle":
3302
+ return [c.rightAngle, c.leg1End, c.leg2End];
3303
+ }
2371
3304
  },
2372
3305
  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}`;
3306
+ const c = obj.attrs.construction;
3307
+ if (c) {
3308
+ if (c.kind === "regular") {
3309
+ const labels2 = regularVertexLabels(labelOf(c.p1, state), labelOf(c.p2, state), c.n);
3310
+ return `${regularPolygonName(c.n)} ${labels2}`;
3311
+ }
3312
+ const name = specialShapeName(c.kind);
3313
+ let labels = [];
3314
+ switch (c.kind) {
3315
+ case "square":
3316
+ labels = [labelOf(c.p1, state), labelOf(c.p2, state)];
3317
+ break;
3318
+ case "rectangle":
3319
+ case "rhombus":
3320
+ case "parallelogram":
3321
+ case "isoTrapezoid":
3322
+ labels = [labelOf(c.p1, state), labelOf(c.p2, state), labelOf(c.p3, state)];
3323
+ break;
3324
+ case "isoTriangle":
3325
+ labels = [labelOf(c.apex, state), labelOf(c.base1, state), labelOf(c.base2, state)];
3326
+ break;
3327
+ case "rightTriangle":
3328
+ labels = [labelOf(c.rightAngle, state), labelOf(c.leg1End, state), labelOf(c.leg2End, state)];
3329
+ break;
3330
+ }
3331
+ return `${name} ${labels.join("")}`;
2377
3332
  }
2378
3333
  return `\u0110a gi\xE1c ${(obj.attrs.vertices ?? []).map((id) => labelOf(id, state)).join("")}`;
2379
3334
  },
@@ -2381,22 +3336,85 @@ var init_polygon = __esm({
2381
3336
  const board = ctx.jxg;
2382
3337
  const label = obj.label;
2383
3338
  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
3339
+ const cons = obj.attrs.construction;
3340
+ const commonAttrs = {
3341
+ name: label,
3342
+ withLabel: obj.attrs.showLabel ?? false,
3343
+ borders: {
3344
+ strokeColor: obj.attrs.color ?? "#0f172a",
3345
+ strokeWidth: obj.attrs.width ?? 2
3346
+ },
3347
+ fillColor: obj.attrs.color ?? "#60a5fa",
3348
+ fillOpacity: obj.attrs.fillOpacity ?? 0.15,
3349
+ visible: obj.visible,
3350
+ fixed: obj.locked
3351
+ };
3352
+ if (cons?.kind === "regular") {
3353
+ const p1 = ctx.resolveRef(cons.p1);
3354
+ const p2 = ctx.resolveRef(cons.p2);
3355
+ return board.create("regularpolygon", [p1, p2, cons.n], commonAttrs);
3356
+ }
3357
+ if (cons?.kind === "square") {
3358
+ const p1 = ctx.resolveRef(cons.p1);
3359
+ const p2 = ctx.resolveRef(cons.p2);
3360
+ return board.create("regularpolygon", [p1, p2, 4], commonAttrs);
3361
+ }
3362
+ if (cons?.kind === "rectangle" || cons?.kind === "rhombus" || cons?.kind === "parallelogram") {
3363
+ const A = ctx.resolveRef(cons.p1);
3364
+ const B = ctx.resolveRef(cons.p2);
3365
+ const C = ctx.resolveRef(cons.p3);
3366
+ const D = board.create(
3367
+ "point",
3368
+ [() => A.X() + C.X() - B.X(), () => A.Y() + C.Y() - B.Y()],
3369
+ { visible: false, withLabel: false, fixed: true, name: "" }
3370
+ );
3371
+ const poly2 = board.create("polygon", [A, B, C, D], commonAttrs);
3372
+ poly2._helpers = [D];
3373
+ return poly2;
3374
+ }
3375
+ if (cons?.kind === "isoTrapezoid") {
3376
+ const A = ctx.resolveRef(cons.p1);
3377
+ const B = ctx.resolveRef(cons.p2);
3378
+ const C = ctx.resolveRef(cons.p3);
3379
+ const Dx = () => {
3380
+ const Mx = (A.X() + B.X()) / 2;
3381
+ const My = (A.Y() + B.Y()) / 2;
3382
+ const ux = B.X() - A.X();
3383
+ const uy = B.Y() - A.Y();
3384
+ const len2 = ux * ux + uy * uy || 1;
3385
+ const proj = ((C.X() - Mx) * ux + (C.Y() - My) * uy) / len2;
3386
+ return C.X() - 2 * proj * ux;
3387
+ };
3388
+ const Dy = () => {
3389
+ const Mx = (A.X() + B.X()) / 2;
3390
+ const My = (A.Y() + B.Y()) / 2;
3391
+ const ux = B.X() - A.X();
3392
+ const uy = B.Y() - A.Y();
3393
+ const len2 = ux * ux + uy * uy || 1;
3394
+ const proj = ((C.X() - Mx) * ux + (C.Y() - My) * uy) / len2;
3395
+ return C.Y() - 2 * proj * uy;
3396
+ };
3397
+ const D = board.create("point", [Dx, Dy], {
3398
+ visible: false,
3399
+ withLabel: false,
3400
+ fixed: true,
3401
+ name: ""
2399
3402
  });
3403
+ const poly2 = board.create("polygon", [A, B, C, D], commonAttrs);
3404
+ poly2._helpers = [D];
3405
+ return poly2;
3406
+ }
3407
+ if (cons?.kind === "isoTriangle") {
3408
+ const Apex = ctx.resolveRef(cons.apex);
3409
+ const B1 = ctx.resolveRef(cons.base1);
3410
+ const B2 = ctx.resolveRef(cons.base2);
3411
+ return board.create("polygon", [Apex, B1, B2], commonAttrs);
3412
+ }
3413
+ if (cons?.kind === "rightTriangle") {
3414
+ const R = ctx.resolveRef(cons.rightAngle);
3415
+ const P = ctx.resolveRef(cons.leg1End);
3416
+ const Q = ctx.resolveRef(cons.leg2End);
3417
+ return board.create("polygon", [R, P, Q], commonAttrs);
2400
3418
  }
2401
3419
  const verts = (obj.attrs.vertices ?? []).map((id) => ctx.resolveRef(id));
2402
3420
  const poly = board.create("polygon", verts, {
@@ -3324,21 +4342,21 @@ var init_handlers = __esm({
3324
4342
  }
3325
4343
  });
3326
4344
  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) => {
4345
+ const [tool, setToolState] = React5.useState(initial);
4346
+ const [pendingIds, setPendingIds] = React5.useState([]);
4347
+ const toolRef = React5.useRef(initial);
4348
+ const pendingIdsRef = React5.useRef([]);
4349
+ const setTool = React5.useCallback((t) => {
3332
4350
  toolRef.current = t;
3333
4351
  pendingIdsRef.current = [];
3334
4352
  setToolState(t);
3335
4353
  setPendingIds([]);
3336
4354
  }, []);
3337
- const pushPending = React.useCallback((id) => {
4355
+ const pushPending = React5.useCallback((id) => {
3338
4356
  pendingIdsRef.current = [...pendingIdsRef.current, id];
3339
4357
  setPendingIds(pendingIdsRef.current);
3340
4358
  }, []);
3341
- const clearPending = React.useCallback(() => {
4359
+ const clearPending = React5.useCallback(() => {
3342
4360
  pendingIdsRef.current = [];
3343
4361
  setPendingIds([]);
3344
4362
  }, []);
@@ -3507,24 +4525,24 @@ var init_MiniBoard = __esm({
3507
4525
  init_safeJsx();
3508
4526
  init_attachJxgWheelZoom();
3509
4527
  init_initJxgBoard();
3510
- MiniBoard = React__namespace.default.forwardRef(
4528
+ MiniBoard = React5__namespace.default.forwardRef(
3511
4529
  function MiniBoard2({ store, selectedTool, showAxis, showGrid, isDark, onReady, onSelectionChange: _onSelectionChange }, ref) {
3512
- const isDarkRef = React.useRef(!!isDark);
4530
+ const isDarkRef = React5.useRef(!!isDark);
3513
4531
  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);
4532
+ const containerId = React5.useId().replace(/:/g, "_") + "_graph_jxg";
4533
+ const containerRef = React5.useRef(null);
4534
+ const boardRef = React5.useRef(null);
4535
+ const jxgRef = React5.useRef(null);
4536
+ const rendererRef = React5.useRef(null);
3519
4537
  const toolSM = useToolStateMachine(selectedTool);
3520
- const showAxisRef = React.useRef(showAxis);
4538
+ const showAxisRef = React5.useRef(showAxis);
3521
4539
  showAxisRef.current = showAxis;
3522
- const showGridRef = React.useRef(showGrid);
4540
+ const showGridRef = React5.useRef(showGrid);
3523
4541
  showGridRef.current = showGrid;
3524
- React.useEffect(() => {
4542
+ React5.useEffect(() => {
3525
4543
  if (toolSM.toolRef.current !== selectedTool) toolSM.setTool(selectedTool);
3526
4544
  }, [selectedTool]);
3527
- React.useEffect(() => {
4545
+ React5.useEffect(() => {
3528
4546
  if (typeof window === "undefined" || !containerRef.current) return;
3529
4547
  let cancelled = false;
3530
4548
  let wheelCleanup = null;
@@ -3610,7 +4628,7 @@ var init_MiniBoard = __esm({
3610
4628
  boardRef.current = null;
3611
4629
  };
3612
4630
  }, [containerId]);
3613
- React.useImperativeHandle(
4631
+ React5.useImperativeHandle(
3614
4632
  ref,
3615
4633
  () => ({
3616
4634
  getState: () => store.getState(),
@@ -3656,27 +4674,27 @@ function reducer(state, action) {
3656
4674
  }
3657
4675
  }
3658
4676
  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);
4677
+ const [items, dispatch] = React5.useReducer(reducer, []);
4678
+ const timersRef = React5.useRef(/* @__PURE__ */ new Map());
4679
+ const itemsRef = React5.useRef(items);
3662
4680
  itemsRef.current = items;
3663
- const clearTimer = React.useCallback((id) => {
4681
+ const clearTimer = React5.useCallback((id) => {
3664
4682
  const t = timersRef.current.get(id);
3665
4683
  if (t) {
3666
4684
  clearTimeout(t);
3667
4685
  timersRef.current.delete(id);
3668
4686
  }
3669
4687
  }, []);
3670
- const dismiss = React.useCallback((id) => {
4688
+ const dismiss = React5.useCallback((id) => {
3671
4689
  clearTimer(id);
3672
4690
  dispatch({ type: "DISMISS", id });
3673
4691
  }, [clearTimer]);
3674
- const scheduleAutoDismiss = React.useCallback((id, duration) => {
4692
+ const scheduleAutoDismiss = React5.useCallback((id, duration) => {
3675
4693
  if (duration <= 0) return;
3676
4694
  const t = setTimeout(() => dismiss(id), duration);
3677
4695
  timersRef.current.set(id, t);
3678
4696
  }, [dismiss]);
3679
- const showToast = React.useCallback((message, opts = {}) => {
4697
+ const showToast = React5.useCallback((message, opts = {}) => {
3680
4698
  const variant = opts.variant ?? "info";
3681
4699
  const duration = opts.duration ?? 3e3;
3682
4700
  const id = opts.id ?? `toast-${++autoIdCounter}`;
@@ -3690,17 +4708,17 @@ function ToastProvider({ children, maxVisible = 3 }) {
3690
4708
  }
3691
4709
  scheduleAutoDismiss(id, duration);
3692
4710
  }, [clearTimer, maxVisible, scheduleAutoDismiss]);
3693
- React.useEffect(() => () => {
4711
+ React5.useEffect(() => () => {
3694
4712
  timersRef.current.forEach((t) => clearTimeout(t));
3695
4713
  timersRef.current.clear();
3696
4714
  }, []);
3697
- const value = React.useMemo(() => ({ items, showToast, dismiss }), [items, showToast, dismiss]);
4715
+ const value = React5.useMemo(() => ({ items, showToast, dismiss }), [items, showToast, dismiss]);
3698
4716
  return /* @__PURE__ */ jsxRuntime.jsx(ToastContext.Provider, { value, children });
3699
4717
  }
3700
4718
  var ToastContext, autoIdCounter;
3701
4719
  var init_ToastProvider = __esm({
3702
4720
  "src/stamps/shared/Toast/ToastProvider.tsx"() {
3703
- ToastContext = React.createContext(null);
4721
+ ToastContext = React5.createContext(null);
3704
4722
  autoIdCounter = 0;
3705
4723
  }
3706
4724
  });
@@ -3758,7 +4776,7 @@ var init_Toast = __esm({
3758
4776
  }
3759
4777
  });
3760
4778
  function useToast() {
3761
- const ctx = React.useContext(ToastContext);
4779
+ const ctx = React5.useContext(ToastContext);
3762
4780
  if (!ctx) {
3763
4781
  throw new Error("useToast must be used inside <ToastProvider>");
3764
4782
  }
@@ -3803,7 +4821,7 @@ var init_EditorPanel = __esm({
3803
4821
  init_scene();
3804
4822
  init_constants();
3805
4823
  init_Toast2();
3806
- GraphEditorPanelInner = React.forwardRef(
4824
+ GraphEditorPanelInner = React5.forwardRef(
3807
4825
  function GraphEditorPanel({
3808
4826
  store,
3809
4827
  onInsert,
@@ -3822,23 +4840,23 @@ var init_EditorPanel = __esm({
3822
4840
  canUndo,
3823
4841
  canRedo
3824
4842
  }, 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(() => {
4843
+ const miniRef = React5.useRef(null);
4844
+ const [ready, setReady] = React5.useState(false);
4845
+ const [hasContent, setHasContent] = React5.useState(false);
4846
+ const onSelectionChangeRef = React5.useRef(onSelectionChange);
4847
+ React5.useEffect(() => {
3830
4848
  onSelectionChangeRef.current = onSelectionChange;
3831
4849
  }, [onSelectionChange]);
3832
4850
  useEditorState({ store, onHistoryChange });
3833
- React.useEffect(() => {
4851
+ React5.useEffect(() => {
3834
4852
  const sync = () => setHasContent(Object.keys(store.getState().objects).length > 0);
3835
4853
  sync();
3836
4854
  return store.subscribe(sync);
3837
4855
  }, [store]);
3838
- const handleReady = React.useCallback(() => {
4856
+ const handleReady = React5.useCallback(() => {
3839
4857
  setReady(true);
3840
4858
  }, []);
3841
- const performInsert = React.useCallback(() => {
4859
+ const performInsert = React5.useCallback(() => {
3842
4860
  const h = miniRef.current;
3843
4861
  if (!h) return false;
3844
4862
  const state = h.getState();
@@ -3856,7 +4874,7 @@ var init_EditorPanel = __esm({
3856
4874
  })();
3857
4875
  return true;
3858
4876
  }, [isDark, onInsert]);
3859
- React.useImperativeHandle(ref, () => ({
4877
+ React5.useImperativeHandle(ref, () => ({
3860
4878
  insert: performInsert,
3861
4879
  hasContent: () => Object.keys(miniRef.current?.getState().objects ?? {}).length > 0,
3862
4880
  getStore: () => miniRef.current?.getStore() ?? null,
@@ -4014,13 +5032,27 @@ var init_EditorPanel = __esm({
4014
5032
  );
4015
5033
  }
4016
5034
  );
4017
- GraphEditorPanel2 = React.forwardRef(
5035
+ GraphEditorPanel2 = React5.forwardRef(
4018
5036
  function GraphEditorPanel3(props, ref) {
4019
5037
  return /* @__PURE__ */ jsxRuntime.jsx(ToastProvider, { children: /* @__PURE__ */ jsxRuntime.jsx(GraphEditorPanelInner, { ...props, ref }) });
4020
5038
  }
4021
5039
  );
4022
5040
  }
4023
5041
  });
5042
+ function clamp(n, min, max) {
5043
+ return Math.max(min, Math.min(max, n));
5044
+ }
5045
+ function readStoredWidth(key, fallback, min, max) {
5046
+ if (!key || typeof window === "undefined") return fallback;
5047
+ try {
5048
+ const raw = window.localStorage.getItem(key);
5049
+ if (!raw) return fallback;
5050
+ const n = parseInt(raw, 10);
5051
+ if (Number.isFinite(n)) return clamp(n, min, max);
5052
+ } catch {
5053
+ }
5054
+ return fallback;
5055
+ }
4024
5056
  function CloseIcon() {
4025
5057
  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
5058
  /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
@@ -4028,8 +5060,64 @@ function CloseIcon() {
4028
5060
  ] });
4029
5061
  }
4030
5062
  function LeftPanelShell(props) {
4031
- const { title, icon, onClose, isDark, tabs, activeTab, onTabChange, testId, children } = props;
5063
+ const {
5064
+ title,
5065
+ icon,
5066
+ onClose,
5067
+ isDark,
5068
+ tabs,
5069
+ activeTab,
5070
+ onTabChange,
5071
+ testId,
5072
+ resizable,
5073
+ widthStorageKey,
5074
+ defaultWidth,
5075
+ minWidth,
5076
+ maxWidth,
5077
+ children
5078
+ } = props;
4032
5079
  const showTabs = !!tabs && tabs.length >= 2;
5080
+ const min = minWidth ?? FALLBACK_MIN_WIDTH;
5081
+ const max = maxWidth ?? FALLBACK_MAX_WIDTH;
5082
+ const initial = clamp(defaultWidth ?? FALLBACK_DEFAULT_WIDTH, min, max);
5083
+ const [width, setWidth] = React5__namespace.useState(
5084
+ () => resizable ? readStoredWidth(widthStorageKey, initial, min, max) : initial
5085
+ );
5086
+ const widthRef = React5__namespace.useRef(width);
5087
+ widthRef.current = width;
5088
+ React5__namespace.useEffect(() => {
5089
+ if (!resizable || !widthStorageKey || typeof window === "undefined") return;
5090
+ try {
5091
+ window.localStorage.setItem(widthStorageKey, String(width));
5092
+ } catch {
5093
+ }
5094
+ }, [resizable, widthStorageKey, width]);
5095
+ const onResizeStart = React5__namespace.useCallback(
5096
+ (e) => {
5097
+ if (!resizable) return;
5098
+ e.preventDefault();
5099
+ const startX = e.clientX;
5100
+ const startW = widthRef.current;
5101
+ const onMove = (ev) => {
5102
+ setWidth(clamp(startW + (ev.clientX - startX), min, max));
5103
+ };
5104
+ const onUp = () => {
5105
+ window.removeEventListener("mousemove", onMove);
5106
+ window.removeEventListener("mouseup", onUp);
5107
+ document.body.style.cursor = "";
5108
+ document.body.style.userSelect = "";
5109
+ };
5110
+ window.addEventListener("mousemove", onMove);
5111
+ window.addEventListener("mouseup", onUp);
5112
+ document.body.style.cursor = "ew-resize";
5113
+ document.body.style.userSelect = "none";
5114
+ },
5115
+ [resizable, min, max]
5116
+ );
5117
+ const onResizeDoubleClick = React5__namespace.useCallback(() => {
5118
+ if (!resizable) return;
5119
+ setWidth(initial);
5120
+ }, [resizable, initial]);
4033
5121
  return /* @__PURE__ */ jsxRuntime.jsxs(
4034
5122
  "aside",
4035
5123
  {
@@ -4037,10 +5125,12 @@ function LeftPanelShell(props) {
4037
5125
  "aria-label": title,
4038
5126
  "data-testid": testId ?? "left-panel",
4039
5127
  "data-stamp-area": "true",
5128
+ style: resizable ? { width: `${width}px` } : void 0,
4040
5129
  className: [
4041
5130
  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(""),
5131
+ "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",
5132
+ resizable ? "" : "w-60"
5133
+ ].join(" "),
4044
5134
  children: [
4045
5135
  /* @__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
5136
  /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold text-slate-800", children: [
@@ -4075,6 +5165,20 @@ function LeftPanelShell(props) {
4075
5165
  className: "min-h-0 flex-1 overflow-y-auto p-3 space-y-3",
4076
5166
  children
4077
5167
  }
5168
+ ),
5169
+ resizable && /* @__PURE__ */ jsxRuntime.jsx(
5170
+ "div",
5171
+ {
5172
+ role: "separator",
5173
+ "aria-orientation": "vertical",
5174
+ "aria-label": "K\xE9o \u0111\u1EC3 \u0111\u1ED5i r\u1ED9ng panel",
5175
+ "data-testid": "left-panel-resizer",
5176
+ onMouseDown: onResizeStart,
5177
+ onDoubleClick: onResizeDoubleClick,
5178
+ className: "group absolute right-0 top-0 z-40 h-full w-1.5 -mr-0.5 cursor-ew-resize select-none",
5179
+ title: "K\xE9o \u0111\u1EC3 \u0111\u1ED5i r\u1ED9ng (double-click \u0111\u1EC3 reset)",
5180
+ 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" })
5181
+ }
4078
5182
  )
4079
5183
  ]
4080
5184
  }
@@ -4104,9 +5208,13 @@ function Section(props) {
4104
5208
  props.children
4105
5209
  ] });
4106
5210
  }
5211
+ var FALLBACK_DEFAULT_WIDTH, FALLBACK_MIN_WIDTH, FALLBACK_MAX_WIDTH;
4107
5212
  var init_LeftPanelShell = __esm({
4108
5213
  "src/core/scene/ui/LeftPanelShell.tsx"() {
4109
5214
  "use client";
5215
+ FALLBACK_DEFAULT_WIDTH = 240;
5216
+ FALLBACK_MIN_WIDTH = 220;
5217
+ FALLBACK_MAX_WIDTH = 480;
4110
5218
  }
4111
5219
  });
4112
5220
 
@@ -4157,7 +5265,7 @@ var init_kindMeta = __esm({
4157
5265
  });
4158
5266
  function ObjectRowMenu(props) {
4159
5267
  const { locked, onToggleLocked, onRename, onChangeColor, onDelete } = props;
4160
- const [open, setOpen] = React__namespace.useState(false);
5268
+ const [open, setOpen] = React5__namespace.useState(false);
4161
5269
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative inline-block", children: [
4162
5270
  /* @__PURE__ */ jsxRuntime.jsx(
4163
5271
  "button",
@@ -4232,11 +5340,11 @@ function formatMeasure(items) {
4232
5340
  return items.map((it) => `${it.label} = ${it.value.toFixed(2)}`).join(", ");
4233
5341
  }
4234
5342
  function ObjectRow(props) {
4235
- const { obj, state, selected, onSelect, onToggleVisible, onToggleLocked, onRename, onChangeColor, onDelete } = props;
5343
+ const { obj, state, selected, onSelect, onToggleVisible, onToggleLocked, onRename, onChangeColor, onDelete, describe } = props;
4236
5344
  const meta = getKindUiMeta(obj.kind);
4237
5345
  let title = "";
4238
5346
  try {
4239
- title = getKind(obj.kind).describe(obj, state);
5347
+ title = describe ? describe(obj, state) : getKind(obj.kind).describe(obj, state);
4240
5348
  } catch {
4241
5349
  title = `${meta.displayName} ${obj.label}`;
4242
5350
  }
@@ -4310,11 +5418,11 @@ var init_ObjectRow = __esm({
4310
5418
  });
4311
5419
  function ObjectListPanel(props) {
4312
5420
  const { store, selectedId, onSelect, renderRow } = props;
4313
- const subscribe = React__namespace.useCallback(
5421
+ const subscribe = React5__namespace.useCallback(
4314
5422
  (cb) => store.subscribe(() => cb()),
4315
5423
  [store]
4316
5424
  );
4317
- const state = React__namespace.useSyncExternalStore(subscribe, store.getState, store.getState);
5425
+ const state = React5__namespace.useSyncExternalStore(subscribe, store.getState, store.getState);
4318
5426
  const objects = listObjects(state);
4319
5427
  function handleSelect(id) {
4320
5428
  onSelect?.(id === selectedId ? null : id);
@@ -4345,7 +5453,7 @@ function ObjectListPanel(props) {
4345
5453
  if (renderRow) {
4346
5454
  const custom = renderRow(obj, { selected, onClick });
4347
5455
  if (custom != null) {
4348
- return /* @__PURE__ */ jsxRuntime.jsx(React__namespace.Fragment, { children: custom }, obj.id);
5456
+ return /* @__PURE__ */ jsxRuntime.jsx(React5__namespace.Fragment, { children: custom }, obj.id);
4349
5457
  }
4350
5458
  }
4351
5459
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -4458,29 +5566,29 @@ var init_AxisGridSection = __esm({
4458
5566
 
4459
5567
  // src/stamps/shared/StampLeftPanel/types.ts
4460
5568
  var TOOLTIP_DELAY_MS;
4461
- var init_types3 = __esm({
5569
+ var init_types4 = __esm({
4462
5570
  "src/stamps/shared/StampLeftPanel/types.ts"() {
4463
5571
  TOOLTIP_DELAY_MS = 400;
4464
5572
  }
4465
5573
  });
4466
5574
  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(() => {
5575
+ const [hover, setHover] = React5.useState(null);
5576
+ const [portalReady, setPortalReady] = React5.useState(false);
5577
+ const hoverTimerRef = React5.useRef(null);
5578
+ React5.useEffect(() => {
4471
5579
  setPortalReady(true);
4472
5580
  return () => {
4473
5581
  if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
4474
5582
  };
4475
5583
  }, []);
4476
- const showHover = React.useCallback((el, t) => {
5584
+ const showHover = React5.useCallback((el, t) => {
4477
5585
  if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
4478
5586
  hoverTimerRef.current = setTimeout(() => {
4479
5587
  const r = el.getBoundingClientRect();
4480
5588
  setHover({ label: t.label, hint: t.hint, x: r.right, y: r.top + r.height / 2 });
4481
5589
  }, TOOLTIP_DELAY_MS);
4482
5590
  }, []);
4483
- const hideHover = React.useCallback(() => {
5591
+ const hideHover = React5.useCallback(() => {
4484
5592
  if (hoverTimerRef.current) {
4485
5593
  clearTimeout(hoverTimerRef.current);
4486
5594
  hoverTimerRef.current = null;
@@ -4491,27 +5599,118 @@ function useToolHoverTooltip() {
4491
5599
  }
4492
5600
  var init_useToolHoverTooltip = __esm({
4493
5601
  "src/stamps/shared/StampLeftPanel/useToolHoverTooltip.ts"() {
4494
- init_types3();
5602
+ init_types4();
4495
5603
  }
4496
5604
  });
5605
+ function normalize(s) {
5606
+ return s.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/đ/g, "d").replace(/Đ/g, "d");
5607
+ }
5608
+ function SearchIcon() {
5609
+ 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: [
5610
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "11", cy: "11", r: "7" }),
5611
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "20", y1: "20", x2: "16.5", y2: "16.5" })
5612
+ ] });
5613
+ }
5614
+ function ClearIcon() {
5615
+ 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: [
5616
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
5617
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
5618
+ ] });
5619
+ }
5620
+ function ToolResultList(props) {
5621
+ const { tools, activeTool, onToolChange } = props;
5622
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-0.5", "data-testid": "tool-result-list", children: tools.map((t) => {
5623
+ const active = activeTool === t.key;
5624
+ return /* @__PURE__ */ jsxRuntime.jsxs(
5625
+ "button",
5626
+ {
5627
+ type: "button",
5628
+ "data-tool": t.key,
5629
+ "aria-label": t.label,
5630
+ "aria-pressed": active,
5631
+ onClick: () => onToolChange(t.key),
5632
+ className: [
5633
+ "flex items-center gap-2 rounded-md px-2 py-1.5 text-left transition",
5634
+ active ? "bg-emerald-600 text-white" : "text-slate-700 hover:bg-slate-100"
5635
+ ].join(" "),
5636
+ children: [
5637
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-6 w-6 shrink-0 items-center justify-center", children: t.icon }),
5638
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "min-w-0", children: [
5639
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block truncate text-[12px] font-medium leading-tight", children: t.label }),
5640
+ t.hint && /* @__PURE__ */ jsxRuntime.jsx("span", { className: ["block truncate text-[10px] leading-tight", active ? "text-emerald-50" : "text-slate-400"].join(" "), children: t.hint })
5641
+ ] })
5642
+ ]
5643
+ },
5644
+ t.key
5645
+ );
5646
+ }) });
5647
+ }
4497
5648
  function ToolGrid(props) {
4498
5649
  const { tools, groupOrder, groupLabels, activeTool, onToolChange, chord } = props;
4499
5650
  const { hover, portalReady, showHover, hideHover } = useToolHoverTooltip();
4500
- const grouped = React.useMemo(() => {
5651
+ const [query, setQuery] = React5.useState("");
5652
+ const normalizedQuery = React5.useMemo(() => normalize(query.trim()), [query]);
5653
+ const filteredTools = React5.useMemo(() => {
5654
+ if (!normalizedQuery) return tools;
5655
+ return tools.filter((t) => {
5656
+ if (normalize(t.label).includes(normalizedQuery)) return true;
5657
+ if (t.hint && normalize(t.hint).includes(normalizedQuery)) return true;
5658
+ return false;
5659
+ });
5660
+ }, [tools, normalizedQuery]);
5661
+ const grouped = React5.useMemo(() => {
4501
5662
  var _a;
4502
5663
  const acc = {};
4503
- for (const t of tools) {
5664
+ for (const t of filteredTools) {
4504
5665
  (acc[_a = t.group] ?? (acc[_a] = [])).push(t);
4505
5666
  }
4506
5667
  return acc;
4507
- }, [tools]);
4508
- const groupKeys = React.useMemo(
4509
- () => groupOrder.filter((g) => grouped[g]),
5668
+ }, [filteredTools]);
5669
+ const groupKeys = React5.useMemo(
5670
+ () => groupOrder.filter((g) => grouped[g] && grouped[g].length > 0),
4510
5671
  [grouped, groupOrder]
4511
5672
  );
4512
- const activeGroupTools = chord?.activeGroup ? grouped[chord.activeGroup] ?? null : null;
5673
+ const noMatch = normalizedQuery !== "" && groupKeys.length === 0;
4513
5674
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4514
- groupKeys.map((group) => {
5675
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
5676
+ /* @__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, {}) }),
5677
+ /* @__PURE__ */ jsxRuntime.jsx(
5678
+ "input",
5679
+ {
5680
+ type: "search",
5681
+ value: query,
5682
+ onChange: (e) => setQuery(e.target.value),
5683
+ placeholder: "T\xECm c\xF4ng c\u1EE5\u2026",
5684
+ "aria-label": "T\xECm c\xF4ng c\u1EE5",
5685
+ "data-testid": "tool-search-input",
5686
+ 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"
5687
+ }
5688
+ ),
5689
+ query && /* @__PURE__ */ jsxRuntime.jsx(
5690
+ "button",
5691
+ {
5692
+ type: "button",
5693
+ onClick: () => setQuery(""),
5694
+ "aria-label": "Xo\xE1 t\xECm ki\u1EBFm",
5695
+ "data-testid": "tool-search-clear",
5696
+ 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",
5697
+ children: /* @__PURE__ */ jsxRuntime.jsx(ClearIcon, {})
5698
+ }
5699
+ )
5700
+ ] }),
5701
+ noMatch && /* @__PURE__ */ jsxRuntime.jsxs(
5702
+ "div",
5703
+ {
5704
+ "data-testid": "tool-search-empty",
5705
+ className: "rounded-md border border-dashed border-slate-200 bg-slate-50 px-3 py-4 text-center text-[11px] text-slate-500",
5706
+ children: [
5707
+ "Kh\xF4ng c\xF3 c\xF4ng c\u1EE5 n\xE0o kh\u1EDBp \u201C",
5708
+ query.trim(),
5709
+ "\u201D."
5710
+ ]
5711
+ }
5712
+ ),
5713
+ normalizedQuery !== "" && !noMatch ? /* @__PURE__ */ jsxRuntime.jsx(ToolResultList, { tools: filteredTools, activeTool, onToolChange }) : groupKeys.map((group) => {
4515
5714
  const isChordActive = chord?.activeGroup === group;
4516
5715
  const dimmed = chord?.activeGroup != null && !isChordActive;
4517
5716
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -4525,23 +5724,10 @@ function ToolGrid(props) {
4525
5724
  dimmed ? "opacity-55" : "opacity-100"
4526
5725
  ].join(" "),
4527
5726
  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) => {
5727
+ /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: groupLabels[group] }),
5728
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-4 gap-1", children: grouped[group].map((t) => {
4543
5729
  const active = activeTool === t.key;
4544
- return /* @__PURE__ */ jsxRuntime.jsxs(
5730
+ return /* @__PURE__ */ jsxRuntime.jsx(
4545
5731
  "button",
4546
5732
  {
4547
5733
  type: "button",
@@ -4558,20 +5744,7 @@ function ToolGrid(props) {
4558
5744
  "relative flex h-10 items-center justify-center rounded-md transition",
4559
5745
  active ? "bg-emerald-600 text-white shadow-sm" : "text-slate-700 hover:bg-slate-100 hover:text-slate-900"
4560
5746
  ].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
- ]
5747
+ children: t.icon
4575
5748
  },
4576
5749
  t.key
4577
5750
  );
@@ -4581,22 +5754,6 @@ function ToolGrid(props) {
4581
5754
  group
4582
5755
  );
4583
5756
  }),
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
5757
  portalReady && hover && typeof document !== "undefined" ? reactDom.createPortal(
4601
5758
  /* @__PURE__ */ jsxRuntime.jsxs(
4602
5759
  "div",
@@ -4643,9 +5800,9 @@ function StampLeftPanelDesktop(props) {
4643
5800
  objects,
4644
5801
  tabs
4645
5802
  } = props;
4646
- const [tab, setTab] = React.useState("tools");
5803
+ const [tab, setTab] = React5.useState("tools");
4647
5804
  const hasObjects = !!objects;
4648
- React.useEffect(() => {
5805
+ React5.useEffect(() => {
4649
5806
  if (!hasObjects && tab === "objects") setTab("tools");
4650
5807
  }, [hasObjects, tab]);
4651
5808
  const tabSpecs = hasObjects ? [
@@ -4663,6 +5820,8 @@ function StampLeftPanelDesktop(props) {
4663
5820
  tabs: tabSpecs,
4664
5821
  activeTab: hasObjects ? tab : void 0,
4665
5822
  onTabChange: hasObjects ? setTab : void 0,
5823
+ resizable: true,
5824
+ widthStorageKey: "xom11.stamp-left-panel.width",
4666
5825
  children: !hasObjects || tab === "tools" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4667
5826
  /* @__PURE__ */ jsxRuntime.jsx(AxisGridSection, { view, history }),
4668
5827
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4724,9 +5883,9 @@ function MobileToolDrawer({
4724
5883
  testId,
4725
5884
  objectsTab
4726
5885
  }) {
4727
- const [mobileTab, setMobileTab] = React__namespace.default.useState("tools");
4728
- const prevOpenRef = React__namespace.default.useRef(drawerOpen);
4729
- React__namespace.default.useEffect(() => {
5886
+ const [mobileTab, setMobileTab] = React5__namespace.default.useState("tools");
5887
+ const prevOpenRef = React5__namespace.default.useRef(drawerOpen);
5888
+ React5__namespace.default.useEffect(() => {
4730
5889
  if (!prevOpenRef.current && drawerOpen) setMobileTab("tools");
4731
5890
  prevOpenRef.current = drawerOpen;
4732
5891
  }, [drawerOpen]);
@@ -4932,7 +6091,7 @@ function StampLeftPanelMobile(props) {
4932
6091
  drawerOpen,
4933
6092
  onDrawerClose
4934
6093
  } = props;
4935
- const groups = React.useMemo(() => {
6094
+ const groups = React5.useMemo(() => {
4936
6095
  const acc = /* @__PURE__ */ new Map();
4937
6096
  for (const t of tools) {
4938
6097
  if (!acc.has(t.group)) acc.set(t.group, []);
@@ -5122,11 +6281,11 @@ function readMatch(query) {
5122
6281
  }
5123
6282
  }
5124
6283
  function useIsMobile() {
5125
- const [state, setState] = React.useState(() => ({
6284
+ const [state, setState] = React5.useState(() => ({
5126
6285
  isMobile: readMatch(MOBILE_QUERY),
5127
6286
  isTouchOnly: readMatch(NO_HOVER_QUERY)
5128
6287
  }));
5129
- React.useEffect(() => {
6288
+ React5.useEffect(() => {
5130
6289
  if (typeof window === "undefined" || !window.matchMedia) return;
5131
6290
  const mql = window.matchMedia(MOBILE_QUERY);
5132
6291
  const tql = window.matchMedia(NO_HOVER_QUERY);
@@ -5152,7 +6311,7 @@ var init_useIsMobile = __esm({
5152
6311
  }
5153
6312
  });
5154
6313
  function useStampStore(domain, editingElement, parseInitial) {
5155
- const ref = React.useRef(null);
6314
+ const ref = React5.useRef(null);
5156
6315
  if (!ref.current) {
5157
6316
  const initial = editingElement?.customData ? parseInitial(editingElement.customData) ?? createEmptyState(domain) : createEmptyState(domain);
5158
6317
  ref.current = createStore(initial);
@@ -5251,9 +6410,9 @@ var init_tools = __esm({
5251
6410
  }
5252
6411
  });
5253
6412
  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(() => {
6413
+ const [local, setLocal] = React5.useState(obj.attrs.expression);
6414
+ const [error, setError] = React5.useState(null);
6415
+ React5.useEffect(() => {
5257
6416
  setLocal(obj.attrs.expression);
5258
6417
  setError(null);
5259
6418
  }, [obj.attrs.expression]);
@@ -5474,35 +6633,35 @@ var init_host = __esm({
5474
6633
  /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 3 L3 21 L21 21" }),
5475
6634
  /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 14 Q9 8 12 10 Q15 12 18 6" })
5476
6635
  ] });
5477
- Graph2DStampHost = React.forwardRef(
6636
+ Graph2DStampHost = React5.forwardRef(
5478
6637
  function Graph2DStampHost2({ api, editingElement, onClose, isDark }, ref) {
5479
- const panelRef = React.useRef(null);
6638
+ const panelRef = React5.useRef(null);
5480
6639
  const { isMobile } = useIsMobile();
5481
- const [drawerOpen, setDrawerOpen] = React.useState(false);
6640
+ const [drawerOpen, setDrawerOpen] = React5.useState(false);
5482
6641
  const sceneStore = useStampStore("graph2d", editingElement, parseInitialState);
5483
- const [selectedObjectId, setSelectedObjectId] = React.useState(void 0);
6642
+ const [selectedObjectId, setSelectedObjectId] = React5.useState(void 0);
5484
6643
  const initialMeta = sceneStore.getState().meta;
5485
6644
  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) => {
6645
+ const [selectedTool, setSelectedTool] = React5.useState("move");
6646
+ const [showAxis, setShowAxisState] = React5.useState(initialView?.showAxis ?? true);
6647
+ const [showGrid, setShowGridState] = React5.useState(initialView?.showGrid ?? true);
6648
+ const [canUndo, setCanUndo] = React5.useState(false);
6649
+ const [canRedo, setCanRedo] = React5.useState(false);
6650
+ const handleHistoryChange = React5.useCallback((u, r) => {
5492
6651
  setCanUndo(u);
5493
6652
  setCanRedo(r);
5494
6653
  }, []);
5495
- const handleUndo = React.useCallback(() => sceneStore.undo(), [sceneStore]);
5496
- const handleRedo = React.useCallback(() => sceneStore.redo(), [sceneStore]);
5497
- const handleShowAxisChange = React.useCallback((b) => {
6654
+ const handleUndo = React5.useCallback(() => sceneStore.undo(), [sceneStore]);
6655
+ const handleRedo = React5.useCallback(() => sceneStore.redo(), [sceneStore]);
6656
+ const handleShowAxisChange = React5.useCallback((b) => {
5498
6657
  setShowAxisState(b);
5499
6658
  sceneStore.dispatch({ type: "UPDATE_VIEW", payload: { patch: { showAxis: b } } });
5500
6659
  }, [sceneStore]);
5501
- const handleShowGridChange = React.useCallback((b) => {
6660
+ const handleShowGridChange = React5.useCallback((b) => {
5502
6661
  setShowGridState(b);
5503
6662
  sceneStore.dispatch({ type: "UPDATE_VIEW", payload: { patch: { showGrid: b } } });
5504
6663
  }, [sceneStore]);
5505
- const handleAddFunction = React.useCallback(() => {
6664
+ const handleAddFunction = React5.useCallback(() => {
5506
6665
  const existing = Object.values(sceneStore.getState().objects).filter((o) => o.kind === "function2d");
5507
6666
  const id = `f${existing.length + 1}`;
5508
6667
  sceneStore.dispatch({
@@ -5521,7 +6680,7 @@ var init_host = __esm({
5521
6680
  }
5522
6681
  });
5523
6682
  }, [sceneStore]);
5524
- const handleAddParameter = React.useCallback(() => {
6683
+ const handleAddParameter = React5.useCallback(() => {
5525
6684
  const existing = Object.values(sceneStore.getState().objects).filter((o) => o.kind === "parameter");
5526
6685
  const labels = "abcdefghijklmnopqrstuvwxyz";
5527
6686
  const usedLabels = new Set(existing.map((o) => o.label));
@@ -5549,7 +6708,7 @@ var init_host = __esm({
5549
6708
  }
5550
6709
  });
5551
6710
  }, [sceneStore]);
5552
- const handleInsert = React.useCallback(
6711
+ const handleInsert = React5.useCallback(
5553
6712
  async (jsonState, svgString) => {
5554
6713
  if (!api) return;
5555
6714
  try {
@@ -5569,7 +6728,7 @@ var init_host = __esm({
5569
6728
  },
5570
6729
  [api, editingElement?.id, onClose]
5571
6730
  );
5572
- React.useImperativeHandle(
6731
+ React5.useImperativeHandle(
5573
6732
  ref,
5574
6733
  () => ({
5575
6734
  tryInsert: () => panelRef.current?.insert() ?? false,
@@ -5577,7 +6736,7 @@ var init_host = __esm({
5577
6736
  }),
5578
6737
  []
5579
6738
  );
5580
- const renderRow = React.useMemo(() => makeRenderRow(sceneStore), [sceneStore]);
6739
+ const renderRow = React5.useMemo(() => makeRenderRow(sceneStore), [sceneStore]);
5581
6740
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5582
6741
  /* @__PURE__ */ jsxRuntime.jsx(
5583
6742
  StampLeftPanel,
@@ -5655,7 +6814,7 @@ init_render();
5655
6814
  init_types();
5656
6815
  init_serialize();
5657
6816
  init_svgToStampFile();
5658
- var Graph2DStampHost3 = React.lazy(
6817
+ var Graph2DStampHost3 = React5.lazy(
5659
6818
  () => Promise.resolve().then(() => (init_host(), host_exports)).then((m) => ({ default: m.Graph2DStampHost }))
5660
6819
  );
5661
6820
  var Graph2DIcon = /* @__PURE__ */ jsxRuntime.jsxs(