@xom11/whiteboard 0.30.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 (88) hide show
  1. package/dist/ai.d.mts +226 -32
  2. package/dist/ai.d.ts +226 -32
  3. package/dist/ai.js +5126 -351
  4. package/dist/ai.js.map +1 -1
  5. package/dist/ai.mjs +4970 -352
  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-XVVLT6B3.mjs → chunk-BMYC2ILT.mjs} +4 -4
  11. package/dist/{chunk-XVVLT6B3.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-SF3U7ZF4.mjs → chunk-DWIEVCGK.mjs} +180 -15
  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-QGNU34T7.mjs → chunk-QLQ4MJNO.mjs} +10 -4
  33. package/dist/chunk-QLQ4MJNO.mjs.map +1 -0
  34. package/dist/{chunk-BU5KLO3P.mjs → chunk-T3N4BSJV.mjs} +4 -4
  35. package/dist/{chunk-BU5KLO3P.mjs.map → chunk-T3N4BSJV.mjs.map} +1 -1
  36. package/dist/{chunk-5JM35CXV.mjs → chunk-TMRFSOM7.mjs} +4 -4
  37. package/dist/{chunk-5JM35CXV.mjs.map → chunk-TMRFSOM7.mjs.map} +1 -1
  38. package/dist/geometry-2d.d.mts +1 -1
  39. package/dist/geometry-2d.d.ts +1 -1
  40. package/dist/geometry-2d.js +449 -172
  41. package/dist/geometry-2d.js.map +1 -1
  42. package/dist/geometry-2d.mjs +5 -5
  43. package/dist/geometry-3d.d.mts +1 -1
  44. package/dist/geometry-3d.d.ts +1 -1
  45. package/dist/geometry-3d.js +172 -22
  46. package/dist/geometry-3d.js.map +1 -1
  47. package/dist/geometry-3d.mjs +4 -4
  48. package/dist/graph-2d.d.mts +1 -1
  49. package/dist/graph-2d.d.ts +1 -1
  50. package/dist/graph-2d.js +307 -100
  51. package/dist/graph-2d.js.map +1 -1
  52. package/dist/graph-2d.mjs +7 -7
  53. package/dist/{host-HOSJHQ5H.mjs → host-4FIUNIDQ.mjs} +13 -12
  54. package/dist/host-4FIUNIDQ.mjs.map +1 -0
  55. package/dist/{host-2ISGVO7O.mjs → host-4ZB4XD4S.mjs} +9 -8
  56. package/dist/host-4ZB4XD4S.mjs.map +1 -0
  57. package/dist/{host-ZQCDAT6O.mjs → host-H2IGOKJU.mjs} +3 -3
  58. package/dist/{host-ZQCDAT6O.mjs.map → host-H2IGOKJU.mjs.map} +1 -1
  59. package/dist/{host-3UFGFMJ2.mjs → host-KMWP7KBT.mjs} +90 -43
  60. package/dist/host-KMWP7KBT.mjs.map +1 -0
  61. package/dist/index.d.mts +2 -2
  62. package/dist/index.d.ts +2 -2
  63. package/dist/index.js +453 -174
  64. package/dist/index.js.map +1 -1
  65. package/dist/index.mjs +21 -21
  66. package/dist/latex.d.mts +1 -1
  67. package/dist/latex.d.ts +1 -1
  68. package/dist/latex.js +8 -2
  69. package/dist/latex.js.map +1 -1
  70. package/dist/latex.mjs +1 -1
  71. package/dist/render-NMS7OAV6.mjs +10 -0
  72. package/dist/{render-ZX2O2IK7.mjs.map → render-NMS7OAV6.mjs.map} +1 -1
  73. package/dist/serialize-PGHQZEPV.mjs +9 -0
  74. package/dist/{serialize-N4G6RFBB.mjs.map → serialize-PGHQZEPV.mjs.map} +1 -1
  75. package/dist/{types-C3FjpoUi.d.ts → types-tePd94vW.d.mts} +8 -0
  76. package/dist/{types-C3FjpoUi.d.mts → types-tePd94vW.d.ts} +8 -0
  77. package/package.json +1 -1
  78. package/dist/chunk-H22OZYTW.mjs.map +0 -1
  79. package/dist/chunk-IHUFOV7L.mjs.map +0 -1
  80. package/dist/chunk-QGNU34T7.mjs.map +0 -1
  81. package/dist/chunk-SF3U7ZF4.mjs.map +0 -1
  82. package/dist/chunk-SZDAS7LK.mjs.map +0 -1
  83. package/dist/chunk-ZTQBUKLJ.mjs.map +0 -1
  84. package/dist/host-2ISGVO7O.mjs.map +0 -1
  85. package/dist/host-3UFGFMJ2.mjs.map +0 -1
  86. package/dist/host-HOSJHQ5H.mjs.map +0 -1
  87. package/dist/render-ZX2O2IK7.mjs +0 -10
  88. package/dist/serialize-N4G6RFBB.mjs +0 -9
