@xom11/whiteboard 0.25.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/{ExcalidrawWithMenus-WENZRYYE.mjs → ExcalidrawWithMenus-2QPPTXJM.mjs} +3 -2
  2. package/dist/ExcalidrawWithMenus-2QPPTXJM.mjs.map +1 -0
  3. package/dist/ai.d.mts +3035 -108
  4. package/dist/ai.d.ts +3035 -108
  5. package/dist/ai.js +6780 -788
  6. package/dist/ai.js.map +1 -1
  7. package/dist/ai.mjs +5140 -577
  8. package/dist/ai.mjs.map +1 -1
  9. package/dist/catalog.json +5 -5
  10. package/dist/{chunk-NDEZJKNY.mjs → chunk-5JM35CXV.mjs} +4 -4
  11. package/dist/{chunk-NDEZJKNY.mjs.map → chunk-5JM35CXV.mjs.map} +1 -1
  12. package/dist/{chunk-VNCCIV6O.mjs → chunk-AJAHD35N.mjs} +779 -9
  13. package/dist/chunk-AJAHD35N.mjs.map +1 -0
  14. package/dist/{chunk-M42TGYT6.mjs → chunk-BNBOIDO5.mjs} +3 -3
  15. package/dist/{chunk-M42TGYT6.mjs.map → chunk-BNBOIDO5.mjs.map} +1 -1
  16. package/dist/{chunk-ONBCUWVI.mjs → chunk-BU5KLO3P.mjs} +3 -3
  17. package/dist/{chunk-ONBCUWVI.mjs.map → chunk-BU5KLO3P.mjs.map} +1 -1
  18. package/dist/{chunk-CJBLJUWG.mjs → chunk-CXHNVYMD.mjs} +4 -4
  19. package/dist/{chunk-CJBLJUWG.mjs.map → chunk-CXHNVYMD.mjs.map} +1 -1
  20. package/dist/chunk-H22OZYTW.mjs +265 -0
  21. package/dist/chunk-H22OZYTW.mjs.map +1 -0
  22. package/dist/chunk-J5LGTIGS.mjs +10 -0
  23. package/dist/chunk-J5LGTIGS.mjs.map +1 -0
  24. package/dist/{chunk-TB4CL25L.mjs → chunk-OQIQNKPQ.mjs} +206 -66
  25. package/dist/chunk-OQIQNKPQ.mjs.map +1 -0
  26. package/dist/{chunk-SGFJLHHG.mjs → chunk-PPKHCRRE.mjs} +3 -3
  27. package/dist/{chunk-SGFJLHHG.mjs.map → chunk-PPKHCRRE.mjs.map} +1 -1
  28. package/dist/{chunk-YSJOVBCD.mjs → chunk-QCZVFEN4.mjs} +4 -4
  29. package/dist/{chunk-YSJOVBCD.mjs.map → chunk-QCZVFEN4.mjs.map} +1 -1
  30. package/dist/{chunk-ESVPQWHX.mjs → chunk-QRUAEXLR.mjs} +5 -5
  31. package/dist/{chunk-ESVPQWHX.mjs.map → chunk-QRUAEXLR.mjs.map} +1 -1
  32. package/dist/{chunk-AYSFWUPK.mjs → chunk-SZDAS7LK.mjs} +79 -2
  33. package/dist/chunk-SZDAS7LK.mjs.map +1 -0
  34. package/dist/chunk-T3SOHYB2.mjs +851 -0
  35. package/dist/chunk-T3SOHYB2.mjs.map +1 -0
  36. package/dist/{chunk-I24QOHPU.mjs → chunk-V3YJ6JFL.mjs} +3 -3
  37. package/dist/{chunk-I24QOHPU.mjs.map → chunk-V3YJ6JFL.mjs.map} +1 -1
  38. package/dist/{chunk-REIJZDVZ.mjs → chunk-ZTQBUKLJ.mjs} +960 -196
  39. package/dist/chunk-ZTQBUKLJ.mjs.map +1 -0
  40. package/dist/geometry-2d.d.mts +1 -1
  41. package/dist/geometry-2d.d.ts +1 -1
  42. package/dist/geometry-2d.js +5521 -1384
  43. package/dist/geometry-2d.js.map +1 -1
  44. package/dist/geometry-2d.mjs +5 -4
  45. package/dist/geometry-3d.d.mts +1 -1
  46. package/dist/geometry-3d.d.ts +1 -1
  47. package/dist/geometry-3d.js +1351 -252
  48. package/dist/geometry-3d.js.map +1 -1
  49. package/dist/geometry-3d.mjs +4 -3
  50. package/dist/graph-2d.d.mts +1 -1
  51. package/dist/graph-2d.d.ts +1 -1
  52. package/dist/graph-2d.js +1517 -341
  53. package/dist/graph-2d.js.map +1 -1
  54. package/dist/graph-2d.mjs +7 -6
  55. package/dist/handleExtractProblem-C-U5KluK.d.mts +158 -0
  56. package/dist/handleExtractProblem-C-U5KluK.d.ts +158 -0
  57. package/dist/{host-A64ITWVX.mjs → host-2ISGVO7O.mjs} +6 -5
  58. package/dist/host-2ISGVO7O.mjs.map +1 -0
  59. package/dist/{host-L7FMFZUW.mjs → host-4P766V4J.mjs} +1363 -463
  60. package/dist/host-4P766V4J.mjs.map +1 -0
  61. package/dist/{host-QK53UYMD.mjs → host-HOSJHQ5H.mjs} +10 -9
  62. package/dist/host-HOSJHQ5H.mjs.map +1 -0
  63. package/dist/{host-QS2EOTRJ.mjs → host-ZQCDAT6O.mjs} +3 -2
  64. package/dist/host-ZQCDAT6O.mjs.map +1 -0
  65. package/dist/index.d.mts +10 -4
  66. package/dist/index.d.ts +10 -4
  67. package/dist/index.js +5746 -1603
  68. package/dist/index.js.map +1 -1
  69. package/dist/index.mjs +26 -22
  70. package/dist/index.mjs.map +1 -1
  71. package/dist/latex.d.mts +1 -1
  72. package/dist/latex.d.ts +1 -1
  73. package/dist/latex.mjs +2 -1
  74. package/dist/render-ZX2O2IK7.mjs +10 -0
  75. package/dist/{render-3WTY7NZB.mjs.map → render-ZX2O2IK7.mjs.map} +1 -1
  76. package/dist/serialize-N4G6RFBB.mjs +9 -0
  77. package/dist/{serialize-SRJVKYUG.mjs.map → serialize-N4G6RFBB.mjs.map} +1 -1
  78. package/dist/{types-DWRyCa2m.d.ts → types-BHYC2Fiw.d.mts} +130 -1
  79. package/dist/{types-DWRyCa2m.d.mts → types-BHYC2Fiw.d.ts} +130 -1
  80. package/package.json +10 -1
  81. package/dist/ExcalidrawWithMenus-WENZRYYE.mjs.map +0 -1
  82. package/dist/chunk-AYSFWUPK.mjs.map +0 -1
  83. package/dist/chunk-REIJZDVZ.mjs.map +0 -1
  84. package/dist/chunk-TB4CL25L.mjs.map +0 -1
  85. package/dist/chunk-VNCCIV6O.mjs.map +0 -1
  86. package/dist/chunk-VRHWDZ66.mjs +0 -96
  87. package/dist/chunk-VRHWDZ66.mjs.map +0 -1
  88. package/dist/host-A64ITWVX.mjs.map +0 -1
  89. package/dist/host-L7FMFZUW.mjs.map +0 -1
  90. package/dist/host-QK53UYMD.mjs.map +0 -1
  91. package/dist/host-QS2EOTRJ.mjs.map +0 -1
  92. package/dist/render-3WTY7NZB.mjs +0 -9
  93. package/dist/serialize-SRJVKYUG.mjs +0 -8
