@xom11/whiteboard 0.25.0 → 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 (93) hide show
  1. package/dist/{ExcalidrawWithMenus-WENZRYYE.mjs → ExcalidrawWithMenus-2QPPTXJM.mjs} +3 -2
  2. package/dist/ExcalidrawWithMenus-2QPPTXJM.mjs.map +1 -0
  3. package/dist/ai.d.mts +3035 -108
  4. package/dist/ai.d.ts +3035 -108
  5. package/dist/ai.js +6736 -775
  6. package/dist/ai.js.map +1 -1
  7. package/dist/ai.mjs +5110 -578
  8. package/dist/ai.mjs.map +1 -1
  9. package/dist/catalog.json +5 -5
  10. package/dist/{chunk-ESVPQWHX.mjs → chunk-4ETJ4CDY.mjs} +5 -5
  11. package/dist/{chunk-ESVPQWHX.mjs.map → chunk-4ETJ4CDY.mjs.map} +1 -1
  12. package/dist/{chunk-VNCCIV6O.mjs → chunk-AJAHD35N.mjs} +779 -9
  13. package/dist/chunk-AJAHD35N.mjs.map +1 -0
  14. package/dist/chunk-AYJPOHCI.mjs +265 -0
  15. package/dist/chunk-AYJPOHCI.mjs.map +1 -0
  16. package/dist/{chunk-M42TGYT6.mjs → chunk-BNBOIDO5.mjs} +3 -3
  17. package/dist/{chunk-M42TGYT6.mjs.map → chunk-BNBOIDO5.mjs.map} +1 -1
  18. package/dist/{chunk-CJBLJUWG.mjs → chunk-CXHNVYMD.mjs} +4 -4
  19. package/dist/{chunk-CJBLJUWG.mjs.map → chunk-CXHNVYMD.mjs.map} +1 -1
  20. package/dist/{chunk-NDEZJKNY.mjs → chunk-D5JLJ3PT.mjs} +4 -4
  21. package/dist/{chunk-NDEZJKNY.mjs.map → chunk-D5JLJ3PT.mjs.map} +1 -1
  22. package/dist/{chunk-REIJZDVZ.mjs → chunk-D5LWSN2Y.mjs} +942 -195
  23. package/dist/chunk-D5LWSN2Y.mjs.map +1 -0
  24. package/dist/{chunk-I24QOHPU.mjs → chunk-HLAOGXEK.mjs} +3 -3
  25. package/dist/{chunk-I24QOHPU.mjs.map → chunk-HLAOGXEK.mjs.map} +1 -1
  26. package/dist/{chunk-TB4CL25L.mjs → chunk-I3L56GVH.mjs} +206 -66
  27. package/dist/chunk-I3L56GVH.mjs.map +1 -0
  28. package/dist/chunk-J5LGTIGS.mjs +10 -0
  29. package/dist/chunk-J5LGTIGS.mjs.map +1 -0
  30. package/dist/{chunk-ONBCUWVI.mjs → chunk-KYMBUTPO.mjs} +3 -3
  31. package/dist/{chunk-ONBCUWVI.mjs.map → chunk-KYMBUTPO.mjs.map} +1 -1
  32. package/dist/{chunk-YSJOVBCD.mjs → chunk-KZGPSTZI.mjs} +4 -4
  33. package/dist/{chunk-YSJOVBCD.mjs.map → chunk-KZGPSTZI.mjs.map} +1 -1
  34. package/dist/{chunk-SGFJLHHG.mjs → chunk-PPKHCRRE.mjs} +3 -3
  35. package/dist/{chunk-SGFJLHHG.mjs.map → chunk-PPKHCRRE.mjs.map} +1 -1
  36. package/dist/{chunk-AYSFWUPK.mjs → chunk-SZDAS7LK.mjs} +79 -2
  37. package/dist/chunk-SZDAS7LK.mjs.map +1 -0
  38. package/dist/chunk-T3SOHYB2.mjs +851 -0
  39. package/dist/chunk-T3SOHYB2.mjs.map +1 -0
  40. package/dist/geometry-2d.d.mts +1 -1
  41. package/dist/geometry-2d.d.ts +1 -1
  42. package/dist/geometry-2d.js +5389 -1362
  43. package/dist/geometry-2d.js.map +1 -1
  44. package/dist/geometry-2d.mjs +5 -4
  45. package/dist/geometry-3d.d.mts +1 -1
  46. package/dist/geometry-3d.d.ts +1 -1
  47. package/dist/geometry-3d.js +1333 -251
  48. package/dist/geometry-3d.js.map +1 -1
  49. package/dist/geometry-3d.mjs +4 -3
  50. package/dist/graph-2d.d.mts +1 -1
  51. package/dist/graph-2d.d.ts +1 -1
  52. package/dist/graph-2d.js +1499 -340
  53. package/dist/graph-2d.js.map +1 -1
  54. package/dist/graph-2d.mjs +7 -6
  55. package/dist/handleExtractProblem-C-U5KluK.d.mts +158 -0
  56. package/dist/handleExtractProblem-C-U5KluK.d.ts +158 -0
  57. package/dist/{host-L7FMFZUW.mjs → host-HAYCJJ2T.mjs} +1258 -441
  58. package/dist/host-HAYCJJ2T.mjs.map +1 -0
  59. package/dist/{host-QK53UYMD.mjs → host-LTJHAY5A.mjs} +10 -9
  60. package/dist/host-LTJHAY5A.mjs.map +1 -0
  61. package/dist/{host-A64ITWVX.mjs → host-M26FS244.mjs} +6 -5
  62. package/dist/host-M26FS244.mjs.map +1 -0
  63. package/dist/{host-QS2EOTRJ.mjs → host-ZQCDAT6O.mjs} +3 -2
  64. package/dist/host-ZQCDAT6O.mjs.map +1 -0
  65. package/dist/index.d.mts +3 -2
  66. package/dist/index.d.ts +3 -2
  67. package/dist/index.js +5621 -1590
  68. package/dist/index.js.map +1 -1
  69. package/dist/index.mjs +22 -20
  70. package/dist/index.mjs.map +1 -1
  71. package/dist/latex.d.mts +1 -1
  72. package/dist/latex.d.ts +1 -1
  73. package/dist/latex.mjs +2 -1
  74. package/dist/render-ZX2O2IK7.mjs +10 -0
  75. package/dist/{render-3WTY7NZB.mjs.map → render-ZX2O2IK7.mjs.map} +1 -1
  76. package/dist/serialize-C3LSUMSA.mjs +9 -0
  77. package/dist/{serialize-SRJVKYUG.mjs.map → serialize-C3LSUMSA.mjs.map} +1 -1
  78. package/dist/{types-DWRyCa2m.d.ts → types-zc_Pa0mp.d.mts} +105 -0
  79. package/dist/{types-DWRyCa2m.d.mts → types-zc_Pa0mp.d.ts} +105 -0
  80. package/package.json +10 -1
  81. package/dist/ExcalidrawWithMenus-WENZRYYE.mjs.map +0 -1
  82. package/dist/chunk-AYSFWUPK.mjs.map +0 -1
  83. package/dist/chunk-REIJZDVZ.mjs.map +0 -1
  84. package/dist/chunk-TB4CL25L.mjs.map +0 -1
  85. package/dist/chunk-VNCCIV6O.mjs.map +0 -1
  86. package/dist/chunk-VRHWDZ66.mjs +0 -96
  87. package/dist/chunk-VRHWDZ66.mjs.map +0 -1
  88. package/dist/host-A64ITWVX.mjs.map +0 -1
  89. package/dist/host-L7FMFZUW.mjs.map +0 -1
  90. package/dist/host-QK53UYMD.mjs.map +0 -1
  91. package/dist/host-QS2EOTRJ.mjs.map +0 -1
  92. package/dist/render-3WTY7NZB.mjs +0 -9
  93. package/dist/serialize-SRJVKYUG.mjs +0 -8
@@ -923,6 +923,31 @@ function constraintRefs2D(c) {
923
923
  return [c.vertices[0], c.vertices[1], c.vertices[2]];
924
924
  case "orthocenter":
925
925
  return [c.vertices[0], c.vertices[1], c.vertices[2]];
926
+ case "onPerpendicular":
927
+ return [c.through, c.perpToA, c.perpToB];
928
+ case "onPerpBisector":
929
+ return [c.p1, c.p2];
930
+ case "onCircleAroundPoint":
931
+ return [c.center, c.radiusPoint];
932
+ case "tangentPointExt":
933
+ return [c.from, c.circle];
934
+ case "circleIntersection":
935
+ return [c.c1, c.c2];
936
+ case "circleSecondIntersection":
937
+ return [c.c1, c.c2, c.exclude];
938
+ case "secondIntersection":
939
+ return [c.line, c.circle, c.other];
940
+ case "tangencyPoint":
941
+ return [c.circle, c.onLine];
942
+ case "arcMidpoint":
943
+ return [c.circle, c.a, c.b, c.notContaining];
944
+ case "pointAtDistance": {
945
+ const d = c.distance;
946
+ const extra = d.kind === "circleRadius" ? [d.circle] : d.kind === "segmentLength" ? [d.p1, d.p2] : [];
947
+ return [c.from, c.through, ...extra];
948
+ }
949
+ case "excenter":
950
+ return [c.vertices[0], c.vertices[1], c.vertices[2]];
926
951
  default:
927
952
  return [];
928
953
  }
@@ -932,7 +957,412 @@ var init_d_constraint2 = __esm({
932
957
  }
933
958
  });
934
959
 