@@ -939,8 +939,12 @@ function constraintRefs2D(c) {
939
939
  return [c.line, c.circle, c.other];
940
940
  case "tangencyPoint":
941
941
  return [c.circle, c.onLine];
942
- case "arcMidpoint":
943
- return [c.circle, c.a, c.b, c.notContaining];
942
+ case "arcMidpoint": {
943
+ const containment = c.notContaining ?? c.containing;
944
+ return containment ? [c.circle, c.a, c.b, containment] : [c.circle, c.a, c.b];
945
+ }
946
+ case "mixtilinearPoint":
947
+ return [c.vertices[0], c.vertices[1], c.vertices[2]];
944
948
  case "pointAtDistance": {
945
949
  const d = c.distance;
946
950
  const extra = d.kind === "circleRadius" ? [d.circle] : d.kind === "segmentLength" ? [d.p1, d.p2] : [];
@@ -1227,7 +1231,7 @@ function dist(p, q) {
1227
1231
  function sideOf(a, b, p) {
1228
1232
  return (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
1229
1233
  }
1230
- function arcMidpoint(center, radius, a, b, notContaining) {
1234
+ function arcMidpoint(center, radius, a, b, reference, sameSide = false) {
1231
1235
  const mcx = (a[0] + b[0]) / 2;
1232
1236
  const mcy = (a[1] + b[1]) / 2;
1233
1237
  let ux = mcx - center[0];
@@ -1242,12 +1246,18 @@ function arcMidpoint(center, radius, a, b, notContaining) {
1242
1246
  uy /= len;
1243
1247
  const cand1 = [center[0] + radius * ux, center[1] + radius * uy];
1244
1248
  const cand2 = [center[0] - radius * ux, center[1] - radius * uy];
1249
+ if (!reference) return cand1[1] >= cand2[1] ? cand1 : cand2;
1250
+ const notContaining = reference;
1245
1251
  const sN = sideOf(a, b, notContaining);
1246
1252
  if (Math.abs(sN) < 1e-9) {
1247
- return dist(cand1, notContaining) >= dist(cand2, notContaining) ? cand1 : cand2;
1253
+ const far = dist(cand1, notContaining) >= dist(cand2, notContaining) ? cand1 : cand2;
1254
+ const near = far === cand1 ? cand2 : cand1;
1255
+ return sameSide ? near : far;
1248
1256
  }
1249
1257
  const s1 = sideOf(a, b, cand1);
1250
- return s1 * sN < 0 ? cand1 : cand2;
1258
+ const opp = s1 * sN < 0 ? cand1 : cand2;
1259
+ const same = opp === cand1 ? cand2 : cand1;
1260
+ return sameSide ? same : opp;
1251
1261
  }
1252
1262
  function excenter(vertices, oppositeIndex) {
1253
1263
  const [A, B, C] = vertices;
@@ -1263,6 +1273,35 @@ function excenter(vertices, oppositeIndex) {
1263
1273
  (w[0] * A[1] + w[1] * B[1] + w[2] * C[1]) / sum
1264
1274
  ];
1265
1275
  }
1276
+ function circumcenterXY(a, b, c) {
1277
+ const ax = a[0], ay = a[1], bx = b[0], by = b[1], cx = c[0], cy = c[1];
1278
+ const d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
1279
+ if (Math.abs(d) < 1e-12) return [(ax + bx + cx) / 3, (ay + by + cy) / 3];
1280
+ const ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay) + (cx * cx + cy * cy) * (ay - by)) / d;
1281
+ const uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx) + (cx * cx + cy * cy) * (bx - ax)) / d;
1282
+ return [ux, uy];
1283
+ }
1284
+ function mixtilinearPoint(a, b, c, which) {
1285
+ const O = circumcenterXY(a, b, c);
1286
+ const R = Math.hypot(O[0] - a[0], O[1] - a[1]);
1287
+ const ux1 = (b[0] - a[0]) / (Math.hypot(b[0] - a[0], b[1] - a[1]) || 1);
1288
+ const uy1 = (b[1] - a[1]) / (Math.hypot(b[0] - a[0], b[1] - a[1]) || 1);
1289
+ const ux2 = (c[0] - a[0]) / (Math.hypot(c[0] - a[0], c[1] - a[1]) || 1);
1290
+ const uy2 = (c[1] - a[1]) / (Math.hypot(c[0] - a[0], c[1] - a[1]) || 1);
1291
+ let bx = ux1 + ux2, by = uy1 + uy2;
1292
+ const bl = Math.hypot(bx, by) || 1;
1293
+ bx /= bl;
1294
+ by /= bl;
1295
+ const cosA = ux1 * ux2 + uy1 * uy2;
1296
+ const sinHalf = Math.sqrt(Math.max(0, (1 - cosA) / 2));
1297
+ const cos2Half = Math.max(1e-9, (1 + cosA) / 2);
1298
+ const dotAO = bx * (O[0] - a[0]) + by * (O[1] - a[1]);
1299
+ const d = 2 * (dotAO - R * sinHalf) / cos2Half;
1300
+ const K = [a[0] + d * bx, a[1] + d * by];
1301
+ if (which === "center") return K;
1302
+ const kl = Math.hypot(K[0] - O[0], K[1] - O[1]) || 1;
1303
+ return [O[0] + R * (K[0] - O[0]) / kl, O[1] + R * (K[1] - O[1]) / kl];
1304
+ }
1266
1305
  function pointAtDistanceCoord(from, through, d) {
1267
1306
  const dx = through[0] - from[0];
1268
1307
  const dy = through[1] - from[1];
@@ -1290,29 +1329,38 @@ var init_arcMidpoint = __esm({
1290
1329
  arcMidpointConstraint = definePointConstraint({
1291
1330
  kind: "arcMidpoint",
1292
1331
  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");
1332
+ if (!c.circle || !c.a || !c.b) {
1333
+ throw new Error("point.arcMidpoint: circle, a, b b\u1EAFt bu\u1ED9c");
1334
+ }
1335
+ if (c.notContaining && c.containing) {
1336
+ throw new Error("point.arcMidpoint: kh\xF4ng th\u1EC3 v\u1EEBa notContaining v\u1EEBa containing");
1295
1337
  }
1296
1338
  },
1297
1339
  describe: (obj, state, c) => {
1298
1340
  const al = state?.objects[c.a]?.label ?? c.a;
1299
1341
  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})`;
1342
+ const ref = c.containing ?? c.notContaining;
1343
+ if (!ref) return `${obj.label} = trung \u0111i\u1EC3m cung ${al}${bl}`;
1344
+ const rl = state?.objects[ref]?.label ?? ref;
1345
+ const rel = c.containing ? "ch\u1EE9a" : "kh\xF4ng ch\u1EE9a";
1346
+ return `${obj.label} = trung \u0111i\u1EC3m cung ${al}${bl} (${rel} ${rl})`;
1302
1347
  },
1303
1348
  render: (obj, ctx, c, opts) => {
1304
1349
  const board = ctx.jxg;
1305
1350
  const circle = ctx.resolveRef(c.circle);
1306
1351
  const A = ctx.resolveRef(c.a);
1307
1352
  const B = ctx.resolveRef(c.b);
1308
- const N = ctx.resolveRef(c.notContaining);
1353
+ const refName = c.containing ?? c.notContaining;
1354
+ const ref = refName ? ctx.resolveRef(refName) : void 0;
1355
+ const sameSide = !!c.containing;
1309
1356
  const O = circle?.center ?? circle?.midpoint ?? circle;
1310
1357
  const am = () => arcMidpoint(
1311
1358
  [O.X(), O.Y()],
1312
1359
  circle.Radius(),
1313
1360
  [A.X(), A.Y()],
1314
1361
  [B.X(), B.Y()],
1315
- [N.X(), N.Y()]
1362
+ ref ? [ref.X(), ref.Y()] : void 0,
1363
+ sameSide
1316
1364
  );
1317
1365
  return board.create("point", [() => am()[0], () => am()[1]], opts);
1318
1366
  }
@@ -1362,6 +1410,59 @@ var init_excenter = __esm({
1362
1410
  }
1363
1411
  });
1364
1412
 
1413
+ // src/core/scene/kinds/point-constraints/mixtilinearPoint.ts
1414
+ var mixtilinearPointConstraint;
1415
+ var init_mixtilinearPoint = __esm({
1416
+ "src/core/scene/kinds/point-constraints/mixtilinearPoint.ts"() {
1417
+ init_pointConstructions();
1418
+ init_types2();
1419
+ mixtilinearPointConstraint = definePointConstraint({
1420
+ kind: "mixtilinearPoint",
1421
+ validate: (c) => {
1422
+ if (!Array.isArray(c.vertices) || c.vertices.length !== 3 || !c.vertices.every(Boolean)) {
1423
+ throw new Error("point.mixtilinearPoint: vertices ph\u1EA3i l\xE0 tuple 3 id non-empty");
1424
+ }
1425
+ if (c.which !== "center" && c.which !== "touch") {
1426
+ throw new Error("point.mixtilinearPoint: which ph\u1EA3i 'center' | 'touch'");
1427
+ }
1428
+ },
1429
+ describe: (obj, state, c) => {
1430
+ const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
1431
+ 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)`;
1432
+ },
1433
+ render: (obj, ctx, c, opts) => {
1434
+ const board = ctx.jxg;
1435
+ const a = ctx.resolveRef(c.vertices[0]);
1436
+ const b = ctx.resolveRef(c.vertices[1]);
1437
+ const d = ctx.resolveRef(c.vertices[2]);
1438
+ const mp = () => mixtilinearPoint(
1439
+ [a.X(), a.Y()],
1440
+ [b.X(), b.Y()],
1441
+ [d.X(), d.Y()],
1442
+ c.which
1443
+ );
1444
+ return board.create("point", [() => mp()[0], () => mp()[1]], opts);
1445
+ }
1446
+ });
1447
+ }
1448
+ });
1449
+
1450
+ // src/core/scene/kinds/_label.ts
1451
+ function labelOpts(labelOffset, dflt) {
1452
+ const offset = labelOffset ?? dflt;
1453
+ return { label: { fixed: false, ...offset ? { offset } : {} } };
1454
+ }
1455
+ function readLabelOffset(label) {
1456
+ const off = label.evalVisProp?.("offset") ?? label.visProp?.offset;
1457
+ const rel = label.relativeCoords?.scrCoords;
1458
+ if (!off || !rel || off.length < 2 || rel.length < 3) return null;
1459
+ return [Math.round(off[0] + rel[1]), Math.round(off[1] - rel[2])];
1460
+ }
1461
+ var init_label = __esm({
1462
+ "src/core/scene/kinds/_label.ts"() {
1463
+ }
1464
+ });
1465
+
1365
1466
  // src/core/scene/kinds/point-constraints/shared.ts
