@xom11/whiteboard 0.29.0 → 0.31.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/ai.d.mts +259 -33
  2. package/dist/ai.d.ts +259 -33
  3. package/dist/ai.js +5424 -470
  4. package/dist/ai.js.map +1 -1
  5. package/dist/ai.mjs +4971 -351
  6. package/dist/ai.mjs.map +1 -1
  7. package/dist/catalog.json +5 -5
  8. package/dist/{chunk-V3YJ6JFL.mjs → chunk-44JY2AKC.mjs} +3 -3
  9. package/dist/{chunk-V3YJ6JFL.mjs.map → chunk-44JY2AKC.mjs.map} +1 -1
  10. package/dist/{chunk-GEC2D2EQ.mjs → chunk-BMYC2ILT.mjs} +4 -4
  11. package/dist/{chunk-GEC2D2EQ.mjs.map → chunk-BMYC2ILT.mjs.map} +1 -1
  12. package/dist/{chunk-PPKHCRRE.mjs → chunk-C76SOFXF.mjs} +3 -3
  13. package/dist/{chunk-PPKHCRRE.mjs.map → chunk-C76SOFXF.mjs.map} +1 -1
  14. package/dist/{chunk-IHUFOV7L.mjs → chunk-CH6SFONH.mjs} +15 -3
  15. package/dist/chunk-CH6SFONH.mjs.map +1 -0
  16. package/dist/{chunk-E6EDOPGT.mjs → chunk-DWIEVCGK.mjs} +254 -16
  17. package/dist/chunk-DWIEVCGK.mjs.map +1 -0
  18. package/dist/{chunk-SZDAS7LK.mjs → chunk-IE2GGHNF.mjs} +131 -81
  19. package/dist/chunk-IE2GGHNF.mjs.map +1 -0
  20. package/dist/{chunk-ZTQBUKLJ.mjs → chunk-JJ4FPCBE.mjs} +142 -22
  21. package/dist/chunk-JJ4FPCBE.mjs.map +1 -0
  22. package/dist/{chunk-QRUAEXLR.mjs → chunk-K5BS2H56.mjs} +5 -5
  23. package/dist/{chunk-QRUAEXLR.mjs.map → chunk-K5BS2H56.mjs.map} +1 -1
  24. package/dist/{chunk-BNBOIDO5.mjs → chunk-K7VJU7LQ.mjs} +3 -3
  25. package/dist/{chunk-BNBOIDO5.mjs.map → chunk-K7VJU7LQ.mjs.map} +1 -1
  26. package/dist/{chunk-H22OZYTW.mjs → chunk-KOXOC2FI.mjs} +48 -39
  27. package/dist/chunk-KOXOC2FI.mjs.map +1 -0
  28. package/dist/{chunk-CXHNVYMD.mjs → chunk-KWDBVLST.mjs} +5 -5
  29. package/dist/{chunk-CXHNVYMD.mjs.map → chunk-KWDBVLST.mjs.map} +1 -1
  30. package/dist/{chunk-OQIQNKPQ.mjs → chunk-LTLLQUMN.mjs} +4 -4
  31. package/dist/{chunk-OQIQNKPQ.mjs.map → chunk-LTLLQUMN.mjs.map} +1 -1
  32. package/dist/chunk-QK6OVDLC.mjs +103 -0
  33. package/dist/chunk-QK6OVDLC.mjs.map +1 -0
  34. package/dist/{chunk-QGNU34T7.mjs → chunk-QLQ4MJNO.mjs} +10 -4
  35. package/dist/chunk-QLQ4MJNO.mjs.map +1 -0
  36. package/dist/{chunk-BU5KLO3P.mjs → chunk-T3N4BSJV.mjs} +4 -4
  37. package/dist/{chunk-BU5KLO3P.mjs.map → chunk-T3N4BSJV.mjs.map} +1 -1
  38. package/dist/{chunk-5JM35CXV.mjs → chunk-TMRFSOM7.mjs} +4 -4
  39. package/dist/{chunk-5JM35CXV.mjs.map → chunk-TMRFSOM7.mjs.map} +1 -1
  40. package/dist/geometry-2d.d.mts +1 -1
  41. package/dist/geometry-2d.d.ts +1 -1
  42. package/dist/geometry-2d.js +841 -204
  43. package/dist/geometry-2d.js.map +1 -1
  44. package/dist/geometry-2d.mjs +5 -5
  45. package/dist/geometry-3d.d.mts +1 -1
  46. package/dist/geometry-3d.d.ts +1 -1
  47. package/dist/geometry-3d.js +172 -22
  48. package/dist/geometry-3d.js.map +1 -1
  49. package/dist/geometry-3d.mjs +4 -4
  50. package/dist/graph-2d.d.mts +1 -1
  51. package/dist/graph-2d.d.ts +1 -1
  52. package/dist/graph-2d.js +307 -100
  53. package/dist/graph-2d.js.map +1 -1
  54. package/dist/graph-2d.mjs +7 -7
  55. package/dist/handleExtractProblem-BrDY9ifM.d.mts +58 -0
  56. package/dist/handleExtractProblem-BrDY9ifM.d.ts +58 -0
  57. package/dist/{host-HOSJHQ5H.mjs → host-4FIUNIDQ.mjs} +13 -12
  58. package/dist/host-4FIUNIDQ.mjs.map +1 -0
  59. package/dist/{host-2ISGVO7O.mjs → host-4ZB4XD4S.mjs} +9 -8
  60. package/dist/host-4ZB4XD4S.mjs.map +1 -0
  61. package/dist/{host-ZQCDAT6O.mjs → host-H2IGOKJU.mjs} +3 -3
  62. package/dist/{host-ZQCDAT6O.mjs.map → host-H2IGOKJU.mjs.map} +1 -1
  63. package/dist/{host-HKMZSCIT.mjs → host-KMWP7KBT.mjs} +286 -74
  64. package/dist/host-KMWP7KBT.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 +849 -206
  68. package/dist/index.js.map +1 -1
  69. package/dist/index.mjs +22 -21
  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.js +8 -2
  74. package/dist/latex.js.map +1 -1
  75. package/dist/latex.mjs +1 -1
  76. package/dist/render-NMS7OAV6.mjs +10 -0
  77. package/dist/{render-ZX2O2IK7.mjs.map → render-NMS7OAV6.mjs.map} +1 -1
  78. package/dist/serialize-PGHQZEPV.mjs +9 -0
  79. package/dist/{serialize-N4G6RFBB.mjs.map → serialize-PGHQZEPV.mjs.map} +1 -1
  80. package/dist/{types-C3FjpoUi.d.ts → types-tePd94vW.d.mts} +8 -0
  81. package/dist/{types-C3FjpoUi.d.mts → types-tePd94vW.d.ts} +8 -0
  82. package/package.json +2 -1
  83. package/dist/chunk-E6EDOPGT.mjs.map +0 -1
  84. package/dist/chunk-H22OZYTW.mjs.map +0 -1
  85. package/dist/chunk-IHUFOV7L.mjs.map +0 -1
  86. package/dist/chunk-QGNU34T7.mjs.map +0 -1
  87. package/dist/chunk-SZDAS7LK.mjs.map +0 -1
  88. package/dist/chunk-ZTQBUKLJ.mjs.map +0 -1
  89. package/dist/host-2ISGVO7O.mjs.map +0 -1
  90. package/dist/host-HKMZSCIT.mjs.map +0 -1
  91. package/dist/host-HOSJHQ5H.mjs.map +0 -1
  92. package/dist/render-ZX2O2IK7.mjs +0 -10
  93. package/dist/serialize-N4G6RFBB.mjs +0 -9