935
- // src/core/scene/kinds/point.ts
960
+ // src/core/scene/kinds/point-constraints/_types.ts
961
+ function definePointConstraint(m) {
962
+ return m;
963
+ }
964
+ var init_types2 = __esm({
965
+ "src/core/scene/kinds/point-constraints/_types.ts"() {
966
+ }
967
+ });
968
+
969
+ // src/core/scene/kinds/point-constraints/free.ts
970
+ var freeConstraint;
971
+ var init_free = __esm({
972
+ "src/core/scene/kinds/point-constraints/free.ts"() {
973
+ init_types2();
974
+ freeConstraint = definePointConstraint({
975
+ kind: "free",
976
+ describe: (obj) => `\u0110i\u1EC3m ${obj.label}`,
977
+ render: (obj, ctx, c, opts) => {
978
+ const board = ctx.jxg;
979
+ return board.create("point", [c.x, c.y], opts);
980
+ }
981
+ });
982
+ }
983
+ });
984
+
985
+ // src/core/scene/kinds/point-constraints/onAxis.ts
986
+ var onAxisConstraint;
987
+ var init_onAxis = __esm({
988
+ "src/core/scene/kinds/point-constraints/onAxis.ts"() {
989
+ init_types2();
990
+ onAxisConstraint = definePointConstraint({
991
+ kind: "onAxis",
992
+ describe: (obj, _state, c) => `${obj.label} tr\xEAn tr\u1EE5c ${c.axis}`,
993
+ render: (obj, ctx, c, opts) => {
994
+ const board = ctx.jxg;
995
+ const coords = c.axis === "x" ? [c.t, 0] : [0, c.t];
996
+ return board.create("point", coords, opts);
997
+ }
998
+ });
999
+ }
1000
+ });
1001
+
1002
+ // src/core/scene/kinds/point-constraints/midpoint.ts
1003
+ var midpointConstraint;
1004
+ var init_midpoint = __esm({
1005
+ "src/core/scene/kinds/point-constraints/midpoint.ts"() {
1006
+ init_types2();
1007
+ midpointConstraint = definePointConstraint({
1008
+ kind: "midpoint",
1009
+ describe: (obj, state, c) => {
1010
+ const l1 = state?.objects[c.p1]?.label ?? c.p1;
1011
+ const l2 = state?.objects[c.p2]?.label ?? c.p2;
1012
+ return `${obj.label} = trung \u0111i\u1EC3m ${l1}${l2}`;
1013
+ },
1014
+ render: (obj, ctx, c, opts) => {
1015
+ const board = ctx.jxg;
1016
+ const p1 = ctx.resolveRef(c.p1);
1017
+ const p2 = ctx.resolveRef(c.p2);
1018
+ return board.create("midpoint", [p1, p2], opts);
1019
+ }
1020
+ });
1021
+ }
1022
+ });
1023
+
1024
+ // src/core/scene/kinds/point-constraints/perpFoot.ts
1025
+ var perpFootConstraint;
1026
+ var init_perpFoot = __esm({
1027
+ "src/core/scene/kinds/point-constraints/perpFoot.ts"() {
1028
+ init_types2();
1029
+ perpFootConstraint = definePointConstraint({
1030
+ kind: "perpFoot",
1031
+ validate: (c) => {
1032
+ if (!c.from || !c.onLine) {
1033
+ throw new Error("point.perpFoot: from v\xE0 onLine b\u1EAFt bu\u1ED9c");
1034
+ }
1035
+ },
1036
+ describe: (obj, state, c) => {
1037
+ const fromLabel = state?.objects[c.from]?.label ?? c.from;
1038
+ const lineLabel = state?.objects[c.onLine]?.label ?? c.onLine;
1039
+ return `${obj.label} = ch\xE2n \u27C2 t\u1EEB ${fromLabel} xu\u1ED1ng ${lineLabel}`;
1040
+ },
1041
+ render: (obj, ctx, c, opts) => {
1042
+ const board = ctx.jxg;
1043
+ const from = ctx.resolveRef(c.from);
1044
+ const onLine = ctx.resolveRef(c.onLine);
1045
+ return board.create("perpendicularpoint", [onLine, from], opts);
1046
+ }
1047
+ });
1048
+ }
1049
+ });
1050
+
1051
+ // src/core/scene/kinds/point-constraints/circumcenter.ts
1052
+ var circumcenterConstraint;
1053
+ var init_circumcenter = __esm({
1054
+ "src/core/scene/kinds/point-constraints/circumcenter.ts"() {
1055
+ init_types2();
1056
+ circumcenterConstraint = definePointConstraint({
1057
+ kind: "circumcenter",
1058
+ validate: (c) => {
1059
+ if (!Array.isArray(c.vertices) || c.vertices.length !== 3) {
1060
+ throw new Error("point.circumcenter: vertices ph\u1EA3i l\xE0 tuple 3 id");
1061
+ }
1062
+ if (!c.vertices[0] || !c.vertices[1] || !c.vertices[2]) {
1063
+ throw new Error("point.circumcenter: 3 vertex id ph\u1EA3i non-empty");
1064
+ }
1065
+ },
1066
+ describe: (obj, state, c) => {
1067
+ const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
1068
+ return `${obj.label} = t\xE2m ngo\u1EA1i ti\u1EBFp \u0394${labels}`;
1069
+ },
1070
+ render: (obj, ctx, c, opts) => {
1071
+ const board = ctx.jxg;
1072
+ const a = ctx.resolveRef(c.vertices[0]);
1073
+ const b = ctx.resolveRef(c.vertices[1]);
1074
+ const c3 = ctx.resolveRef(c.vertices[2]);
1075
+ return board.create("circumcenter", [a, b, c3], opts);
1076
+ }
1077
+ });
1078
+ }
1079
+ });
1080
+
1081
+ // src/core/scene/kinds/point-constraints/incenter.ts
1082
+ var incenterConstraint;
1083
+ var init_incenter = __esm({
1084
+ "src/core/scene/kinds/point-constraints/incenter.ts"() {
1085
+ init_types2();
1086
+ incenterConstraint = definePointConstraint({
1087
+ kind: "incenter",
1088
+ validate: (c) => {
1089
+ if (!Array.isArray(c.vertices) || c.vertices.length !== 3) {
1090
+ throw new Error("point.incenter: vertices ph\u1EA3i l\xE0 tuple 3 id");
1091
+ }
1092
+ if (!c.vertices[0] || !c.vertices[1] || !c.vertices[2]) {
1093
+ throw new Error("point.incenter: 3 vertex id ph\u1EA3i non-empty");
1094
+ }
1095
+ },
1096
+ describe: (obj, state, c) => {
1097
+ const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
1098
+ return `${obj.label} = t\xE2m n\u1ED9i ti\u1EBFp \u0394${labels}`;
1099
+ },
1100
+ render: (obj, ctx, c, opts) => {
1101
+ const board = ctx.jxg;
1102
+ const a = ctx.resolveRef(c.vertices[0]);
1103
+ const b = ctx.resolveRef(c.vertices[1]);
1104
+ const c3 = ctx.resolveRef(c.vertices[2]);
1105
+ return board.create("incenter", [a, b, c3], opts);
1106
+ }
1107
+ });
1108
+ }
1109
+ });
1110
+
1111
+ // src/core/scene/kinds/point-constraints/onLine.ts
1112
+ var onLineConstraint;
1113
+ var init_onLine = __esm({
1114
+ "src/core/scene/kinds/point-constraints/onLine.ts"() {
1115
+ init_types2();
1116
+ onLineConstraint = definePointConstraint({
1117
+ kind: "onLine",
1118
+ describe: (obj, state, c) => `${obj.label} tr\xEAn \u0111\u01B0\u1EDDng ${state?.objects[c.lineId]?.label ?? c.lineId}`,
1119
+ render: (obj, ctx, c, opts) => {
1120
+ const board = ctx.jxg;
1121
+ const line = ctx.resolveRef(c.lineId);
1122
+ const p1 = line.point1;
1123
+ const p2 = line.point2;
1124
+ const sx = p1 && p2 ? p1.X() + c.t * (p2.X() - p1.X()) : c.t;
1125
+ const sy = p1 && p2 ? p1.Y() + c.t * (p2.Y() - p1.Y()) : c.t;
1126
+ return board.create("glider", [sx, sy, line], opts);
1127
+ }
1128
+ });
1129
+ }
1130
+ });
1131
+
1132
+ // src/core/scene/kinds/point-constraints/onSegment.ts
1133
+ var onSegmentConstraint;
1134
+ var init_onSegment = __esm({
1135
+ "src/core/scene/kinds/point-constraints/onSegment.ts"() {
1136
+ init_types2();
1137
+ onSegmentConstraint = definePointConstraint({
1138
+ kind: "onSegment",
1139
+ describe: (obj, state, c) => `${obj.label} tr\xEAn \u0111o\u1EA1n ${state?.objects[c.segmentId]?.label ?? c.segmentId}`,
1140
+ render: (obj, ctx, c, opts) => {
1141
+ const board = ctx.jxg;
1142
+ const seg = ctx.resolveRef(c.segmentId);
1143
+ const p1 = seg.point1;
1144
+ const p2 = seg.point2;
1145
+ const sx = p1 && p2 ? p1.X() + c.t * (p2.X() - p1.X()) : c.t;
1146
+ const sy = p1 && p2 ? p1.Y() + c.t * (p2.Y() - p1.Y()) : c.t;
1147
+ return board.create("glider", [sx, sy, seg], opts);
1148
+ }
1149
+ });
1150
+ }
1151
+ });
1152
+
1153
+ // src/core/scene/kinds/point-constraints/onCircle.ts
1154
+ var onCircleConstraint;
1155
+ var init_onCircle = __esm({
1156
+ "src/core/scene/kinds/point-constraints/onCircle.ts"() {
1157
+ init_types2();
1158
+ onCircleConstraint = definePointConstraint({
1159
+ kind: "onCircle",
1160
+ describe: (obj, state, c) => `${obj.label} tr\xEAn \u0111\u01B0\u1EDDng tr\xF2n ${state?.objects[c.circleId]?.label ?? c.circleId}`,
1161
+ render: (obj, ctx, c, opts) => {
1162
+ const board = ctx.jxg;
1163
+ const circle = ctx.resolveRef(c.circleId);
1164
+ const O = circle.center ?? circle.midpoint;
1165
+ const ox = O ? O.X() : 0;
1166
+ const oy = O ? O.Y() : 0;
1167
+ return board.create("glider", [ox + Math.cos(c.theta), oy + Math.sin(c.theta), circle], opts);
1168
+ }
1169
+ });
1170
+ }
1171
+ });
1172
+
1173
+ // src/core/scene/kinds/point-constraints/onPolygon.ts
1174
+ var onPolygonConstraint;
1175
+ var init_onPolygon = __esm({
1176
+ "src/core/scene/kinds/point-constraints/onPolygon.ts"() {
1177
+ init_types2();
1178
+ onPolygonConstraint = definePointConstraint({
1179
+ kind: "onPolygon",
1180
+ describe: (obj, state, c) => `${obj.label} tr\xEAn \u0111a gi\xE1c ${state?.objects[c.polygonId]?.label ?? c.polygonId}`,
1181
+ render: (obj, ctx, c, opts) => {
1182
+ const board = ctx.jxg;
1183
+ const poly = ctx.resolveRef(c.polygonId);
1184
+ return board.create("glider", [c.u, c.v, poly], opts);
1185
+ }
1186
+ });
1187
+ }
1188
+ });
1189
+
1190
+ // src/core/scene/kinds/point-constraints/centroid.ts
1191
+ var centroidConstraint;
1192
+ var init_centroid = __esm({
1193
+ "src/core/scene/kinds/point-constraints/centroid.ts"() {
1194
+ init_types2();
1195
+ centroidConstraint = definePointConstraint({
1196
+ kind: "centroid",
1197
+ validate: (c) => {
1198
+ if (!Array.isArray(c.vertices) || c.vertices.length !== 3) {
1199
+ throw new Error("point.centroid: vertices ph\u1EA3i l\xE0 tuple 3 id");
1200
+ }
1201
+ if (!c.vertices[0] || !c.vertices[1] || !c.vertices[2]) {
1202
+ throw new Error("point.centroid: 3 vertex id ph\u1EA3i non-empty");
1203
+ }
1204
+ },
1205
+ describe: (obj, state, c) => {
1206
+ const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
1207
+ return `${obj.label} = tr\u1ECDng t\xE2m \u0394${labels}`;
1208
+ },
1209
+ render: (obj, ctx, c, opts) => {
1210
+ const board = ctx.jxg;
1211
+ const a = ctx.resolveRef(c.vertices[0]);
1212
+ const b = ctx.resolveRef(c.vertices[1]);
1213
+ const c3 = ctx.resolveRef(c.vertices[2]);
1214
+ return board.create("point", [
1215
+ () => (a.X() + b.X() + c3.X()) / 3,
1216
+ () => (a.Y() + b.Y() + c3.Y()) / 3
1217
+ ], opts);
1218
+ }
1219
+ });
1220
+ }
1221
+ });
1222
+
1223
+ // src/core/scene/kinds/pointConstructions.ts
1224
+ function dist(p, q) {
1225
+ return Math.hypot(p[0] - q[0], p[1] - q[1]);
1226
+ }
1227
+ function sideOf(a, b, p) {
1228
+ return (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
1229
+ }
1230
+ function arcMidpoint(center, radius, a, b, notContaining) {
1231
+ const mcx = (a[0] + b[0]) / 2;
1232
+ const mcy = (a[1] + b[1]) / 2;
1233
+ let ux = mcx - center[0];
1234
+ let uy = mcy - center[1];
1235
+ let len = Math.hypot(ux, uy);
1236
+ if (len < 1e-9) {
1237
+ ux = -(b[1] - a[1]);
1238
+ uy = b[0] - a[0];
1239
+ len = Math.hypot(ux, uy) || 1;
1240
+ }
1241
+ ux /= len;
1242
+ uy /= len;
1243
+ const cand1 = [center[0] + radius * ux, center[1] + radius * uy];
1244
+ const cand2 = [center[0] - radius * ux, center[1] - radius * uy];
1245
+ const sN = sideOf(a, b, notContaining);
1246
+ if (Math.abs(sN) < 1e-9) {
1247
+ return dist(cand1, notContaining) >= dist(cand2, notContaining) ? cand1 : cand2;
1248
+ }
1249
+ const s1 = sideOf(a, b, cand1);
1250
+ return s1 * sN < 0 ? cand1 : cand2;
1251
+ }
1252
+ function excenter(vertices, oppositeIndex) {
1253
+ const [A, B, C] = vertices;
1254
+ const a = dist(B, C);
1255
+ const b = dist(C, A);
1256
+ const c = dist(A, B);
1257
+ const w = [a, b, c];
1258
+ w[oppositeIndex] = -w[oppositeIndex];
1259
+ const sum = w[0] + w[1] + w[2];
1260
+ if (Math.abs(sum) < 1e-9) return A;
1261
+ return [
1262
+ (w[0] * A[0] + w[1] * B[0] + w[2] * C[0]) / sum,
1263
+ (w[0] * A[1] + w[1] * B[1] + w[2] * C[1]) / sum
1264
+ ];
1265
+ }
1266
+ function pointAtDistanceCoord(from, through, d) {
1267
+ const dx = through[0] - from[0];
1268
+ const dy = through[1] - from[1];
1269
+ const len = Math.hypot(dx, dy) || 1;
1270
+ return [through[0] + d * dx / len, through[1] + d * dy / len];
1271
+ }
1272
+ function radicalAxisFoot(o1, r1, o2, r2) {
1273
+ const dx = o2[0] - o1[0], dy = o2[1] - o1[1];
1274
+ const d2 = dx * dx + dy * dy;
1275
+ if (d2 < 1e-12) return o1;
1276
+ const t = (d2 + r1 * r1 - r2 * r2) / (2 * d2);
1277
+ return [o1[0] + t * dx, o1[1] + t * dy];
1278
+ }
1279
+ var init_pointConstructions = __esm({
1280
+ "src/core/scene/kinds/pointConstructions.ts"() {
1281
+ }
1282
+ });
1283
+
1284
+ // src/core/scene/kinds/point-constraints/arcMidpoint.ts
1285
+ var arcMidpointConstraint;
1286
+ var init_arcMidpoint = __esm({
1287
+ "src/core/scene/kinds/point-constraints/arcMidpoint.ts"() {
1288
+ init_pointConstructions();
1289
+ init_types2();
1290
+ arcMidpointConstraint = definePointConstraint({
1291
+ kind: "arcMidpoint",
1292
+ validate: (c) => {
1293
+ if (!c.circle || !c.a || !c.b || !c.notContaining) {
1294
+ throw new Error("point.arcMidpoint: circle, a, b, notContaining b\u1EAFt bu\u1ED9c");
1295
+ }
1296
+ },
1297
+ describe: (obj, state, c) => {
1298
+ const al = state?.objects[c.a]?.label ?? c.a;
1299
+ const bl = state?.objects[c.b]?.label ?? c.b;
1300
+ const nl = state?.objects[c.notContaining]?.label ?? c.notContaining;
1301
+ return `${obj.label} = trung \u0111i\u1EC3m cung ${al}${bl} (kh\xF4ng ch\u1EE9a ${nl})`;
1302
+ },
1303
+ render: (obj, ctx, c, opts) => {
1304
+ const board = ctx.jxg;
1305
+ const circle = ctx.resolveRef(c.circle);
1306
+ const A = ctx.resolveRef(c.a);
1307
+ const B = ctx.resolveRef(c.b);
1308
+ const N = ctx.resolveRef(c.notContaining);
1309
+ const O = circle?.center ?? circle?.midpoint ?? circle;
1310
+ const am = () => arcMidpoint(
1311
+ [O.X(), O.Y()],
1312
+ circle.Radius(),
1313
+ [A.X(), A.Y()],
1314
+ [B.X(), B.Y()],
1315
+ [N.X(), N.Y()]
1316
+ );
1317
+ return board.create("point", [() => am()[0], () => am()[1]], opts);
1318
+ }
1319
+ });
1320
+ }
1321
+ });
1322
+
1323
+ // src/core/scene/kinds/point-constraints/excenter.ts
1324
+ var excenterConstraint;
1325
+ var init_excenter = __esm({
1326
+ "src/core/scene/kinds/point-constraints/excenter.ts"() {
1327
+ init_pointConstructions();
1328
+ init_types2();
1329
+ excenterConstraint = definePointConstraint({
1330
+ kind: "excenter",
1331
+ validate: (c) => {
1332
+ if (!Array.isArray(c.vertices) || c.vertices.length !== 3) {
1333
+ throw new Error("point.excenter: vertices ph\u1EA3i l\xE0 tuple 3 id");
1334
+ }
1335
+ if (!c.opposite) throw new Error("point.excenter: opposite b\u1EAFt bu\u1ED9c");
1336
+ if (!c.vertices[0] || !c.vertices[1] || !c.vertices[2]) {
1337
+ throw new Error("point.excenter: 3 vertex id ph\u1EA3i non-empty");
1338
+ }
1339
+ if (!c.vertices.includes(c.opposite)) {
1340
+ throw new Error("point.excenter: opposite ph\u1EA3i l\xE0 m\u1ED9t trong vertices");
1341
+ }
1342
+ },
1343
+ describe: (obj, state, c) => {
1344
+ const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
1345
+ const opp = state?.objects[c.opposite]?.label ?? c.opposite;
1346
+ return `${obj.label} = t\xE2m b\xE0ng ti\u1EBFp \u0394${labels} \u0111\u1ED1i di\u1EC7n ${opp}`;
1347
+ },
1348
+ render: (obj, ctx, c, opts) => {
1349
+ const board = ctx.jxg;
1350
+ const a = ctx.resolveRef(c.vertices[0]);
1351
+ const b = ctx.resolveRef(c.vertices[1]);
1352
+ const c3 = ctx.resolveRef(c.vertices[2]);
1353
+ const oppIdx = c.vertices.indexOf(c.opposite);
1354
+ const idx = oppIdx < 0 ? 0 : oppIdx;
1355
+ const ex = () => excenter(
1356
+ [[a.X(), a.Y()], [b.X(), b.Y()], [c3.X(), c3.Y()]],
1357
+ idx
1358
+ );
1359
+ return board.create("point", [() => ex()[0], () => ex()[1]], opts);
1360
+ }
1361
+ });
1362
+ }
1363
+ });
1364
+
1365
+ // src/core/scene/kinds/point-constraints/shared.ts
936
1366
  function buildJxgTransforms(board, ctx, t) {
937
1367
  switch (t.kind) {
938
1368
  case "translate":
@@ -959,11 +1389,392 @@ function buildJxgTransforms(board, ctx, t) {
959
1389
  }
960
1390
  }
961
1391
  }
1392
+ function makeDistanceFn(ctx, d) {
1393
+ const scale3 = d.scale ?? 1;
1394
+ const offset = d.offset ?? 0;
1395
+ if (d.kind === "literal") {
1396
+ const v = d.value;
1397
+ return () => scale3 * v + offset;
1398
+ }
1399
+ if (d.kind === "segmentLength") {
1400
+ const p = ctx.resolveRef(d.p1);
1401
+ const q = ctx.resolveRef(d.p2);
1402
+ return () => scale3 * Math.hypot(p.X() - q.X(), p.Y() - q.Y()) + offset;
1403
+ }
1404
+ const circle = ctx.resolveRef(d.circle);
1405
+ return () => scale3 * circle.Radius() + offset;
1406
+ }
1407
+ function buildPointOpts(obj) {
1408
+ return {
1409
+ name: obj.label,
1410
+ withLabel: obj.attrs.showLabel ?? true,
1411
+ visible: obj.visible,
1412
+ fixed: obj.locked,
1413
+ strokeColor: obj.attrs.color ?? "#1e40af",
1414
+ fillColor: obj.attrs.color ?? "#1e40af",
1415
+ face: obj.attrs.face ?? "o",
1416
+ size: obj.attrs.size ?? 4
1417
+ };
1418
+ }
1419
+ var init_shared = __esm({
1420
+ "src/core/scene/kinds/point-constraints/shared.ts"() {
1421
+ }
1422
+ });
1423
+
1424
+ // src/core/scene/kinds/point-constraints/pointAtDistance.ts
1425
+ var pointAtDistanceConstraint;
1426
+ var init_pointAtDistance = __esm({
1427
+ "src/core/scene/kinds/point-constraints/pointAtDistance.ts"() {
1428
+ init_pointConstructions();
1429
+ init_types2();
1430
+ init_shared();
1431
+ pointAtDistanceConstraint = definePointConstraint({
1432
+ kind: "pointAtDistance",
1433
+ describe: (obj, state, c) => {
1434
+ const fromL = state?.objects[c.from]?.label ?? c.from;
1435
+ const thrL = state?.objects[c.through]?.label ?? c.through;
1436
+ const d = c.distance;
1437
+ 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})`;
1438
+ return `${obj.label} = tr\xEAn tia ${fromL}${thrL} k\xE9o d\xE0i, c\xE1ch ${thrL} kho\u1EA3ng ${dLabel}`;
1439
+ },
1440
+ render: (obj, ctx, c, opts) => {
1441
+ const board = ctx.jxg;
1442
+ const A = ctx.resolveRef(c.from);
1443
+ const B = ctx.resolveRef(c.through);
1444
+ const dFn = makeDistanceFn(ctx, c.distance);
1445
+ const pc = () => pointAtDistanceCoord([A.X(), A.Y()], [B.X(), B.Y()], dFn());
1446
+ return board.create("point", [() => pc()[0], () => pc()[1]], opts);
1447
+ }
1448
+ });
1449
+ }
1450
+ });
1451
+
1452
+ // src/core/scene/kinds/point-constraints/circleIntersection.ts
1453
+ var circleIntersectionConstraint;
1454
+ var init_circleIntersection = __esm({
1455
+ "src/core/scene/kinds/point-constraints/circleIntersection.ts"() {
1456
+ init_types2();
1457
+ circleIntersectionConstraint = definePointConstraint({
1458
+ kind: "circleIntersection",
1459
+ // Không có describe-arm riêng trong point.ts → giữ fallback `Điểm ${label}`.
1460
+ describe: (obj) => `\u0110i\u1EC3m ${obj.label}`,
1461
+ render: (obj, ctx, c, opts) => {
1462
+ const board = ctx.jxg;
1463
+ const k1 = ctx.resolveRef(c.c1);
1464
+ const k2 = ctx.resolveRef(c.c2);
1465
+ return board.create("intersection", [k1, k2, c.which], opts);
1466
+ }
1467
+ });
1468
+ }
1469
+ });
1470
+
1471
+ // src/core/scene/kinds/point-constraints/circleSecondIntersection.ts
1472
+ var circleSecondIntersectionConstraint;
1473
+ var init_circleSecondIntersection = __esm({
1474
+ "src/core/scene/kinds/point-constraints/circleSecondIntersection.ts"() {
1475
+ init_types2();
1476
+ circleSecondIntersectionConstraint = definePointConstraint({
1477
+ kind: "circleSecondIntersection",
1478
+ // Không có describe-arm riêng trong point.ts → giữ fallback `Điểm ${label}`.
1479
+ describe: (obj) => `\u0110i\u1EC3m ${obj.label}`,
1480
+ render: (obj, ctx, c, opts) => {
1481
+ const board = ctx.jxg;
1482
+ const k1 = ctx.resolveRef(c.c1);
1483
+ const k2 = ctx.resolveRef(c.c2);
1484
+ const ex = ctx.resolveRef(c.exclude);
1485
+ return board.create("otherintersection", [k1, k2, ex], opts);
1486
+ }
1487
+ });
1488
+ }
1489
+ });
1490
+
1491
+ // src/core/scene/kinds/point-constraints/secondIntersection.ts
1492
+ var secondIntersectionConstraint;
1493
+ var init_secondIntersection = __esm({
1494
+ "src/core/scene/kinds/point-constraints/secondIntersection.ts"() {
1495
+ init_types2();
1496
+ secondIntersectionConstraint = definePointConstraint({
1497
+ kind: "secondIntersection",
1498
+ // Không có describe-arm riêng trong point.ts → giữ fallback `Điểm ${label}`.
1499
+ describe: (obj) => `\u0110i\u1EC3m ${obj.label}`,
1500
+ render: (obj, ctx, c, opts) => {
1501
+ const board = ctx.jxg;
1502
+ const line = ctx.resolveRef(c.line);
1503
+ const circle = ctx.resolveRef(c.circle);
1504
+ const other = ctx.resolveRef(c.other);
1505
+ return board.create("otherintersection", [circle, line, other], opts);
1506
+ }
1507
+ });
1508
+ }
1509
+ });
1510
+
1511
+ // src/core/scene/kinds/point-constraints/tangencyPoint.ts
1512
+ var tangencyPointConstraint;
1513
+ var init_tangencyPoint = __esm({
1514
+ "src/core/scene/kinds/point-constraints/tangencyPoint.ts"() {
1515
+ init_types2();
1516
+ tangencyPointConstraint = definePointConstraint({
1517
+ kind: "tangencyPoint",
1518
+ // Không có describe-arm riêng trong point.ts → giữ fallback `Điểm ${label}`.
1519
+ describe: (obj) => `\u0110i\u1EC3m ${obj.label}`,
1520
+ render: (obj, ctx, c, opts) => {
1521
+ const board = ctx.jxg;
1522
+ const circle = ctx.resolveRef(c.circle);
1523
+ const line = ctx.resolveRef(c.onLine);
1524
+ const O = circle?.center ?? circle?.midpoint ?? circle;
1525
+ return board.create("perpendicularpoint", [line, O], opts);
1526
+ }
1527
+ });
1528
+ }
1529
+ });
1530
+
1531
+ // src/core/scene/kinds/point-constraints/transformed.ts
1532
+ var transformedConstraint;
1533
+ var init_transformed = __esm({
1534
+ "src/core/scene/kinds/point-constraints/transformed.ts"() {
1535
+ init_types2();
1536
+ init_shared();
1537
+ transformedConstraint = definePointConstraint({
1538
+ kind: "transformed",
1539
+ describe: (obj, state, c) => {
1540
+ const t = c.transform;
1541
+ const labelRef = (id) => state?.objects[id]?.label ?? id;
1542
+ 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)}` : "";
1543
+ return `${obj.label} = \u1EA3nh c\u1EE7a ${labelRef(c.source)} (${op})`;
1544
+ },
1545
+ render: (obj, ctx, c, opts) => {
1546
+ const board = ctx.jxg;
1547
+ const src = ctx.resolveRef(c.source);
1548
+ const transforms = buildJxgTransforms(board, ctx, c.transform);
1549
+ const parent = transforms.length === 1 ? transforms[0] : transforms;
1550
+ const pt = board.create("point", [src, parent], opts);
1551
+ pt._helpers = transforms;
1552
+ return pt;
1553
+ }
1554
+ });
1555
+ }
1556
+ });
1557
+
1558
+ // src/core/scene/kinds/point-constraints/orthocenter.ts
1559
+ var orthocenterConstraint;
1560
+ var init_orthocenter = __esm({
1561
+ "src/core/scene/kinds/point-constraints/orthocenter.ts"() {
1562
+ init_types2();
1563
+ orthocenterConstraint = definePointConstraint({
1564
+ kind: "orthocenter",
1565
+ validate: (c) => {
1566
+ if (!Array.isArray(c.vertices) || c.vertices.length !== 3) {
1567
+ throw new Error("point.orthocenter: vertices ph\u1EA3i l\xE0 tuple 3 id");
1568
+ }
1569
+ if (!c.vertices[0] || !c.vertices[1] || !c.vertices[2]) {
1570
+ throw new Error("point.orthocenter: 3 vertex id ph\u1EA3i non-empty");
1571
+ }
1572
+ },
1573
+ describe: (obj, state, c) => {
1574
+ const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
1575
+ return `${obj.label} = tr\u1EF1c t\xE2m \u0394${labels}`;
1576
+ },
1577
+ render: (obj, ctx, c, opts) => {
1578
+ const board = ctx.jxg;
1579
+ const a = ctx.resolveRef(c.vertices[0]);
1580
+ const b = ctx.resolveRef(c.vertices[1]);
1581
+ const c3 = ctx.resolveRef(c.vertices[2]);
1582
+ const hide = { visible: false, withLabel: false, fixed: true, name: "" };
1583
+ const lineBC = board.create("line", [b, c3], hide);
1584
+ const altA = board.create("perpendicular", [lineBC, a], hide);
1585
+ const lineAC = board.create("line", [a, c3], hide);
1586
+ const altB = board.create("perpendicular", [lineAC, b], hide);
1587
+ const ortho = board.create("intersection", [altA, altB, 0], opts);
1588
+ ortho._helpers = [lineBC, altA, lineAC, altB];
1589
+ return ortho;
1590
+ }
1591
+ });
1592
+ }
1593
+ });
1594
+
1595
+ // src/core/scene/kinds/point-constraints/onPerpendicular.ts
1596
+ var onPerpendicularConstraint;
1597
+ var init_onPerpendicular = __esm({
1598
+ "src/core/scene/kinds/point-constraints/onPerpendicular.ts"() {
1599
+ init_types2();
1600
+ onPerpendicularConstraint = definePointConstraint({
1601
+ kind: "onPerpendicular",
1602
+ describe: (obj) => `\u0110i\u1EC3m ${obj.label}`,
1603
+ render: (obj, ctx, c, opts) => {
1604
+ const board = ctx.jxg;
1605
+ const T = ctx.resolveRef(c.through);
1606
+ const A = ctx.resolveRef(c.perpToA);
1607
+ const B = ctx.resolveRef(c.perpToB);
1608
+ const hide = { visible: false, withLabel: false, fixed: true, name: "" };
1609
+ const refLine = board.create("line", [A, B], hide);
1610
+ const perpLine = board.create("perpendicular", [refLine, T], hide);
1611
+ const dx = B.X() - A.X();
1612
+ const dy = B.Y() - A.Y();
1613
+ const len = Math.hypot(dx, dy) || 1;
1614
+ const ux = -dy / len;
1615
+ const uy = dx / len;
1616
+ const x0 = T.X() + c.t * ux;
1617
+ const y0 = T.Y() + c.t * uy;
1618
+ const gl = board.create("glider", [x0, y0, perpLine], opts);
1619
+ gl._helpers = [refLine, perpLine];
1620
+ return gl;
1621
+ }
1622
+ });
1623
+ }
1624
+ });
1625
+
1626
+ // src/core/scene/kinds/point-constraints/onPerpBisector.ts
1627
+ var onPerpBisectorConstraint;
1628
+ var init_onPerpBisector = __esm({
1629
+ "src/core/scene/kinds/point-constraints/onPerpBisector.ts"() {
1630
+ init_types2();
1631
+ onPerpBisectorConstraint = definePointConstraint({
1632
+ kind: "onPerpBisector",
1633
+ describe: (obj) => `\u0110i\u1EC3m ${obj.label}`,
1634
+ render: (obj, ctx, c, opts) => {
1635
+ const board = ctx.jxg;
1636
+ const A = ctx.resolveRef(c.p1);
1637
+ const B = ctx.resolveRef(c.p2);
1638
+ const hide = { visible: false, withLabel: false, fixed: true, name: "" };
1639
+ const refLine = board.create("line", [A, B], hide);
1640
+ const mid = board.create("midpoint", [A, B], hide);
1641
+ const bisLine = board.create("perpendicular", [refLine, mid], hide);
1642
+ const Mx = (A.X() + B.X()) / 2;
1643
+ const My = (A.Y() + B.Y()) / 2;
1644
+ const dx = B.X() - A.X();
1645
+ const dy = B.Y() - A.Y();
1646
+ const len = Math.hypot(dx, dy) || 1;
1647
+ const ux = -dy / len;
1648
+ const uy = dx / len;
1649
+ const x0 = Mx + c.t * ux;
1650
+ const y0 = My + c.t * uy;
1651
+ const gl = board.create("glider", [x0, y0, bisLine], opts);
1652
+ gl._helpers = [refLine, mid, bisLine];
1653
+ return gl;
1654
+ }
1655
+ });
1656
+ }
1657
+ });
1658
+
1659
+ // src/core/scene/kinds/point-constraints/onCircleAroundPoint.ts
1660
+ var onCircleAroundPointConstraint;
1661
+ var init_onCircleAroundPoint = __esm({
1662
+ "src/core/scene/kinds/point-constraints/onCircleAroundPoint.ts"() {
1663
+ init_types2();
1664
+ onCircleAroundPointConstraint = definePointConstraint({
1665
+ kind: "onCircleAroundPoint",
1666
+ describe: (obj) => `\u0110i\u1EC3m ${obj.label}`,
1667
+ render: (obj, ctx, c, opts) => {
1668
+ const board = ctx.jxg;
1669
+ const C = ctx.resolveRef(c.center);
1670
+ const R = ctx.resolveRef(c.radiusPoint);
1671
+ const hide = { visible: false, withLabel: false, fixed: true, name: "" };
1672
+ const auxCircle = board.create("circle", [C, R], hide);
1673
+ const r = Math.hypot(R.X() - C.X(), R.Y() - C.Y());
1674
+ const x0 = C.X() + r * Math.cos(c.theta);
1675
+ const y0 = C.Y() + r * Math.sin(c.theta);
1676
+ const gl = board.create("glider", [x0, y0, auxCircle], opts);
1677
+ gl._helpers = [auxCircle];
1678
+ return gl;
1679
+ }
1680
+ });
1681
+ }
1682
+ });
1683
+
1684
+ // src/core/scene/kinds/point-constraints/tangentPointExt.ts
1685
+ var tangentPointExtConstraint;
1686
+ var init_tangentPointExt = __esm({
1687
+ "src/core/scene/kinds/point-constraints/tangentPointExt.ts"() {
1688
+ init_types2();
1689
+ tangentPointExtConstraint = definePointConstraint({
1690
+ kind: "tangentPointExt",
1691
+ describe: (obj, state, c) => {
1692
+ const fromLabel = state?.objects[c.from]?.label ?? c.from;
1693
+ const circleLabel = state?.objects[c.circle]?.label ?? c.circle;
1694
+ return `${obj.label} = ti\u1EBFp \u0111i\u1EC3m c\u1EE7a (${circleLabel}) v\u1EDBi ti\u1EBFp tuy\u1EBFn t\u1EEB ${fromLabel}`;
1695
+ },
1696
+ render: (obj, ctx, c, opts) => {
1697
+ const board = ctx.jxg;
1698
+ const P = ctx.resolveRef(c.from);
1699
+ const K = ctx.resolveRef(c.circle);
1700
+ const O = K.center ?? K.midpoint;
1701
+ const hide = { visible: false, withLabel: false, fixed: true, name: "" };
1702
+ const mid = board.create("midpoint", [P, O], hide);
1703
+ const thales = board.create("circle", [mid, P], hide);
1704
+ const inter = board.create("intersection", [thales, K, c.which], opts);
1705
+ inter._helpers = [mid, thales];
1706
+ return inter;
1707
+ }
1708
+ });
1709
+ }
1710
+ });
1711
+
1712
+ // src/core/scene/kinds/point-constraints/registry.ts
1713
+ var ALL, POINT_CONSTRAINTS;
1714
+ var init_registry2 = __esm({
1715
+ "src/core/scene/kinds/point-constraints/registry.ts"() {
1716
+ init_free();
1717
+ init_onAxis();
1718
+ init_midpoint();
1719
+ init_perpFoot();
1720
+ init_circumcenter();
1721
+ init_incenter();
1722
+ init_onLine();
1723
+ init_onSegment();
1724
+ init_onCircle();
1725
+ init_onPolygon();
1726
+ init_centroid();
1727
+ init_arcMidpoint();
1728
+ init_excenter();
1729
+ init_pointAtDistance();
1730
+ init_circleIntersection();
1731
+ init_circleSecondIntersection();
1732
+ init_secondIntersection();
1733
+ init_tangencyPoint();
1734
+ init_transformed();
1735
+ init_orthocenter();
1736
+ init_onPerpendicular();
1737
+ init_onPerpBisector();
1738
+ init_onCircleAroundPoint();
1739
+ init_tangentPointExt();
1740
+ ALL = [
1741
+ freeConstraint,
1742
+ onAxisConstraint,
1743
+ midpointConstraint,
1744
+ perpFootConstraint,
1745
+ circumcenterConstraint,
1746
+ incenterConstraint,
1747
+ onLineConstraint,
1748
+ onSegmentConstraint,
1749
+ onCircleConstraint,
1750
+ onPolygonConstraint,
1751
+ centroidConstraint,
1752
+ arcMidpointConstraint,
1753
+ excenterConstraint,
1754
+ pointAtDistanceConstraint,
1755
+ circleIntersectionConstraint,
1756
+ circleSecondIntersectionConstraint,
1757
+ secondIntersectionConstraint,
1758
+ tangencyPointConstraint,
1759
+ transformedConstraint,
1760
+ orthocenterConstraint,
1761
+ onPerpendicularConstraint,
1762
+ onPerpBisectorConstraint,
1763
+ onCircleAroundPointConstraint,
1764
+ tangentPointExtConstraint
1765
+ ];
1766
+ POINT_CONSTRAINTS = new Map(ALL.map((m) => [m.kind, m]));
1767
+ }
1768
+ });
1769
+
1770
+ // src/core/scene/kinds/point.ts
962
1771
  var def3;