1366
1467
  function buildJxgTransforms(board, ctx, t) {
1367
1468
  switch (t.kind) {
@@ -1413,11 +1514,13 @@ function buildPointOpts(obj) {
1413
1514
  strokeColor: obj.attrs.color ?? "#1e40af",
1414
1515
  fillColor: obj.attrs.color ?? "#1e40af",
1415
1516
  face: obj.attrs.face ?? "o",
1416
- size: obj.attrs.size ?? 4
1517
+ size: obj.attrs.size ?? 4,
1518
+ ...labelOpts(obj.attrs.labelOffset, [10, 10])
1417
1519
  };
1418
1520
  }
1419
1521
  var init_shared = __esm({
1420
1522
  "src/core/scene/kinds/point-constraints/shared.ts"() {
1523
+ init_label();
1421
1524
  }
1422
1525
  });
1423
1526
 
@@ -1726,6 +1829,7 @@ var init_registry2 = __esm({
1726
1829
  init_centroid();
1727
1830
  init_arcMidpoint();
1728
1831
  init_excenter();
1832
+ init_mixtilinearPoint();
1729
1833
  init_pointAtDistance();
1730
1834
  init_circleIntersection();
1731
1835
  init_circleSecondIntersection();
@@ -1751,6 +1855,7 @@ var init_registry2 = __esm({
1751
1855
  centroidConstraint,
1752
1856
  arcMidpointConstraint,
1753
1857
  excenterConstraint,
1858
+ mixtilinearPointConstraint,
1754
1859
  pointAtDistanceConstraint,
1755
1860
  circleIntersectionConstraint,
1756
1861
  circleSecondIntersectionConstraint,
@@ -1775,6 +1880,7 @@ var init_point = __esm({
1775
1880
  init_d_constraint2();
1776
1881
  init_registry2();
1777
1882
  init_shared();
1883
+ init_label();
1778
1884
  def3 = {
1779
1885
  type: "point",
1780
1886
  schemaVersion: 1,
@@ -1842,7 +1948,8 @@ var init_point = __esm({
1842
1948
  strokeColor: obj.attrs.color ?? "#1e40af",
1843
1949
  fillColor: obj.attrs.color ?? "#1e40af",
1844
1950
  face: obj.attrs.face ?? "o",
1845
- size: obj.attrs.size ?? 4
1951
+ size: obj.attrs.size ?? 4,
1952
+ ...labelOpts(obj.attrs.labelOffset, [10, 10])
1846
1953
  });
1847
1954
  } catch {
1848
1955
  }
@@ -1862,6 +1969,7 @@ var init_segment = __esm({
1862
1969
  "src/core/scene/kinds/segment.ts"() {
1863
1970
  init_registry();
1864
1971
  init_labelOf();
1972
+ init_label();
1865
1973
  def4 = {
1866
1974
  type: "segment",
1867
1975
  schemaVersion: 1,
@@ -1893,7 +2001,8 @@ var init_segment = __esm({
1893
2001
  strokeWidth: obj.attrs.width ?? 2,
1894
2002
  dash: obj.attrs.dash ?? 0,
1895
2003
  visible: obj.visible,
1896
- fixed: obj.locked
2004
+ fixed: obj.locked,
2005
+ ...labelOpts(obj.attrs.labelOffset)
1897
2006
  });
1898
2007
  }
1899
2008
  };
@@ -1930,6 +2039,7 @@ var init_line = __esm({
1930
2039
  "src/core/scene/kinds/line.ts"() {
1931
2040
  init_registry();
1932
2041
  init_labelOf();
2042
+ init_label();
1933
2043
  init_pointConstructions();
1934
2044
  def5 = {
1935
2045
  type: "line",
@@ -1972,7 +2082,8 @@ var init_line = __esm({
1972
2082
  strokeWidth: obj.attrs.width ?? 2,
1973
2083
  dash: obj.attrs.dash ?? 0,
1974
2084
  visible: obj.visible,
1975
- fixed: obj.locked
2085
+ fixed: obj.locked,
2086
+ ...labelOpts(obj.attrs.labelOffset)
1976
2087
  };
1977
2088
  const c = obj.attrs.construction;
1978
2089
  if (!c) {
@@ -2228,6 +2339,7 @@ var init_circle = __esm({
2228
2339
  "src/core/scene/kinds/circle.ts"() {
2229
2340
  init_registry();
2230
2341
  init_labelOf();
2342
+ init_label();
2231
2343
  init_pointConstructions();
2232
2344
  def8 = {
2233
2345
  type: "circle",
@@ -2289,15 +2401,17 @@ var init_circle = __esm({
2289
2401
  const board = ctx.jxg;
2290
2402
  const isCenterLabel = (l) => /^[A-Z]['′]?\d*$/u.test(l);
2291
2403
  const isCenter = isCenterLabel(obj.label);
2404
+ const isRenamedCircle = /_c$/.test(obj.label);
2292
2405
  const baseOpts = {
2293
2406
  name: obj.label,
2294
- withLabel: isCenter ? obj.attrs.showLabel ?? false : true,
2407
+ withLabel: isCenter ? obj.attrs.showLabel ?? false : isRenamedCircle ? false : true,
2295
2408
  strokeColor: obj.attrs.color ?? "#0f172a",
2296
2409
  strokeWidth: obj.attrs.width ?? 2,
2297
2410
  dash: obj.attrs.dash ?? 0,
2298
2411
  fillColor: "none",
2299
2412
  visible: obj.visible,
2300
- fixed: obj.locked
2413
+ fixed: obj.locked,
2414
+ ...labelOpts(obj.attrs.labelOffset)
2301
2415
  };
2302
2416
  const c = asConstruction(obj.attrs);
2303
2417
  if (c?.kind === "circumscribed") {
@@ -2782,8 +2896,11 @@ var init_intersection = __esm({
2782
2896
  const opts = {
2783
2897
  name: obj.label,
2784
2898
  withLabel: true,
2785
- strokeColor: obj.attrs.color ?? "#dc2626",
2786
- fillColor: obj.attrs.color ?? "#dc2626",
2899
+ // Cùng màu xanh với mọi điểm khác (buildPointOpts dùng '#1e40af').
2900
+ // Trước đây điểm giao tô đỏ '#dc2626' → tách biệt thị giác nhưng gây
2901
+ // khó hiểu ("vì sao điểm này khác màu?"). Giữ chung một màu.
2902
+ strokeColor: obj.attrs.color ?? "#1e40af",
2903
+ fillColor: obj.attrs.color ?? "#1e40af",
2787
2904
  visible: obj.visible,
2788
2905
  fixed: obj.locked
2789
2906
  };
@@ -2792,6 +2909,38 @@ var init_intersection = __esm({
2792
2909
  }
2793
2910
  const branch = obj.attrs.branch ?? 0;
2794
2911
  return board.create("intersection", [a, b, branch], opts);
2912
+ },
2913
+ /**
2914
+ * Cập nhật TẠI CHỖ các thuộc tính "trang trí" (tên/màu/ẩn-hiện/khoá) qua
2915
+ * setAttribute — giữ nguyên JxgObj identity nên các object phụ thuộc điểm
2916
+ * giao (đường thẳng qua nó, …) KHÔNG bị stale parent ref. Mô phỏng update
2917
+ * hook của point.ts.
2918
+ *
2919
+ * Nếu định nghĩa hình học đổi (kind/ref1/ref2/branch) thì throw → renderer
2920
+ * fallback remove + create để JSXGraph dựng lại phép giao đúng.
2921
+ */
2922
+ update: (obj, prev, ctx, existing) => {
2923
+ const a = obj.attrs;
2924
+ const p = prev.attrs;
2925
+ const branchA = a.branch;
2926
+ const branchP = p.branch;
2927
+ if (a.kind !== p.kind || a.ref1 !== p.ref1 || a.ref2 !== p.ref2 || branchA !== branchP) {
2928
+ throw new Error("intersection: \u0111\u1ECBnh ngh\u0129a h\xECnh h\u1ECDc \u0111\u1ED5i \u2014 recreate");
2929
+ }
2930
+ const el = existing;
2931
+ if (typeof el.setAttribute === "function") {
2932
+ try {
2933
+ el.setAttribute({
2934
+ name: obj.label,
2935
+ withLabel: true,
2936
+ strokeColor: a.color ?? "#1e40af",
2937
+ fillColor: a.color ?? "#1e40af",
2938
+ visible: obj.visible,
2939
+ fixed: obj.locked
2940
+ });
2941
+ } catch {
2942
+ }
2943
+ }
2795
2944
  }
2796
2945
  };
2797
2946
  registerKind(def12);
@@ -3478,6 +3627,7 @@ var init_JxgRenderer = __esm({
3478
3627
  init_registry();
3479
3628
  init_types2d();
3480
3629
  init_parser();
3630
+ init_label();
3481
3631
  JxgRenderer = class {
3482
3632
  constructor(store, board, options = {}) {
3483
3633
  this.elements = /* @__PURE__ */ new Map();
@@ -3546,6 +3696,7 @@ var init_JxgRenderer = __esm({
3546
3696
  this.elements.set(obj.id, el);
3547
3697
  this.attachFreePointDragSync(obj, el);
3548
3698
  this.attachGliderDragSync(obj, el);
3699
+ this.attachLabelDragSync(obj, el);
3549
3700
  } catch (err) {
3550
3701
  console.warn(`[scene/render/2d] kh\xF4ng render \u0111\u01B0\u1EE3c ${obj.kind} id="${obj.id}":`, err);
3551
3702
  }
@@ -3660,6 +3811,54 @@ var init_JxgRenderer = __esm({
3660
3811
  });
3661
3812
  });
3662
3813
  }
3814
+ /**
3815
+ * Cho phép kéo NHÃN của bất kỳ object nào có label (point/line/segment/
3816
+ * circle...). Khi user kéo nhãn, JSXGraph cộng dồn vào label.relativeCoords
3817
+ * (drag-delta screen px) nhưng KHÔNG đổi attribute offset → vị trí cuối =
3818
+ * offset + relativeCoords. Ta gộp lại thành offset thuần (readLabelOffset),
3819
+ * zero relativeCoords để khỏi double-count khi update-hook/recreate áp lại
3820
+ * offset, rồi dispatch UPDATE_ATTRS { labelOffset }. Right-click → reset.
3821
+ */
3822
+ attachLabelDragSync(obj, el) {
3823
+ const label = el?.label;
3824
+ if (!label || typeof label.on !== "function") return;
3825
+ const sceneId = obj.id;
3826
+ label.on("up", () => {
3827
+ if (this.disposed) return;
3828
+ const off = readLabelOffset(label);
3829
+ if (!off) return;
3830
+ const cur = this.store.getState().objects[sceneId];
3831
+ if (!cur) return;
3832
+ const prev = cur.attrs.labelOffset;
3833
+ if (prev && prev[0] === off[0] && prev[1] === off[1]) return;
3834
+ try {
3835
+ label.setAttribute({ offset: off });
3836
+ if (label.relativeCoords?.scrCoords) {
3837
+ label.relativeCoords.scrCoords[1] = 0;
3838
+ label.relativeCoords.scrCoords[2] = 0;
3839
+ }
3840
+ } catch {
3841
+ }
3842
+ this.store.dispatch({
3843
+ type: "UPDATE_ATTRS",
3844
+ payload: { id: sceneId, patch: { labelOffset: off } }
3845
+ });
3846
+ });
3847
+ const node = label.rendNode;
3848
+ if (node?.addEventListener) {
3849
+ node.addEventListener("contextmenu", (ev) => {
3850
+ if (this.disposed) return;
3851
+ ev.preventDefault();
3852
+ const c = this.store.getState().objects[sceneId];
3853
+ if (!c) return;
3854
+ if (c.attrs.labelOffset === void 0) return;
3855
+ this.store.dispatch({
3856
+ type: "UPDATE_ATTRS",
3857
+ payload: { id: sceneId, patch: { labelOffset: void 0 } }
3858
+ });
3859
+ });
3860
+ }
3861
+ }
3663
3862
  remove(id) {
3664
3863
  this.removeHalo(id);
3665
3864
  this.selectedIds.delete(id);
@@ -3796,88 +3995,89 @@ var init_JxgRenderer = __esm({
3796
3995
  layer: 4,
3797
3996
  needsRegularUpdate: true
3798
3997
  };
3998
+ const elementClass = el.elementClass;
3999
+ const elType = el.elType;
4000
+ const isPointLike = elementClass === 1 || elType === "point" || elType === "glider" || elType === "intersection";
4001
+ const isPolygonLike = Array.isArray(el.vertices) && el.vertices.length >= 3;
4002
+ const isCircleLike = elementClass === 3 || elType === "circle" || elType === "circumcircle" || elType === "incircle";
4003
+ const isSegment = elType === "segment";
4004
+ const isLineLike = elementClass === 2 || elType === "line" || elType === "arrow" || elType === "ray" || elType === "vector" || elType === "tangent" || elType === "normal" || elType === "parallel" || elType === "perpendicular" || elType === "bisector";
3799
4005
  const halos = [];
3800
4006
  try {
3801
- switch (el.elType) {
3802
- case "point":
3803
- case "glider":
3804
- case "intersection": {
3805
- const baseSize = el.getAttribute?.("size") ?? 4;
3806
- const halo = board.create("point", [
3807
- () => el.X?.() ?? 0,
3808
- () => el.Y?.() ?? 0
3809
- ], {
3810
- ...haloBase,
3811
- size: baseSize + 6,
3812
- face: "o",
3813
- strokeWidth: 2,
3814
- strokeOpacity: 0.75,
3815
- fillOpacity: 0.25
3816
- });
3817
- halos.push(halo);
3818
- break;
3819
- }
3820
- case "segment": {
3821
- if (el.point1 && el.point2) {
3822
- const halo = board.create("segment", [el.point1, el.point2], {
3823
- ...haloBase,
3824
- strokeWidth: 9,
3825
- straightFirst: false,
3826
- straightLast: false
3827
- });
3828
- halos.push(halo);
3829
- }
3830
- break;
3831
- }
3832
- case "line":
3833
- case "arrow":
3834
- case "ray":
3835
- case "vector":
3836
- case "tangent":
3837
- case "normal":
3838
- case "parallel":
3839
- case "perpendicular":
3840
- case "bisector": {
3841
- if (el.point1 && el.point2) {
3842
- const halo = board.create("line", [el.point1, el.point2], {
3843
- ...haloBase,
3844
- strokeWidth: 9
3845
- });
3846
- halos.push(halo);
3847
- }
3848
- break;
3849
- }
3850
- case "circle": {
3851
- if (el.center && typeof el.Radius === "function") {
3852
- const halo = board.create("circle", [el.center, () => el.Radius?.() ?? 0], {
3853
- ...haloBase,
3854
- strokeWidth: 9,
3855
- fillOpacity: 0
3856
- });
3857
- halos.push(halo);
3858
- }
3859
- break;
3860
- }
3861
- case "polygon": {
3862
- if (Array.isArray(el.vertices) && el.vertices.length >= 3) {
3863
- const last = el.vertices.length - 1;
3864
- const verts = el.vertices[last] === el.vertices[0] ? el.vertices.slice(0, last) : el.vertices.slice();
3865
- const halo = board.create("polygon", verts, {
3866
- ...haloBase,
3867
- fillOpacity: 0.2,
3868
- borders: {
3869
- strokeColor: SEL_STROKE,
3870
- strokeWidth: 7,
3871
- strokeOpacity: 0.55,
3872
- highlight: false
3873
- }
3874
- });
3875
- halos.push(halo);
4007
+ if (isPointLike) {
4008
+ const baseSize = el.getAttribute?.("size") ?? 4;
4009
+ const halo = board.create("point", [
4010
+ () => el.X?.() ?? 0,
4011
+ () => el.Y?.() ?? 0
4012
+ ], {
4013
+ ...haloBase,
4014
+ size: baseSize + 6,
4015
+ face: "o",
4016
+ strokeWidth: 2,
4017
+ strokeOpacity: 0.75,
4018
+ fillOpacity: 0.25
4019
+ });
4020
+ halos.push(halo);
4021
+ } else if (isPolygonLike) {
4022
+ const verts = el.vertices;
4023
+ const last = verts.length - 1;
4024
+ const trimmed = verts[last] === verts[0] ? verts.slice(0, last) : verts.slice();
4025
+ const halo = board.create("polygon", trimmed, {
4026
+ ...haloBase,
4027
+ fillOpacity: 0.2,
4028
+ borders: {
4029
+ strokeColor: SEL_STROKE,
4030
+ strokeWidth: 7,
4031
+ strokeOpacity: 0.55,
4032
+ highlight: false
3876
4033
  }
3877
- break;
4034
+ });
4035
+ halos.push(halo);
4036
+ } else if (isCircleLike && el.center && typeof el.Radius === "function") {
4037
+ const halo = board.create("circle", [el.center, () => el.Radius?.() ?? 0], {
4038
+ ...haloBase,
4039
+ strokeWidth: 9,
4040
+ fillOpacity: 0
4041
+ });
4042
+ halos.push(halo);
4043
+ } else if (isSegment && el.point1 && el.point2) {
4044
+ const halo = board.create("segment", [el.point1, el.point2], {
4045
+ ...haloBase,
4046
+ strokeWidth: 9,
4047
+ straightFirst: false,
4048
+ straightLast: false
4049
+ });
4050
+ halos.push(halo);
4051
+ } else if (isLineLike && el.point1 && el.point2) {
4052
+ const halo = board.create("line", [el.point1, el.point2], {
4053
+ ...haloBase,
4054
+ strokeWidth: 9
4055
+ });
4056
+ halos.push(halo);
4057
+ } else if (el.center && (el.radiuspoint ?? el.radiusPoint) && (el.anglepoint ?? el.anglePoint) && (elType === "arc" || elType === "semicircle" || elType === "circumcirclearc" || elType === "sector" || elType === "angle")) {
4058
+ const rp = el.radiuspoint ?? el.radiusPoint;
4059
+ const ap = el.anglepoint ?? el.anglePoint;
4060
+ if (elType === "sector") {
4061
+ halos.push(board.create("sector", [el.center, rp, ap], {
4062
+ ...haloBase,
4063
+ strokeWidth: 7,
4064
+ fillOpacity: 0.2
4065
+ }));
4066
+ } else if (elType === "angle") {
4067
+ halos.push(board.create("angle", [rp, el.center, ap], {
4068
+ ...haloBase,
4069
+ strokeWidth: 7,
4070
+ fillOpacity: 0.2,
4071
+ radius: el.getAttribute?.("radius") ?? 1
4072
+ }));
4073
+ } else {
4074
+ halos.push(board.create("arc", [el.center, rp, ap], {
4075
+ ...haloBase,
4076
+ strokeWidth: 9,
4077
+ fillColor: "none",
4078
+ fillOpacity: 0
4079
+ }));
3878
4080
  }
3879
- default:
3880
- break;
3881
4081
  }
3882
4082
  } catch (err) {
3883
4083
  console.warn("[scene/render/2d] halo create fail:", err);
@@ -4037,6 +4237,49 @@ var init_autoFitBbox = __esm({
4037
4237
  }
4038
4238
  });
4039
4239
 
4240
+ // src/stamps/geometry-2d/autoFitBoard.ts
4241
+ function isDefaultBbox(bbox) {
4242
+ const d = DEFAULT_VIEW_2D.bbox;
4243
+ return bbox.length === 4 && bbox[0] === d[0] && bbox[1] === d[1] && bbox[2] === d[2] && bbox[3] === d[3];
4244
+ }
4245
+ function fittedBboxFromBoard(board, aspect) {
4246
+ const objs = board?.objectsList;
4247
+ if (!Array.isArray(objs)) return null;
4248
+ const points = [];
4249
+ const circles = [];
4250
+ for (const raw of objs) {
4251
+ const o = raw;
4252
+ if (o?.elementClass === 1 && typeof o.X === "function" && typeof o.Y === "function") {
4253
+ const x = o.X(), y = o.Y();
4254
+ if (Number.isFinite(x) && Number.isFinite(y)) points.push([x, y]);
4255
+ } else if (o?.elementClass === 3 && o.center?.X && typeof o.Radius === "function") {
4256
+ const cx = o.center.X(), cy = o.center.Y(), r = o.Radius();
4257
+ if (Number.isFinite(cx) && Number.isFinite(cy) && Number.isFinite(r)) {
4258
+ circles.push({ cx, cy, r });
4259
+ }
4260
+ }
4261
+ }
4262
+ const safeAspect = Number.isFinite(aspect) && aspect > 0 ? aspect : 1;
4263
+ return computeAutoFitBbox(points, circles, safeAspect);
4264
+ }
4265
+ function autoFitBoardToContent(board, aspect) {
4266
+ const bbox = fittedBboxFromBoard(board, aspect);
4267
+ if (!bbox) return;
4268
+ const b = board;
4269
+ try {
4270
+ b.setBoundingBox?.(bbox);
4271
+ if (typeof b.update === "function") b.update();
4272
+ if (typeof b.fullUpdate === "function") b.fullUpdate();
4273
+ } catch {
4274
+ }
4275
+ }
4276
+ var init_autoFitBoard = __esm({
4277
+ "src/stamps/geometry-2d/autoFitBoard.ts"() {
4278
+ init_types();
4279
+ init_autoFitBbox();
4280
+ }
4281
+ });
4282
+
4040
4283
  // src/stamps/geometry-2d/labelLayout.ts
4041
4284
  function radialLabelOffsets(points, R = 14) {
4042
4285
  const out = /* @__PURE__ */ new Map();
@@ -4071,8 +4314,9 @@ function containerDimsForBbox(bbox) {
4071
4314
  if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) {
4072
4315
  return { width: FALLBACK_W, height: FALLBACK_H };
4073
4316
  }
4074
- let width = w * PIXELS_PER_UNIT;
4075
- let height = h * PIXELS_PER_UNIT;
4317
+ const pxPerUnit = DEFAULT_VIEW_PX / Math.max(w, h);
4318
+ let width = w * pxPerUnit;
4319
+ let height = h * pxPerUnit;
4076
4320
  const maxAxis = Math.max(width, height);
4077
4321
  if (maxAxis > MAX_DIM) {
4078
4322
  const ratio = MAX_DIM / maxAxis;
@@ -4087,31 +4331,6 @@ function containerDimsForBbox(bbox) {
4087
4331
  }
4088
4332
  return { width: Math.round(width), height: Math.round(height) };
4089
4333
  }
4090
- function autoFitBboxFromBoard(board, aspect) {
4091
- const objs = board?.objectsList;
4092
- if (!Array.isArray(objs)) return;
4093
- const points = [];
4094
- const circles = [];
4095
- for (const o of objs) {
4096
- if (o?.elementClass === 1 && typeof o.X === "function" && typeof o.Y === "function") {
4097
- const x = o.X(), y = o.Y();
4098
- if (Number.isFinite(x) && Number.isFinite(y)) points.push([x, y]);
4099
- } else if (o?.elementClass === 3 && o.center?.X && typeof o.Radius === "function") {
4100
- const cx = o.center.X(), cy = o.center.Y(), r = o.Radius();
4101
- if (Number.isFinite(cx) && Number.isFinite(cy) && Number.isFinite(r)) {
4102
- circles.push({ cx, cy, r });
4103
- }
4104
- }
4105
- }
4106
- const bbox = computeAutoFitBbox(points, circles, aspect);
4107
- if (!bbox) return;
4108
- try {
4109
- board.setBoundingBox(bbox);
4110
- if (typeof board.update === "function") board.update();
4111
- if (typeof board.fullUpdate === "function") board.fullUpdate();
4112
- } catch {
4113
- }
4114
- }
4115
4334
  function applyRadialLabelOffsets(board) {
4116
4335
  const objs = board?.objectsList;
4117
4336
  if (!Array.isArray(objs)) return;
@@ -4137,10 +4356,6 @@ function applyRadialLabelOffsets(board) {
4137
4356
  } catch {
4138
4357
  }
4139
4358
  }
4140
- function isDefaultBbox(bbox) {
4141
- const d = DEFAULT_VIEW_2D.bbox;
4142
- return bbox.length === 4 && bbox[0] === d[0] && bbox[1] === d[1] && bbox[2] === d[2] && bbox[3] === d[3];
4143
- }
4144
4359
  async function renderGeometrySvgFromState(jsonState) {
4145
4360
  const state = deserializeBoard(jsonState);
4146
4361
  const view = state.meta.domain === "2d" ? state.meta.view : DEFAULT_VIEW_2D;
@@ -4175,7 +4390,7 @@ async function renderGeometrySvgFromState(jsonState) {
4175
4390
  const store = createStore(state);
4176
4391
  const renderer = new JxgRenderer(store, board);
4177
4392
  if (shouldAutoFit) {
4178
- autoFitBboxFromBoard(board, dims.width / dims.height);
4393
+ autoFitBoardToContent(board, dims.width / dims.height);
4179
4394
  applyRadialLabelOffsets(board);
4180
4395
  }
4181
4396
  return renderer;
@@ -4183,7 +4398,7 @@ async function renderGeometrySvgFromState(jsonState) {
4183
4398
  });
4184
4399
  return svgString;
4185
4400
  }
4186
- var PIXELS_PER_UNIT, MIN_DIM, MAX_DIM, FALLBACK_W, FALLBACK_H;
4401
+ var DEFAULT_VIEW_PX, MIN_DIM, MAX_DIM, FALLBACK_W, FALLBACK_H;
4187
4402
  var init_render = __esm({
4188
4403
  "src/stamps/geometry-2d/render.ts"() {
4189
4404
  init_serialize();
@@ -4192,9 +4407,9 @@ var init_render = __esm({
4192
4407
  init_types();
4193
4408
  init_JxgRenderer();
4194
4409
  init_jxgOffscreenRender();
4195
- init_autoFitBbox();
4410
+ init_autoFitBoard();
4196
4411
  init_labelLayout();
4197
- PIXELS_PER_UNIT = 20;
4412
+ DEFAULT_VIEW_PX = 500;
4198
4413
  MIN_DIM = 100;
4199
4414
  MAX_DIM = 1200;
4200
4415
  FALLBACK_W = 400;
@@ -5754,41 +5969,48 @@ var init_icons = __esm({
5754
5969
  /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4", cy: "4", r: "1.7", fill: C_POINT })
5755
5970
  ] }),
5756
5971
  // ===== Nâng cao / kind chưa có icon =====
5757
- excenter: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
5758
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 16 L12 5 L19 16 Z", stroke: "currentColor", strokeWidth: "1.3" }),
5759
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "20", r: "3", fill: "none", stroke: C_CONSTRUCT, strokeWidth: "1.3" }),
5760
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "20", r: "1.4", fill: C_CONSTRUCT })
5972
+ // Hệ màu đồng bộ: XANH = input click sẵn, ĐỎ = output suy ra. Toạ độ tính để
5973
+ // điểm nằm ĐÚNG trên đường tròn/cung/tiếp tuyến (không trôi nổi).
5974
+ excenter: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", strokeLinejoin: "round", children: [
5975
+ /* @__PURE__ */ jsxRuntime.jsx("polygon", { points: "12,3 15,15 9,15", stroke: "currentColor", strokeWidth: "1.4", fill: "none" }),
5976
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "9", y1: "15", x2: "8", y2: "18.9", stroke: "#94a3b8", strokeWidth: "1", strokeDasharray: "1.6 1.6" }),
5977
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "15", y1: "15", x2: "16", y2: "18.9", stroke: "#94a3b8", strokeWidth: "1", strokeDasharray: "1.6 1.6" }),
5978
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "18.9", r: "3.8", stroke: "currentColor", strokeWidth: "1.3" }),
5979
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "3", r: "1.6", fill: C_POINT }),
5980
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9", cy: "15", r: "1.6", fill: C_POINT }),
5981
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "15", cy: "15", r: "1.6", fill: C_POINT }),
5982
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "18.9", r: "2.2", fill: C_CONSTRUCT })
5761
5983
  ] }),
5762
- tangencyPoint: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
5763
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "10", cy: "12", r: "6.5", stroke: "currentColor", strokeWidth: "1.3" }),
5764
- /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "16.5", y1: "3", x2: "16.5", y2: "21", stroke: "currentColor", strokeWidth: "1.3" }),
5765
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "16.5", cy: "12", r: "2.2", fill: C_CONSTRUCT })
5984
+ tangencyPoint: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
5985
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9.5", cy: "12", r: "7", stroke: "currentColor", strokeWidth: "1.4" }),
5986
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "16.5", y1: "3.5", x2: "16.5", y2: "20.5", stroke: "currentColor", strokeWidth: "1.4" }),
5987
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "16.5", cy: "12", r: "2.3", fill: C_CONSTRUCT })
5766
5988
  ] }),
5767
- secondIntersection: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
5768
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "11", cy: "12", r: "6.5", stroke: "currentColor", strokeWidth: "1.3" }),
5769
- /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "2", y1: "8", x2: "22", y2: "16", stroke: "currentColor", strokeWidth: "1.3" }),
5770
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "6.2", cy: "9.6", r: "1.6", fill: "currentColor" }),
5771
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "15.8", cy: "14.4", r: "2.4", fill: C_CONSTRUCT })
5989
+ secondIntersection: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
5990
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "11", cy: "12", r: "6.5", stroke: "currentColor", strokeWidth: "1.4" }),
5991
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "2.4", y1: "8.2", x2: "20.4", y2: "11.4", stroke: "currentColor", strokeWidth: "1.4" }),
5992
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "5.37", cy: "8.75", r: "1.8", fill: C_POINT }),
5993
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "17.4", cy: "10.87", r: "2.4", fill: C_CONSTRUCT })
5772
5994
  ] }),