package/dist/index.js CHANGED
@@ -942,8 +942,12 @@ function constraintRefs2D(c) {
942
942
  return [c.line, c.circle, c.other];
943
943
  case "tangencyPoint":
944
944
  return [c.circle, c.onLine];
945
- case "arcMidpoint":
946
- return [c.circle, c.a, c.b, c.notContaining];
945
+ case "arcMidpoint": {
946
+ const containment = c.notContaining ?? c.containing;
947
+ return containment ? [c.circle, c.a, c.b, containment] : [c.circle, c.a, c.b];
948
+ }
949
+ case "mixtilinearPoint":
950
+ return [c.vertices[0], c.vertices[1], c.vertices[2]];
947
951
  case "pointAtDistance": {
948
952
  const d = c.distance;
949
953
  const extra = d.kind === "circleRadius" ? [d.circle] : d.kind === "segmentLength" ? [d.p1, d.p2] : [];
@@ -1230,7 +1234,7 @@ function dist(p, q) {
1230
1234
  function sideOf(a, b, p) {
1231
1235
  return (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
1232
1236
  }
1233
- function arcMidpoint(center, radius, a, b, notContaining) {
1237
+ function arcMidpoint(center, radius, a, b, reference, sameSide = false) {
1234
1238
  const mcx = (a[0] + b[0]) / 2;
1235
1239
  const mcy = (a[1] + b[1]) / 2;
1236
1240
  let ux = mcx - center[0];
@@ -1245,12 +1249,18 @@ function arcMidpoint(center, radius, a, b, notContaining) {
1245
1249
  uy /= len;
1246
1250
  const cand1 = [center[0] + radius * ux, center[1] + radius * uy];
1247
1251
  const cand2 = [center[0] - radius * ux, center[1] - radius * uy];
1252
+ if (!reference) return cand1[1] >= cand2[1] ? cand1 : cand2;
1253
+ const notContaining = reference;
1248
1254
  const sN = sideOf(a, b, notContaining);
1249
1255
  if (Math.abs(sN) < 1e-9) {
1250
- return dist(cand1, notContaining) >= dist(cand2, notContaining) ? cand1 : cand2;
1256
+ const far = dist(cand1, notContaining) >= dist(cand2, notContaining) ? cand1 : cand2;
1257
+ const near = far === cand1 ? cand2 : cand1;
1258
+ return sameSide ? near : far;
1251
1259
  }
1252
1260
  const s1 = sideOf(a, b, cand1);
1253
- return s1 * sN < 0 ? cand1 : cand2;
1261
+ const opp = s1 * sN < 0 ? cand1 : cand2;
1262
+ const same = opp === cand1 ? cand2 : cand1;
1263
+ return sameSide ? same : opp;
1254
1264
  }
1255
1265
  function excenter(vertices, oppositeIndex) {
1256
1266
  const [A, B, C] = vertices;
@@ -1266,6 +1276,35 @@ function excenter(vertices, oppositeIndex) {
1266
1276
  (w[0] * A[1] + w[1] * B[1] + w[2] * C[1]) / sum
1267
1277
  ];
1268
1278
  }
1279
+ function circumcenterXY(a, b, c) {
1280
+ const ax = a[0], ay = a[1], bx = b[0], by = b[1], cx = c[0], cy = c[1];
1281
+ const d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
1282
+ if (Math.abs(d) < 1e-12) return [(ax + bx + cx) / 3, (ay + by + cy) / 3];
1283
+ const ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay) + (cx * cx + cy * cy) * (ay - by)) / d;
1284
+ const uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx) + (cx * cx + cy * cy) * (bx - ax)) / d;
1285
+ return [ux, uy];
1286
+ }
1287
+ function mixtilinearPoint(a, b, c, which) {
1288
+ const O = circumcenterXY(a, b, c);
1289
+ const R = Math.hypot(O[0] - a[0], O[1] - a[1]);
1290
+ const ux1 = (b[0] - a[0]) / (Math.hypot(b[0] - a[0], b[1] - a[1]) || 1);
1291
+ const uy1 = (b[1] - a[1]) / (Math.hypot(b[0] - a[0], b[1] - a[1]) || 1);
1292
+ const ux2 = (c[0] - a[0]) / (Math.hypot(c[0] - a[0], c[1] - a[1]) || 1);
1293
+ const uy2 = (c[1] - a[1]) / (Math.hypot(c[0] - a[0], c[1] - a[1]) || 1);
1294
+ let bx = ux1 + ux2, by = uy1 + uy2;
1295
+ const bl = Math.hypot(bx, by) || 1;
1296
+ bx /= bl;
1297
+ by /= bl;
1298
+ const cosA = ux1 * ux2 + uy1 * uy2;
1299
+ const sinHalf = Math.sqrt(Math.max(0, (1 - cosA) / 2));
1300
+ const cos2Half = Math.max(1e-9, (1 + cosA) / 2);
1301
+ const dotAO = bx * (O[0] - a[0]) + by * (O[1] - a[1]);
1302
+ const d = 2 * (dotAO - R * sinHalf) / cos2Half;
1303
+ const K = [a[0] + d * bx, a[1] + d * by];
1304
+ if (which === "center") return K;
1305
+ const kl = Math.hypot(K[0] - O[0], K[1] - O[1]) || 1;
1306
+ return [O[0] + R * (K[0] - O[0]) / kl, O[1] + R * (K[1] - O[1]) / kl];
1307
+ }
1269
1308
  function pointAtDistanceCoord(from, through, d) {
1270
1309
  const dx = through[0] - from[0];
1271
1310
  const dy = through[1] - from[1];
@@ -1293,29 +1332,38 @@ var init_arcMidpoint = __esm({
1293
1332
  arcMidpointConstraint = definePointConstraint({
1294
1333
  kind: "arcMidpoint",
1295
1334
  validate: (c) => {
1296
- if (!c.circle || !c.a || !c.b || !c.notContaining) {
1297
- throw new Error("point.arcMidpoint: circle, a, b, notContaining b\u1EAFt bu\u1ED9c");
1335
+ if (!c.circle || !c.a || !c.b) {
1336
+ throw new Error("point.arcMidpoint: circle, a, b b\u1EAFt bu\u1ED9c");
1337
+ }
1338
+ if (c.notContaining && c.containing) {
1339
+ throw new Error("point.arcMidpoint: kh\xF4ng th\u1EC3 v\u1EEBa notContaining v\u1EEBa containing");
1298
1340
  }
1299
1341
  },
1300
1342
  describe: (obj, state, c) => {
1301
1343
  const al = state?.objects[c.a]?.label ?? c.a;
1302
1344
  const bl = state?.objects[c.b]?.label ?? c.b;
1303
- const nl = state?.objects[c.notContaining]?.label ?? c.notContaining;
1304
- return `${obj.label} = trung \u0111i\u1EC3m cung ${al}${bl} (kh\xF4ng ch\u1EE9a ${nl})`;
1345
+ const ref = c.containing ?? c.notContaining;
1346
+ if (!ref) return `${obj.label} = trung \u0111i\u1EC3m cung ${al}${bl}`;
1347
+ const rl = state?.objects[ref]?.label ?? ref;
1348
+ const rel = c.containing ? "ch\u1EE9a" : "kh\xF4ng ch\u1EE9a";
1349
+ return `${obj.label} = trung \u0111i\u1EC3m cung ${al}${bl} (${rel} ${rl})`;
1305
1350
  },
1306
1351
  render: (obj, ctx, c, opts) => {
1307
1352
  const board = ctx.jxg;
1308
1353
  const circle = ctx.resolveRef(c.circle);
1309
1354
  const A = ctx.resolveRef(c.a);
1310
1355
  const B = ctx.resolveRef(c.b);
1311
- const N = ctx.resolveRef(c.notContaining);
1356
+ const refName = c.containing ?? c.notContaining;
1357
+ const ref = refName ? ctx.resolveRef(refName) : void 0;
1358
+ const sameSide = !!c.containing;
1312
1359
  const O = circle?.center ?? circle?.midpoint ?? circle;
1313
1360
  const am = () => arcMidpoint(
1314
1361
  [O.X(), O.Y()],
1315
1362
  circle.Radius(),
1316
1363
  [A.X(), A.Y()],
1317
1364
  [B.X(), B.Y()],
1318
- [N.X(), N.Y()]
1365
+ ref ? [ref.X(), ref.Y()] : void 0,
1366
+ sameSide
1319
1367
  );
1320
1368
  return board.create("point", [() => am()[0], () => am()[1]], opts);
1321
1369
  }
@@ -1365,6 +1413,59 @@ var init_excenter = __esm({
1365
1413
  }
1366
1414
  });
1367
1415
 
1416
+ // src/core/scene/kinds/point-constraints/mixtilinearPoint.ts
1417
+ var mixtilinearPointConstraint;
1418
+ var init_mixtilinearPoint = __esm({
1419
+ "src/core/scene/kinds/point-constraints/mixtilinearPoint.ts"() {
1420
+ init_pointConstructions();
1421
+ init_types2();
1422
+ mixtilinearPointConstraint = definePointConstraint({
1423
+ kind: "mixtilinearPoint",
1424
+ validate: (c) => {
1425
+ if (!Array.isArray(c.vertices) || c.vertices.length !== 3 || !c.vertices.every(Boolean)) {
1426
+ throw new Error("point.mixtilinearPoint: vertices ph\u1EA3i l\xE0 tuple 3 id non-empty");
1427
+ }
1428
+ if (c.which !== "center" && c.which !== "touch") {
1429
+ throw new Error("point.mixtilinearPoint: which ph\u1EA3i 'center' | 'touch'");
1430
+ }
1431
+ },
1432
+ describe: (obj, state, c) => {
1433
+ const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
1434
+ return c.which === "center" ? `${obj.label} = t\xE2m \u0111\u01B0\u1EDDng tr\xF2n mixtilinear \u0394${labels}` : `${obj.label} = ti\u1EBFp \u0111i\u1EC3m mixtilinear \u0394${labels} v\u1EDBi (O)`;
1435
+ },
1436
+ render: (obj, ctx, c, opts) => {
1437
+ const board = ctx.jxg;
1438
+ const a = ctx.resolveRef(c.vertices[0]);
1439
+ const b = ctx.resolveRef(c.vertices[1]);
1440
+ const d = ctx.resolveRef(c.vertices[2]);
1441
+ const mp = () => mixtilinearPoint(
1442
+ [a.X(), a.Y()],
1443
+ [b.X(), b.Y()],
1444
+ [d.X(), d.Y()],
1445
+ c.which
1446
+ );
1447
+ return board.create("point", [() => mp()[0], () => mp()[1]], opts);
1448
+ }
1449
+ });
1450
+ }
1451
+ });
1452
+
1453
+ // src/core/scene/kinds/_label.ts
1454
+ function labelOpts(labelOffset, dflt) {
1455
+ const offset = labelOffset ?? dflt;
1456
+ return { label: { fixed: false, ...offset ? { offset } : {} } };
1457
+ }
1458
+ function readLabelOffset(label) {
1459
+ const off = label.evalVisProp?.("offset") ?? label.visProp?.offset;
1460
+ const rel = label.relativeCoords?.scrCoords;
1461
+ if (!off || !rel || off.length < 2 || rel.length < 3) return null;
1462
+ return [Math.round(off[0] + rel[1]), Math.round(off[1] - rel[2])];
1463
+ }
1464
+ var init_label = __esm({
1465
+ "src/core/scene/kinds/_label.ts"() {
1466
+ }
1467
+ });
1468
+
1368
1469
  // src/core/scene/kinds/point-constraints/shared.ts
1369
1470
  function buildJxgTransforms(board, ctx, t) {
1370
1471
  switch (t.kind) {
@@ -1416,11 +1517,13 @@ function buildPointOpts(obj) {
1416
1517
  strokeColor: obj.attrs.color ?? "#1e40af",
1417
1518
  fillColor: obj.attrs.color ?? "#1e40af",
1418
1519
  face: obj.attrs.face ?? "o",
1419
- size: obj.attrs.size ?? 4
1520
+ size: obj.attrs.size ?? 4,
1521
+ ...labelOpts(obj.attrs.labelOffset, [10, 10])
1420
1522
  };
1421
1523
  }
1422
1524
  var init_shared = __esm({
1423
1525
  "src/core/scene/kinds/point-constraints/shared.ts"() {
1526
+ init_label();
1424
1527
  }
1425
1528
  });
1426
1529
 
@@ -1729,6 +1832,7 @@ var init_registry2 = __esm({
1729
1832
  init_centroid();
1730
1833
  init_arcMidpoint();
1731
1834
  init_excenter();
1835
+ init_mixtilinearPoint();
1732
1836
  init_pointAtDistance();
1733
1837
  init_circleIntersection();
1734
1838
  init_circleSecondIntersection();
@@ -1754,6 +1858,7 @@ var init_registry2 = __esm({
1754
1858
  centroidConstraint,
1755
1859
  arcMidpointConstraint,
1756
1860
  excenterConstraint,
1861
+ mixtilinearPointConstraint,
1757
1862
  pointAtDistanceConstraint,
1758
1863
  circleIntersectionConstraint,
1759
1864
  circleSecondIntersectionConstraint,
@@ -1778,6 +1883,7 @@ var init_point = __esm({
1778
1883
  init_d_constraint2();
1779
1884
  init_registry2();
1780
1885
  init_shared();
1886
+ init_label();
1781
1887
  def3 = {
1782
1888
  type: "point",
1783
1889
  schemaVersion: 1,
@@ -1845,7 +1951,8 @@ var init_point = __esm({
1845
1951
  strokeColor: obj.attrs.color ?? "#1e40af",
1846
1952
  fillColor: obj.attrs.color ?? "#1e40af",
1847
1953
  face: obj.attrs.face ?? "o",
1848
- size: obj.attrs.size ?? 4
1954
+ size: obj.attrs.size ?? 4,
1955
+ ...labelOpts(obj.attrs.labelOffset, [10, 10])
1849
1956
  });
1850
1957
  } catch {
1851
1958
  }
@@ -1865,6 +1972,7 @@ var init_segment = __esm({
1865
1972
  "src/core/scene/kinds/segment.ts"() {
1866
1973
  init_registry();
1867
1974
  init_labelOf();
1975
+ init_label();
1868
1976
  def4 = {
1869
1977
  type: "segment",
1870
1978
  schemaVersion: 1,
@@ -1896,7 +2004,8 @@ var init_segment = __esm({
1896
2004
  strokeWidth: obj.attrs.width ?? 2,
1897
2005
  dash: obj.attrs.dash ?? 0,
1898
2006
  visible: obj.visible,
1899
- fixed: obj.locked
2007
+ fixed: obj.locked,
2008
+ ...labelOpts(obj.attrs.labelOffset)
1900
2009
  });
1901
2010
  }
1902
2011
  };
@@ -1933,6 +2042,7 @@ var init_line = __esm({
1933
2042
  "src/core/scene/kinds/line.ts"() {
1934
2043
  init_registry();
1935
2044
  init_labelOf();
2045
+ init_label();
1936
2046
  init_pointConstructions();
1937
2047
  def5 = {
1938
2048
  type: "line",
@@ -1975,7 +2085,8 @@ var init_line = __esm({
1975
2085
  strokeWidth: obj.attrs.width ?? 2,
1976
2086
  dash: obj.attrs.dash ?? 0,
1977
2087
  visible: obj.visible,
1978
- fixed: obj.locked
2088
+ fixed: obj.locked,
2089
+ ...labelOpts(obj.attrs.labelOffset)
1979
2090
  };
1980
2091
  const c = obj.attrs.construction;
1981
2092
  if (!c) {
@@ -2231,6 +2342,7 @@ var init_circle = __esm({
2231
2342
  "src/core/scene/kinds/circle.ts"() {
2232
2343
  init_registry();
2233
2344
  init_labelOf();
2345
+ init_label();
2234
2346
  init_pointConstructions();
2235
2347
  def8 = {
2236
2348
  type: "circle",
@@ -2292,15 +2404,17 @@ var init_circle = __esm({
2292
2404
  const board = ctx.jxg;
2293
2405
  const isCenterLabel = (l) => /^[A-Z]['′]?\d*$/u.test(l);
2294
2406
  const isCenter = isCenterLabel(obj.label);
2407
+ const isRenamedCircle = /_c$/.test(obj.label);
2295
2408
  const baseOpts = {
2296
2409
  name: obj.label,
2297
- withLabel: isCenter ? obj.attrs.showLabel ?? false : true,
2410
+ withLabel: isCenter ? obj.attrs.showLabel ?? false : isRenamedCircle ? false : true,
2298
2411
  strokeColor: obj.attrs.color ?? "#0f172a",
2299
2412
  strokeWidth: obj.attrs.width ?? 2,
2300
2413
  dash: obj.attrs.dash ?? 0,
2301
2414
  fillColor: "none",
2302
2415
  visible: obj.visible,
2303
- fixed: obj.locked
2416
+ fixed: obj.locked,
2417
+ ...labelOpts(obj.attrs.labelOffset)
2304
2418
  };
2305
2419
  const c = asConstruction(obj.attrs);
2306
2420
  if (c?.kind === "circumscribed") {
@@ -2785,8 +2899,11 @@ var init_intersection = __esm({
2785
2899
  const opts = {
2786
2900
  name: obj.label,
2787
2901
  withLabel: true,
2788
- strokeColor: obj.attrs.color ?? "#dc2626",
2789
- fillColor: obj.attrs.color ?? "#dc2626",
2902
+ // Cùng màu xanh với mọi điểm khác (buildPointOpts dùng '#1e40af').
2903
+ // Trước đây điểm giao tô đỏ '#dc2626' → tách biệt thị giác nhưng gây
2904
+ // khó hiểu ("vì sao điểm này khác màu?"). Giữ chung một màu.
2905
+ strokeColor: obj.attrs.color ?? "#1e40af",
2906
+ fillColor: obj.attrs.color ?? "#1e40af",
2790
2907
  visible: obj.visible,
2791
2908
  fixed: obj.locked
2792
2909
  };
@@ -2795,6 +2912,38 @@ var init_intersection = __esm({
2795
2912
  }
2796
2913
  const branch = obj.attrs.branch ?? 0;
2797
2914
  return board.create("intersection", [a, b, branch], opts);
2915
+ },
2916
+ /**
2917
+ * Cập nhật TẠI CHỖ các thuộc tính "trang trí" (tên/màu/ẩn-hiện/khoá) qua
2918
+ * setAttribute — giữ nguyên JxgObj identity nên các object phụ thuộc điểm
2919
+ * giao (đường thẳng qua nó, …) KHÔNG bị stale parent ref. Mô phỏng update
2920
+ * hook của point.ts.
2921
+ *
2922
+ * Nếu định nghĩa hình học đổi (kind/ref1/ref2/branch) thì throw → renderer
2923
+ * fallback remove + create để JSXGraph dựng lại phép giao đúng.
2924
+ */
2925
+ update: (obj, prev, ctx, existing) => {
2926
+ const a = obj.attrs;
2927
+ const p = prev.attrs;
2928
+ const branchA = a.branch;
2929
+ const branchP = p.branch;
2930
+ if (a.kind !== p.kind || a.ref1 !== p.ref1 || a.ref2 !== p.ref2 || branchA !== branchP) {
2931
+ throw new Error("intersection: \u0111\u1ECBnh ngh\u0129a h\xECnh h\u1ECDc \u0111\u1ED5i \u2014 recreate");
2932
+ }
2933
+ const el = existing;
2934
+ if (typeof el.setAttribute === "function") {
2935
+ try {
2936
+ el.setAttribute({
2937
+ name: obj.label,
2938
+ withLabel: true,
2939
+ strokeColor: a.color ?? "#1e40af",
2940
+ fillColor: a.color ?? "#1e40af",
2941
+ visible: obj.visible,
2942
+ fixed: obj.locked
2943
+ });
2944
+ } catch {
2945
+ }
2946
+ }
2798
2947
  }
2799
2948
  };
2800
2949
  registerKind(def12);
@@ -3481,6 +3630,7 @@ var init_JxgRenderer = __esm({
3481
3630
  init_registry();
3482
3631
  init_types2d();
3483
3632
  init_parser();
3633
+ init_label();
3484
3634
  JxgRenderer = class {
3485
3635
  constructor(store, board, options = {}) {
3486
3636
  this.elements = /* @__PURE__ */ new Map();
@@ -3549,6 +3699,7 @@ var init_JxgRenderer = __esm({
3549
3699
  this.elements.set(obj.id, el);
3550
3700
  this.attachFreePointDragSync(obj, el);
3551
3701
  this.attachGliderDragSync(obj, el);
3702
+ this.attachLabelDragSync(obj, el);
3552
3703
  } catch (err) {
3553
3704
  console.warn(`[scene/render/2d] kh\xF4ng render \u0111\u01B0\u1EE3c ${obj.kind} id="${obj.id}":`, err);
3554
3705
  }
@@ -3663,6 +3814,54 @@ var init_JxgRenderer = __esm({
3663
3814
  });
3664
3815
  });
3665
3816
  }
3817
+ /**
3818
+ * Cho phép kéo NHÃN của bất kỳ object nào có label (point/line/segment/
3819
+ * circle...). Khi user kéo nhãn, JSXGraph cộng dồn vào label.relativeCoords
3820
+ * (drag-delta screen px) nhưng KHÔNG đổi attribute offset → vị trí cuối =
3821
+ * offset + relativeCoords. Ta gộp lại thành offset thuần (readLabelOffset),
3822
+ * zero relativeCoords để khỏi double-count khi update-hook/recreate áp lại
3823
+ * offset, rồi dispatch UPDATE_ATTRS { labelOffset }. Right-click → reset.
3824
+ */
3825
+ attachLabelDragSync(obj, el) {
3826
+ const label = el?.label;
3827
+ if (!label || typeof label.on !== "function") return;
3828
+ const sceneId = obj.id;
3829
+ label.on("up", () => {
3830
+ if (this.disposed) return;
3831
+ const off = readLabelOffset(label);
3832
+ if (!off) return;
3833
+ const cur = this.store.getState().objects[sceneId];
3834
+ if (!cur) return;
3835
+ const prev = cur.attrs.labelOffset;
3836
+ if (prev && prev[0] === off[0] && prev[1] === off[1]) return;
3837
+ try {
3838
+ label.setAttribute({ offset: off });
3839
+ if (label.relativeCoords?.scrCoords) {
3840
+ label.relativeCoords.scrCoords[1] = 0;
3841
+ label.relativeCoords.scrCoords[2] = 0;
3842
+ }
3843
+ } catch {
3844
+ }
3845
+ this.store.dispatch({
3846
+ type: "UPDATE_ATTRS",
3847
+ payload: { id: sceneId, patch: { labelOffset: off } }
3848
+ });
3849
+ });
3850
+ const node = label.rendNode;
3851
+ if (node?.addEventListener) {
3852
+ node.addEventListener("contextmenu", (ev) => {
3853
+ if (this.disposed) return;
3854
+ ev.preventDefault();
3855
+ const c = this.store.getState().objects[sceneId];
3856
+ if (!c) return;
3857
+ if (c.attrs.labelOffset === void 0) return;
3858
+ this.store.dispatch({
3859
+ type: "UPDATE_ATTRS",
3860
+ payload: { id: sceneId, patch: { labelOffset: void 0 } }
3861
+ });
3862
+ });
3863
+ }
3864
+ }
3666
3865
  remove(id) {
3667
3866
  this.removeHalo(id);
3668
3867
  this.selectedIds.delete(id);
@@ -3799,88 +3998,89 @@ var init_JxgRenderer = __esm({
3799
3998
  layer: 4,
3800
3999
  needsRegularUpdate: true
3801
4000
  };
4001
+ const elementClass = el.elementClass;
4002
+ const elType = el.elType;
4003
+ const isPointLike = elementClass === 1 || elType === "point" || elType === "glider" || elType === "intersection";
4004
+ const isPolygonLike = Array.isArray(el.vertices) && el.vertices.length >= 3;
4005
+ const isCircleLike = elementClass === 3 || elType === "circle" || elType === "circumcircle" || elType === "incircle";
4006
+ const isSegment = elType === "segment";
4007
+ const isLineLike = elementClass === 2 || elType === "line" || elType === "arrow" || elType === "ray" || elType === "vector" || elType === "tangent" || elType === "normal" || elType === "parallel" || elType === "perpendicular" || elType === "bisector";
3802
4008
  const halos = [];
3803
4009
  try {
3804
- switch (el.elType) {
3805
- case "point":
3806
- case "glider":
3807
- case "intersection": {
3808
- const baseSize = el.getAttribute?.("size") ?? 4;
3809
- const halo = board.create("point", [
3810
- () => el.X?.() ?? 0,
3811
- () => el.Y?.() ?? 0
3812
- ], {
3813
- ...haloBase,
3814
- size: baseSize + 6,
3815
- face: "o",
3816
- strokeWidth: 2,
3817
- strokeOpacity: 0.75,
3818
- fillOpacity: 0.25
3819
- });
3820
- halos.push(halo);
3821
- break;
3822
- }
3823
- case "segment": {
3824
- if (el.point1 && el.point2) {
3825
- const halo = board.create("segment", [el.point1, el.point2], {
3826
- ...haloBase,
3827
- strokeWidth: 9,
3828
- straightFirst: false,
3829
- straightLast: false
3830
- });
3831
- halos.push(halo);
3832
- }
3833
- break;
3834
- }
3835
- case "line":
3836
- case "arrow":
3837
- case "ray":
3838
- case "vector":
3839
- case "tangent":
3840
- case "normal":
3841
- case "parallel":
3842
- case "perpendicular":
3843
- case "bisector": {
3844
- if (el.point1 && el.point2) {
3845
- const halo = board.create("line", [el.point1, el.point2], {
3846
- ...haloBase,
3847
- strokeWidth: 9
3848
- });
3849
- halos.push(halo);
3850
- }
3851
- break;
3852
- }
3853
- case "circle": {
3854
- if (el.center && typeof el.Radius === "function") {
3855
- const halo = board.create("circle", [el.center, () => el.Radius?.() ?? 0], {
3856
- ...haloBase,
3857
- strokeWidth: 9,
3858
- fillOpacity: 0
3859
- });
3860
- halos.push(halo);
3861
- }
3862
- break;
3863
- }
3864
- case "polygon": {
3865
- if (Array.isArray(el.vertices) && el.vertices.length >= 3) {
3866
- const last = el.vertices.length - 1;
3867
- const verts = el.vertices[last] === el.vertices[0] ? el.vertices.slice(0, last) : el.vertices.slice();
3868
- const halo = board.create("polygon", verts, {
3869
- ...haloBase,
3870
- fillOpacity: 0.2,
3871
- borders: {
3872
- strokeColor: SEL_STROKE,
3873
- strokeWidth: 7,
3874
- strokeOpacity: 0.55,
3875
- highlight: false
3876
- }
3877
- });
3878
- halos.push(halo);
4010
+ if (isPointLike) {
4011
+ const baseSize = el.getAttribute?.("size") ?? 4;
4012
+ const halo = board.create("point", [
4013
+ () => el.X?.() ?? 0,
4014
+ () => el.Y?.() ?? 0
4015
+ ], {
4016
+ ...haloBase,
4017
+ size: baseSize + 6,
4018
+ face: "o",
4019
+ strokeWidth: 2,
4020
+ strokeOpacity: 0.75,
4021
+ fillOpacity: 0.25
4022
+ });
4023
+ halos.push(halo);
4024
+ } else if (isPolygonLike) {
4025
+ const verts = el.vertices;
4026
+ const last = verts.length - 1;
4027
+ const trimmed = verts[last] === verts[0] ? verts.slice(0, last) : verts.slice();
4028
+ const halo = board.create("polygon", trimmed, {
4029
+ ...haloBase,
4030
+ fillOpacity: 0.2,
4031
+ borders: {
4032
+ strokeColor: SEL_STROKE,
4033
+ strokeWidth: 7,
4034
+ strokeOpacity: 0.55,
4035
+ highlight: false
3879
4036
  }
3880
- break;
4037
+ });
4038
+ halos.push(halo);
4039
+ } else if (isCircleLike && el.center && typeof el.Radius === "function") {
4040
+ const halo = board.create("circle", [el.center, () => el.Radius?.() ?? 0], {
4041
+ ...haloBase,
4042
+ strokeWidth: 9,
4043
+ fillOpacity: 0
4044
+ });
4045
+ halos.push(halo);
4046
+ } else if (isSegment && el.point1 && el.point2) {
4047
+ const halo = board.create("segment", [el.point1, el.point2], {
4048
+ ...haloBase,
4049
+ strokeWidth: 9,
4050
+ straightFirst: false,
4051
+ straightLast: false
4052
+ });
4053
+ halos.push(halo);
4054
+ } else if (isLineLike && el.point1 && el.point2) {
4055
+ const halo = board.create("line", [el.point1, el.point2], {
4056
+ ...haloBase,
4057
+ strokeWidth: 9
4058
+ });
4059
+ halos.push(halo);
4060
+ } else if (el.center && (el.radiuspoint ?? el.radiusPoint) && (el.anglepoint ?? el.anglePoint) && (elType === "arc" || elType === "semicircle" || elType === "circumcirclearc" || elType === "sector" || elType === "angle")) {
4061
+ const rp = el.radiuspoint ?? el.radiusPoint;
4062
+ const ap = el.anglepoint ?? el.anglePoint;
4063
+ if (elType === "sector") {
4064
+ halos.push(board.create("sector", [el.center, rp, ap], {
4065
+ ...haloBase,
4066
+ strokeWidth: 7,
4067
+ fillOpacity: 0.2
4068
+ }));
4069
+ } else if (elType === "angle") {
4070
+ halos.push(board.create("angle", [rp, el.center, ap], {
4071
+ ...haloBase,
4072
+ strokeWidth: 7,
4073
+ fillOpacity: 0.2,
4074
+ radius: el.getAttribute?.("radius") ?? 1
4075
+ }));
4076
+ } else {
4077
+ halos.push(board.create("arc", [el.center, rp, ap], {
4078
+ ...haloBase,
4079
+ strokeWidth: 9,
4080
+ fillColor: "none",
4081
+ fillOpacity: 0
4082
+ }));
3881
4083
  }
3882
- default:
3883
- break;
3884
4084
  }
3885
4085
  } catch (err) {
3886
4086
  console.warn("[scene/render/2d] halo create fail:", err);
@@ -4040,6 +4240,49 @@ var init_autoFitBbox = __esm({
4040
4240
  }
4041
4241
  });
4042
4242
 
4243
+ // src/stamps/geometry-2d/autoFitBoard.ts
4244
+ function isDefaultBbox(bbox) {
4245
+ const d = DEFAULT_VIEW_2D.bbox;
4246
+ return bbox.length === 4 && bbox[0] === d[0] && bbox[1] === d[1] && bbox[2] === d[2] && bbox[3] === d[3];
4247
+ }
4248
+ function fittedBboxFromBoard(board, aspect) {
4249
+ const objs = board?.objectsList;
4250
+ if (!Array.isArray(objs)) return null;
4251
+ const points = [];
4252
+ const circles = [];
4253
+ for (const raw of objs) {
4254
+ const o = raw;
4255
+ if (o?.elementClass === 1 && typeof o.X === "function" && typeof o.Y === "function") {
4256
+ const x = o.X(), y = o.Y();
4257
+ if (Number.isFinite(x) && Number.isFinite(y)) points.push([x, y]);
4258
+ } else if (o?.elementClass === 3 && o.center?.X && typeof o.Radius === "function") {
4259
+ const cx = o.center.X(), cy = o.center.Y(), r = o.Radius();
4260
+ if (Number.isFinite(cx) && Number.isFinite(cy) && Number.isFinite(r)) {
4261
+ circles.push({ cx, cy, r });
4262
+ }
4263
+ }
4264
+ }
4265
+ const safeAspect = Number.isFinite(aspect) && aspect > 0 ? aspect : 1;
4266
+ return computeAutoFitBbox(points, circles, safeAspect);
4267
+ }
4268
+ function autoFitBoardToContent(board, aspect) {
4269
+ const bbox = fittedBboxFromBoard(board, aspect);
4270
+ if (!bbox) return;
4271
+ const b = board;
4272
+ try {
4273
+ b.setBoundingBox?.(bbox);
4274
+ if (typeof b.update === "function") b.update();
4275
+ if (typeof b.fullUpdate === "function") b.fullUpdate();
4276
+ } catch {
4277
+ }
4278
+ }
4279
+ var init_autoFitBoard = __esm({
4280
+ "src/stamps/geometry-2d/autoFitBoard.ts"() {
4281
+ init_types();
4282
+ init_autoFitBbox();
4283
+ }
4284
+ });
4285
+
4043
4286
  // src/stamps/geometry-2d/labelLayout.ts
4044
4287
  function radialLabelOffsets(points, R = 14) {
4045
4288
  const out = /* @__PURE__ */ new Map();
@@ -4074,8 +4317,9 @@ function containerDimsForBbox(bbox) {
4074
4317
  if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) {
4075
4318
  return { width: FALLBACK_W, height: FALLBACK_H };
4076
4319
  }
4077
- let width = w * PIXELS_PER_UNIT;
4078
- let height = h * PIXELS_PER_UNIT;
4320
+ const pxPerUnit = DEFAULT_VIEW_PX / Math.max(w, h);
4321
+ let width = w * pxPerUnit;
4322
+ let height = h * pxPerUnit;
4079
4323
  const maxAxis = Math.max(width, height);
4080
4324
  if (maxAxis > MAX_DIM) {
4081
4325
  const ratio = MAX_DIM / maxAxis;
@@ -4090,31 +4334,6 @@ function containerDimsForBbox(bbox) {
4090
4334
  }
4091
4335
  return { width: Math.round(width), height: Math.round(height) };
4092
4336
  }
4093
- function autoFitBboxFromBoard(board, aspect) {
4094
- const objs = board?.objectsList;
4095
- if (!Array.isArray(objs)) return;
4096
- const points = [];
4097
- const circles = [];
4098
- for (const o of objs) {
4099
- if (o?.elementClass === 1 && typeof o.X === "function" && typeof o.Y === "function") {
4100
- const x = o.X(), y = o.Y();
4101
- if (Number.isFinite(x) && Number.isFinite(y)) points.push([x, y]);
4102
- } else if (o?.elementClass === 3 && o.center?.X && typeof o.Radius === "function") {
4103
- const cx = o.center.X(), cy = o.center.Y(), r = o.Radius();
4104
- if (Number.isFinite(cx) && Number.isFinite(cy) && Number.isFinite(r)) {
4105
- circles.push({ cx, cy, r });
4106
- }
4107
- }
4108
- }
4109
- const bbox = computeAutoFitBbox(points, circles, aspect);
4110
- if (!bbox) return;
4111
- try {
4112
- board.setBoundingBox(bbox);
4113
- if (typeof board.update === "function") board.update();
4114
- if (typeof board.fullUpdate === "function") board.fullUpdate();
4115
- } catch {
4116
- }
4117
- }
4118
4337
  function applyRadialLabelOffsets(board) {
4119
4338
  const objs = board?.objectsList;
4120
4339
  if (!Array.isArray(objs)) return;
@@ -4140,10 +4359,6 @@ function applyRadialLabelOffsets(board) {
4140
4359
  } catch {
4141
4360
  }
4142
4361
  }
4143
- function isDefaultBbox(bbox) {
4144
- const d = DEFAULT_VIEW_2D.bbox;
4145
- return bbox.length === 4 && bbox[0] === d[0] && bbox[1] === d[1] && bbox[2] === d[2] && bbox[3] === d[3];
4146
- }
4147
4362
  async function renderGeometrySvgFromState(jsonState) {
4148
4363
  const state = deserializeBoard(jsonState);
4149
4364
  const view = state.meta.domain === "2d" ? state.meta.view : DEFAULT_VIEW_2D;
@@ -4178,7 +4393,7 @@ async function renderGeometrySvgFromState(jsonState) {
4178
4393
  const store = createStore(state);
4179
4394
  const renderer = new JxgRenderer(store, board);
4180
4395
  if (shouldAutoFit) {
4181
- autoFitBboxFromBoard(board, dims.width / dims.height);
4396
+ autoFitBoardToContent(board, dims.width / dims.height);
4182
4397
  applyRadialLabelOffsets(board);
4183
4398
  }
4184
4399
  return renderer;
@@ -4186,7 +4401,7 @@ async function renderGeometrySvgFromState(jsonState) {
4186
4401
  });
4187
4402
  return svgString;
4188
4403
  }
4189
- var PIXELS_PER_UNIT, MIN_DIM, MAX_DIM, FALLBACK_W, FALLBACK_H;
4404
+ var DEFAULT_VIEW_PX, MIN_DIM, MAX_DIM, FALLBACK_W, FALLBACK_H;
4190
4405
  var init_render = __esm({
4191
4406
  "src/stamps/geometry-2d/render.ts"() {
4192
4407
  init_serialize();
@@ -4195,9 +4410,9 @@ var init_render = __esm({
4195
4410
  init_types();
4196
4411
  init_JxgRenderer();
4197
4412
  init_jxgOffscreenRender();
4198
- init_autoFitBbox();
4413
+ init_autoFitBoard();
4199
4414
  init_labelLayout();
4200
- PIXELS_PER_UNIT = 20;
4415
+ DEFAULT_VIEW_PX = 500;
4201
4416
  MIN_DIM = 100;
4202
4417
  MAX_DIM = 1200;
4203
4418
  FALLBACK_W = 400;
@@ -5757,41 +5972,48 @@ var init_icons = __esm({
5757
5972
  /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4", cy: "4", r: "1.7", fill: C_POINT })
5758
5973
  ] }),
5759
5974
  // ===== Nâng cao / kind chưa có icon =====
5760
- excenter: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
5761
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 16 L12 5 L19 16 Z", stroke: "currentColor", strokeWidth: "1.3" }),
5762
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "20", r: "3", fill: "none", stroke: C_CONSTRUCT, strokeWidth: "1.3" }),
5763
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "20", r: "1.4", fill: C_CONSTRUCT })
5975
+ // Hệ màu đồng bộ: XANH = input click sẵn, ĐỎ = output suy ra. Toạ độ tính để
5976
+ // điểm nằm ĐÚNG trên đường tròn/cung/tiếp tuyến (không trôi nổi).
5977
+ excenter: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", strokeLinejoin: "round", children: [
5978
+ /* @__PURE__ */ jsxRuntime.jsx("polygon", { points: "12,3 15,15 9,15", stroke: "currentColor", strokeWidth: "1.4", fill: "none" }),
5979
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "9", y1: "15", x2: "8", y2: "18.9", stroke: "#94a3b8", strokeWidth: "1", strokeDasharray: "1.6 1.6" }),
5980
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "15", y1: "15", x2: "16", y2: "18.9", stroke: "#94a3b8", strokeWidth: "1", strokeDasharray: "1.6 1.6" }),
5981
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "18.9", r: "3.8", stroke: "currentColor", strokeWidth: "1.3" }),
5982
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "3", r: "1.6", fill: C_POINT }),
5983
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9", cy: "15", r: "1.6", fill: C_POINT }),
5984
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "15", cy: "15", r: "1.6", fill: C_POINT }),
5985
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "18.9", r: "2.2", fill: C_CONSTRUCT })
5764
5986
  ] }),
5765
- tangencyPoint: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
5766
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "10", cy: "12", r: "6.5", stroke: "currentColor", strokeWidth: "1.3" }),
5767
- /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "16.5", y1: "3", x2: "16.5", y2: "21", stroke: "currentColor", strokeWidth: "1.3" }),
5768
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "16.5", cy: "12", r: "2.2", fill: C_CONSTRUCT })
5987
+ tangencyPoint: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
5988
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9.5", cy: "12", r: "7", stroke: "currentColor", strokeWidth: "1.4" }),
5989
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "16.5", y1: "3.5", x2: "16.5", y2: "20.5", stroke: "currentColor", strokeWidth: "1.4" }),
5990
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "16.5", cy: "12", r: "2.3", fill: C_CONSTRUCT })
5769
5991
  ] }),
5770
- secondIntersection: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
5771
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "11", cy: "12", r: "6.5", stroke: "currentColor", strokeWidth: "1.3" }),
5772
- /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "2", y1: "8", x2: "22", y2: "16", stroke: "currentColor", strokeWidth: "1.3" }),
5773
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "6.2", cy: "9.6", r: "1.6", fill: "currentColor" }),
5774
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "15.8", cy: "14.4", r: "2.4", fill: C_CONSTRUCT })
5992
+ secondIntersection: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
5993
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "11", cy: "12", r: "6.5", stroke: "currentColor", strokeWidth: "1.4" }),
5994
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "2.4", y1: "8.2", x2: "20.4", y2: "11.4", stroke: "currentColor", strokeWidth: "1.4" }),
5995
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "5.37", cy: "8.75", r: "1.8", fill: C_POINT }),
5996
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "17.4", cy: "10.87", r: "2.4", fill: C_CONSTRUCT })
5775
5997
  ] }),