963
1772
  var init_point = __esm({
964
1773
  "src/core/scene/kinds/point.ts"() {
965
1774
  init_registry();
966
1775
  init_d_constraint2();
1776
+ init_registry2();
1777
+ init_shared();
967
1778
  def3 = {
968
1779
  type: "point",
969
1780
  schemaVersion: 1,
@@ -973,43 +1784,7 @@ var init_point = __esm({
973
1784
  throw new Error("point: constraint required");
974
1785
  }
975
1786
  const c = a.constraint;
976
- if (c.kind === "perpFoot") {
977
- if (!c.from || !c.onLine) {
978
- throw new Error("point.perpFoot: from v\xE0 onLine b\u1EAFt bu\u1ED9c");
979
- }
980
- }
981
- if (c.kind === "circumcenter") {
982
- if (!Array.isArray(c.vertices) || c.vertices.length !== 3) {
983
- throw new Error("point.circumcenter: vertices ph\u1EA3i l\xE0 tuple 3 id");
984
- }
985
- if (!c.vertices[0] || !c.vertices[1] || !c.vertices[2]) {
986
- throw new Error("point.circumcenter: 3 vertex id ph\u1EA3i non-empty");
987
- }
988
- }
989
- if (c.kind === "incenter") {
990
- if (!Array.isArray(c.vertices) || c.vertices.length !== 3) {
991
- throw new Error("point.incenter: vertices ph\u1EA3i l\xE0 tuple 3 id");
992
- }
993
- if (!c.vertices[0] || !c.vertices[1] || !c.vertices[2]) {
994
- throw new Error("point.incenter: 3 vertex id ph\u1EA3i non-empty");
995
- }
996
- }
997
- if (c.kind === "centroid") {
998
- if (!Array.isArray(c.vertices) || c.vertices.length !== 3) {
999
- throw new Error("point.centroid: vertices ph\u1EA3i l\xE0 tuple 3 id");
1000
- }
1001
- if (!c.vertices[0] || !c.vertices[1] || !c.vertices[2]) {
1002
- throw new Error("point.centroid: 3 vertex id ph\u1EA3i non-empty");
1003
- }
1004
- }
1005
- if (c.kind === "orthocenter") {
1006
- if (!Array.isArray(c.vertices) || c.vertices.length !== 3) {
1007
- throw new Error("point.orthocenter: vertices ph\u1EA3i l\xE0 tuple 3 id");
1008
- }
1009
- if (!c.vertices[0] || !c.vertices[1] || !c.vertices[2]) {
1010
- throw new Error("point.orthocenter: 3 vertex id ph\u1EA3i non-empty");
1011
- }
1012
- }
1787
+ POINT_CONSTRAINTS.get(c.kind)?.validate?.(c);
1013
1788
  },
1014
1789
  dependsOn: (a) => constraintRefs2D(a.constraint),
1015
1790
  measure: (obj) => {
@@ -1024,132 +1799,16 @@ var init_point = __esm({
1024
1799
  },
1025
1800
  describe: (obj, state) => {
1026
1801
  const c = obj.attrs.constraint;
1027
- if (c.kind === "free") return `\u0110i\u1EC3m ${obj.label}`;
1028
- if (c.kind === "onAxis") return `${obj.label} tr\xEAn tr\u1EE5c ${c.axis}`;
1029
- if (c.kind === "onLine") return `${obj.label} tr\xEAn \u0111\u01B0\u1EDDng ${state?.objects[c.lineId]?.label ?? c.lineId}`;
1030
- if (c.kind === "onSegment") return `${obj.label} tr\xEAn \u0111o\u1EA1n ${state?.objects[c.segmentId]?.label ?? c.segmentId}`;
1031
- if (c.kind === "onCircle") return `${obj.label} tr\xEAn \u0111\u01B0\u1EDDng tr\xF2n ${state?.objects[c.circleId]?.label ?? c.circleId}`;
1032
- if (c.kind === "onPolygon") return `${obj.label} tr\xEAn \u0111a gi\xE1c ${state?.objects[c.polygonId]?.label ?? c.polygonId}`;
1033
- if (c.kind === "midpoint") {
1034
- const l1 = state?.objects[c.p1]?.label ?? c.p1;
1035
- const l2 = state?.objects[c.p2]?.label ?? c.p2;
1036
- return `${obj.label} = trung \u0111i\u1EC3m ${l1}${l2}`;
1037
- }
1038
- if (c.kind === "transformed") {
1039
- const t = c.transform;
1040
- const labelRef = (id) => state?.objects[id]?.label ?? id;
1041
- 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)}` : "";
1042
- return `${obj.label} = \u1EA3nh c\u1EE7a ${labelRef(c.source)} (${op})`;
1043
- }
1044
- if (c.kind === "perpFoot") {
1045
- const fromLabel = state?.objects[c.from]?.label ?? c.from;
1046
- const lineLabel = state?.objects[c.onLine]?.label ?? c.onLine;
1047
- return `${obj.label} = ch\xE2n \u27C2 t\u1EEB ${fromLabel} xu\u1ED1ng ${lineLabel}`;
1048
- }
1049
- if (c.kind === "circumcenter") {
1050
- const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
1051
- return `${obj.label} = t\xE2m ngo\u1EA1i ti\u1EBFp \u0394${labels}`;
1052
- }
1053
- if (c.kind === "incenter") {
1054
- const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
1055
- return `${obj.label} = t\xE2m n\u1ED9i ti\u1EBFp \u0394${labels}`;
1056
- }
1057
- if (c.kind === "centroid") {
1058
- const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
1059
- return `${obj.label} = tr\u1ECDng t\xE2m \u0394${labels}`;
1060
- }
1061
- if (c.kind === "orthocenter") {
1062
- const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
1063
- return `${obj.label} = tr\u1EF1c t\xE2m \u0394${labels}`;
1064
- }
1802
+ const mod = POINT_CONSTRAINTS.get(c.kind);
1803
+ if (mod) return mod.describe(obj, state, c);
1065
1804
  return `\u0110i\u1EC3m ${obj.label}`;
1066
1805
  },
1067
1806
  render: (obj, ctx) => {
1068
1807
  const board = ctx.jxg;
1069
1808
  const c = obj.attrs.constraint;
1070
- const opts = {
1071
- name: obj.label,
1072
- withLabel: obj.attrs.showLabel ?? true,
1073
- visible: obj.visible,
1074
- fixed: obj.locked,
1075
- strokeColor: obj.attrs.color ?? "#1e40af",
1076
- fillColor: obj.attrs.color ?? "#1e40af",
1077
- face: obj.attrs.face ?? "o",
1078
- size: obj.attrs.size ?? 4
1079
- };
1080
- if (c.kind === "free") return board.create("point", [c.x, c.y], opts);
1081
- if (c.kind === "onAxis") {
1082
- const coords = c.axis === "x" ? [c.t, 0] : [0, c.t];
1083
- return board.create("point", coords, opts);
1084
- }
1085
- if (c.kind === "onLine") {
1086
- const line = ctx.resolveRef(c.lineId);
1087
- return board.create("glider", [c.t, c.t, line], opts);
1088
- }
1089
- if (c.kind === "onSegment") {
1090
- const seg = ctx.resolveRef(c.segmentId);
1091
- return board.create("glider", [c.t, c.t, seg], opts);
1092
- }
1093
- if (c.kind === "onCircle") {
1094
- const circle = ctx.resolveRef(c.circleId);
1095
- return board.create("glider", [Math.cos(c.theta), Math.sin(c.theta), circle], opts);
1096
- }
1097
- if (c.kind === "onPolygon") {
1098
- const poly = ctx.resolveRef(c.polygonId);
1099
- return board.create("glider", [c.u, c.v, poly], opts);
1100
- }
1101
- if (c.kind === "midpoint") {
1102
- const p1 = ctx.resolveRef(c.p1);
1103
- const p2 = ctx.resolveRef(c.p2);
1104
- return board.create("midpoint", [p1, p2], opts);
1105
- }
1106
- if (c.kind === "transformed") {
1107
- const src = ctx.resolveRef(c.source);
1108
- const transforms = buildJxgTransforms(board, ctx, c.transform);
1109
- const parent = transforms.length === 1 ? transforms[0] : transforms;
1110
- const pt = board.create("point", [src, parent], opts);
1111
- pt._helpers = transforms;
1112
- return pt;
1113
- }
1114
- if (c.kind === "perpFoot") {
1115
- const from = ctx.resolveRef(c.from);
1116
- const onLine = ctx.resolveRef(c.onLine);
1117
- return board.create("perpendicularpoint", [onLine, from], opts);
1118
- }
1119
- if (c.kind === "circumcenter") {
1120
- const a = ctx.resolveRef(c.vertices[0]);
1121
- const b = ctx.resolveRef(c.vertices[1]);
1122
- const c3 = ctx.resolveRef(c.vertices[2]);
1123
- return board.create("circumcenter", [a, b, c3], opts);
1124
- }
1125
- if (c.kind === "incenter") {
1126
- const a = ctx.resolveRef(c.vertices[0]);
1127
- const b = ctx.resolveRef(c.vertices[1]);
1128
- const c3 = ctx.resolveRef(c.vertices[2]);
1129
- return board.create("incenter", [a, b, c3], opts);
1130
- }
1131
- if (c.kind === "centroid") {
1132
- const a = ctx.resolveRef(c.vertices[0]);
1133
- const b = ctx.resolveRef(c.vertices[1]);
1134
- const c3 = ctx.resolveRef(c.vertices[2]);
1135
- return board.create("point", [
1136
- () => (a.X() + b.X() + c3.X()) / 3,
1137
- () => (a.Y() + b.Y() + c3.Y()) / 3
1138
- ], opts);
1139
- }
1140
- if (c.kind === "orthocenter") {
1141
- const a = ctx.resolveRef(c.vertices[0]);
1142
- const b = ctx.resolveRef(c.vertices[1]);
1143
- const c3 = ctx.resolveRef(c.vertices[2]);
1144
- const hide = { visible: false, withLabel: false, fixed: true, name: "" };
1145
- const lineBC = board.create("line", [b, c3], hide);
1146
- const altA = board.create("perpendicular", [lineBC, a], hide);
1147
- const lineAC = board.create("line", [a, c3], hide);
1148
- const altB = board.create("perpendicular", [lineAC, b], hide);
1149
- const ortho = board.create("intersection", [altA, altB, 0], opts);
1150
- ortho._helpers = [lineBC, altA, lineAC, altB];
1151
- return ortho;
1152
- }
1809
+ const opts = buildPointOpts(obj);
1810
+ const mod = POINT_CONSTRAINTS.get(c.kind);
1811
+ if (mod) return mod.render(obj, ctx, c, opts);
1153
1812
  return board.create("point", [0, 0], opts);
1154
1813
  },
1155
1814
  /**
@@ -1258,6 +1917,10 @@ function constructionRefs(c) {
1258
1917
  return [c.p1, c.vertex, c.p2];
1259
1918
  case "angleBisectorLines":
1260
1919
  return [stripBorderSuffix(c.line1), stripBorderSuffix(c.line2)];
1920
+ case "lineThrough":
1921
+ return [...c.points];
1922
+ case "radicalAxis":
1923
+ return [c.circle1, c.circle2];
1261
1924
  case "tangent":
1262
1925
  return [c.throughPoint, c.toCircle];
1263
1926
  }
@@ -1267,6 +1930,7 @@ var init_line = __esm({
1267
1930
  "src/core/scene/kinds/line.ts"() {
1268
1931
  init_registry();
1269
1932
  init_labelOf();
1933
+ init_pointConstructions();
1270
1934
  def5 = {
1271
1935
  type: "line",
1272
1936
  schemaVersion: 1,
@@ -1291,6 +1955,10 @@ var init_line = __esm({
1291
1955
  return `${obj.label}: ph\xE2n gi\xE1c g\xF3c ${L(c.p1)}${L(c.vertex)}${L(c.p2)}`;
1292
1956
  case "angleBisectorLines":
1293
1957
  return `${obj.label}: ph\xE2n gi\xE1c ${L(c.line1)} & ${L(c.line2)} (${c.branch === 0 ? "1" : "2"})`;
1958
+ case "lineThrough":
1959
+ return `${obj.label}: \u0111\u01B0\u1EDDng qua ${c.points.map(L).join("")}`;
1960
+ case "radicalAxis":
1961
+ return `${obj.label}: tr\u1EE5c \u0111\u1EB3ng ph\u01B0\u01A1ng ${L(c.circle1)} & ${L(c.circle2)}`;
1294
1962
  case "tangent":
1295
1963
  return `${obj.label}: ti\u1EBFp tuy\u1EBFn ${L(c.toCircle)} qua ${L(c.throughPoint)}`;
1296
1964
  }
@@ -1371,6 +2039,47 @@ var init_line = __esm({
1371
2039
  selected._helpers = [other];
1372
2040
  return selected;
1373
2041
  }
2042
+ case "lineThrough": {
2043
+ const pts = c.points.map((id) => ctx.resolveRef(id));
2044
+ let bi = 0, bj = 1, best = -1;
2045
+ for (let i = 0; i < pts.length; i++) {
2046
+ for (let j = i + 1; j < pts.length; j++) {
2047
+ const dx = pts[i].X() - pts[j].X();
2048
+ const dy = pts[i].Y() - pts[j].Y();
2049
+ const d = dx * dx + dy * dy;
2050
+ if (d > best) {
2051
+ best = d;
2052
+ bi = i;
2053
+ bj = j;
2054
+ }
2055
+ }
2056
+ }
2057
+ return board.create("line", [pts[bi], pts[bj]], {
2058
+ ...baseOpts,
2059
+ straightFirst: true,
2060
+ straightLast: true
2061
+ });
2062
+ }
2063
+ case "radicalAxis": {
2064
+ const k1 = ctx.resolveRef(c.circle1);
2065
+ const k2 = ctx.resolveRef(c.circle2);
2066
+ const o1 = () => [k1.center.X(), k1.center.Y()];
2067
+ const o2 = () => [k2.center.X(), k2.center.Y()];
2068
+ const foot = () => radicalAxisFoot(o1(), k1.Radius(), o2(), k2.Radius());
2069
+ const hide = { visible: false, withLabel: false, fixed: true, name: "" };
2070
+ const f1 = board.create("point", [() => foot()[0], () => foot()[1]], hide);
2071
+ const f2 = board.create("point", [
2072
+ () => foot()[0] - (o2()[1] - o1()[1]),
2073
+ () => foot()[1] + (o2()[0] - o1()[0])
2074
+ ], hide);
2075
+ const line = board.create("line", [f1, f2], {
2076
+ ...baseOpts,
2077
+ straightFirst: true,
2078
+ straightLast: true
2079
+ });
2080
+ line._helpers = [f1, f2];
2081
+ return line;
2082
+ }
1374
2083
  case "tangent": {
1375
2084
  const through = ctx.resolveRef(c.throughPoint);
1376
2085
  const toCircle = ctx.resolveRef(c.toCircle);
@@ -1489,10 +2198,29 @@ var init_vector = __esm({
1489
2198
  });
1490
2199
 
1491
2200
  // src/core/scene/kinds/circle.ts
2201
+ function asConstruction(a) {
2202
+ if (a.construction) return a.construction;
2203
+ const raw = a;
2204
+ if (raw.kind === "incircle" && raw.vertices) {
2205
+ return {
2206
+ kind: "incircle",
2207
+ p1: raw.vertices[0],
2208
+ p2: raw.vertices[1],
2209
+ p3: raw.vertices[2]
2210
+ };
2211
+ }
2212
+ return void 0;
2213
+ }
1492
2214
  function constructionRefs2(c) {
1493
2215
  switch (c.kind) {
1494
2216
  case "circumscribed":
1495
2217
  return [c.p1, c.p2, c.p3];
2218
+ case "incircle":
2219
+ return [c.p1, c.p2, c.p3];
2220
+ case "excircle":
2221
+ return [c.p1, c.p2, c.p3];
2222
+ case "diameter":
2223
+ return [c.p1, c.p2];
1496
2224
  }
1497
2225
  }
1498
2226
  var def8;
@@ -1500,19 +2228,33 @@ var init_circle = __esm({
1500
2228
  "src/core/scene/kinds/circle.ts"() {
1501
2229
  init_registry();
1502
2230
  init_labelOf();
2231
+ init_pointConstructions();
1503
2232
  def8 = {
1504
2233
  type: "circle",
1505
2234
  schemaVersion: 1,
1506
2235
  migrate: {},
1507
2236
  validate: (a) => {
1508
- if (a?.construction) return;
2237
+ if (asConstruction(a)) return;
2238
+ if (typeof a?.radius === "number") {
2239
+ if (!a.center) throw new Error("circle: center b\u1EAFt bu\u1ED9c khi d\xF9ng radius");
2240
+ if (!(a.radius > 0)) throw new Error("circle: radius ph\u1EA3i > 0");
2241
+ return;
2242
+ }
1509
2243
  if (!a?.center || !a?.surfacePoint) {
1510
- throw new Error("circle: center v\xE0 surfacePoint b\u1EAFt bu\u1ED9c (ho\u1EB7c construction)");
2244
+ throw new Error("circle: center v\xE0 surfacePoint b\u1EAFt bu\u1ED9c (ho\u1EB7c construction / radius)");
1511
2245
  }
1512
2246
  },
1513
- dependsOn: (a) => a.construction ? constructionRefs2(a.construction) : [a.center, a.surfacePoint],
2247
+ dependsOn: (a) => {
2248
+ const c = asConstruction(a);
2249
+ if (c) return constructionRefs2(c);
2250
+ if (typeof a.radius === "number") return [a.center];
2251
+ return [a.center, a.surfacePoint];
2252
+ },
1514
2253
  measure: (obj, state) => {
1515
- if (obj.attrs.construction) return null;
2254
+ if (asConstruction(obj.attrs)) return null;
2255
+ if (typeof obj.attrs.radius === "number") {
2256
+ return [{ label: "r", value: obj.attrs.radius }];
2257
+ }
1516
2258
  const center = obj.attrs.center ? state.objects[obj.attrs.center] : void 0;
1517
2259
  const surface = obj.attrs.surfacePoint ? state.objects[obj.attrs.surfacePoint] : void 0;
1518
2260
  if (!center || !surface) return null;
@@ -1525,10 +2267,22 @@ var init_circle = __esm({
1525
2267
  },
1526
2268
  describe: (obj, state) => {
1527
2269
  const L = (id) => labelOf(id, state);
1528
- const c = obj.attrs.construction;
2270
+ const c = asConstruction(obj.attrs);
1529
2271
  if (c?.kind === "circumscribed") {
1530
2272
  return `\u0110\u01B0\u1EDDng tr\xF2n \u0111i qua ${L(c.p1)}${L(c.p2)}${L(c.p3)}`;
1531
2273
  }
2274
+ if (c?.kind === "incircle") {
2275
+ return `\u0110\u01B0\u1EDDng tr\xF2n n\u1ED9i ti\u1EBFp \u0394${L(c.p1)}${L(c.p2)}${L(c.p3)}`;
2276
+ }
2277
+ if (c?.kind === "excircle") {
2278
+ 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)}`;
2279
+ }
2280
+ if (c?.kind === "diameter") {
2281
+ return `\u0110\u01B0\u1EDDng tr\xF2n \u0111\u01B0\u1EDDng k\xEDnh ${L(c.p1)}${L(c.p2)}`;
2282
+ }
2283
+ if (typeof obj.attrs.radius === "number") {
2284
+ return `\u0110\u01B0\u1EDDng tr\xF2n t\xE2m ${L(obj.attrs.center)} b\xE1n k\xEDnh ${obj.attrs.radius}`;
2285
+ }
1532
2286
  return `\u0110\u01B0\u1EDDng tr\xF2n t\xE2m ${L(obj.attrs.center)} b\xE1n k\xEDnh ${L(obj.attrs.center)}${L(obj.attrs.surfacePoint)}`;
1533
2287
  },
1534
2288
  render: (obj, ctx) => {
@@ -1543,13 +2297,66 @@ var init_circle = __esm({
1543
2297
  visible: obj.visible,
1544
2298
  fixed: obj.locked
1545
2299
  };
1546
- const c = obj.attrs.construction;
2300
+ const c = asConstruction(obj.attrs);
1547
2301
  if (c?.kind === "circumscribed") {
1548
2302
  const p1 = ctx.resolveRef(c.p1);
1549
2303
  const p2 = ctx.resolveRef(c.p2);
1550
2304
  const p3 = ctx.resolveRef(c.p3);
1551
2305
  return board.create("circumcircle", [p1, p2, p3], baseOpts);
1552
2306
  }
2307
+ if (c?.kind === "incircle") {
2308
+ const p1 = ctx.resolveRef(c.p1);
2309
+ const p2 = ctx.resolveRef(c.p2);
2310
+ const p3 = ctx.resolveRef(c.p3);
2311
+ const center2 = board.create("incenter", [p1, p2, p3], {
2312
+ visible: obj.visible,
2313
+ withLabel: true,
2314
+ fixed: true,
2315
+ name: obj.label
2316
+ });
2317
+ const circ = board.create("incircle", [p1, p2, p3], baseOpts);
2318
+ circ.center = circ.center ?? center2;
2319
+ circ._helpers = [center2];
2320
+ return circ;
2321
+ }
2322
+ if (c?.kind === "excircle") {
2323
+ const P = [ctx.resolveRef(c.p1), ctx.resolveRef(c.p2), ctx.resolveRef(c.p3)];
2324
+ const ids = [c.p1, c.p2, c.p3];
2325
+ const oppIdx = Math.max(0, ids.indexOf(c.opposite));
2326
+ const verts = () => [
2327
+ [P[0].X(), P[0].Y()],
2328
+ [P[1].X(), P[1].Y()],
2329
+ [P[2].X(), P[2].Y()]
2330
+ ];
2331
+ const ctr = () => excenter(verts(), oppIdx);
2332
+ const radius = () => {
2333
+ const I = ctr();
2334
+ const others = [0, 1, 2].filter((i) => i !== oppIdx);
2335
+ const v = verts();
2336
+ const a = v[others[0]];
2337
+ const b = v[others[1]];
2338
+ const dx = b[0] - a[0];
2339
+ const dy = b[1] - a[1];
2340
+ const len = Math.hypot(dx, dy) || 1;
2341
+ return Math.abs((I[0] - a[0]) * dy - (I[1] - a[1]) * dx) / len;
2342
+ };
2343
+ const center2 = board.create("point", [() => ctr()[0], () => ctr()[1]], { visible: false, withLabel: false, fixed: true, name: "" });
2344
+ const circ = board.create("circle", [center2, () => radius()], baseOpts);
2345
+ circ._helpers = [center2];
2346
+ return circ;
2347
+ }
2348
+ if (c?.kind === "diameter") {
2349
+ const p1 = ctx.resolveRef(c.p1);
2350
+ const p2 = ctx.resolveRef(c.p2);
2351
+ const center2 = board.create("midpoint", [p1, p2], { visible: false, withLabel: false, fixed: true, name: "" });
2352
+ const circ = board.create("circle", [center2, p2], baseOpts);
2353
+ circ._helpers = [center2];
2354
+ return circ;
2355
+ }
2356
+ if (typeof obj.attrs.radius === "number") {
2357
+ const center2 = ctx.resolveRef(obj.attrs.center);
2358
+ return board.create("circle", [center2, obj.attrs.radius], baseOpts);
2359
+ }
1553
2360
  const center = ctx.resolveRef(obj.attrs.center);
1554
2361
  const surface = ctx.resolveRef(obj.attrs.surfacePoint);
1555
2362
  return board.create("circle", [center, surface], baseOpts);
@@ -1709,6 +2516,26 @@ function regularVertexLabels(p1Label, p2Label, n) {
1709
2516
  }
1710
2517
  return `${p1Label}${p2Label}\u2026`;
1711
2518
  }
2519
+ function specialShapeName(kind) {
2520
+ switch (kind) {
2521
+ case "square":
2522
+ return "H\xECnh vu\xF4ng";
2523
+ case "rectangle":
2524
+ return "H\xECnh ch\u1EEF nh\u1EADt";
2525
+ case "rhombus":
2526
+ return "H\xECnh thoi";
2527
+ case "parallelogram":
2528
+ return "H\xECnh b\xECnh h\xE0nh";
2529
+ case "isoTrapezoid":
2530
+ return "H\xECnh thang c\xE2n";
2531
+ case "isoTriangle":
2532
+ return "Tam gi\xE1c c\xE2n";
2533
+ case "rightTriangle":
2534
+ return "Tam gi\xE1c vu\xF4ng";
2535
+ case "regular":
2536
+ return "";
2537
+ }
2538
+ }
1712
2539
  var def11;
1713
2540
  var init_polygon = __esm({
1714
2541
  "src/core/scene/kinds/polygon.ts"() {
@@ -1720,13 +2547,27 @@ var init_polygon = __esm({
1720
2547
  migrate: {},
1721
2548
  validate: (a) => {
1722
2549
  if (a?.construction) {
1723
- if (a.construction.kind === "regular") {
1724
- if (!a.construction.p1 || !a.construction.p2) {
1725
- throw new Error("polygon (regular): p1 v\xE0 p2 b\u1EAFt bu\u1ED9c");
1726
- }
1727
- if (!Number.isFinite(a.construction.n) || a.construction.n < 3) {
1728
- throw new Error("polygon (regular): n \u2265 3");
1729
- }
2550
+ const c = a.construction;
2551
+ if (c.kind === "regular") {
2552
+ if (!c.p1 || !c.p2) throw new Error("polygon (regular): p1 v\xE0 p2 b\u1EAFt bu\u1ED9c");
2553
+ if (!Number.isFinite(c.n) || c.n < 3) throw new Error("polygon (regular): n \u2265 3");
2554
+ return;
2555
+ }
2556
+ if (c.kind === "square") {
2557
+ if (!c.p1 || !c.p2) throw new Error("polygon (square): p1 v\xE0 p2 b\u1EAFt bu\u1ED9c");
2558
+ return;
2559
+ }
2560
+ if (c.kind === "rectangle" || c.kind === "rhombus" || c.kind === "parallelogram" || c.kind === "isoTrapezoid") {
2561
+ if (!c.p1 || !c.p2 || !c.p3) throw new Error(`polygon (${c.kind}): p1, p2, p3 b\u1EAFt bu\u1ED9c`);
2562
+ return;
2563
+ }
2564
+ if (c.kind === "isoTriangle") {
2565
+ if (!c.base1 || !c.base2 || !c.apex) throw new Error("polygon (isoTriangle): base1, base2, apex b\u1EAFt bu\u1ED9c");
2566
+ return;
2567
+ }
2568
+ if (c.kind === "rightTriangle") {
2569
+ if (!c.rightAngle || !c.leg1End || !c.leg2End) throw new Error("polygon (rightTriangle): rightAngle, leg1End, leg2End b\u1EAFt bu\u1ED9c");
2570
+ return;
1730
2571
  }
1731
2572
  return;
1732
2573
  }
@@ -1735,14 +2576,51 @@ var init_polygon = __esm({
1735
2576
  }
1736
2577
  },
1737
2578
  dependsOn: (a) => {
1738
- if (a.construction?.kind === "regular") return [a.construction.p1, a.construction.p2];
1739
- return [...a.vertices ?? []];
2579
+ const c = a.construction;
2580
+ if (!c) return [...a.vertices ?? []];
2581
+ switch (c.kind) {
2582
+ case "regular":
2583
+ return [c.p1, c.p2];
2584
+ case "square":
2585
+ return [c.p1, c.p2];
2586
+ case "rectangle":
2587
+ case "rhombus":
2588
+ case "parallelogram":
2589
+ case "isoTrapezoid":
2590
+ return [c.p1, c.p2, c.p3];
2591
+ case "isoTriangle":
2592
+ return [c.base1, c.base2, c.apex];
2593
+ case "rightTriangle":
2594
+ return [c.rightAngle, c.leg1End, c.leg2End];
2595
+ }
1740
2596
  },
1741
2597
  describe: (obj, state) => {
1742
- if (obj.attrs.construction?.kind === "regular") {
1743
- const c = obj.attrs.construction;
1744
- const labels = regularVertexLabels(labelOf(c.p1, state), labelOf(c.p2, state), c.n);
1745
- return `${regularPolygonName(c.n)} ${labels}`;
2598
+ const c = obj.attrs.construction;
2599
+ if (c) {
2600
+ if (c.kind === "regular") {
2601
+ const labels2 = regularVertexLabels(labelOf(c.p1, state), labelOf(c.p2, state), c.n);
2602
+ return `${regularPolygonName(c.n)} ${labels2}`;
2603
+ }
2604
+ const name = specialShapeName(c.kind);
2605
+ let labels = [];
2606
+ switch (c.kind) {
2607
+ case "square":
2608
+ labels = [labelOf(c.p1, state), labelOf(c.p2, state)];
2609
+ break;
2610
+ case "rectangle":
2611
+ case "rhombus":
2612
+ case "parallelogram":
2613
+ case "isoTrapezoid":
2614
+ labels = [labelOf(c.p1, state), labelOf(c.p2, state), labelOf(c.p3, state)];
2615
+ break;
2616
+ case "isoTriangle":
2617
+ labels = [labelOf(c.apex, state), labelOf(c.base1, state), labelOf(c.base2, state)];
2618
+ break;
2619
+ case "rightTriangle":
2620
+ labels = [labelOf(c.rightAngle, state), labelOf(c.leg1End, state), labelOf(c.leg2End, state)];
2621
+ break;
2622
+ }
2623
+ return `${name} ${labels.join("")}`;
1746
2624
  }
1747
2625
  return `\u0110a gi\xE1c ${(obj.attrs.vertices ?? []).map((id) => labelOf(id, state)).join("")}`;
1748
2626
  },
@@ -1750,22 +2628,85 @@ var init_polygon = __esm({
1750
2628
  const board = ctx.jxg;
1751
2629
  const label = obj.label;
1752
2630
  const showValue = obj.attrs.showValue ?? false;
1753
- if (obj.attrs.construction?.kind === "regular") {
1754
- const c = obj.attrs.construction;
1755
- const p1 = ctx.resolveRef(c.p1);
1756
- const p2 = ctx.resolveRef(c.p2);
1757
- return board.create("regularpolygon", [p1, p2, c.n], {
1758
- name: label,
1759
- withLabel: obj.attrs.showLabel ?? false,
1760
- borders: {
1761
- strokeColor: obj.attrs.color ?? "#0f172a",
1762
- strokeWidth: obj.attrs.width ?? 2
1763
- },
1764
- fillColor: obj.attrs.color ?? "#60a5fa",
1765
- fillOpacity: obj.attrs.fillOpacity ?? 0.15,
1766
- visible: obj.visible,
1767
- fixed: obj.locked
2631
+ const cons = obj.attrs.construction;
2632
+ const commonAttrs = {
2633
+ name: label,
2634
+ withLabel: obj.attrs.showLabel ?? false,
2635
+ borders: {
2636
+ strokeColor: obj.attrs.color ?? "#0f172a",
2637
+ strokeWidth: obj.attrs.width ?? 2
2638
+ },
2639
+ fillColor: obj.attrs.color ?? "#60a5fa",
2640
+ fillOpacity: obj.attrs.fillOpacity ?? 0.15,
2641
+ visible: obj.visible,
2642
+ fixed: obj.locked
2643
+ };
2644
+ if (cons?.kind === "regular") {
2645
+ const p1 = ctx.resolveRef(cons.p1);
2646
+ const p2 = ctx.resolveRef(cons.p2);
2647
+ return board.create("regularpolygon", [p1, p2, cons.n], commonAttrs);
2648
+ }
2649
+ if (cons?.kind === "square") {
2650
+ const p1 = ctx.resolveRef(cons.p1);
2651
+ const p2 = ctx.resolveRef(cons.p2);
2652
+ return board.create("regularpolygon", [p1, p2, 4], commonAttrs);
2653
+ }
2654
+ if (cons?.kind === "rectangle" || cons?.kind === "rhombus" || cons?.kind === "parallelogram") {
2655
+ const A = ctx.resolveRef(cons.p1);
2656
+ const B = ctx.resolveRef(cons.p2);
2657
+ const C = ctx.resolveRef(cons.p3);
2658
+ const D = board.create(
2659
+ "point",
2660
+ [() => A.X() + C.X() - B.X(), () => A.Y() + C.Y() - B.Y()],
2661
+ { visible: false, withLabel: false, fixed: true, name: "" }
2662
+ );
2663
+ const poly2 = board.create("polygon", [A, B, C, D], commonAttrs);
2664
+ poly2._helpers = [D];
2665
+ return poly2;
2666
+ }
2667
+ if (cons?.kind === "isoTrapezoid") {
2668
+ const A = ctx.resolveRef(cons.p1);
2669
+ const B = ctx.resolveRef(cons.p2);
2670
+ const C = ctx.resolveRef(cons.p3);
2671
+ const Dx = () => {
2672
+ const Mx = (A.X() + B.X()) / 2;
2673
+ const My = (A.Y() + B.Y()) / 2;
2674
+ const ux = B.X() - A.X();
2675
+ const uy = B.Y() - A.Y();
2676
+ const len2 = ux * ux + uy * uy || 1;
2677
+ const proj = ((C.X() - Mx) * ux + (C.Y() - My) * uy) / len2;
2678
+ return C.X() - 2 * proj * ux;
2679
+ };
2680
+ const Dy = () => {
2681
+ const Mx = (A.X() + B.X()) / 2;
2682
+ const My = (A.Y() + B.Y()) / 2;
2683
+ const ux = B.X() - A.X();
2684
+ const uy = B.Y() - A.Y();
2685
+ const len2 = ux * ux + uy * uy || 1;
2686
+ const proj = ((C.X() - Mx) * ux + (C.Y() - My) * uy) / len2;
2687
+ return C.Y() - 2 * proj * uy;
2688
+ };
2689
+ const D = board.create("point", [Dx, Dy], {
2690
+ visible: false,
2691
+ withLabel: false,
2692
+ fixed: true,
2693
+ name: ""
1768
2694
  });
2695
+ const poly2 = board.create("polygon", [A, B, C, D], commonAttrs);
2696
+ poly2._helpers = [D];
2697
+ return poly2;
2698
+ }
2699
+ if (cons?.kind === "isoTriangle") {
2700
+ const Apex = ctx.resolveRef(cons.apex);
2701
+ const B1 = ctx.resolveRef(cons.base1);
2702
+ const B2 = ctx.resolveRef(cons.base2);
2703
+ return board.create("polygon", [Apex, B1, B2], commonAttrs);
2704
+ }
2705
+ if (cons?.kind === "rightTriangle") {
2706
+ const R = ctx.resolveRef(cons.rightAngle);
2707
+ const P = ctx.resolveRef(cons.leg1End);
2708
+ const Q = ctx.resolveRef(cons.leg2End);
2709
+ return board.create("polygon", [R, P, Q], commonAttrs);
1769
2710
  }
1770
2711
  const verts = (obj.attrs.vertices ?? []).map((id) => ctx.resolveRef(id));
1771
2712
  const poly = board.create("polygon", verts, {
@@ -2464,7 +3405,7 @@ var init_serialize = __esm({
2464
3405
 
2465
3406
  // src/core/scene/render/types.ts
2466
3407
  var DEFAULT_THEME_3D;
2467
- var init_types2 = __esm({
3408
+ var init_types3 = __esm({
2468
3409
  "src/core/scene/render/types.ts"() {
2469
3410
  DEFAULT_THEME_3D = {
2470
3411
  point: { size: 4, color: "#1e40af" },
@@ -2479,7 +3420,7 @@ var JxgRenderer3D;
2479
3420
  var init_JxgRenderer3D = __esm({
2480
3421
  "src/core/scene/render/JxgRenderer3D.ts"() {
2481
3422
  init_registry();
2482
- init_types2();
3423
+ init_types3();
2483
3424
  JxgRenderer3D = class {
2484
3425
  constructor(store, view, options = {}) {
2485
3426
  this.elements = /* @__PURE__ */ new Map();
@@ -5499,6 +6440,20 @@ var init_EditorPanel = __esm({
5499
6440
  );
5500
6441
  }
5501
6442
  });
6443
+ function clamp(n, min, max) {
6444
+ return Math.max(min, Math.min(max, n));
6445
+ }
6446
+ function readStoredWidth(key, fallback, min, max) {
6447
+ if (!key || typeof window === "undefined") return fallback;
6448
+ try {
6449
+ const raw = window.localStorage.getItem(key);
6450
+ if (!raw) return fallback;
6451
+ const n = parseInt(raw, 10);
6452
+ if (Number.isFinite(n)) return clamp(n, min, max);
6453
+ } catch {
6454
+ }
6455
+ return fallback;
6456
+ }
5502
6457
  function CloseIcon() {
5503
6458
  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: [
5504
6459
  /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
@@ -5506,8 +6461,64 @@ function CloseIcon() {
5506
6461
  ] });
5507
6462
  }
5508
6463
  function LeftPanelShell(props) {
5509
- const { title, icon, onClose, isDark, tabs, activeTab, onTabChange, testId, children } = props;
6464
+ const {
6465
+ title,
6466
+ icon,
6467
+ onClose,
6468
+ isDark,
6469
+ tabs,
6470
+ activeTab,
6471
+ onTabChange,
6472
+ testId,
6473
+ resizable,
6474
+ widthStorageKey,
6475
+ defaultWidth,
6476
+ minWidth,
6477
+ maxWidth,
6478
+ children
6479
+ } = props;
5510
6480
  const showTabs = !!tabs && tabs.length >= 2;
6481
+ const min = minWidth ?? FALLBACK_MIN_WIDTH;
6482
+ const max = maxWidth ?? FALLBACK_MAX_WIDTH;
6483
+ const initial = clamp(defaultWidth ?? FALLBACK_DEFAULT_WIDTH, min, max);
6484
+ const [width, setWidth] = React5__namespace.useState(
6485
+ () => resizable ? readStoredWidth(widthStorageKey, initial, min, max) : initial
6486
+ );
6487
+ const widthRef = React5__namespace.useRef(width);
6488
+ widthRef.current = width;
6489
+ React5__namespace.useEffect(() => {
6490
+ if (!resizable || !widthStorageKey || typeof window === "undefined") return;
6491
+ try {
6492
+ window.localStorage.setItem(widthStorageKey, String(width));
6493
+ } catch {
6494
+ }
6495
+ }, [resizable, widthStorageKey, width]);
6496
+ const onResizeStart = React5__namespace.useCallback(
6497
+ (e) => {
6498
+ if (!resizable) return;
6499
+ e.preventDefault();
6500
+ const startX = e.clientX;
6501
+ const startW = widthRef.current;
6502
+ const onMove = (ev) => {
6503
+ setWidth(clamp(startW + (ev.clientX - startX), min, max));
6504
+ };
6505
+ const onUp = () => {
6506
+ window.removeEventListener("mousemove", onMove);
6507
+ window.removeEventListener("mouseup", onUp);
6508
+ document.body.style.cursor = "";
6509
+ document.body.style.userSelect = "";
6510
+ };
6511
+ window.addEventListener("mousemove", onMove);
6512
+ window.addEventListener("mouseup", onUp);
6513
+ document.body.style.cursor = "ew-resize";
6514
+ document.body.style.userSelect = "none";
6515
+ },
6516
+ [resizable, min, max]
6517
+ );
6518
+ const onResizeDoubleClick = React5__namespace.useCallback(() => {
6519
+ if (!resizable) return;
6520
+ setWidth(initial);
6521
+ }, [resizable, initial]);
5511
6522
  return /* @__PURE__ */ jsxRuntime.jsxs(
5512
6523
  "aside",
5513
6524
  {
@@ -5515,10 +6526,12 @@ function LeftPanelShell(props) {
5515
6526
  "aria-label": title,
5516
6527
  "data-testid": testId ?? "left-panel",
5517
6528
  "data-stamp-area": "true",
6529
+ style: resizable ? { width: `${width}px` } : void 0,
5518
6530
  className: [
5519
6531
  isDark ? "theme--dark " : "",
5520
- "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"
5521
- ].join(""),
6532
+ "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",
6533
+ resizable ? "" : "w-60"
6534
+ ].join(" "),
5522
6535
  children: [
5523
6536
  /* @__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: [
5524
6537
  /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold text-slate-800", children: [
@@ -5553,6 +6566,20 @@ function LeftPanelShell(props) {
5553
6566
  className: "min-h-0 flex-1 overflow-y-auto p-3 space-y-3",
5554
6567
  children
5555
6568
  }
6569
+ ),
6570
+ resizable && /* @__PURE__ */ jsxRuntime.jsx(
6571
+ "div",
6572
+ {
6573
+ role: "separator",
6574
+ "aria-orientation": "vertical",
6575
+ "aria-label": "K\xE9o \u0111\u1EC3 \u0111\u1ED5i r\u1ED9ng panel",
6576
+ "data-testid": "left-panel-resizer",
6577
+ onMouseDown: onResizeStart,
6578
+ onDoubleClick: onResizeDoubleClick,
6579
+ className: "group absolute right-0 top-0 z-40 h-full w-1.5 -mr-0.5 cursor-ew-resize select-none",
6580
+ title: "K\xE9o \u0111\u1EC3 \u0111\u1ED5i r\u1ED9ng (double-click \u0111\u1EC3 reset)",
6581
+ 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" })
6582
+ }
5556
6583
  )
5557
6584
  ]
5558
6585
  }
@@ -5582,9 +6609,13 @@ function Section(props) {
5582
6609
  props.children
5583
6610
  ] });
5584
6611
  }
6612
+ var FALLBACK_DEFAULT_WIDTH, FALLBACK_MIN_WIDTH, FALLBACK_MAX_WIDTH;
5585
6613
  var init_LeftPanelShell = __esm({
5586
6614
  "src/core/scene/ui/LeftPanelShell.tsx"() {
5587
6615
  "use client";
6616
+ FALLBACK_DEFAULT_WIDTH = 240;
6617
+ FALLBACK_MIN_WIDTH = 220;
6618
+ FALLBACK_MAX_WIDTH = 480;
5588
6619
  }
5589
6620
  });
5590
6621
 
@@ -5936,7 +6967,7 @@ var init_AxisGridSection = __esm({
5936
6967
 
5937
6968
  // src/stamps/shared/StampLeftPanel/types.ts
5938
6969
  var TOOLTIP_DELAY_MS;
5939
- var init_types3 = __esm({
6970
+ var init_types4 = __esm({
5940
6971
  "src/stamps/shared/StampLeftPanel/types.ts"() {
5941
6972
  TOOLTIP_DELAY_MS = 400;
5942
6973
  }
@@ -5969,27 +7000,118 @@ function useToolHoverTooltip() {
5969
7000
  }
5970
7001
  var init_useToolHoverTooltip = __esm({
5971
7002
  "src/stamps/shared/StampLeftPanel/useToolHoverTooltip.ts"() {
5972
- init_types3();
7003
+ init_types4();
5973
7004
  }
5974
7005
  });
7006
+ function normalize2(s) {
7007
+ return s.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/đ/g, "d").replace(/Đ/g, "d");
7008
+ }
7009
+ function SearchIcon() {
7010
+ 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: [
7011
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "11", cy: "11", r: "7" }),
7012
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "20", y1: "20", x2: "16.5", y2: "16.5" })
7013
+ ] });
7014
+ }
7015
+ function ClearIcon() {
7016
+ 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: [
7017
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
7018
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
7019
+ ] });
7020
+ }
7021
+ function ToolResultList(props) {
7022
+ const { tools, activeTool, onToolChange } = props;
7023
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-0.5", "data-testid": "tool-result-list", children: tools.map((t) => {
7024
+ const active = activeTool === t.key;
7025
+ return /* @__PURE__ */ jsxRuntime.jsxs(
7026
+ "button",
7027
+ {
7028
+ type: "button",
7029
+ "data-tool": t.key,
7030
+ "aria-label": t.label,
7031
+ "aria-pressed": active,
7032
+ onClick: () => onToolChange(t.key),
7033
+ className: [
7034
+ "flex items-center gap-2 rounded-md px-2 py-1.5 text-left transition",
7035
+ active ? "bg-emerald-600 text-white" : "text-slate-700 hover:bg-slate-100"
7036
+ ].join(" "),
7037
+ children: [
7038
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-6 w-6 shrink-0 items-center justify-center", children: t.icon }),
7039
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "min-w-0", children: [
7040
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block truncate text-[12px] font-medium leading-tight", children: t.label }),
7041
+ t.hint && /* @__PURE__ */ jsxRuntime.jsx("span", { className: ["block truncate text-[10px] leading-tight", active ? "text-emerald-50" : "text-slate-400"].join(" "), children: t.hint })
7042
+ ] })
7043
+ ]
7044
+ },
7045
+ t.key
7046
+ );
7047
+ }) });
7048
+ }
5975
7049
  function ToolGrid(props) {
5976
7050
  const { tools, groupOrder, groupLabels, activeTool, onToolChange, chord } = props;
5977
7051
  const { hover, portalReady, showHover, hideHover } = useToolHoverTooltip();
7052
+ const [query, setQuery] = React5.useState("");
7053
+ const normalizedQuery = React5.useMemo(() => normalize2(query.trim()), [query]);
7054
+ const filteredTools = React5.useMemo(() => {
7055
+ if (!normalizedQuery) return tools;
7056
+ return tools.filter((t) => {
7057
+ if (normalize2(t.label).includes(normalizedQuery)) return true;
7058
+ if (t.hint && normalize2(t.hint).includes(normalizedQuery)) return true;
7059
+ return false;
7060
+ });
7061
+ }, [tools, normalizedQuery]);
5978
7062
  const grouped = React5.useMemo(() => {
5979
7063
  var _a;
5980
7064
  const acc = {};
5981
- for (const t of tools) {
7065
+ for (const t of filteredTools) {
5982
7066
  (acc[_a = t.group] ?? (acc[_a] = [])).push(t);
5983
7067
  }
5984
7068
  return acc;
5985
- }, [tools]);
7069
+ }, [filteredTools]);
5986
7070
  const groupKeys = React5.useMemo(
5987
- () => groupOrder.filter((g) => grouped[g]),
7071
+ () => groupOrder.filter((g) => grouped[g] && grouped[g].length > 0),
5988
7072
  [grouped, groupOrder]
5989
7073
  );
5990
- const activeGroupTools = chord?.activeGroup ? grouped[chord.activeGroup] ?? null : null;
7074
+ const noMatch = normalizedQuery !== "" && groupKeys.length === 0;
5991
7075
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5992
- groupKeys.map((group) => {
7076
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
7077
+ /* @__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, {}) }),
7078
+ /* @__PURE__ */ jsxRuntime.jsx(
7079
+ "input",
7080
+ {
7081
+ type: "search",
7082
+ value: query,
7083
+ onChange: (e) => setQuery(e.target.value),
7084
+ placeholder: "T\xECm c\xF4ng c\u1EE5\u2026",
7085
+ "aria-label": "T\xECm c\xF4ng c\u1EE5",
7086
+ "data-testid": "tool-search-input",
7087
+ 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"
7088
+ }
7089
+ ),
7090
+ query && /* @__PURE__ */ jsxRuntime.jsx(
7091
+ "button",
7092
+ {
7093
+ type: "button",
7094
+ onClick: () => setQuery(""),
7095
+ "aria-label": "Xo\xE1 t\xECm ki\u1EBFm",
7096
+ "data-testid": "tool-search-clear",
7097
+ 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",
7098
+ children: /* @__PURE__ */ jsxRuntime.jsx(ClearIcon, {})
7099
+ }
7100
+ )
7101
+ ] }),
7102
+ noMatch && /* @__PURE__ */ jsxRuntime.jsxs(
7103
+ "div",
7104
+ {
7105
+ "data-testid": "tool-search-empty",
7106
+ className: "rounded-md border border-dashed border-slate-200 bg-slate-50 px-3 py-4 text-center text-[11px] text-slate-500",
7107
+ children: [
7108
+ "Kh\xF4ng c\xF3 c\xF4ng c\u1EE5 n\xE0o kh\u1EDBp \u201C",
7109
+ query.trim(),
7110
+ "\u201D."
7111
+ ]
7112
+ }
7113
+ ),
7114
+ normalizedQuery !== "" && !noMatch ? /* @__PURE__ */ jsxRuntime.jsx(ToolResultList, { tools: filteredTools, activeTool, onToolChange }) : groupKeys.map((group) => {
5993
7115
  const isChordActive = chord?.activeGroup === group;
5994
7116
  const dimmed = chord?.activeGroup != null && !isChordActive;
5995
7117
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -6003,23 +7125,10 @@ function ToolGrid(props) {
6003
7125
  dimmed ? "opacity-55" : "opacity-100"
6004
7126
  ].join(" "),
6005
7127
  children: [
6006
- /* @__PURE__ */ jsxRuntime.jsxs("h4", { className: "mb-1.5 flex items-center justify-between text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: [
6007
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: groupLabels[group] }),
6008
- chord && /* @__PURE__ */ jsxRuntime.jsx(
6009
- "span",
6010
- {
6011
- "data-testid": `chord-letter-${group}`,
6012
- className: [
6013
- "font-mono text-[10px] leading-none transition",
6014
- isChordActive ? "text-emerald-700 font-bold" : "text-slate-400"
6015
- ].join(" "),
6016
- children: chord.letterForGroup(group)
6017
- }
6018
- )
6019
- ] }),
6020
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-4 gap-1", children: grouped[group].map((t, i) => {
7128
+ /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: groupLabels[group] }),
7129
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-4 gap-1", children: grouped[group].map((t) => {
6021
7130
  const active = activeTool === t.key;
6022
- return /* @__PURE__ */ jsxRuntime.jsxs(
7131
+ return /* @__PURE__ */ jsxRuntime.jsx(
6023
7132
  "button",
6024
7133
  {
6025
7134
  type: "button",
@@ -6036,20 +7145,7 @@ function ToolGrid(props) {
6036
7145
  "relative flex h-10 items-center justify-center rounded-md transition",
6037
7146
  active ? "bg-emerald-600 text-white shadow-sm" : "text-slate-700 hover:bg-slate-100 hover:text-slate-900"
6038
7147
  ].join(" "),
6039
- children: [
6040
- t.icon,
6041
- chord && /* @__PURE__ */ jsxRuntime.jsx(
6042
- "span",
6043
- {
6044
- "data-testid": `chord-num-${t.key}`,
6045
- className: [
6046
- "pointer-events-none absolute bottom-0 right-0.5 font-mono text-[9px] leading-none transition",
6047
- active ? "text-white/70" : isChordActive ? "text-emerald-700 font-bold" : "text-slate-400"
6048
- ].join(" "),
6049
- children: i + 1
6050
- }
6051
- )
6052
- ]
7148
+ children: t.icon
6053
7149
  },
6054
7150
  t.key
6055
7151
  );
@@ -6059,22 +7155,6 @@ function ToolGrid(props) {
6059
7155
  group
6060
7156
  );
6061
7157
  }),
6062
- chord?.activeGroup && activeGroupTools && /* @__PURE__ */ jsxRuntime.jsxs(
6063
- "div",
6064
- {
6065
- "data-testid": "chord-hint",
6066
- className: "mt-1 rounded border border-emerald-200 bg-emerald-50/60 px-2 py-1 text-[11px] leading-snug text-slate-600",
6067
- children: [
6068
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono font-semibold text-emerald-700", children: chord.letterForGroup(chord.activeGroup) }),
6069
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mx-1 text-slate-400", children: "\u2192" }),
6070
- activeGroupTools.map((t, i) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "mr-2 inline-block", children: [
6071
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono font-semibold text-emerald-700", children: i + 1 }),
6072
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-1", children: t.label })
6073
- ] }, t.key)),
6074
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-slate-400", children: "Esc hu\u1EF7" })
6075
- ]
6076
- }
6077
- ),
6078
7158
  portalReady && hover && typeof document !== "undefined" ? reactDom.createPortal(
6079
7159
  /* @__PURE__ */ jsxRuntime.jsxs(
6080
7160
  "div",
@@ -6141,6 +7221,8 @@ function StampLeftPanelDesktop(props) {
6141
7221
  tabs: tabSpecs,
6142
7222
  activeTab: hasObjects ? tab : void 0,
6143
7223
  onTabChange: hasObjects ? setTab : void 0,
7224
+ resizable: true,
7225
+ widthStorageKey: "xom11.stamp-left-panel.width",
6144
7226
  children: !hasObjects || tab === "tools" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
6145
7227
  /* @__PURE__ */ jsxRuntime.jsx(AxisGridSection, { view, history }),
6146
7228
  /* @__PURE__ */ jsxRuntime.jsx(