5773
5995
  arcMidpoint: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
5774
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 17 A 9 9 0 0 1 20 17", stroke: "currentColor", strokeWidth: "1.4" }),
5775
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4", cy: "17", r: "1.6", fill: "currentColor" }),
5776
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "20", cy: "17", r: "1.6", fill: "currentColor" }),
5777
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "8.2", r: "2.4", fill: C_CONSTRUCT })
5996
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 17 A 8.5 8.5 0 0 1 20 17", stroke: "currentColor", strokeWidth: "1.6", fill: "none" }),
5997
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4", cy: "17", r: "1.8", fill: C_POINT }),
5998
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "20", cy: "17", r: "1.8", fill: C_POINT }),
5999
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "11.4", r: "2.3", fill: C_CONSTRUCT })
5778
6000
  ] }),
5779
6001
  circleIntersection: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
5780
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9", cy: "12", r: "6", stroke: "currentColor", strokeWidth: "1.3" }),
5781
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "15", cy: "12", r: "6", stroke: "currentColor", strokeWidth: "1.3" }),
5782
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "7.6", r: "2", fill: C_CONSTRUCT }),
5783
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "16.4", r: "2", fill: C_CONSTRUCT })
6002
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9", cy: "12", r: "6", stroke: "currentColor", strokeWidth: "1.4" }),
6003
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "15", cy: "12", r: "6", stroke: "currentColor", strokeWidth: "1.4" }),
6004
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "6.8", r: "2", fill: C_CONSTRUCT }),
6005
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "17.2", r: "2", fill: C_CONSTRUCT })
5784
6006
  ] }),