5776
5998
  arcMidpoint: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
5777
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 17 A 9 9 0 0 1 20 17", stroke: "currentColor", strokeWidth: "1.4" }),
5778
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4", cy: "17", r: "1.6", fill: "currentColor" }),
5779
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "20", cy: "17", r: "1.6", fill: "currentColor" }),
5780
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "8.2", r: "2.4", fill: C_CONSTRUCT })
5999
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 17 A 8.5 8.5 0 0 1 20 17", stroke: "currentColor", strokeWidth: "1.6", fill: "none" }),
6000
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4", cy: "17", r: "1.8", fill: C_POINT }),
6001
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "20", cy: "17", r: "1.8", fill: C_POINT }),
6002
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "11.4", r: "2.3", fill: C_CONSTRUCT })
5781
6003
  ] }),
5782
6004
  circleIntersection: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
5783
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9", cy: "12", r: "6", stroke: "currentColor", strokeWidth: "1.3" }),
5784
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "15", cy: "12", r: "6", stroke: "currentColor", strokeWidth: "1.3" }),
5785
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "7.6", r: "2", fill: C_CONSTRUCT }),
5786
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "16.4", r: "2", fill: C_CONSTRUCT })
6005
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9", cy: "12", r: "6", stroke: "currentColor", strokeWidth: "1.4" }),
6006
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "15", cy: "12", r: "6", stroke: "currentColor", strokeWidth: "1.4" }),
6007
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "6.8", r: "2", fill: C_CONSTRUCT }),
6008
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "17.2", r: "2", fill: C_CONSTRUCT })
5787
6009
  ] }),