@@ -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,17 +2267,31 @@ 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) => {
1535
2289
  const board = ctx.jxg;
2290
+ const isCenterLabel = (l) => /^[A-Z]['′]?\d*$/u.test(l);
2291
+ const isCenter = isCenterLabel(obj.label);
1536
2292
  const baseOpts = {
1537
2293
  name: obj.label,
1538
- withLabel: obj.attrs.showLabel ?? false,
2294
+ withLabel: isCenter ? obj.attrs.showLabel ?? false : true,
1539
2295
  strokeColor: obj.attrs.color ?? "#0f172a",
1540
2296
  strokeWidth: obj.attrs.width ?? 2,
1541
2297
  dash: obj.attrs.dash ?? 0,
@@ -1543,13 +2299,81 @@ var init_circle = __esm({
1543
2299
  visible: obj.visible,
1544
2300
  fixed: obj.locked
1545
2301
  };
1546
- const c = obj.attrs.construction;
2302
+ const c = asConstruction(obj.attrs);
1547
2303
  if (c?.kind === "circumscribed") {
1548
2304
  const p1 = ctx.resolveRef(c.p1);
1549
2305
  const p2 = ctx.resolveRef(c.p2);
1550
2306
  const p3 = ctx.resolveRef(c.p3);
2307
+ if (isCenter) {
2308
+ const center2 = board.create("circumcenter", [p1, p2, p3], {
2309
+ visible: obj.visible,
2310
+ withLabel: true,
2311
+ fixed: true,
2312
+ name: obj.label
2313
+ });
2314
+ const circ = board.create("circumcircle", [p1, p2, p3], { ...baseOpts, withLabel: false });
2315
+ circ.center = circ.center ?? center2;
2316
+ circ._helpers = [center2];
2317
+ return circ;
2318
+ }
1551
2319
  return board.create("circumcircle", [p1, p2, p3], baseOpts);
1552
2320
  }
2321
+ if (c?.kind === "incircle") {
2322
+ const p1 = ctx.resolveRef(c.p1);
2323
+ const p2 = ctx.resolveRef(c.p2);
2324
+ const p3 = ctx.resolveRef(c.p3);
2325
+ if (isCenter) {
2326
+ const center2 = board.create("incenter", [p1, p2, p3], {
2327
+ visible: obj.visible,
2328
+ withLabel: true,
2329
+ fixed: true,
2330
+ name: obj.label
2331
+ });
2332
+ const circ = board.create("incircle", [p1, p2, p3], { ...baseOpts, withLabel: false });
2333
+ circ.center = circ.center ?? center2;
2334
+ circ._helpers = [center2];
2335
+ return circ;
2336
+ }
2337
+ return board.create("incircle", [p1, p2, p3], baseOpts);
2338
+ }
2339
+ if (c?.kind === "excircle") {
2340
+ const P = [ctx.resolveRef(c.p1), ctx.resolveRef(c.p2), ctx.resolveRef(c.p3)];
2341
+ const ids = [c.p1, c.p2, c.p3];
2342
+ const oppIdx = Math.max(0, ids.indexOf(c.opposite));
2343
+ const verts = () => [
2344
+ [P[0].X(), P[0].Y()],
2345
+ [P[1].X(), P[1].Y()],
2346
+ [P[2].X(), P[2].Y()]
2347
+ ];
2348
+ const ctr = () => excenter(verts(), oppIdx);
2349
+ const radius = () => {
2350
+ const I = ctr();
2351
+ const others = [0, 1, 2].filter((i) => i !== oppIdx);
2352
+ const v = verts();
2353
+ const a = v[others[0]];
2354
+ const b = v[others[1]];
2355
+ const dx = b[0] - a[0];
2356
+ const dy = b[1] - a[1];
2357
+ const len = Math.hypot(dx, dy) || 1;
2358
+ return Math.abs((I[0] - a[0]) * dy - (I[1] - a[1]) * dx) / len;
2359
+ };
2360
+ const center2 = board.create("point", [() => ctr()[0], () => ctr()[1]], { visible: false, withLabel: false, fixed: true, name: "" });
2361
+ const circ = board.create("circle", [center2, () => radius()], baseOpts);
2362
+ circ._helpers = [center2];
2363
+ return circ;
2364
+ }
2365
+ if (c?.kind === "diameter") {
2366
+ const p1 = ctx.resolveRef(c.p1);
2367
+ const p2 = ctx.resolveRef(c.p2);
2368
+ const center2 = board.create("midpoint", [p1, p2], { visible: false, withLabel: false, fixed: true, name: "" });
2369
+ const circ = board.create("circle", [center2, p2], baseOpts);
2370
+ circ._helpers = [center2];
2371
+ return circ;
2372
+ }
2373
+ if (typeof obj.attrs.radius === "number") {
2374
+ const center2 = ctx.resolveRef(obj.attrs.center);
2375
+ return board.create("circle", [center2, obj.attrs.radius], baseOpts);
2376
+ }
1553
2377
  const center = ctx.resolveRef(obj.attrs.center);
1554
2378
  const surface = ctx.resolveRef(obj.attrs.surfacePoint);
1555
2379
  return board.create("circle", [center, surface], baseOpts);
@@ -1709,6 +2533,26 @@ function regularVertexLabels(p1Label, p2Label, n) {
1709
2533
  }
1710
2534
  return `${p1Label}${p2Label}\u2026`;
1711
2535
  }
2536
+ function specialShapeName(kind) {
2537
+ switch (kind) {
2538
+ case "square":
2539
+ return "H\xECnh vu\xF4ng";
2540
+ case "rectangle":
2541
+ return "H\xECnh ch\u1EEF nh\u1EADt";
2542
+ case "rhombus":
2543
+ return "H\xECnh thoi";
2544
+ case "parallelogram":
2545
+ return "H\xECnh b\xECnh h\xE0nh";
2546
+ case "isoTrapezoid":
2547
+ return "H\xECnh thang c\xE2n";
2548
+ case "isoTriangle":
2549
+ return "Tam gi\xE1c c\xE2n";
2550
+ case "rightTriangle":
2551
+ return "Tam gi\xE1c vu\xF4ng";
2552
+ case "regular":
2553
+ return "";
2554
+ }
2555
+ }
1712
2556
  var def11;
1713
2557
  var init_polygon = __esm({
1714
2558
  "src/core/scene/kinds/polygon.ts"() {
@@ -1720,13 +2564,27 @@ var init_polygon = __esm({
1720
2564
  migrate: {},
1721
2565
  validate: (a) => {
1722
2566
  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
- }
2567
+ const c = a.construction;
2568
+ if (c.kind === "regular") {
2569
+ if (!c.p1 || !c.p2) throw new Error("polygon (regular): p1 v\xE0 p2 b\u1EAFt bu\u1ED9c");
2570
+ if (!Number.isFinite(c.n) || c.n < 3) throw new Error("polygon (regular): n \u2265 3");
2571
+ return;
2572
+ }
2573
+ if (c.kind === "square") {
2574
+ if (!c.p1 || !c.p2) throw new Error("polygon (square): p1 v\xE0 p2 b\u1EAFt bu\u1ED9c");
2575
+ return;
2576
+ }
2577
+ if (c.kind === "rectangle" || c.kind === "rhombus" || c.kind === "parallelogram" || c.kind === "isoTrapezoid") {
2578
+ if (!c.p1 || !c.p2 || !c.p3) throw new Error(`polygon (${c.kind}): p1, p2, p3 b\u1EAFt bu\u1ED9c`);
2579
+ return;
2580
+ }
2581
+ if (c.kind === "isoTriangle") {
2582
+ if (!c.base1 || !c.base2 || !c.apex) throw new Error("polygon (isoTriangle): base1, base2, apex b\u1EAFt bu\u1ED9c");
2583
+ return;
2584
+ }
2585
+ if (c.kind === "rightTriangle") {
2586
+ if (!c.rightAngle || !c.leg1End || !c.leg2End) throw new Error("polygon (rightTriangle): rightAngle, leg1End, leg2End b\u1EAFt bu\u1ED9c");
2587
+ return;
1730
2588
  }
1731
2589
  return;
1732
2590
  }
@@ -1735,14 +2593,51 @@ var init_polygon = __esm({
1735
2593
  }
1736
2594
  },
1737
2595
  dependsOn: (a) => {
1738
- if (a.construction?.kind === "regular") return [a.construction.p1, a.construction.p2];
1739
- return [...a.vertices ?? []];
2596
+ const c = a.construction;
2597
+ if (!c) return [...a.vertices ?? []];
2598
+ switch (c.kind) {
2599
+ case "regular":
2600
+ return [c.p1, c.p2];
2601
+ case "square":
2602
+ return [c.p1, c.p2];
2603
+ case "rectangle":
2604
+ case "rhombus":
2605
+ case "parallelogram":
2606
+ case "isoTrapezoid":
2607
+ return [c.p1, c.p2, c.p3];
2608
+ case "isoTriangle":
2609
+ return [c.base1, c.base2, c.apex];
2610
+ case "rightTriangle":
2611
+ return [c.rightAngle, c.leg1End, c.leg2End];
2612
+ }
1740
2613
  },
1741
2614
  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}`;
2615
+ const c = obj.attrs.construction;
2616
+ if (c) {
2617
+ if (c.kind === "regular") {
2618
+ const labels2 = regularVertexLabels(labelOf(c.p1, state), labelOf(c.p2, state), c.n);
2619
+ return `${regularPolygonName(c.n)} ${labels2}`;
2620
+ }
2621
+ const name = specialShapeName(c.kind);
2622
+ let labels = [];
2623
+ switch (c.kind) {
2624
+ case "square":
2625
+ labels = [labelOf(c.p1, state), labelOf(c.p2, state)];
2626
+ break;
2627
+ case "rectangle":
2628
+ case "rhombus":
2629
+ case "parallelogram":
2630
+ case "isoTrapezoid":
2631
+ labels = [labelOf(c.p1, state), labelOf(c.p2, state), labelOf(c.p3, state)];
2632
+ break;
2633
+ case "isoTriangle":
2634
+ labels = [labelOf(c.apex, state), labelOf(c.base1, state), labelOf(c.base2, state)];
2635
+ break;
2636
+ case "rightTriangle":
2637
+ labels = [labelOf(c.rightAngle, state), labelOf(c.leg1End, state), labelOf(c.leg2End, state)];
2638
+ break;
2639
+ }
2640
+ return `${name} ${labels.join("")}`;
1746
2641
  }
1747
2642
  return `\u0110a gi\xE1c ${(obj.attrs.vertices ?? []).map((id) => labelOf(id, state)).join("")}`;
1748
2643
  },
@@ -1750,22 +2645,85 @@ var init_polygon = __esm({
1750
2645
  const board = ctx.jxg;
1751
2646
  const label = obj.label;
1752
2647
  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
2648
+ const cons = obj.attrs.construction;
2649
+ const commonAttrs = {
2650
+ name: label,
2651
+ withLabel: obj.attrs.showLabel ?? false,
2652
+ borders: {
2653
+ strokeColor: obj.attrs.color ?? "#0f172a",
2654
+ strokeWidth: obj.attrs.width ?? 2
2655
+ },
2656
+ fillColor: obj.attrs.color ?? "#60a5fa",
2657
+ fillOpacity: obj.attrs.fillOpacity ?? 0.15,
2658
+ visible: obj.visible,
2659
+ fixed: obj.locked
2660
+ };
2661
+ if (cons?.kind === "regular") {
2662
+ const p1 = ctx.resolveRef(cons.p1);
2663
+ const p2 = ctx.resolveRef(cons.p2);
2664
+ return board.create("regularpolygon", [p1, p2, cons.n], commonAttrs);
2665
+ }
2666
+ if (cons?.kind === "square") {
2667
+ const p1 = ctx.resolveRef(cons.p1);
2668
+ const p2 = ctx.resolveRef(cons.p2);
2669
+ return board.create("regularpolygon", [p1, p2, 4], commonAttrs);
2670
+ }
2671
+ if (cons?.kind === "rectangle" || cons?.kind === "rhombus" || cons?.kind === "parallelogram") {
2672
+ const A = ctx.resolveRef(cons.p1);
2673
+ const B = ctx.resolveRef(cons.p2);
2674
+ const C = ctx.resolveRef(cons.p3);
2675
+ const D = board.create(
2676
+ "point",
2677
+ [() => A.X() + C.X() - B.X(), () => A.Y() + C.Y() - B.Y()],
2678
+ { visible: false, withLabel: false, fixed: true, name: "" }
2679
+ );
2680
+ const poly2 = board.create("polygon", [A, B, C, D], commonAttrs);
2681
+ poly2._helpers = [D];
2682
+ return poly2;
2683
+ }
2684
+ if (cons?.kind === "isoTrapezoid") {
2685
+ const A = ctx.resolveRef(cons.p1);
2686
+ const B = ctx.resolveRef(cons.p2);
2687
+ const C = ctx.resolveRef(cons.p3);
2688
+ const Dx = () => {
2689
+ const Mx = (A.X() + B.X()) / 2;
2690
+ const My = (A.Y() + B.Y()) / 2;
2691
+ const ux = B.X() - A.X();
2692
+ const uy = B.Y() - A.Y();
2693
+ const len2 = ux * ux + uy * uy || 1;
2694
+ const proj = ((C.X() - Mx) * ux + (C.Y() - My) * uy) / len2;
2695
+ return C.X() - 2 * proj * ux;
2696
+ };
2697
+ const Dy = () => {
2698
+ const Mx = (A.X() + B.X()) / 2;
2699
+ const My = (A.Y() + B.Y()) / 2;
2700
+ const ux = B.X() - A.X();
2701
+ const uy = B.Y() - A.Y();
2702
+ const len2 = ux * ux + uy * uy || 1;
2703
+ const proj = ((C.X() - Mx) * ux + (C.Y() - My) * uy) / len2;
2704
+ return C.Y() - 2 * proj * uy;
2705
+ };
2706
+ const D = board.create("point", [Dx, Dy], {
2707
+ visible: false,
2708
+ withLabel: false,
2709
+ fixed: true,
2710
+ name: ""
1768
2711
  });
2712
+ const poly2 = board.create("polygon", [A, B, C, D], commonAttrs);
2713
+ poly2._helpers = [D];
2714
+ return poly2;
2715
+ }
2716
+ if (cons?.kind === "isoTriangle") {
2717
+ const Apex = ctx.resolveRef(cons.apex);
2718
+ const B1 = ctx.resolveRef(cons.base1);
2719
+ const B2 = ctx.resolveRef(cons.base2);
2720
+ return board.create("polygon", [Apex, B1, B2], commonAttrs);
2721
+ }
2722
+ if (cons?.kind === "rightTriangle") {
2723
+ const R = ctx.resolveRef(cons.rightAngle);
2724
+ const P = ctx.resolveRef(cons.leg1End);
2725
+ const Q = ctx.resolveRef(cons.leg2End);
2726
+ return board.create("polygon", [R, P, Q], commonAttrs);
1769
2727
  }
1770
2728
  const verts = (obj.attrs.vertices ?? []).map((id) => ctx.resolveRef(id));
1771
2729
  const poly = board.create("polygon", verts, {
@@ -2464,7 +3422,7 @@ var init_serialize = __esm({
2464
3422
 
2465
3423
  // src/core/scene/render/types.ts
2466
3424
  var DEFAULT_THEME_3D;
2467
- var init_types2 = __esm({
3425
+ var init_types3 = __esm({
2468
3426
  "src/core/scene/render/types.ts"() {
2469
3427
  DEFAULT_THEME_3D = {
2470
3428
  point: { size: 4, color: "#1e40af" },
@@ -2479,7 +3437,7 @@ var JxgRenderer3D;
2479
3437
  var init_JxgRenderer3D = __esm({
2480
3438
  "src/core/scene/render/JxgRenderer3D.ts"() {
2481
3439
  init_registry();
2482
- init_types2();
3440
+ init_types3();
2483
3441
  JxgRenderer3D = class {
2484
3442
  constructor(store, view, options = {}) {
2485
3443
  this.elements = /* @__PURE__ */ new Map();
@@ -5499,6 +6457,20 @@ var init_EditorPanel = __esm({
5499
6457
  );
5500
6458
  }
5501
6459
  });
6460
+ function clamp(n, min, max) {
6461
+ return Math.max(min, Math.min(max, n));
6462
+ }
6463
+ function readStoredWidth(key, fallback, min, max) {
6464
+ if (!key || typeof window === "undefined") return fallback;
6465
+ try {
6466
+ const raw = window.localStorage.getItem(key);
6467
+ if (!raw) return fallback;
6468
+ const n = parseInt(raw, 10);
6469
+ if (Number.isFinite(n)) return clamp(n, min, max);
6470
+ } catch {
6471
+ }
6472
+ return fallback;
6473
+ }
5502
6474
  function CloseIcon() {
5503
6475
  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
6476
  /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
@@ -5506,8 +6478,64 @@ function CloseIcon() {
5506
6478
  ] });
5507
6479
  }
5508
6480
  function LeftPanelShell(props) {
5509
- const { title, icon, onClose, isDark, tabs, activeTab, onTabChange, testId, children } = props;
6481
+ const {
6482
+ title,
6483
+ icon,
6484
+ onClose,
6485
+ isDark,
6486
+ tabs,
6487
+ activeTab,
6488
+ onTabChange,
6489
+ testId,
6490
+ resizable,
6491
+ widthStorageKey,
6492
+ defaultWidth,
6493
+ minWidth,
6494
+ maxWidth,
6495
+ children
6496
+ } = props;
5510
6497
  const showTabs = !!tabs && tabs.length >= 2;
6498
+ const min = minWidth ?? FALLBACK_MIN_WIDTH;
6499
+ const max = maxWidth ?? FALLBACK_MAX_WIDTH;
6500
+ const initial = clamp(defaultWidth ?? FALLBACK_DEFAULT_WIDTH, min, max);
6501
+ const [width, setWidth] = React5__namespace.useState(
6502
+ () => resizable ? readStoredWidth(widthStorageKey, initial, min, max) : initial
6503
+ );
6504
+ const widthRef = React5__namespace.useRef(width);
6505
+ widthRef.current = width;
6506
+ React5__namespace.useEffect(() => {
6507
+ if (!resizable || !widthStorageKey || typeof window === "undefined") return;
6508
+ try {
6509
+ window.localStorage.setItem(widthStorageKey, String(width));
6510
+ } catch {
6511
+ }
6512
+ }, [resizable, widthStorageKey, width]);
6513
+ const onResizeStart = React5__namespace.useCallback(
6514
+ (e) => {
6515
+ if (!resizable) return;
6516
+ e.preventDefault();
6517
+ const startX = e.clientX;
6518
+ const startW = widthRef.current;
6519
+ const onMove = (ev) => {
6520
+ setWidth(clamp(startW + (ev.clientX - startX), min, max));
6521
+ };
6522
+ const onUp = () => {
6523
+ window.removeEventListener("mousemove", onMove);
6524
+ window.removeEventListener("mouseup", onUp);
6525
+ document.body.style.cursor = "";
6526
+ document.body.style.userSelect = "";
6527
+ };
6528
+ window.addEventListener("mousemove", onMove);
6529
+ window.addEventListener("mouseup", onUp);
6530
+ document.body.style.cursor = "ew-resize";
6531
+ document.body.style.userSelect = "none";
6532
+ },
6533
+ [resizable, min, max]
6534
+ );
6535
+ const onResizeDoubleClick = React5__namespace.useCallback(() => {
6536
+ if (!resizable) return;
6537
+ setWidth(initial);
6538
+ }, [resizable, initial]);
5511
6539
  return /* @__PURE__ */ jsxRuntime.jsxs(
5512
6540
  "aside",
5513
6541
  {
@@ -5515,10 +6543,12 @@ function LeftPanelShell(props) {
5515
6543
  "aria-label": title,
5516
6544
  "data-testid": testId ?? "left-panel",
5517
6545
  "data-stamp-area": "true",
6546
+ style: resizable ? { width: `${width}px` } : void 0,
5518
6547
  className: [
5519
6548
  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(""),
6549
+ "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",
6550
+ resizable ? "" : "w-60"
6551
+ ].join(" "),
5522
6552
  children: [
5523
6553
  /* @__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
6554
  /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold text-slate-800", children: [
@@ -5553,6 +6583,20 @@ function LeftPanelShell(props) {
5553
6583
  className: "min-h-0 flex-1 overflow-y-auto p-3 space-y-3",
5554
6584
  children
5555
6585
  }
6586
+ ),
6587
+ resizable && /* @__PURE__ */ jsxRuntime.jsx(
6588
+ "div",
6589
+ {
6590
+ role: "separator",
6591
+ "aria-orientation": "vertical",
6592
+ "aria-label": "K\xE9o \u0111\u1EC3 \u0111\u1ED5i r\u1ED9ng panel",
6593
+ "data-testid": "left-panel-resizer",
6594
+ onMouseDown: onResizeStart,
6595
+ onDoubleClick: onResizeDoubleClick,
6596
+ className: "group absolute right-0 top-0 z-40 h-full w-1.5 -mr-0.5 cursor-ew-resize select-none",
6597
+ title: "K\xE9o \u0111\u1EC3 \u0111\u1ED5i r\u1ED9ng (double-click \u0111\u1EC3 reset)",
6598
+ 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" })
6599
+ }
5556
6600
  )
5557
6601
  ]
5558
6602
  }
@@ -5582,9 +6626,13 @@ function Section(props) {
5582
6626
  props.children
5583
6627
  ] });
5584
6628
  }
6629
+ var FALLBACK_DEFAULT_WIDTH, FALLBACK_MIN_WIDTH, FALLBACK_MAX_WIDTH;
5585
6630
  var init_LeftPanelShell = __esm({
5586
6631
  "src/core/scene/ui/LeftPanelShell.tsx"() {
5587
6632
  "use client";
6633
+ FALLBACK_DEFAULT_WIDTH = 240;
6634
+ FALLBACK_MIN_WIDTH = 220;
6635
+ FALLBACK_MAX_WIDTH = 480;
5588
6636
  }
5589
6637
  });
5590
6638
 
@@ -5936,7 +6984,7 @@ var init_AxisGridSection = __esm({
5936
6984
 
5937
6985
  // src/stamps/shared/StampLeftPanel/types.ts
5938
6986
  var TOOLTIP_DELAY_MS;
5939
- var init_types3 = __esm({
6987
+ var init_types4 = __esm({
5940
6988
  "src/stamps/shared/StampLeftPanel/types.ts"() {
5941
6989
  TOOLTIP_DELAY_MS = 400;
5942
6990
  }
@@ -5969,27 +7017,118 @@ function useToolHoverTooltip() {
5969
7017
  }
5970
7018
  var init_useToolHoverTooltip = __esm({
5971
7019
  "src/stamps/shared/StampLeftPanel/useToolHoverTooltip.ts"() {
5972
- init_types3();
7020
+ init_types4();
5973
7021
  }
5974
7022
  });
7023
+ function normalize2(s) {
7024
+ return s.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/đ/g, "d").replace(/Đ/g, "d");
7025
+ }
7026
+ function SearchIcon() {
7027
+ 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: [
7028
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "11", cy: "11", r: "7" }),
7029
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "20", y1: "20", x2: "16.5", y2: "16.5" })
7030
+ ] });
7031
+ }
7032
+ function ClearIcon() {
7033
+ 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: [
7034
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
7035
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
7036
+ ] });
7037
+ }
7038
+ function ToolResultList(props) {
7039
+ const { tools, activeTool, onToolChange } = props;
7040
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-0.5", "data-testid": "tool-result-list", children: tools.map((t) => {
7041
+ const active = activeTool === t.key;
7042
+ return /* @__PURE__ */ jsxRuntime.jsxs(
7043
+ "button",
7044
+ {
7045
+ type: "button",
7046
+ "data-tool": t.key,
7047
+ "aria-label": t.label,
7048
+ "aria-pressed": active,
7049
+ onClick: () => onToolChange(t.key),
7050
+ className: [
7051
+ "flex items-center gap-2 rounded-md px-2 py-1.5 text-left transition",
7052
+ active ? "bg-emerald-600 text-white" : "text-slate-700 hover:bg-slate-100"
7053
+ ].join(" "),
7054
+ children: [
7055
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-6 w-6 shrink-0 items-center justify-center", children: t.icon }),
7056
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "min-w-0", children: [
7057
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block truncate text-[12px] font-medium leading-tight", children: t.label }),
7058
+ t.hint && /* @__PURE__ */ jsxRuntime.jsx("span", { className: ["block truncate text-[10px] leading-tight", active ? "text-emerald-50" : "text-slate-400"].join(" "), children: t.hint })
7059
+ ] })
7060
+ ]
7061
+ },
7062
+ t.key
7063
+ );
7064
+ }) });
7065
+ }
5975
7066
  function ToolGrid(props) {
5976
7067
  const { tools, groupOrder, groupLabels, activeTool, onToolChange, chord } = props;
5977
7068
  const { hover, portalReady, showHover, hideHover } = useToolHoverTooltip();
7069
+ const [query, setQuery] = React5.useState("");
7070
+ const normalizedQuery = React5.useMemo(() => normalize2(query.trim()), [query]);
7071
+ const filteredTools = React5.useMemo(() => {
7072
+ if (!normalizedQuery) return tools;
7073
+ return tools.filter((t) => {
7074
+ if (normalize2(t.label).includes(normalizedQuery)) return true;
7075
+ if (t.hint && normalize2(t.hint).includes(normalizedQuery)) return true;
7076
+ return false;
7077
+ });
7078
+ }, [tools, normalizedQuery]);
5978
7079
  const grouped = React5.useMemo(() => {
5979
7080
  var _a;
5980
7081
  const acc = {};
5981
- for (const t of tools) {
7082
+ for (const t of filteredTools) {
5982
7083
  (acc[_a = t.group] ?? (acc[_a] = [])).push(t);
5983
7084
  }
5984
7085
  return acc;
5985
- }, [tools]);
7086
+ }, [filteredTools]);
5986
7087
  const groupKeys = React5.useMemo(
5987
- () => groupOrder.filter((g) => grouped[g]),
7088
+ () => groupOrder.filter((g) => grouped[g] && grouped[g].length > 0),
5988
7089
  [grouped, groupOrder]
5989
7090
  );
5990
- const activeGroupTools = chord?.activeGroup ? grouped[chord.activeGroup] ?? null : null;
7091
+ const noMatch = normalizedQuery !== "" && groupKeys.length === 0;
5991
7092
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5992
- groupKeys.map((group) => {
7093
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
7094
+ /* @__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, {}) }),
7095
+ /* @__PURE__ */ jsxRuntime.jsx(
7096
+ "input",
7097
+ {
7098
+ type: "search",
7099
+ value: query,
7100
+ onChange: (e) => setQuery(e.target.value),
7101
+ placeholder: "T\xECm c\xF4ng c\u1EE5\u2026",
7102
+ "aria-label": "T\xECm c\xF4ng c\u1EE5",
7103
+ "data-testid": "tool-search-input",
7104
+ 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"
7105
+ }
7106
+ ),
7107
+ query && /* @__PURE__ */ jsxRuntime.jsx(
7108
+ "button",
7109
+ {
7110
+ type: "button",
7111
+ onClick: () => setQuery(""),
7112
+ "aria-label": "Xo\xE1 t\xECm ki\u1EBFm",
7113
+ "data-testid": "tool-search-clear",
7114
+ 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",
7115
+ children: /* @__PURE__ */ jsxRuntime.jsx(ClearIcon, {})
7116
+ }
7117
+ )
7118
+ ] }),
7119
+ noMatch && /* @__PURE__ */ jsxRuntime.jsxs(
7120
+ "div",
7121
+ {
7122
+ "data-testid": "tool-search-empty",
7123
+ className: "rounded-md border border-dashed border-slate-200 bg-slate-50 px-3 py-4 text-center text-[11px] text-slate-500",
7124
+ children: [
7125
+ "Kh\xF4ng c\xF3 c\xF4ng c\u1EE5 n\xE0o kh\u1EDBp \u201C",
7126
+ query.trim(),
7127
+ "\u201D."
7128
+ ]
7129
+ }
7130
+ ),
7131
+ normalizedQuery !== "" && !noMatch ? /* @__PURE__ */ jsxRuntime.jsx(ToolResultList, { tools: filteredTools, activeTool, onToolChange }) : groupKeys.map((group) => {
5993
7132
  const isChordActive = chord?.activeGroup === group;
5994
7133
  const dimmed = chord?.activeGroup != null && !isChordActive;
5995
7134
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -6003,23 +7142,10 @@ function ToolGrid(props) {
6003
7142
  dimmed ? "opacity-55" : "opacity-100"
6004
7143
  ].join(" "),
6005
7144
  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) => {
7145
+ /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: groupLabels[group] }),
7146
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-4 gap-1", children: grouped[group].map((t) => {
6021
7147
  const active = activeTool === t.key;
6022
- return /* @__PURE__ */ jsxRuntime.jsxs(
7148
+ return /* @__PURE__ */ jsxRuntime.jsx(
6023
7149
  "button",
6024
7150
  {
6025
7151
  type: "button",
@@ -6036,20 +7162,7 @@ function ToolGrid(props) {
6036
7162
  "relative flex h-10 items-center justify-center rounded-md transition",
6037
7163
  active ? "bg-emerald-600 text-white shadow-sm" : "text-slate-700 hover:bg-slate-100 hover:text-slate-900"
6038
7164
  ].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
- ]
7165
+ children: t.icon
6053
7166
  },
6054
7167
  t.key
6055
7168
  );
@@ -6059,22 +7172,6 @@ function ToolGrid(props) {
6059
7172
  group
6060
7173
  );
6061
7174
  }),
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
7175
  portalReady && hover && typeof document !== "undefined" ? reactDom.createPortal(
6079
7176
  /* @__PURE__ */ jsxRuntime.jsxs(
6080
7177
  "div",
@@ -6141,6 +7238,8 @@ function StampLeftPanelDesktop(props) {
6141
7238
  tabs: tabSpecs,
6142
7239
  activeTab: hasObjects ? tab : void 0,
6143
7240
  onTabChange: hasObjects ? setTab : void 0,
7241
+ resizable: true,
7242
+ widthStorageKey: "xom11.stamp-left-panel.width",
6144
7243
  children: !hasObjects || tab === "tools" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
6145
7244
  /* @__PURE__ */ jsxRuntime.jsx(AxisGridSection, { view, history }),
6146
7245
  /* @__PURE__ */ jsxRuntime.jsx(