5785
6007
  tangentPointExt: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
5786
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "14", cy: "12", r: "6", stroke: "currentColor", strokeWidth: "1.3" }),
5787
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "3.5", cy: "12", r: "1.8", fill: C_POINT }),
5788
- /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3.5", y1: "12", x2: "17.5", y2: "7.5", stroke: "currentColor", strokeWidth: "1.2" }),
5789
- /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3.5", y1: "12", x2: "17.5", y2: "16.5", stroke: "currentColor", strokeWidth: "1.2" }),
5790
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "17.5", cy: "7.5", r: "1.8", fill: C_CONSTRUCT }),
5791
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "17.5", cy: "16.5", r: "1.8", fill: C_CONSTRUCT })
6008
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "13", cy: "12", r: "6", stroke: "currentColor", strokeWidth: "1.4" }),
6009
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "12", x2: "11.8", y2: "18.6", stroke: "currentColor", strokeWidth: "1.3" }),
6010
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "12", x2: "11.8", y2: "5.4", stroke: "currentColor", strokeWidth: "1.3" }),
6011
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "3", cy: "12", r: "2", fill: C_POINT }),
6012
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9.4", cy: "16.8", r: "1.8", fill: C_CONSTRUCT }),
6013
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9.4", cy: "7.2", r: "1.8", fill: C_CONSTRUCT })
5792
6014
  ] }),