5788
6010
  tangentPointExt: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
5789
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "14", cy: "12", r: "6", stroke: "currentColor", strokeWidth: "1.3" }),
5790
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "3.5", cy: "12", r: "1.8", fill: C_POINT }),
5791
- /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3.5", y1: "12", x2: "17.5", y2: "7.5", stroke: "currentColor", strokeWidth: "1.2" }),
5792
- /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3.5", y1: "12", x2: "17.5", y2: "16.5", stroke: "currentColor", strokeWidth: "1.2" }),
5793
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "17.5", cy: "7.5", r: "1.8", fill: C_CONSTRUCT }),
5794
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "17.5", cy: "16.5", r: "1.8", fill: C_CONSTRUCT })
6011
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "13", cy: "12", r: "6", stroke: "currentColor", strokeWidth: "1.4" }),
6012
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "12", x2: "11.8", y2: "18.6", stroke: "currentColor", strokeWidth: "1.3" }),
6013
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "12", x2: "11.8", y2: "5.4", stroke: "currentColor", strokeWidth: "1.3" }),
6014
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "3", cy: "12", r: "2", fill: C_POINT }),
6015
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9.4", cy: "16.8", r: "1.8", fill: C_CONSTRUCT }),
6016
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9.4", cy: "7.2", r: "1.8", fill: C_CONSTRUCT })
5795
6017
  ] }),
