@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
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
@@ -9531,6 +9765,7 @@ function AiFigurePrompt({ generator, onGenerated }) {
9531
9765
  setPrompt,
9532
9766
  isLoading,
9533
9767
  error,
9768
+ notice,
9534
9769
  submit,
9535
9770
  cancel,
9536
9771
  tokens
@@ -9544,6 +9779,10 @@ function AiFigurePrompt({ generator, onGenerated }) {
9544
9779
  const id = setInterval(() => setElapsed((s) => s + 1), 1e3);
9545
9780
  return () => clearInterval(id);
9546
9781
  }, [isLoading]);
9782
+ const [noticeDismissed, setNoticeDismissed] = React19.useState(false);
9783
+ React19.useEffect(() => {
9784
+ setNoticeDismissed(false);
9785
+ }, [notice]);
9547
9786
  const [image, setImage] = React19.useState(null);
9548
9787
  const [ocrLoading, setOcrLoading] = React19.useState(false);
9549
9788
  const [ocrError, setOcrError] = React19.useState(null);
@@ -9759,7 +9998,30 @@ function AiFigurePrompt({ generator, onGenerated }) {
9759
9998
  }
9760
9999
  ),
9761
10000
  ocrError && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "mt-1 px-1 text-xs text-red-600", children: ocrError }),
9762
- error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "mt-1 px-1 text-xs text-red-600", children: error })
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
+ )
9763
10025
  ] });
9764
10026
  }
9765
10027
  var ArrowUpIcon, StopIcon, PaperclipIcon;
@@ -10384,11 +10646,17 @@ async function insertStampImage(api, opts) {
10384
10646
  const elements = api.getSceneElements();
10385
10647
  const editingId = opts.editingElementId ?? null;
10386
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;
10387
10655
  const updated = elements.map(
10388
- (e) => e.id === editingId ? { ...e, fileId, customData, width, height } : e
10656
+ (e) => e.id === editingId ? { ...e, fileId, customData, width: w, height: h } : e
10389
10657
  );
10390
10658
  api.updateScene({ elements: updated, appState: clearAppStateAfterInsert() });
10391
- return { fileId, width, height, elementId: editingId };
10659
+ return { fileId, width: w, height: h, elementId: editingId };
10392
10660
  }
10393
10661
  const newElement = buildStampImageElement(
10394
10662
  api,
@@ -10542,8 +10810,11 @@ function serializePoint(obj, state) {
10542
10810
  };
10543
10811
  }
10544
10812
  case "arcMidpoint": {
10545
- const refs = resolveRefs([c.circle, c.a, c.b, c.notContaining], state);
10546
- 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(","));
10547
10818
  return {
10548
10819
  ok: true,
10549
10820
  entity: {
@@ -10552,7 +10823,7 @@ function serializePoint(obj, state) {
10552
10823
  circle: refs[0],
10553
10824
  a: refs[1],
10554
10825
  b: refs[2],
10555
- notContaining: refs[3]
10826
+ ...c.containing ? { containing: refs[3] } : c.notContaining ? { notContaining: refs[3] } : {}
10556
10827
  }
10557
10828
  };
10558
10829
  }
@@ -10596,6 +10867,7 @@ function serializePoint(obj, state) {
10596
10867
  case "onPerpendicular":
10597
10868
  case "onPerpBisector":
10598
10869
  case "onCircleAroundPoint":
10870
+ case "mixtilinearPoint":
10599
10871
  return fail("unsupported-constraint", c.kind);
10600
10872
  default: {
10601
10873
  return fail("unsupported-constraint");
@@ -10898,9 +11170,11 @@ function describeEntity(e) {
10898
11170
  return `${e.name} = \u0111\u01B0\u1EDDng tr\xF2n b\xE0ng ti\u1EBFp ${e.vertices.join("")} \u0111\u1ED1i di\u1EC7n ${e.opposite}`;
10899
11171
  // Cụm A
10900
11172
  case "arcMidpoint":
10901
- 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}`;
10902
11174
  case "excenter":
10903
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("")}`;
10904
11178
  case "reflectPoint":
10905
11179
  return `${e.name} = \u0111\u1ED1i x\u1EE9ng ${e.of} qua \u0111i\u1EC3m ${e.through}`;
10906
11180
  case "reflectLine":
@@ -10911,6 +11185,8 @@ function describeEntity(e) {
10911
11185
  const distStr = d.kind === "circleRadius" ? `r(${d.circle})` : d.kind === "segmentLength" ? `|${d.p1}${d.p2}|` : `${d.value}`;
10912
11186
  return `${e.name} = \u0111i\u1EC3m tr\xEAn tia ${e.from}${e.through} c\xE1ch ${e.through} m\u1ED9t kho\u1EA3ng ${distStr}`;
10913
11187
  }
11188
+ case "onPerpBisector":
11189
+ return `${e.name} = \u0111i\u1EC3m tr\xEAn trung tr\u1EF1c ${e.p1}${e.p2}`;
10914
11190
  default: {
10915
11191
  return "";
10916
11192
  }
@@ -11033,7 +11309,8 @@ var init_host = __esm({
11033
11309
  version: 1,
11034
11310
  jsonState
11035
11311
  }),
11036
- editingElementId: editingElement?.id ?? null
11312
+ editingElementId: editingElement?.id ?? null,
11313
+ preserveExistingSize: true
11037
11314
  });
11038
11315
  } catch (err) {
11039
11316
  console.error("Geometry insert failed:", err);
@@ -14576,7 +14853,8 @@ var init_host3 = __esm({
14576
14853
  version: 2,
14577
14854
  jsonState
14578
14855
  }),
14579
- editingElementId: editingElement?.id ?? null
14856
+ editingElementId: editingElement?.id ?? null,
14857
+ preserveExistingSize: true
14580
14858
  });
14581
14859
  onClose();
14582
14860
  },
@@ -15879,7 +16157,8 @@ var init_host4 = __esm({
15879
16157
  version: 2,
15880
16158
  jsonState
15881
16159
  }),
15882
- editingElementId: editingElement?.id ?? null
16160
+ editingElementId: editingElement?.id ?? null,
16161
+ preserveExistingSize: true
15883
16162
  });
15884
16163
  } catch (err) {
15885
16164
  console.error("Graph2D insert failed:", err);