5793
6015
  circleCR: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
5794
6016
  /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "7.5", stroke: "currentColor", strokeWidth: "1.3" }),
@@ -5799,9 +6021,11 @@ var init_icons = __esm({
5799
6021
  /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 19 L12 4 L21 19 Z", stroke: "currentColor", strokeWidth: "1.3", strokeLinejoin: "round" }),
5800
6022
  /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "14", r: "4.2", fill: "none", stroke: C_CONSTRUCT, strokeWidth: "1.3" })
5801
6023
  ] }),
5802
- excircle: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
5803
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 9 L14 4 L18 13 Z", stroke: "currentColor", strokeWidth: "1.2" }),
5804
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9", cy: "17", r: "4.6", fill: "none", stroke: C_CONSTRUCT, strokeWidth: "1.3" })
6024
+ excircle: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", strokeLinejoin: "round", children: [
6025
+ /* @__PURE__ */ jsxRuntime.jsx("polygon", { points: "12,3 15,15 9,15", stroke: "currentColor", strokeWidth: "1.4", fill: "none" }),
6026
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "9", y1: "15", x2: "8", y2: "18.9", stroke: "#94a3b8", strokeWidth: "1", strokeDasharray: "1.6 1.6" }),
6027
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "15", y1: "15", x2: "16", y2: "18.9", stroke: "#94a3b8", strokeWidth: "1", strokeDasharray: "1.6 1.6" }),
6028
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "18.9", r: "3.8", stroke: C_CONSTRUCT, strokeWidth: "1.5", fill: C_CONSTRUCT, fillOpacity: "0.12" })
5805
6029
  ] }),