5796
6018
  circleCR: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
5797
6019
  /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "7.5", stroke: "currentColor", strokeWidth: "1.3" }),
@@ -5802,9 +6024,11 @@ var init_icons = __esm({
5802
6024
  /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 19 L12 4 L21 19 Z", stroke: "currentColor", strokeWidth: "1.3", strokeLinejoin: "round" }),
5803
6025
  /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "14", r: "4.2", fill: "none", stroke: C_CONSTRUCT, strokeWidth: "1.3" })
5804
6026
  ] }),
5805
- excircle: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
5806
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 9 L14 4 L18 13 Z", stroke: "currentColor", strokeWidth: "1.2" }),
5807
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9", cy: "17", r: "4.6", fill: "none", stroke: C_CONSTRUCT, strokeWidth: "1.3" })
6027
+ excircle: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", strokeLinejoin: "round", children: [
6028
+ /* @__PURE__ */ jsxRuntime.jsx("polygon", { points: "12,3 15,15 9,15", stroke: "currentColor", strokeWidth: "1.4", fill: "none" }),
6029
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "9", y1: "15", x2: "8", y2: "18.9", stroke: "#94a3b8", strokeWidth: "1", strokeDasharray: "1.6 1.6" }),
6030
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "15", y1: "15", x2: "16", y2: "18.9", stroke: "#94a3b8", strokeWidth: "1", strokeDasharray: "1.6 1.6" }),
6031
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "18.9", r: "3.8", stroke: C_CONSTRUCT, strokeWidth: "1.5", fill: C_CONSTRUCT, fillOpacity: "0.12" })
5808
6032
  ] }),
5809
6033
  pointOn: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
5810
6034
  /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "7.5", stroke: "currentColor", strokeWidth: "1.3" }),