5806
6030
  pointOn: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
5807
6031
  /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "7.5", stroke: "currentColor", strokeWidth: "1.3" }),
@@ -7811,11 +8035,11 @@ function buildObjectSnapshot(state, id, anchorScreen) {
7811
8035
  const obj = state.objects[id];
7812
8036
  if (!obj) return null;
7813
8037
  const k = obj.kind;
7814
- if (k !== "point" && k !== "line" && k !== "circle" && k !== "segment" && k !== "ray" && k !== "vector") {
8038
+ if (k !== "point" && k !== "line" && k !== "circle" && k !== "segment" && k !== "ray" && k !== "vector" && k !== "intersection") {
7815
8039
  return null;
7816
8040
  }
7817
8041
  const a = obj.attrs;
7818
- const jKind = k === "point" ? "point" : k === "circle" ? "circle" : "line";
8042
+ const jKind = k === "point" || k === "intersection" ? "point" : k === "circle" ? "circle" : "line";
7819
8043
  return {
7820
8044
  id,
7821
8045
  kind: jKind,
@@ -8223,6 +8447,7 @@ var init_MiniBoard = __esm({
8223
8447
  init_safeJsx();
8224
8448
  init_attachJxgWheelZoom();
8225
8449
  init_initJxgBoard();
8450
+ init_autoFitBoard();
8226
8451
  init_snapshot();
8227
8452
  init_previewActions();
8228
8453
  init_buildHandle();
@@ -8462,6 +8687,11 @@ var init_MiniBoard = __esm({
8462
8687
  rendererRef.current = new JxgRenderer(store, board, {
8463
8688
  theme: paletteFor(isDarkRef.current)
8464
8689
  });
8690
+ if (isDefaultBbox(initialView.bbox)) {
8691
+ const el = containerRef.current;
8692
+ const aspect = el && el.clientHeight > 0 ? el.clientWidth / el.clientHeight : 1;
8693
+ safeJsx("MiniBoard.autoFit", () => autoFitBoardToContent(board, aspect));
8694
+ }
8465
8695
  if (containerRef.current) {
8466
8696
  wheelCleanup = attachJxgWheelZoom(containerRef.current, board, "MiniBoard.2d");
8467
8697
  }
@@ -9259,6 +9489,7 @@ function useAiFigure(generator) {
9259
9489
  const [prompt, setPrompt] = React2.useState("");
9260
9490
  const [isLoading, setIsLoading] = React2.useState(false);
9261
9491
  const [error, setError] = React2.useState(null);
9492
+ const [notice, setNotice] = React2.useState(null);
9262
9493
  const [tokens, setTokens] = React2.useState(0);
9263
9494
  const abortRef = React2.useRef(null);
9264
9495
  const requestIdRef = React2.useRef(0);
@@ -9279,6 +9510,7 @@ function useAiFigure(generator) {
9279
9510
  abortRef.current = controller;
9280
9511
  setIsLoading(true);
9281
9512
  setError(null);
9513
+ setNotice(null);
9282
9514
  setTokens(0);
9283
9515
  try {
9284
9516
  const generated = await generator(problem, {
@@ -9292,6 +9524,7 @@ function useAiFigure(generator) {
9292
9524
  setError(generated.message);
9293
9525
  return null;
9294
9526
  }
9527
+ if (generated.partial) setNotice(generated.partial.message);
9295
9528
  return generated.state;
9296
9529
  } catch (caught) {
9297
9530
  if (controller.signal.aborted || caught instanceof DOMException && caught.name === "AbortError") {
@@ -9318,6 +9551,7 @@ function useAiFigure(generator) {
9318
9551
  setPrompt,
9319
9552
  isLoading,
9320
9553
  error,
9554
+ notice,
9321
9555
  submit,
9322
9556
  cancel,
9323
9557
  tokens
@@ -9528,6 +9762,7 @@ function AiFigurePrompt({ generator, onGenerated }) {
9528
9762
  setPrompt,
9529
9763
  isLoading,
9530
9764
  error,
9765
+ notice,
9531
9766
  submit,
9532
9767
  cancel,
9533
9768
  tokens
@@ -9541,6 +9776,10 @@ function AiFigurePrompt({ generator, onGenerated }) {
9541
9776
  const id = setInterval(() => setElapsed((s) => s + 1), 1e3);
9542
9777
  return () => clearInterval(id);
9543
9778
  }, [isLoading]);
9779
+ const [noticeDismissed, setNoticeDismissed] = React2.useState(false);
9780
+ React2.useEffect(() => {
9781
+ setNoticeDismissed(false);
9782
+ }, [notice]);
9544
9783
  const [image, setImage] = React2.useState(null);
9545
9784
  const [ocrLoading, setOcrLoading] = React2.useState(false);
9546
9785
  const [ocrError, setOcrError] = React2.useState(null);
@@ -9756,7 +9995,30 @@ function AiFigurePrompt({ generator, onGenerated }) {
9756
9995
  }
9757
9996
  ),
9758
9997
  ocrError && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "mt-1 px-1 text-xs text-red-600", children: ocrError }),
9759
- error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "mt-1 px-1 text-xs text-red-600", children: error })
9998
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "mt-1 px-1 text-xs text-red-600", children: error }),
9999
+ notice && !noticeDismissed && /* @__PURE__ */ jsxRuntime.jsxs(
10000
+ "div",
10001
+ {
10002
+ role: "status",
10003
+ "data-testid": "geometry-ai-partial-notice",
10004
+ 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",
10005
+ children: [
10006
+ notice,
10007
+ /* @__PURE__ */ jsxRuntime.jsx(
10008
+ "button",
10009
+ {
10010
+ type: "button",
10011
+ onClick: () => setNoticeDismissed(true),
10012
+ "aria-label": "\u0110\xF3ng th\xF4ng b\xE1o",
10013
+ title: "\u0110\xF3ng th\xF4ng b\xE1o",
10014
+ "data-testid": "geometry-ai-partial-dismiss",
10015
+ 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",
10016
+ children: "\xD7"
10017
+ }
10018
+ )
10019
+ ]
10020
+ }
10021
+ )
9760
10022
  ] });
9761
10023
  }
9762
10024
  var ArrowUpIcon, StopIcon, PaperclipIcon;
@@ -10381,11 +10643,17 @@ async function insertStampImage(api, opts) {
10381
10643
  const elements = api.getSceneElements();
10382
10644
  const editingId = opts.editingElementId ?? null;
10383
10645
  if (editingId) {
10646
+ const old = elements.find((e) => e.id === editingId) ;
10647
+ const oldLongest = old ? Math.max(old.width ?? 0, old.height ?? 0) : 0;
10648
+ const newLongest = Math.max(width, height);
10649
+ const scale = oldLongest > 0 && newLongest > 0 ? oldLongest / newLongest : 1;
10650
+ const w = width * scale;
10651
+ const h = height * scale;
10384
10652
  const updated = elements.map(
10385
- (e) => e.id === editingId ? { ...e, fileId, customData, width, height } : e
10653
+ (e) => e.id === editingId ? { ...e, fileId, customData, width: w, height: h } : e
10386
10654
  );
10387
10655
  api.updateScene({ elements: updated, appState: clearAppStateAfterInsert() });
10388
- return { fileId, width, height, elementId: editingId };
10656
+ return { fileId, width: w, height: h, elementId: editingId };
10389
10657
  }
10390
10658
  const newElement = buildStampImageElement(
10391
10659
  api,
@@ -10539,8 +10807,11 @@ function serializePoint(obj, state) {
10539
10807
  };
10540
10808
  }
10541
10809
  case "arcMidpoint": {
10542
- const refs = resolveRefs([c.circle, c.a, c.b, c.notContaining], state);
10543
- if (!refs) return fail("unresolved-ref", `${c.circle},${c.a},${c.b},${c.notContaining}`);
10810
+ const containment = c.containing ?? c.notContaining;
10811
+ const baseRefs = [c.circle, c.a, c.b];
10812
+ if (containment) baseRefs.push(containment);
10813
+ const refs = resolveRefs(baseRefs, state);
10814
+ if (!refs) return fail("unresolved-ref", baseRefs.join(","));
10544
10815
  return {
10545
10816
  ok: true,
10546
10817
  entity: {
@@ -10549,7 +10820,7 @@ function serializePoint(obj, state) {
10549
10820
  circle: refs[0],
10550
10821
  a: refs[1],
10551
10822
  b: refs[2],
10552
- notContaining: refs[3]
10823
+ ...c.containing ? { containing: refs[3] } : c.notContaining ? { notContaining: refs[3] } : {}
10553
10824
  }
10554
10825
  };
10555
10826
  }
@@ -10593,6 +10864,7 @@ function serializePoint(obj, state) {
10593
10864
  case "onPerpendicular":
10594
10865
  case "onPerpBisector":
10595
10866
  case "onCircleAroundPoint":
10867
+ case "mixtilinearPoint":
10596
10868
  return fail("unsupported-constraint", c.kind);
10597
10869
  default: {
10598
10870
  return fail("unsupported-constraint");
@@ -10895,9 +11167,11 @@ function describeEntity(e) {
10895
11167
  return `${e.name} = \u0111\u01B0\u1EDDng tr\xF2n b\xE0ng ti\u1EBFp ${e.vertices.join("")} \u0111\u1ED1i di\u1EC7n ${e.opposite}`;
10896
11168
  // Cụm A
10897
11169
  case "arcMidpoint":
10898
- return `${e.name} = trung \u0111i\u1EC3m cung ${e.a}${e.b} (kh\xF4ng ch\u1EE9a ${e.notContaining}) tr\xEAn ${e.circle}`;
11170
+ 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}`;
10899
11171
  case "excenter":
10900
11172
  return `${e.name} = t\xE2m b\xE0ng ti\u1EBFp ${e.vertices.join("")} \u0111\u1ED1i di\u1EC7n ${e.opposite}`;
11173
+ case "mixtilinearPoint":
11174
+ return `${e.name} = ${e.which === "center" ? "t\xE2m" : "ti\u1EBFp \u0111i\u1EC3m"} mixtilinear ${e.vertices.join("")}`;
10901
11175
  case "reflectPoint":
10902
11176
  return `${e.name} = \u0111\u1ED1i x\u1EE9ng ${e.of} qua \u0111i\u1EC3m ${e.through}`;
10903
11177
  case "reflectLine":
@@ -10908,6 +11182,8 @@ function describeEntity(e) {
10908
11182
  const distStr = d.kind === "circleRadius" ? `r(${d.circle})` : d.kind === "segmentLength" ? `|${d.p1}${d.p2}|` : `${d.value}`;
10909
11183
  return `${e.name} = \u0111i\u1EC3m tr\xEAn tia ${e.from}${e.through} c\xE1ch ${e.through} m\u1ED9t kho\u1EA3ng ${distStr}`;
10910
11184
  }
11185
+ case "onPerpBisector":
11186
+ return `${e.name} = \u0111i\u1EC3m tr\xEAn trung tr\u1EF1c ${e.p1}${e.p2}`;
10911
11187
  default: {
10912
11188
  return "";
10913
11189
  }
@@ -11030,7 +11306,8 @@ var init_host = __esm({
11030
11306
  version: 1,
11031
11307
  jsonState
11032
11308
  }),
11033
- editingElementId: editingElement?.id ?? null
11309
+ editingElementId: editingElement?.id ?? null,
11310
+ preserveExistingSize: true
11034
11311
  });
11035
11312
  } catch (err) {
11036
11313
  console.error("Geometry insert failed:", err);