@@ -7814,11 +8038,11 @@ function buildObjectSnapshot(state, id, anchorScreen) {
7814
8038
  const obj = state.objects[id];
7815
8039
  if (!obj) return null;
7816
8040
  const k = obj.kind;
7817
- if (k !== "point" && k !== "line" && k !== "circle" && k !== "segment" && k !== "ray" && k !== "vector") {
8041
+ if (k !== "point" && k !== "line" && k !== "circle" && k !== "segment" && k !== "ray" && k !== "vector" && k !== "intersection") {
7818
8042
  return null;
7819
8043
  }
7820
8044
  const a = obj.attrs;
7821
- const jKind = k === "point" ? "point" : k === "circle" ? "circle" : "line";
8045
+ const jKind = k === "point" || k === "intersection" ? "point" : k === "circle" ? "circle" : "line";
7822
8046
  return {
7823
8047
  id,
7824
8048
  kind: jKind,
@@ -8226,6 +8450,7 @@ var init_MiniBoard = __esm({
8226
8450
  init_safeJsx();
8227
8451
  init_attachJxgWheelZoom();
8228
8452
  init_initJxgBoard();
8453
+ init_autoFitBoard();
8229
8454
  init_snapshot();
8230
8455
  init_previewActions();
8231
8456
  init_buildHandle();
@@ -8465,6 +8690,11 @@ var init_MiniBoard = __esm({
8465
8690
  rendererRef.current = new JxgRenderer(store, board, {
8466
8691
  theme: paletteFor(isDarkRef.current)
8467
8692
  });
8693
+ if (isDefaultBbox(initialView.bbox)) {
8694
+ const el = containerRef.current;
8695
+ const aspect = el && el.clientHeight > 0 ? el.clientWidth / el.clientHeight : 1;
8696
+ safeJsx("MiniBoard.autoFit", () => autoFitBoardToContent(board, aspect));
8697
+ }
8468
8698
  if (containerRef.current) {
8469
8699
  wheelCleanup = attachJxgWheelZoom(containerRef.current, board, "MiniBoard.2d");
8470
8700
  }
@@ -9262,6 +9492,7 @@ function useAiFigure(generator) {
9262
9492
  const [prompt, setPrompt] = React19.useState("");
9263
9493
  const [isLoading, setIsLoading] = React19.useState(false);
9264
9494
  const [error, setError] = React19.useState(null);
9495
+ const [notice, setNotice] = React19.useState(null);
9265
9496
  const [tokens, setTokens] = React19.useState(0);
9266
9497
  const abortRef = React19.useRef(null);
9267
9498
  const requestIdRef = React19.useRef(0);
@@ -9282,6 +9513,7 @@ function useAiFigure(generator) {
9282
9513
  abortRef.current = controller;
9283
9514
  setIsLoading(true);
9284
9515
  setError(null);
9516
+ setNotice(null);
9285
9517
  setTokens(0);
9286
9518
  try {
9287
9519
  const generated = await generator(problem, {
@@ -9295,6 +9527,7 @@ function useAiFigure(generator) {
9295
9527
  setError(generated.message);
9296
9528
  return null;
9297
9529
  }
9530
+ if (generated.partial) setNotice(generated.partial.message);
9298
9531
  return generated.state;
9299
9532
  } catch (caught) {
9300
9533
  if (controller.signal.aborted || caught instanceof DOMException && caught.name === "AbortError") {
@@ -9321,6 +9554,7 @@ function useAiFigure(generator) {
9321
9554
  setPrompt,
9322
9555
  isLoading,
9323
9556
  error,
9557
+ notice,
9324
9558
  submit,
9325
9559
  cancel,
9326
9560
  tokens
@@ -9331,12 +9565,207 @@ var init_useAiFigure = __esm({
9331
9565
  "use client";
9332
9566
  }
9333
9567
  });
9568
+
9569
+ // src/stamps/geometry-2d/ai/vision/tesseract.ts
9570
+ async function runTesseractOcr(image, opts = {}) {
9571
+ if (opts.signal?.aborted) {
9572
+ const err = new Error("Tesseract OCR aborted");
9573
+ err.name = "AbortError";
9574
+ throw err;
9575
+ }
9576
+ const { createWorker } = await import('tesseract.js');
9577
+ const lang = opts.lang ?? DEFAULT_LANG;
9578
+ const worker = await createWorker(lang);
9579
+ try {
9580
+ const dataUrl = `data:${image.mediaType};base64,${image.base64}`;
9581
+ const { data } = await worker.recognize(dataUrl);
9582
+ return { text: data.text, confidence: data.confidence };
9583
+ } finally {
9584
+ await worker.terminate();
9585
+ }
9586
+ }
9587
+ var DEFAULT_LANG;
9588
+ var init_tesseract = __esm({
9589
+ "src/stamps/geometry-2d/ai/vision/tesseract.ts"() {
9590
+ DEFAULT_LANG = "vie+eng";
9591
+ }
9592
+ });
9593
+
9594
+ // src/stamps/geometry-2d/ai/vision/extractProblem.ts
9595
+ async function extractProblemFromImage(image, opts = {}) {
9596
+ let raw;
9597
+ try {
9598
+ raw = await runTesseractOcr(image, {
9599
+ ...opts.tesseractLang ? { lang: opts.tesseractLang } : {},
9600
+ ...opts.signal ? { signal: opts.signal } : {}
9601
+ });
9602
+ } catch (e) {
9603
+ const err = e;
9604
+ return {
9605
+ ok: false,
9606
+ reason: "unreadable",
9607
+ message: "Tesseract OCR fail: " + (err.message ?? "?")
9608
+ };
9609
+ }
9610
+ const text = postProcess(raw.text);
9611
+ if (text.length === 0) {
9612
+ return { ok: false, reason: "empty", message: "Tesseract kh\xF4ng tr\xEDch \u0111\u01B0\u1EE3c text." };
9613
+ }
9614
+ const tooShort = text.length < MIN_HIGH_CONFIDENCE_CHARS;
9615
+ const lowConf = raw.confidence < TESSERACT_HIGH_CONFIDENCE_THRESHOLD;
9616
+ const confidence = tooShort || lowConf ? "low" : "high";
9617
+ return {
9618
+ ok: true,
9619
+ text,
9620
+ confidence,
9621
+ usage: { inputTokens: 0, outputTokens: 0 }
9622
+ };
9623
+ }
9624
+ function postProcess(raw) {
9625
+ let t = raw.trim();
9626
+ t = t.replace(/\*\*(.+?)\*\*/g, "$1");
9627
+ t = t.replace(/\*(.+?)\*/g, "$1");
9628
+ t = t.replace(/_(.+?)_/g, "$1");
9629
+ t = t.replace(/```[\s\S]*?```/g, "").replace(/`([^`]+)`/g, "$1");
9630
+ t = t.replace(/\s+/g, " ").trim();
9631
+ t = t.normalize("NFC");
9632
+ if (t.length > MAX_TEXT_CHARS) t = t.slice(0, MAX_TEXT_CHARS);
9633
+ return t;
9634
+ }
9635
+ var MIN_HIGH_CONFIDENCE_CHARS, MAX_TEXT_CHARS, TESSERACT_HIGH_CONFIDENCE_THRESHOLD;
9636
+ var init_extractProblem = __esm({
9637
+ "src/stamps/geometry-2d/ai/vision/extractProblem.ts"() {
9638
+ init_tesseract();
9639
+ MIN_HIGH_CONFIDENCE_CHARS = 10;
9640
+ MAX_TEXT_CHARS = 2e3;
9641
+ TESSERACT_HIGH_CONFIDENCE_THRESHOLD = 70;
9642
+ }
9643
+ });
9644
+
9645
+ // src/stamps/geometry-2d/ai/handleExtractProblem.ts
9646
+ async function handleExtractProblem(image, opts = {}) {
9647
+ try {
9648
+ const r = await extractProblemFromImage(image, opts);
9649
+ if (r.ok) {
9650
+ if (r.confidence === "low") {
9651
+ return {
9652
+ kind: "low-confidence",
9653
+ text: r.text,
9654
+ warning: "OCR c\xF3 th\u1EC3 kh\xF4ng ch\xEDnh x\xE1c, ki\u1EC3m tra tr\u01B0\u1EDBc khi v\u1EBD.",
9655
+ usage: r.usage
9656
+ };
9657
+ }
9658
+ return { kind: "success", text: r.text, usage: r.usage };
9659
+ }
9660
+ if (r.reason === "not-math") {
9661
+ return { kind: "refused", reason: "not-math", message: r.message };
9662
+ }
9663
+ if (r.reason === "unsupported") {
9664
+ return { kind: "error", code: "unsupported", message: r.message };
9665
+ }
9666
+ if (r.reason === "unreadable") {
9667
+ return { kind: "error", code: "network", message: r.message };
9668
+ }
9669
+ return { kind: "error", code: "empty", message: r.message };
9670
+ } catch (e) {
9671
+ return {
9672
+ kind: "error",
9673
+ code: "unexpected",
9674
+ message: e instanceof Error ? e.message : String(e)
9675
+ };
9676
+ }
9677
+ }
9678
+ var init_handleExtractProblem = __esm({
9679
+ "src/stamps/geometry-2d/ai/handleExtractProblem.ts"() {
9680
+ init_extractProblem();
9681
+ }
9682
+ });
9683
+
9684
+ // src/stamps/geometry-2d/ai/vision/preprocess.ts
9685
+ function inferMediaType(file) {
9686
+ const t = file.type.toLowerCase();
9687
+ if (ALLOWED_TYPES.includes(t)) return t;
9688
+ return null;
9689
+ }
9690
+ function validateFile(file) {
9691
+ const mt = inferMediaType(file);
9692
+ if (mt == null) {
9693
+ return {
9694
+ ok: false,
9695
+ code: "invalid-format",
9696
+ message: "Ch\u1EC9 h\u1ED7 tr\u1EE3 PNG, JPEG, WEBP."
9697
+ };
9698
+ }
9699
+ if (file.size > MAX_RAW_BYTES) {
9700
+ return {
9701
+ ok: false,
9702
+ code: "too-large",
9703
+ message: `\u1EA2nh qu\xE1 l\u1EDBn (> ${Math.round(MAX_RAW_BYTES / 1024 / 1024)} MB). Crop ho\u1EB7c resize tr\u01B0\u1EDBc.`
9704
+ };
9705
+ }
9706
+ return { ok: true, mediaType: mt };
9707
+ }
9708
+ async function fileToImagePart(file) {
9709
+ const v = validateFile(file);
9710
+ if (!v.ok) throw new Error(v.message);
9711
+ const bitmap = await createImageBitmap(file);
9712
+ const { width, height } = bitmap;
9713
+ const maxEdge = Math.max(width, height);
9714
+ const scale3 = maxEdge > MAX_EDGE_PX ? MAX_EDGE_PX / maxEdge : 1;
9715
+ const targetW = Math.round(width * scale3);
9716
+ const targetH = Math.round(height * scale3);
9717
+ const canvas = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(targetW, targetH) : Object.assign(document.createElement("canvas"), { width: targetW, height: targetH });
9718
+ const ctx = canvas.getContext("2d");
9719
+ if (!ctx) throw new Error("Kh\xF4ng t\u1EA1o \u0111\u01B0\u1EE3c canvas 2D context");
9720
+ ctx.drawImage(bitmap, 0, 0, targetW, targetH);
9721
+ bitmap.close();
9722
+ let outputType = v.mediaType === "image/png" ? "image/png" : "image/jpeg";
9723
+ let finalBlob = await canvasToBlob(canvas, outputType, 0.92);
9724
+ if (finalBlob.size > MAX_ENCODED_BYTES) {
9725
+ outputType = "image/jpeg";
9726
+ finalBlob = await canvasToBlob(canvas, "image/jpeg", 0.7);
9727
+ }
9728
+ const base64 = await blobToBase64(finalBlob);
9729
+ return { mediaType: outputType, base64 };
9730
+ }
9731
+ async function canvasToBlob(canvas, type, quality) {
9732
+ if ("convertToBlob" in canvas) {
9733
+ return canvas.convertToBlob({ type, quality });
9734
+ }
9735
+ return new Promise((resolve, reject) => {
9736
+ canvas.toBlob(
9737
+ (b) => b ? resolve(b) : reject(new Error("Canvas encode fail")),
9738
+ type,
9739
+ quality
9740
+ );
9741
+ });
9742
+ }
9743
+ async function blobToBase64(blob) {
9744
+ const buf = await blob.arrayBuffer();
9745
+ let binary = "";
9746
+ const bytes = new Uint8Array(buf);
9747
+ const chunk = 32768;
9748
+ for (let i = 0; i < bytes.length; i += chunk) {
9749
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
9750
+ }
9751
+ return typeof btoa === "function" ? btoa(binary) : Buffer.from(binary, "binary").toString("base64");
9752
+ }
9753
+ var MAX_EDGE_PX, MAX_RAW_BYTES, MAX_ENCODED_BYTES, ALLOWED_TYPES;
9754
+ var init_preprocess = __esm({
9755
+ "src/stamps/geometry-2d/ai/vision/preprocess.ts"() {
9756
+ MAX_EDGE_PX = 2048;
9757
+ MAX_RAW_BYTES = 10 * 1024 * 1024;
9758
+ MAX_ENCODED_BYTES = 3 * 1024 * 1024;
9759
+ ALLOWED_TYPES = ["image/png", "image/jpeg", "image/webp"];
9760
+ }
9761
+ });
9334
9762
  function AiFigurePrompt({ generator, onGenerated }) {
9335
9763
  const {
9336
9764
  prompt,
9337
9765
  setPrompt,
9338
9766
  isLoading,
9339
9767
  error,
9768
+ notice,
9340
9769
  submit,
9341
9770
  cancel,
9342
9771
  tokens
@@ -9350,20 +9779,136 @@ function AiFigurePrompt({ generator, onGenerated }) {
9350
9779
  const id = setInterval(() => setElapsed((s) => s + 1), 1e3);
9351
9780
  return () => clearInterval(id);
9352
9781
  }, [isLoading]);
9782
+ const [noticeDismissed, setNoticeDismissed] = React19.useState(false);
9783
+ React19.useEffect(() => {
9784
+ setNoticeDismissed(false);
9785
+ }, [notice]);
9786
+ const [image, setImage] = React19.useState(null);
9787
+ const [ocrLoading, setOcrLoading] = React19.useState(false);
9788
+ const [ocrError, setOcrError] = React19.useState(null);
9789
+ const [ocrWarning, setOcrWarning] = React19.useState(null);
9790
+ const [isDragOver, setIsDragOver] = React19.useState(false);
9791
+ const fileInputRef = React19.useRef(null);
9353
9792
  const textareaRef = React19.useRef(null);
9793
+ const imagePreview = image ? `data:${image.mediaType};base64,${image.base64}` : null;
9794
+ React19.useEffect(() => {
9795
+ setOcrError(null);
9796
+ setOcrWarning(null);
9797
+ }, [image]);
9798
+ const handleFile = React19.useCallback(
9799
+ async (file) => {
9800
+ if (isLoading || ocrLoading) return;
9801
+ const v = validateFile(file);
9802
+ if (!v.ok) {
9803
+ setOcrError(v.message);
9804
+ return;
9805
+ }
9806
+ try {
9807
+ const part = await fileToImagePart(file);
9808
+ setImage(part);
9809
+ } catch (e) {
9810
+ setOcrError(e instanceof Error ? e.message : "Kh\xF4ng decode \u0111\u01B0\u1EE3c \u1EA3nh");
9811
+ }
9812
+ },
9813
+ [isLoading, ocrLoading]
9814
+ );
9815
+ const handleFileInput = React19.useCallback(
9816
+ (e) => {
9817
+ const file = e.target.files?.[0];
9818
+ if (file) void handleFile(file);
9819
+ e.target.value = "";
9820
+ },
9821
+ [handleFile]
9822
+ );
9823
+ const handlePaste = React19.useCallback(
9824
+ (e) => {
9825
+ const item = Array.from(e.clipboardData.items).find(
9826
+ (it) => it.kind === "file" && it.type.startsWith("image/")
9827
+ );
9828
+ if (!item) return;
9829
+ const file = item.getAsFile();
9830
+ if (!file) return;
9831
+ e.preventDefault();
9832
+ void handleFile(file);
9833
+ },
9834
+ [handleFile]
9835
+ );
9836
+ const handleDrop = React19.useCallback(
9837
+ (e) => {
9838
+ e.preventDefault();
9839
+ setIsDragOver(false);
9840
+ const file = Array.from(e.dataTransfer.files).find(
9841
+ (f) => f.type.startsWith("image/")
9842
+ );
9843
+ if (file) void handleFile(file);
9844
+ },
9845
+ [handleFile]
9846
+ );
9847
+ const runOcr = React19.useCallback(async () => {
9848
+ if (!image) return;
9849
+ setOcrLoading(true);
9850
+ setOcrError(null);
9851
+ setOcrWarning(null);
9852
+ try {
9853
+ const r = await handleExtractProblem(image);
9854
+ if (r.kind === "success" || r.kind === "low-confidence") {
9855
+ setPrompt(r.text);
9856
+ if (r.kind === "low-confidence") setOcrWarning(r.warning);
9857
+ requestAnimationFrame(() => textareaRef.current?.focus());
9858
+ } else {
9859
+ setOcrError(r.message);
9860
+ }
9861
+ } finally {
9862
+ setOcrLoading(false);
9863
+ }
9864
+ }, [image, setPrompt]);
9354
9865
  const handleSendClick = React19.useCallback(async () => {
9866
+ if (image && !prompt.trim() && !ocrLoading) {
9867
+ await runOcr();
9868
+ return;
9869
+ }
9355
9870
  const generated = await submit();
9356
9871
  if (generated) onGenerated(generated);
9357
- }, [submit, onGenerated]);
9872
+ }, [image, prompt, ocrLoading, runOcr, submit, onGenerated]);
9358
9873
  const promptEmpty = !prompt.trim();
9359
- const sendDisabled = promptEmpty || isLoading;
9874
+ const willOcr = image != null && promptEmpty;
9875
+ const sendDisabled = !image && promptEmpty || ocrLoading || isLoading && !willOcr;
9876
+ const placeholder = willOcr ? "B\u1EA5m g\u1EEDi \u0111\u1EC3 \u0111\u1ECDc \u0111\u1EC1 t\u1EEB \u1EA3nh \u2014 ho\u1EB7c t\u1EF1 g\xF5 \u1EDF \u0111\xE2y\u2026" : "M\xF4 t\u1EA3 \u0111\u1EC1 b\xE0i c\u1EA7n d\u1EF1ng \u2014 ho\u1EB7c d\xE1n/\u0111\xEDnh \u1EA3nh \u0111\u1EC1 (Ctrl+V).";
9360
9877
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-b border-slate-200 bg-slate-50 px-3 py-3", children: [
9361
9878
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-2 flex items-center justify-between gap-2", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium tracking-wide text-slate-600", children: "D\u1EF1ng h\xECnh b\u1EB1ng AI" }) }),
9362
9879
  /* @__PURE__ */ jsxRuntime.jsxs(
9363
9880
  "div",
9364
9881
  {
9365
- className: "group relative flex flex-col rounded-2xl bg-white shadow-sm transition-all duration-150 ring-1 ring-slate-200 focus-within:ring-2 focus-within:ring-emerald-400/70 focus-within:shadow-md",
9882
+ onDragOver: (e) => {
9883
+ e.preventDefault();
9884
+ setIsDragOver(true);
9885
+ },
9886
+ onDragLeave: () => setIsDragOver(false),
9887
+ onDrop: handleDrop,
9888
+ onPaste: handlePaste,
9889
+ className: "group relative flex flex-col rounded-2xl bg-white shadow-sm transition-all duration-150 ring-1 ring-slate-200 focus-within:ring-2 focus-within:ring-emerald-400/70 focus-within:shadow-md " + (isDragOver ? "ring-2 ring-emerald-500 bg-emerald-50/40" : ""),
9366
9890
  children: [
9891
+ image && imagePreview && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2 px-3 pt-2.5", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "group/chip relative", children: [
9892
+ /* @__PURE__ */ jsxRuntime.jsx(
9893
+ "img",
9894
+ {
9895
+ src: imagePreview,
9896
+ alt: "\u1EA2nh \u0111\u1EC1 b\xE0i",
9897
+ className: "max-h-48 max-w-full h-auto w-auto rounded-lg border border-slate-200 shadow-sm"
9898
+ }
9899
+ ),
9900
+ /* @__PURE__ */ jsxRuntime.jsx(
9901
+ "button",
9902
+ {
9903
+ type: "button",
9904
+ onClick: () => setImage(null),
9905
+ disabled: ocrLoading || isLoading,
9906
+ "aria-label": "Xo\xE1 \u1EA3nh",
9907
+ className: "absolute -right-1.5 -top-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-slate-900/85 text-[11px] font-medium text-white shadow ring-2 ring-white transition hover:bg-slate-900 disabled:opacity-50",
9908
+ children: "\xD7"
9909
+ }
9910
+ )
9911
+ ] }) }),
9367
9912
  /* @__PURE__ */ jsxRuntime.jsx(
9368
9913
  "textarea",
9369
9914
  {
@@ -9381,48 +9926,111 @@ function AiFigurePrompt({ generator, onGenerated }) {
9381
9926
  },
9382
9927
  disabled: isLoading,
9383
9928
  rows: 2,
9384
- placeholder: "M\xF4 t\u1EA3 \u0111\u1EC1 b\xE0i c\u1EA7n d\u1EF1ng.",
9929
+ placeholder,
9385
9930
  className: "block w-full resize-none rounded-2xl bg-transparent px-3.5 pt-2.5 pb-1 text-sm leading-relaxed text-slate-800 placeholder:text-slate-400 outline-none disabled:opacity-60 field-sizing-content max-h-44"
9386
9931
  }
9387
9932
  ),
9388
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-end gap-2 px-2 pb-2 pt-1", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
9389
- isLoading && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-[10px] tabular-nums text-slate-500", children: tokens > 0 ? `${tokens}tok \xB7 ${elapsed}s` : `${elapsed}s` }),
9390
- isLoading ? /* @__PURE__ */ jsxRuntime.jsx(
9391
- "button",
9392
- {
9393
- type: "button",
9394
- onClick: cancel,
9395
- "aria-label": "Hu\u1EF7 d\u1EF1ng h\xECnh AI",
9396
- "data-testid": "geometry-ai-cancel",
9397
- title: `\u0110ang d\u1EF1ng\u2026 ${elapsed}s \u2014 b\u1EA5m \u0111\u1EC3 hu\u1EF7`,
9398
- className: "flex h-8 w-8 items-center justify-center rounded-full bg-amber-500 text-white shadow-sm transition hover:scale-105 hover:bg-amber-600 active:scale-95",
9399
- children: /* @__PURE__ */ jsxRuntime.jsx(StopIcon, { className: "h-3.5 w-3.5" })
9400
- }
9401
- ) : /* @__PURE__ */ jsxRuntime.jsx(
9402
- "button",
9403
- {
9404
- type: "button",
9405
- onClick: () => void handleSendClick(),
9406
- disabled: sendDisabled,
9407
- "aria-label": "D\u1EF1ng b\u1EB1ng AI",
9408
- title: "D\u1EF1ng b\u1EB1ng AI (Ctrl/\u2318+Enter)",
9409
- "data-testid": "geometry-ai-submit",
9410
- className: "flex h-8 w-8 items-center justify-center rounded-full bg-emerald-600 text-white shadow-sm transition hover:scale-105 hover:bg-emerald-700 active:scale-95 disabled:cursor-not-allowed disabled:bg-slate-300 disabled:hover:scale-100",
9411
- children: /* @__PURE__ */ jsxRuntime.jsx(ArrowUpIcon, { className: "h-[18px] w-[18px]" })
9412
- }
9413
- )
9414
- ] }) })
9933
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2 px-2 pb-2 pt-1", children: [
9934
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
9935
+ /* @__PURE__ */ jsxRuntime.jsx(
9936
+ "button",
9937
+ {
9938
+ type: "button",
9939
+ onClick: () => fileInputRef.current?.click(),
9940
+ disabled: isLoading || ocrLoading,
9941
+ "aria-label": "\u0110\xEDnh \u1EA3nh \u0111\u1EC1 b\xE0i",
9942
+ title: "\u0110\xEDnh \u1EA3nh (c\u0169ng c\xF3 th\u1EC3 d\xE1n b\u1EB1ng Ctrl+V ho\u1EB7c k\xE9o th\u1EA3)",
9943
+ className: "flex h-8 w-8 items-center justify-center rounded-full text-slate-500 transition hover:bg-slate-100 hover:text-emerald-700 disabled:opacity-40",
9944
+ children: /* @__PURE__ */ jsxRuntime.jsx(PaperclipIcon, { className: "h-[18px] w-[18px]" })
9945
+ }
9946
+ ),
9947
+ /* @__PURE__ */ jsxRuntime.jsx(
9948
+ "input",
9949
+ {
9950
+ ref: fileInputRef,
9951
+ type: "file",
9952
+ accept: "image/png,image/jpeg,image/webp",
9953
+ className: "sr-only",
9954
+ onChange: handleFileInput,
9955
+ disabled: isLoading || ocrLoading,
9956
+ "aria-label": "Ch\u1ECDn \u1EA3nh \u0111\u1EC1 b\xE0i"
9957
+ }
9958
+ )
9959
+ ] }),
9960
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
9961
+ (isLoading || ocrLoading) && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-[10px] tabular-nums text-slate-500", children: ocrLoading ? "\u0111\u1ECDc \u1EA3nh\u2026" : tokens > 0 ? `${tokens}tok \xB7 ${elapsed}s` : `${elapsed}s` }),
9962
+ isLoading ? /* @__PURE__ */ jsxRuntime.jsx(
9963
+ "button",
9964
+ {
9965
+ type: "button",
9966
+ onClick: cancel,
9967
+ "aria-label": "Hu\u1EF7 d\u1EF1ng h\xECnh AI",
9968
+ "data-testid": "geometry-ai-cancel",
9969
+ title: `\u0110ang d\u1EF1ng\u2026 ${elapsed}s \u2014 b\u1EA5m \u0111\u1EC3 hu\u1EF7`,
9970
+ className: "flex h-8 w-8 items-center justify-center rounded-full bg-amber-500 text-white shadow-sm transition hover:scale-105 hover:bg-amber-600 active:scale-95",
9971
+ children: /* @__PURE__ */ jsxRuntime.jsx(StopIcon, { className: "h-3.5 w-3.5" })
9972
+ }
9973
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
9974
+ "button",
9975
+ {
9976
+ type: "button",
9977
+ onClick: () => void handleSendClick(),
9978
+ disabled: sendDisabled,
9979
+ "aria-label": willOcr ? "\u0110\u1ECDc \u0111\u1EC1 t\u1EEB \u1EA3nh" : "D\u1EF1ng b\u1EB1ng AI",
9980
+ title: willOcr ? "\u0110\u1ECDc \u0111\u1EC1 t\u1EEB \u1EA3nh (s\u1EBD \u0111i\u1EC1n v\xE0o \xF4 chat)" : "D\u1EF1ng b\u1EB1ng AI (Ctrl/\u2318+Enter)",
9981
+ "data-testid": willOcr ? "geometry-ai-ocr" : "geometry-ai-submit",
9982
+ className: "flex h-8 w-8 items-center justify-center rounded-full bg-emerald-600 text-white shadow-sm transition hover:scale-105 hover:bg-emerald-700 active:scale-95 disabled:cursor-not-allowed disabled:bg-slate-300 disabled:hover:scale-100",
9983
+ children: /* @__PURE__ */ jsxRuntime.jsx(ArrowUpIcon, { className: "h-[18px] w-[18px]" })
9984
+ }
9985
+ )
9986
+ ] })
9987
+ ] }),
9988
+ isDragOver && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center rounded-2xl bg-emerald-50/60 text-xs font-medium text-emerald-700", children: "Th\u1EA3 \u1EA3nh v\xE0o \u0111\xE2y" })
9415
9989
  ]
9416
9990
  }
9417
9991
  ),
9418
- error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "mt-1 px-1 text-xs text-red-600", children: error })
9992
+ ocrWarning && /* @__PURE__ */ jsxRuntime.jsx(
9993
+ "p",
9994
+ {
9995
+ className: "mt-1 px-1 text-xs text-amber-700",
9996
+ "data-testid": "geometry-ai-ocr-warning",
9997
+ children: ocrWarning
9998
+ }
9999
+ ),
10000
+ ocrError && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "mt-1 px-1 text-xs text-red-600", children: ocrError }),
10001
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "mt-1 px-1 text-xs text-red-600", children: error }),
10002
+ notice && !noticeDismissed && /* @__PURE__ */ jsxRuntime.jsxs(
10003
+ "div",
10004
+ {
10005
+ role: "status",
10006
+ "data-testid": "geometry-ai-partial-notice",
10007
+ className: "relative mt-2 whitespace-pre-wrap rounded-lg border border-amber-200 bg-amber-50 py-2 pl-3 pr-8 text-xs leading-relaxed text-amber-800",
10008
+ children: [
10009
+ notice,
10010
+ /* @__PURE__ */ jsxRuntime.jsx(
10011
+ "button",
10012
+ {
10013
+ type: "button",
10014
+ onClick: () => setNoticeDismissed(true),
10015
+ "aria-label": "\u0110\xF3ng th\xF4ng b\xE1o",
10016
+ title: "\u0110\xF3ng th\xF4ng b\xE1o",
10017
+ "data-testid": "geometry-ai-partial-dismiss",
10018
+ className: "absolute right-1 top-1 flex h-5 w-5 items-center justify-center rounded text-amber-500 transition hover:bg-amber-100 hover:text-amber-800",
10019
+ children: "\xD7"
10020
+ }
10021
+ )
10022
+ ]
10023
+ }
10024
+ )
9419
10025
  ] });
9420
10026
  }
9421
- var ArrowUpIcon, StopIcon;
10027
+ var ArrowUpIcon, StopIcon, PaperclipIcon;
9422
10028
  var init_AiFigurePrompt = __esm({
9423
10029
  "src/stamps/geometry-2d/editor/AiFigurePrompt.tsx"() {
9424
10030
  "use client";
9425
10031
  init_useAiFigure();
10032
+ init_handleExtractProblem();
10033
+ init_preprocess();
9426
10034
  ArrowUpIcon = (props) => /* @__PURE__ */ jsxRuntime.jsxs(
9427
10035
  "svg",
9428
10036
  {
@@ -9441,6 +10049,20 @@ var init_AiFigurePrompt = __esm({
9441
10049
  }
9442
10050
  );
9443
10051
  StopIcon = (props) => /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": true, ...props, children: /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "6", y: "6", width: "12", height: "12", rx: "2" }) });
10052
+ PaperclipIcon = (props) => /* @__PURE__ */ jsxRuntime.jsx(
10053
+ "svg",
10054
+ {
10055
+ viewBox: "0 0 24 24",
10056
+ fill: "none",
10057
+ stroke: "currentColor",
10058
+ strokeWidth: 1.75,
10059
+ strokeLinecap: "round",
10060
+ strokeLinejoin: "round",
10061
+ "aria-hidden": true,
10062
+ ...props,
10063
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" })
10064
+ }
10065
+ );
9444
10066
  }
9445
10067
  });
9446
10068
 
@@ -10024,11 +10646,17 @@ async function insertStampImage(api, opts) {
10024
10646
  const elements = api.getSceneElements();
10025
10647
  const editingId = opts.editingElementId ?? null;
10026
10648
  if (editingId) {
10649
+ const old = opts.preserveExistingSize ? elements.find((e) => e.id === editingId) : void 0;
10650
+ const oldLongest = old ? Math.max(old.width ?? 0, old.height ?? 0) : 0;
10651
+ const newLongest = Math.max(width, height);
10652
+ const scale3 = oldLongest > 0 && newLongest > 0 ? oldLongest / newLongest : 1;
10653
+ const w = width * scale3;
10654
+ const h = height * scale3;
10027
10655
  const updated = elements.map(
10028
- (e) => e.id === editingId ? { ...e, fileId, customData, width, height } : e
10656
+ (e) => e.id === editingId ? { ...e, fileId, customData, width: w, height: h } : e
10029
10657
  );
10030
10658
  api.updateScene({ elements: updated, appState: clearAppStateAfterInsert() });
10031
- return { fileId, width, height, elementId: editingId };
10659
+ return { fileId, width: w, height: h, elementId: editingId };
10032
10660
  }
10033
10661
  const newElement = buildStampImageElement(
10034
10662
  api,
@@ -10182,8 +10810,11 @@ function serializePoint(obj, state) {
10182
10810
  };
10183
10811
  }
10184
10812
  case "arcMidpoint": {
10185
- const refs = resolveRefs([c.circle, c.a, c.b, c.notContaining], state);
10186
- if (!refs) return fail("unresolved-ref", `${c.circle},${c.a},${c.b},${c.notContaining}`);
10813
+ const containment = c.containing ?? c.notContaining;
10814
+ const baseRefs = [c.circle, c.a, c.b];
10815
+ if (containment) baseRefs.push(containment);
10816
+ const refs = resolveRefs(baseRefs, state);
10817
+ if (!refs) return fail("unresolved-ref", baseRefs.join(","));
10187
10818
  return {
10188
10819
  ok: true,
10189
10820
  entity: {
@@ -10192,7 +10823,7 @@ function serializePoint(obj, state) {
10192
10823
  circle: refs[0],
10193
10824
  a: refs[1],
10194
10825
  b: refs[2],
10195
- notContaining: refs[3]
10826
+ ...c.containing ? { containing: refs[3] } : c.notContaining ? { notContaining: refs[3] } : {}
10196
10827
  }
10197
10828
  };
10198
10829
  }
@@ -10236,6 +10867,7 @@ function serializePoint(obj, state) {
10236
10867
  case "onPerpendicular":
10237
10868
  case "onPerpBisector":
10238
10869
  case "onCircleAroundPoint":
10870
+ case "mixtilinearPoint":
10239
10871
  return fail("unsupported-constraint", c.kind);
10240
10872
  default: {
10241
10873
  return fail("unsupported-constraint");
@@ -10538,9 +11170,11 @@ function describeEntity(e) {
10538
11170
  return `${e.name} = \u0111\u01B0\u1EDDng tr\xF2n b\xE0ng ti\u1EBFp ${e.vertices.join("")} \u0111\u1ED1i di\u1EC7n ${e.opposite}`;
10539
11171
  // Cụm A
10540
11172
  case "arcMidpoint":
10541
- return `${e.name} = trung \u0111i\u1EC3m cung ${e.a}${e.b} (kh\xF4ng ch\u1EE9a ${e.notContaining}) tr\xEAn ${e.circle}`;
11173
+ return `${e.name} = trung \u0111i\u1EC3m cung ${e.a}${e.b}${e.containing || e.notContaining ? ` (${e.containing ? "ch\u1EE9a" : "kh\xF4ng ch\u1EE9a"} ${e.containing ?? e.notContaining})` : ""} tr\xEAn ${e.circle}`;
10542
11174
  case "excenter":
10543
11175
  return `${e.name} = t\xE2m b\xE0ng ti\u1EBFp ${e.vertices.join("")} \u0111\u1ED1i di\u1EC7n ${e.opposite}`;
11176
+ case "mixtilinearPoint":
11177
+ return `${e.name} = ${e.which === "center" ? "t\xE2m" : "ti\u1EBFp \u0111i\u1EC3m"} mixtilinear ${e.vertices.join("")}`;
10544
11178
  case "reflectPoint":
10545
11179
  return `${e.name} = \u0111\u1ED1i x\u1EE9ng ${e.of} qua \u0111i\u1EC3m ${e.through}`;
10546
11180
  case "reflectLine":
@@ -10551,6 +11185,8 @@ function describeEntity(e) {
10551
11185
  const distStr = d.kind === "circleRadius" ? `r(${d.circle})` : d.kind === "segmentLength" ? `|${d.p1}${d.p2}|` : `${d.value}`;
10552
11186
  return `${e.name} = \u0111i\u1EC3m tr\xEAn tia ${e.from}${e.through} c\xE1ch ${e.through} m\u1ED9t kho\u1EA3ng ${distStr}`;
10553
11187
  }
11188
+ case "onPerpBisector":
11189
+ return `${e.name} = \u0111i\u1EC3m tr\xEAn trung tr\u1EF1c ${e.p1}${e.p2}`;
10554
11190
  default: {
10555
11191
  return "";
10556
11192
  }
@@ -10673,7 +11309,8 @@ var init_host = __esm({
10673
11309
  version: 1,
10674
11310
  jsonState
10675
11311
  }),
10676
- editingElementId: editingElement?.id ?? null
11312
+ editingElementId: editingElement?.id ?? null,
11313
+ preserveExistingSize: true
10677
11314
  });
10678
11315
  } catch (err) {
10679
11316
  console.error("Geometry insert failed:", err);
@@ -14216,7 +14853,8 @@ var init_host3 = __esm({
14216
14853
  version: 2,
14217
14854
  jsonState
14218
14855
  }),
14219
- editingElementId: editingElement?.id ?? null
14856
+ editingElementId: editingElement?.id ?? null,
14857
+ preserveExistingSize: true
14220
14858
  });
14221
14859
  onClose();
14222
14860
  },
@@ -15519,7 +16157,8 @@ var init_host4 = __esm({
15519
16157
  version: 2,
15520
16158
  jsonState
15521
16159
  }),
15522
- editingElementId: editingElement?.id ?? null
16160
+ editingElementId: editingElement?.id ?? null,
16161
+ preserveExistingSize: true
15523
16162
  });
15524
16163
  } catch (err) {
15525
16164
  console.error("Graph2D insert failed:", err);
@@ -17944,6 +18583,9 @@ function Whiteboard({
17944
18583
  ] });
17945
18584
  }
17946
18585
 
18586
+ // src/index.ts
18587
+ init_handleExtractProblem();
18588
+
17947
18589
  exports.ALL_STAMPS = ALL_STAMPS;
17948
18590
  exports.DEFAULT_STAMPS = DEFAULT_STAMPS;
17949
18591
  exports.EXPERIMENTAL_STAMPS = EXPERIMENTAL_STAMPS;
@@ -17957,6 +18599,7 @@ exports.findStampForCustomData = findStampForCustomData;
17957
18599
  exports.geometry3dStamp = geometry3dStamp;
17958
18600
  exports.geometryStamp = geometryStamp;
17959
18601
  exports.graph2dStamp = graph2dStamp;
18602
+ exports.handleExtractProblem = handleExtractProblem;
17960
18603
  exports.insertPdfPages = insertPdfPages;
17961
18604
  exports.insertRasterizedPagesIntoScene = insertRasterizedPagesIntoScene;
17962
18605
  exports.isStampElement = isStampElement;