@xom11/whiteboard 0.30.0 → 0.32.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 (99) hide show
  1. package/dist/ai.d.mts +374 -59
  2. package/dist/ai.d.ts +374 -59
  3. package/dist/ai.js +7542 -545
  4. package/dist/ai.js.map +1 -1
  5. package/dist/ai.mjs +7243 -569
  6. package/dist/ai.mjs.map +1 -1
  7. package/dist/catalog.json +5 -5
  8. package/dist/{chunk-ZTQBUKLJ.mjs → chunk-4EXRNZRX.mjs} +781 -95
  9. package/dist/chunk-4EXRNZRX.mjs.map +1 -0
  10. package/dist/{chunk-PPKHCRRE.mjs → chunk-C76SOFXF.mjs} +3 -3
  11. package/dist/{chunk-PPKHCRRE.mjs.map → chunk-C76SOFXF.mjs.map} +1 -1
  12. package/dist/{chunk-IHUFOV7L.mjs → chunk-CH6SFONH.mjs} +15 -3
  13. package/dist/chunk-CH6SFONH.mjs.map +1 -0
  14. package/dist/{chunk-XVVLT6B3.mjs → chunk-DBG6DZ2V.mjs} +4 -4
  15. package/dist/{chunk-XVVLT6B3.mjs.map → chunk-DBG6DZ2V.mjs.map} +1 -1
  16. package/dist/{chunk-BU5KLO3P.mjs → chunk-DCFZTKFE.mjs} +14 -6
  17. package/dist/chunk-DCFZTKFE.mjs.map +1 -0
  18. package/dist/{chunk-SF3U7ZF4.mjs → chunk-DJM5EWTT.mjs} +231 -16
  19. package/dist/chunk-DJM5EWTT.mjs.map +1 -0
  20. package/dist/{chunk-V3YJ6JFL.mjs → chunk-F42MZ745.mjs} +3 -3
  21. package/dist/{chunk-V3YJ6JFL.mjs.map → chunk-F42MZ745.mjs.map} +1 -1
  22. package/dist/{chunk-5JM35CXV.mjs → chunk-FI2MGX5H.mjs} +4 -4
  23. package/dist/{chunk-5JM35CXV.mjs.map → chunk-FI2MGX5H.mjs.map} +1 -1
  24. package/dist/{chunk-SZDAS7LK.mjs → chunk-IE2GGHNF.mjs} +131 -81
  25. package/dist/chunk-IE2GGHNF.mjs.map +1 -0
  26. package/dist/{chunk-BNBOIDO5.mjs → chunk-K7VJU7LQ.mjs} +3 -3
  27. package/dist/{chunk-BNBOIDO5.mjs.map → chunk-K7VJU7LQ.mjs.map} +1 -1
  28. package/dist/{chunk-H22OZYTW.mjs → chunk-KLGPAMJS.mjs} +55 -43
  29. package/dist/chunk-KLGPAMJS.mjs.map +1 -0
  30. package/dist/{chunk-CXHNVYMD.mjs → chunk-KWDBVLST.mjs} +5 -5
  31. package/dist/{chunk-CXHNVYMD.mjs.map → chunk-KWDBVLST.mjs.map} +1 -1
  32. package/dist/{chunk-73Q7ADVL.mjs → chunk-MNHOFOM5.mjs} +2 -2
  33. package/dist/{chunk-73Q7ADVL.mjs.map → chunk-MNHOFOM5.mjs.map} +1 -1
  34. package/dist/{chunk-QRUAEXLR.mjs → chunk-MOAXHVKI.mjs} +5 -5
  35. package/dist/{chunk-QRUAEXLR.mjs.map → chunk-MOAXHVKI.mjs.map} +1 -1
  36. package/dist/{chunk-QGNU34T7.mjs → chunk-QLQ4MJNO.mjs} +10 -4
  37. package/dist/chunk-QLQ4MJNO.mjs.map +1 -0
  38. package/dist/chunk-SUXIMLYN.mjs +238 -0
  39. package/dist/chunk-SUXIMLYN.mjs.map +1 -0
  40. package/dist/{chunk-OQIQNKPQ.mjs → chunk-XSTGTEGP.mjs} +5 -5
  41. package/dist/{chunk-OQIQNKPQ.mjs.map → chunk-XSTGTEGP.mjs.map} +1 -1
  42. package/dist/geometry-2d.d.mts +1 -1
  43. package/dist/geometry-2d.d.ts +1 -1
  44. package/dist/geometry-2d.js +1329 -288
  45. package/dist/geometry-2d.js.map +1 -1
  46. package/dist/geometry-2d.mjs +6 -6
  47. package/dist/geometry-3d.d.mts +1 -1
  48. package/dist/geometry-3d.d.ts +1 -1
  49. package/dist/geometry-3d.js +4136 -462
  50. package/dist/geometry-3d.js.map +1 -1
  51. package/dist/geometry-3d.mjs +5 -5
  52. package/dist/graph-2d.d.mts +1 -1
  53. package/dist/graph-2d.d.ts +1 -1
  54. package/dist/graph-2d.js +1010 -208
  55. package/dist/graph-2d.js.map +1 -1
  56. package/dist/graph-2d.mjs +8 -8
  57. package/dist/{handleExtractProblem-BrDY9ifM.d.ts → handleExtractProblem-mtJyhEw3.d.mts} +2 -0
  58. package/dist/{handleExtractProblem-BrDY9ifM.d.mts → handleExtractProblem-mtJyhEw3.d.ts} +2 -0
  59. package/dist/{host-ZQCDAT6O.mjs → host-H2IGOKJU.mjs} +3 -3
  60. package/dist/{host-ZQCDAT6O.mjs.map → host-H2IGOKJU.mjs.map} +1 -1
  61. package/dist/{host-3UFGFMJ2.mjs → host-JNLJ5BVR.mjs} +92 -45
  62. package/dist/host-JNLJ5BVR.mjs.map +1 -0
  63. package/dist/{host-HOSJHQ5H.mjs → host-VRWLQSMH.mjs} +14 -13
  64. package/dist/host-VRWLQSMH.mjs.map +1 -0
  65. package/dist/host-W4U6YCSX.mjs +4973 -0
  66. package/dist/host-W4U6YCSX.mjs.map +1 -0
  67. package/dist/index.d.mts +3 -3
  68. package/dist/index.d.ts +3 -3
  69. package/dist/index.js +4577 -600
  70. package/dist/index.js.map +1 -1
  71. package/dist/index.mjs +35 -29
  72. package/dist/index.mjs.map +1 -1
  73. package/dist/latex.d.mts +1 -1
  74. package/dist/latex.d.ts +1 -1
  75. package/dist/latex.js +8 -2
  76. package/dist/latex.js.map +1 -1
  77. package/dist/latex.mjs +1 -1
  78. package/dist/render-NMS7OAV6.mjs +10 -0
  79. package/dist/{render-ZX2O2IK7.mjs.map → render-NMS7OAV6.mjs.map} +1 -1
  80. package/dist/serialize-7UZWQQ4I.mjs +9 -0
  81. package/dist/{serialize-N4G6RFBB.mjs.map → serialize-7UZWQQ4I.mjs.map} +1 -1
  82. package/dist/{types-C3FjpoUi.d.mts → types-tePd94vW.d.mts} +8 -0
  83. package/dist/{types-C3FjpoUi.d.ts → types-tePd94vW.d.ts} +8 -0
  84. package/package.json +4 -1
  85. package/dist/chunk-BU5KLO3P.mjs.map +0 -1
  86. package/dist/chunk-H22OZYTW.mjs.map +0 -1
  87. package/dist/chunk-IHUFOV7L.mjs.map +0 -1
  88. package/dist/chunk-QGNU34T7.mjs.map +0 -1
  89. package/dist/chunk-QK6OVDLC.mjs +0 -103
  90. package/dist/chunk-QK6OVDLC.mjs.map +0 -1
  91. package/dist/chunk-SF3U7ZF4.mjs.map +0 -1
  92. package/dist/chunk-SZDAS7LK.mjs.map +0 -1
  93. package/dist/chunk-ZTQBUKLJ.mjs.map +0 -1
  94. package/dist/host-2ISGVO7O.mjs +0 -2377
  95. package/dist/host-2ISGVO7O.mjs.map +0 -1
  96. package/dist/host-3UFGFMJ2.mjs.map +0 -1
  97. package/dist/host-HOSJHQ5H.mjs.map +0 -1
  98. package/dist/render-ZX2O2IK7.mjs +0 -10
  99. package/dist/serialize-N4G6RFBB.mjs +0 -9
@@ -73,16 +73,16 @@ var init_types = __esm({
73
73
  });
74
74
 
75
75
  // src/core/scene/registry.ts
76
- function registerKind(def22) {
77
- if (registry.has(def22.type)) {
78
- console.warn(`[scene] kind "${def22.type}" \u0111\xE3 \u0111\u01B0\u1EE3c \u0111\u0103ng k\xFD \u2014 ghi \u0111\xE8 \u0111\u1ECBnh ngh\u0129a c\u0169`);
76
+ function registerKind(def24) {
77
+ if (registry.has(def24.type)) {
78
+ console.warn(`[scene] kind "${def24.type}" \u0111\xE3 \u0111\u01B0\u1EE3c \u0111\u0103ng k\xFD \u2014 ghi \u0111\xE8 \u0111\u1ECBnh ngh\u0129a c\u0169`);
79
79
  }
80
- registry.set(def22.type, def22);
80
+ registry.set(def24.type, def24);
81
81
  }
82
82
  function getKind(type) {
83
- const def22 = registry.get(type);
84
- if (!def22) throw new Error(`[scene] unknown kind: ${type}`);
85
- return def22;
83
+ const def24 = registry.get(type);
84
+ if (!def24) throw new Error(`[scene] unknown kind: ${type}`);
85
+ return def24;
86
86
  }
87
87
  var registry;
88
88
  var init_registry = __esm({
@@ -168,8 +168,8 @@ function reduce(draft, action) {
168
168
  return;
169
169
  }
170
170
  case "TRANSACTION": {
171
- for (const sub of action.payload.actions) {
172
- reduce(draft, sub);
171
+ for (const sub2 of action.payload.actions) {
172
+ reduce(draft, sub2);
173
173
  }
174
174
  return;
175
175
  }
@@ -382,8 +382,36 @@ function constraintRefs(c) {
382
382
  return [c.polygonId];
383
383
  case "onSphere":
384
384
  return [c.sphereId];
385
- default:
385
+ case "midpoint":
386
+ return [c.p1, c.p2];
387
+ case "centroid":
388
+ return [...c.vertices];
389
+ case "intersectionLines":
390
+ return [c.a1, c.b1, c.a2, c.b2];
391
+ case "intersectionLinePlane":
392
+ return [c.a, c.b, c.plane];
393
+ case "perpFootLine":
394
+ return [c.from, c.a, c.b];
395
+ case "perpFootPlane":
396
+ return [c.from, c.plane];
397
+ case "circumsphereCenter":
398
+ return [...c.vertices];
399
+ case "pyramidInsphereCenter":
400
+ return [c.apex, ...c.vertices];
401
+ case "faceCircumcenter":
402
+ return [...c.vertices];
403
+ case "pointAboveFace":
404
+ return [c.base, c.apex, ...c.vertices];
405
+ // Kind KHÔNG có ref scene-object (toạ độ literal): liệt kê TƯỜNG MINH để
406
+ // exhaustive never-guard bên dưới buộc khai báo khi thêm constraint kind mới
407
+ // (mô phỏng constraintRefs2D — quên case = cascade-delete/deps sai âm thầm).
408
+ case "free":
409
+ case "onGround":
410
+ case "onAxis":
411
+ return [];
412
+ default: {
386
413
  return [];
414
+ }
387
415
  }
388
416
  }
389
417
  var init_d_constraint = __esm({
@@ -391,12 +419,359 @@ var init_d_constraint = __esm({
391
419
  }
392
420
  });
393
421
 
422
+ // src/core/scene/kinds/constraint3d-math.ts
423
+ function sub(a, b) {
424
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
425
+ }
426
+ function add(a, b) {
427
+ return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
428
+ }
429
+ function scale(a, k) {
430
+ return [a[0] * k, a[1] * k, a[2] * k];
431
+ }
432
+ function dot(a, b) {
433
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
434
+ }
435
+ function cross(a, b) {
436
+ return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
437
+ }
438
+ function norm(a) {
439
+ return Math.sqrt(dot(a, a));
440
+ }
441
+ function normalize(a) {
442
+ const n = norm(a);
443
+ return n === 0 ? a : scale(a, 1 / n);
444
+ }
445
+ function getPointWorld(id, state) {
446
+ const obj = state.objects[id];
447
+ if (!obj || obj.kind !== "point3d") {
448
+ throw new Error(`constraintMath: point ${id} not found`);
449
+ }
450
+ const attrs = obj.attrs;
451
+ return constraintToWorld(attrs.constraint, state);
452
+ }
453
+ function getPlaneBasis(planeObj, state) {
454
+ const a = planeObj.attrs;
455
+ let p1, p2, p3;
456
+ if (a.construction) {
457
+ const r = planeConstructionWorld(a.construction, state);
458
+ p1 = r.p1;
459
+ p2 = r.p2;
460
+ p3 = r.p3;
461
+ } else {
462
+ p1 = getPointWorld(a.p1, state);
463
+ p2 = getPointWorld(a.p2, state);
464
+ p3 = getPointWorld(a.p3, state);
465
+ }
466
+ const basis1 = sub(p2, p1);
467
+ const tmp = sub(p3, p1);
468
+ const normal = normalize(cross(basis1, tmp));
469
+ const basis2 = cross(normal, basis1);
470
+ return { origin: p1, basis1, basis2, normal };
471
+ }
472
+ function orthoBasisFromNormal(n) {
473
+ const seed = Math.abs(n[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0];
474
+ const basis1 = normalize(cross(n, seed));
475
+ const basis2 = cross(n, basis1);
476
+ return { basis1, basis2 };
477
+ }
478
+ function planeConstructionWorld(c, state) {
479
+ if (_recDepth >= MAX_REC_DEPTH) throw new Error("planeConstructionWorld: \u0111\u1EC7 quy qu\xE1 s\xE2u \u2014 chu tr\xECnh tham chi\u1EBFu m\u1EB7t?");
480
+ _recDepth++;
481
+ try {
482
+ return planeConstructionWorldInner(c, state);
483
+ } finally {
484
+ _recDepth--;
485
+ }
486
+ }
487
+ function planeConstructionWorldInner(c, state) {
488
+ switch (c.kind) {
489
+ case "planeParallelThrough": {
490
+ const P = getPointWorld(c.point, state);
491
+ const ref = state.objects[c.refPlane];
492
+ if (!ref || ref.kind !== "plane3d") throw new Error("planeParallelThrough: m\u1EB7t tham chi\u1EBFu thi\u1EBFu");
493
+ const { basis1, basis2 } = getPlaneBasis(ref, state);
494
+ if (norm(basis1) < 1e-12 || norm(basis2) < 1e-12) {
495
+ return { p1: P, p2: add(P, [1, 0, 0]), p3: add(P, [0, 1, 0]) };
496
+ }
497
+ return { p1: P, p2: add(P, basis1), p3: add(P, basis2) };
498
+ }
499
+ case "planePerpToLine": {
500
+ const P = getPointWorld(c.point, state);
501
+ const dir = sub(getPointWorld(c.lineB, state), getPointWorld(c.lineA, state));
502
+ const dn = norm(dir);
503
+ if (dn < 1e-12) return { p1: P, p2: add(P, [1, 0, 0]), p3: add(P, [0, 1, 0]) };
504
+ const { basis1, basis2 } = orthoBasisFromNormal(scale(dir, 1 / dn));
505
+ return { p1: P, p2: add(P, basis1), p3: add(P, basis2) };
506
+ }
507
+ default: {
508
+ throw new Error("planeConstructionWorld: kind kh\xF4ng h\u1ED7 tr\u1EE3");
509
+ }
510
+ }
511
+ }
512
+ function lineDefiningPointIds(line) {
513
+ if (line.kind === "line3d" || line.kind === "segment3d") {
514
+ const a = line.attrs;
515
+ return a.p1 && a.p2 ? [a.p1, a.p2] : null;
516
+ }
517
+ if (line.kind === "ray3d") {
518
+ const a = line.attrs;
519
+ return [a.origin, a.through];
520
+ }
521
+ if (line.kind === "vector3d") {
522
+ const a = line.attrs;
523
+ return [a.from, a.to];
524
+ }
525
+ return null;
526
+ }
527
+ function lineEndpointsWorld(lineId, state, ctx) {
528
+ const line = state.objects[lineId];
529
+ if (!line) throw new Error(`${ctx}: \u0111\u01B0\u1EDDng ${lineId} kh\xF4ng t\u1ED3n t\u1EA1i`);
530
+ const ids = lineDefiningPointIds(line);
531
+ if (!ids) throw new Error(`${ctx}: kind ${line.kind} kh\xF4ng ph\u1EA3i \u0111\u01B0\u1EDDng`);
532
+ return { a: getPointWorld(ids[0], state), b: getPointWorld(ids[1], state) };
533
+ }
534
+ function lineLineClosestMidpoint(A, B, C, D) {
535
+ const u = sub(B, A), v = sub(D, C), w0 = sub(A, C);
536
+ const a = dot(u, u), b = dot(u, v), cc = dot(v, v), d = dot(u, w0), e = dot(v, w0);
537
+ const denom = a * cc - b * b;
538
+ if (Math.abs(denom) < 1e-12) return scale(add(A, C), 0.5);
539
+ const sc = (b * e - cc * d) / denom;
540
+ const tc = (a * e - b * d) / denom;
541
+ return scale(add(add(A, scale(u, sc)), add(C, scale(v, tc))), 0.5);
542
+ }
543
+ function planeOriginNormal(planeId, state, ctx) {
544
+ const plane = state.objects[planeId];
545
+ if (!plane || plane.kind !== "plane3d") throw new Error(`${ctx}: m\u1EB7t ph\u1EB3ng ${planeId} thi\u1EBFu`);
546
+ const { origin, normal } = getPlaneBasis(plane, state);
547
+ return { origin, normal };
548
+ }
549
+ function lineConstructionWorld(c, state) {
550
+ switch (c.kind) {
551
+ case "planePlaneIntersection": {
552
+ const P1 = planeOriginNormal(c.plane1, state, "planePlaneIntersection");
553
+ const P2 = planeOriginNormal(c.plane2, state, "planePlaneIntersection");
554
+ const d = cross(P1.normal, P2.normal);
555
+ const dd = dot(d, d);
556
+ if (dd < 1e-12) {
557
+ return { a: P1.origin, b: add(P1.origin, P2.normal) };
558
+ }
559
+ const c1 = dot(P1.normal, P1.origin);
560
+ const c2 = dot(P2.normal, P2.origin);
561
+ const p0 = scale(add(scale(cross(P2.normal, d), c1), scale(cross(d, P1.normal), c2)), 1 / dd);
562
+ return { a: p0, b: add(p0, d) };
563
+ }
564
+ case "lineParallelThrough": {
565
+ const P = getPointWorld(c.point, state);
566
+ const dir = sub(getPointWorld(c.dirB, state), getPointWorld(c.dirA, state));
567
+ if (norm(dir) < 1e-12) return { a: P, b: add(P, [1, 0, 0]) };
568
+ return { a: P, b: add(P, dir) };
569
+ }
570
+ case "linePerpToPlane": {
571
+ const P = getPointWorld(c.point, state);
572
+ const { normal } = planeOriginNormal(c.plane, state, "linePerpToPlane");
573
+ return { a: P, b: add(P, normal) };
574
+ }
575
+ default: {
576
+ throw new Error("lineConstructionWorld: kind kh\xF4ng h\u1ED7 tr\u1EE3");
577
+ }
578
+ }
579
+ }
580
+ function solve3(M, b) {
581
+ const det3 = (m) => m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
582
+ const det = det3(M);
583
+ if (Math.abs(det) < 1e-9) return null;
584
+ const colDet = (j) => {
585
+ const A = M.map((r) => r.slice());
586
+ for (let i = 0; i < 3; i++) A[i][j] = b[i];
587
+ return det3(A);
588
+ };
589
+ return [colDet(0) / det, colDet(1) / det, colDet(2) / det];
590
+ }
591
+ function constraintToWorld(c, state) {
592
+ if (_recDepth >= MAX_REC_DEPTH) throw new Error("constraintToWorld: \u0111\u1EC7 quy qu\xE1 s\xE2u \u2014 chu tr\xECnh tham chi\u1EBFu?");
593
+ _recDepth++;
594
+ try {
595
+ return constraintToWorldInner(c, state);
596
+ } finally {
597
+ _recDepth--;
598
+ }
599
+ }
600
+ function constraintToWorldInner(c, state) {
601
+ switch (c.kind) {
602
+ case "free":
603
+ return [c.x, c.y, c.z];
604
+ case "onGround":
605
+ return [c.x, c.y, 0];
606
+ case "onAxis": {
607
+ if (c.axis === "x") return [c.t, 0, 0];
608
+ if (c.axis === "y") return [0, c.t, 0];
609
+ return [0, 0, c.t];
610
+ }
611
+ case "onPlane": {
612
+ const plane = state.objects[c.planeId];
613
+ if (!plane || plane.kind !== "plane3d") throw new Error("onPlane: plane missing");
614
+ const { origin, basis1, basis2 } = getPlaneBasis(plane, state);
615
+ return add(add(origin, scale(basis1, c.u)), scale(basis2, c.v));
616
+ }
617
+ case "onLine": {
618
+ const { a, b } = lineEndpointsWorld(c.lineId, state, "onLine");
619
+ return add(a, scale(sub(b, a), c.t));
620
+ }
621
+ case "onPolygon": {
622
+ const pg = state.objects[c.polygonId];
623
+ if (!pg || pg.kind !== "polygon3d") throw new Error("onPolygon: parent missing");
624
+ const v = pg.attrs.vertices;
625
+ if (v.length < 3) throw new Error("onPolygon: < 3 vertices");
626
+ const p1 = getPointWorld(v[0], state);
627
+ const p2 = getPointWorld(v[1], state);
628
+ const p3 = getPointWorld(v[2], state);
629
+ const basis1 = sub(p2, p1);
630
+ const tmp = sub(p3, p1);
631
+ const normal = normalize(cross(basis1, tmp));
632
+ const basis2 = cross(normal, basis1);
633
+ return add(add(p1, scale(basis1, c.u)), scale(basis2, c.v));
634
+ }
635
+ case "onSphere": {
636
+ const sph = state.objects[c.sphereId];
637
+ if (!sph || sph.kind !== "sphere3d") throw new Error("onSphere: parent missing");
638
+ const a = sph.attrs;
639
+ const center = getPointWorld(a.center, state);
640
+ const surface = getPointWorld(a.surfacePoint, state);
641
+ const radius = norm(sub(surface, center));
642
+ const x = center[0] + radius * Math.sin(c.phi) * Math.cos(c.theta);
643
+ const y = center[1] + radius * Math.sin(c.phi) * Math.sin(c.theta);
644
+ const z = center[2] + radius * Math.cos(c.phi);
645
+ return [x, y, z];
646
+ }
647
+ // ───── Điểm phái sinh (v1) ─────
648
+ case "midpoint": {
649
+ const p1 = getPointWorld(c.p1, state);
650
+ const p2 = getPointWorld(c.p2, state);
651
+ return scale(add(p1, p2), 0.5);
652
+ }
653
+ case "centroid": {
654
+ const n = c.vertices.length;
655
+ if (n === 0) return [0, 0, 0];
656
+ let acc = [0, 0, 0];
657
+ for (const id of c.vertices) acc = add(acc, getPointWorld(id, state));
658
+ return scale(acc, 1 / n);
659
+ }
660
+ case "circumsphereCenter": {
661
+ const P = c.vertices.map((id) => getPointWorld(id, state));
662
+ if (P.length < 4) return P.length ? P[0] : [0, 0, 0];
663
+ const p0 = P[0];
664
+ const M = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
665
+ const rhs = [0, 0, 0];
666
+ for (let i = 1; i < P.length; i++) {
667
+ const r = [2 * (P[i][0] - p0[0]), 2 * (P[i][1] - p0[1]), 2 * (P[i][2] - p0[2])];
668
+ const bi = dot(P[i], P[i]) - dot(p0, p0);
669
+ for (let a = 0; a < 3; a++) {
670
+ for (let bcol = 0; bcol < 3; bcol++) M[a][bcol] += r[a] * r[bcol];
671
+ rhs[a] += r[a] * bi;
672
+ }
673
+ }
674
+ const sol = solve3(M, rhs);
675
+ if (sol) return sol;
676
+ let acc = [0, 0, 0];
677
+ for (const p of P) acc = add(acc, p);
678
+ return scale(acc, 1 / P.length);
679
+ }
680
+ case "pyramidInsphereCenter": {
681
+ const S = getPointWorld(c.apex, state);
682
+ const base = c.vertices.map((id) => getPointWorld(id, state));
683
+ const n = base.length;
684
+ if (n < 3) return S;
685
+ let G = [0, 0, 0];
686
+ for (const p of base) G = add(G, p);
687
+ G = scale(G, 1 / n);
688
+ let nb = normalize(cross(sub(base[1], base[0]), sub(base[2], base[0])));
689
+ if (dot(nb, sub(S, G)) < 0) nb = scale(nb, -1);
690
+ let m = normalize(cross(sub(base[0], S), sub(base[1], S)));
691
+ if (dot(m, sub(G, base[0])) < 0) m = scale(m, -1);
692
+ const denom = 1 - dot(m, nb);
693
+ const s = Math.abs(denom) < 1e-9 ? NaN : dot(m, sub(G, base[0])) / denom;
694
+ if (!Number.isFinite(s) || s <= 0) {
695
+ return scale(add(S, scale(G, n)), 1 / (n + 1));
696
+ }
697
+ return add(G, scale(nb, s));
698
+ }
699
+ case "faceCircumcenter": {
700
+ const P = c.vertices.map((id) => getPointWorld(id, state));
701
+ if (P.length < 3) return P.length ? P[0] : [0, 0, 0];
702
+ const [p0, p1, p2] = P;
703
+ const e1 = sub(p1, p0), e2 = sub(p2, p0);
704
+ const nrm = cross(e1, e2);
705
+ const M = [
706
+ [2 * e1[0], 2 * e1[1], 2 * e1[2]],
707
+ [2 * e2[0], 2 * e2[1], 2 * e2[2]],
708
+ [nrm[0], nrm[1], nrm[2]]
709
+ ];
710
+ const rhs = [dot(p1, p1) - dot(p0, p0), dot(p2, p2) - dot(p0, p0), dot(nrm, p0)];
711
+ const sol = solve3(M, rhs);
712
+ if (sol) return sol;
713
+ return scale(add(add(p0, p1), p2), 1 / 3);
714
+ }
715
+ case "pointAboveFace": {
716
+ const G = getPointWorld(c.base, state);
717
+ const P = c.vertices.map((id) => getPointWorld(id, state));
718
+ if (P.length < 3) return G;
719
+ let n = normalize(cross(sub(P[1], P[0]), sub(P[2], P[0])));
720
+ const S = getPointWorld(c.apex, state);
721
+ if (dot(n, sub(S, P[0])) < 0) n = scale(n, -1);
722
+ const h = dot(sub(S, P[0]), n);
723
+ if (!Number.isFinite(h) || h <= 1e-9) return G;
724
+ return add(G, scale(n, h));
725
+ }
726
+ case "intersectionLines": {
727
+ const A = getPointWorld(c.a1, state), B = getPointWorld(c.b1, state);
728
+ const C = getPointWorld(c.a2, state), D = getPointWorld(c.b2, state);
729
+ return lineLineClosestMidpoint(A, B, C, D);
730
+ }
731
+ case "intersectionLinePlane": {
732
+ const A = getPointWorld(c.a, state), B = getPointWorld(c.b, state);
733
+ const plane = state.objects[c.plane];
734
+ if (!plane || plane.kind !== "plane3d") throw new Error("intersectionLinePlane: m\u1EB7t ph\u1EB3ng thi\u1EBFu");
735
+ const { origin, normal } = getPlaneBasis(plane, state);
736
+ const dir = sub(B, A);
737
+ const dn = dot(dir, normal);
738
+ if (Math.abs(dn) < 1e-12) return A;
739
+ const t = dot(sub(origin, A), normal) / dn;
740
+ return add(A, scale(dir, t));
741
+ }
742
+ case "perpFootLine": {
743
+ const P = getPointWorld(c.from, state);
744
+ const A = getPointWorld(c.a, state), B = getPointWorld(c.b, state);
745
+ const u = sub(B, A);
746
+ const uu = dot(u, u);
747
+ const t = uu === 0 ? 0 : dot(sub(P, A), u) / uu;
748
+ return add(A, scale(u, t));
749
+ }
750
+ case "perpFootPlane": {
751
+ const P = getPointWorld(c.from, state);
752
+ const plane = state.objects[c.plane];
753
+ if (!plane || plane.kind !== "plane3d") throw new Error("perpFootPlane: m\u1EB7t ph\u1EB3ng thi\u1EBFu");
754
+ const { origin, normal } = getPlaneBasis(plane, state);
755
+ const dist2 = dot(sub(P, origin), normal);
756
+ return sub(P, scale(normal, dist2));
757
+ }
758
+ }
759
+ }
760
+ var MAX_REC_DEPTH, _recDepth;
761
+ var init_constraint3d_math = __esm({
762
+ "src/core/scene/kinds/constraint3d-math.ts"() {
763
+ MAX_REC_DEPTH = 512;
764
+ _recDepth = 0;
765
+ }
766
+ });
767
+
394
768
  // src/core/scene/kinds/point3d.ts
395
769
  var def;
396
770
  var init_point3d = __esm({
397
771
  "src/core/scene/kinds/point3d.ts"() {
398
772
  init_registry();
399
773
  init_d_constraint();
774
+ init_constraint3d_math();
400
775
  def = {
401
776
  type: "point3d",
402
777
  schemaVersion: 1,
@@ -434,6 +809,15 @@ var init_point3d = __esm({
434
809
  if (c.kind === "onLine") return `${obj.label} tr\xEAn \u0111\u01B0\u1EDDng ${c.lineId}`;
435
810
  if (c.kind === "onPolygon") return `${obj.label} tr\xEAn \u0111a gi\xE1c ${c.polygonId}`;
436
811
  if (c.kind === "onSphere") return `${obj.label} tr\xEAn m\u1EB7t c\u1EA7u ${c.sphereId}`;
812
+ if (c.kind === "midpoint") return `${obj.label} = trung \u0111i\u1EC3m ${c.p1}${c.p2}`;
813
+ if (c.kind === "centroid") return `${obj.label} = tr\u1ECDng t\xE2m ${c.vertices.join("")}`;
814
+ if (c.kind === "intersectionLines") return `${obj.label} = giao 2 \u0111\u01B0\u1EDDng (${c.a1}${c.b1}, ${c.a2}${c.b2})`;
815
+ if (c.kind === "intersectionLinePlane") return `${obj.label} = giao ${c.a}${c.b} \u2229 ${c.plane}`;
816
+ if (c.kind === "perpFootLine") return `${obj.label} = ch\xE2n \u22A5 t\u1EEB ${c.from} xu\u1ED1ng ${c.a}${c.b}`;
817
+ if (c.kind === "perpFootPlane") return `${obj.label} = ch\xE2n \u22A5 t\u1EEB ${c.from} xu\u1ED1ng ${c.plane}`;
818
+ if (c.kind === "pyramidInsphereCenter") return `${obj.label} = t\xE2m c\u1EA7u n\u1ED9i ti\u1EBFp ch\xF3p ${c.apex}.${c.vertices.join("")}`;
819
+ if (c.kind === "faceCircumcenter") return `${obj.label} = t\xE2m ngo\u1EA1i ti\u1EBFp ${c.vertices.join("")}`;
820
+ if (c.kind === "pointAboveFace") return `${obj.label} = \u0111\u1EC9nh tr\u1EE5c tr\u1EE5 \u22A5 m\u1EB7t ${c.vertices.join("")}`;
437
821
  return obj.label;
438
822
  },
439
823
  render: (obj, ctx) => {
@@ -483,6 +867,15 @@ var init_point3d = __esm({
483
867
  () => sph.F(c.theta, c.phi)[2]
484
868
  ], opts);
485
869
  }
870
+ const getState = ctx.getState;
871
+ if (getState) {
872
+ const cw = () => constraintToWorld(c, getState());
873
+ return view.create("point3d", [
874
+ () => cw()[0],
875
+ () => cw()[1],
876
+ () => cw()[2]
877
+ ], { ...opts, needsRegularUpdate: true });
878
+ }
486
879
  return view.create("point3d", [0, 0, 0], opts);
487
880
  }
488
881
  };
@@ -544,32 +937,77 @@ var init_segment3d = __esm({
544
937
  });
545
938
 
546
939
  // src/core/scene/kinds/line3d.ts
940
+ function line3dConstructionRefs(c) {
941
+ switch (c.kind) {
942
+ case "planePlaneIntersection":
943
+ return [c.plane1, c.plane2];
944
+ case "lineParallelThrough":
945
+ return [c.point, c.dirA, c.dirB];
946
+ case "linePerpToPlane":
947
+ return [c.point, c.plane];
948
+ default: {
949
+ return [];
950
+ }
951
+ }
952
+ }
953
+ var def3;
547
954
  var init_line3d = __esm({
548
955
  "src/core/scene/kinds/line3d.ts"() {
549
956
  init_registry();
550
957
  init_labelOf();
551
- registerKind({
958
+ init_constraint3d_math();
959
+ def3 = {
552
960
  type: "line3d",
553
961
  schemaVersion: 1,
554
962
  migrate: {},
555
963
  validate: (a) => {
556
- if (!a?.p1 || !a?.p2) throw new Error("line3d: p1/p2 required");
964
+ if (a?.construction) return;
965
+ if (!a?.p1 || !a?.p2) throw new Error("line3d: p1/p2 required (ho\u1EB7c construction)");
966
+ },
967
+ dependsOn: (a) => a.construction ? line3dConstructionRefs(a.construction) : [a.p1, a.p2],
968
+ describe: (obj, state) => {
969
+ const c = obj.attrs.construction;
970
+ if (!c) return `\u0110\u01B0\u1EDDng ${obj.label} qua ${labelOf(obj.attrs.p1, state)}, ${labelOf(obj.attrs.p2, state)}`;
971
+ switch (c.kind) {
972
+ case "planePlaneIntersection":
973
+ return `Giao tuy\u1EBFn ${obj.label} = ${labelOf(c.plane1, state)} \u2229 ${labelOf(c.plane2, state)}`;
974
+ case "lineParallelThrough":
975
+ return `${obj.label} \u2225 ${labelOf(c.dirA, state)}${labelOf(c.dirB, state)} qua ${labelOf(c.point, state)}`;
976
+ case "linePerpToPlane":
977
+ return `${obj.label} \u22A5 ${labelOf(c.plane, state)} qua ${labelOf(c.point, state)}`;
978
+ default:
979
+ return obj.label;
980
+ }
557
981
  },
558
- dependsOn: (a) => [a.p1, a.p2],
559
- describe: (obj, state) => `\u0110\u01B0\u1EDDng ${obj.label} qua ${labelOf(obj.attrs.p1, state)}, ${labelOf(obj.attrs.p2, state)}`,
560
982
  render: (obj, ctx) => {
561
983
  const view = ctx.jxg;
984
+ const baseOpts = {
985
+ strokeColor: obj.attrs.color ?? "#0f172a",
986
+ strokeWidth: 2,
987
+ visible: obj.visible
988
+ };
989
+ const c = obj.attrs.construction;
990
+ if (c) {
991
+ const getState = ctx.getState;
992
+ if (!getState) throw new Error("line3d construction: c\u1EA7n ctx.getState");
993
+ const lc = () => lineConstructionWorld(c, getState());
994
+ return view.create("line3d", [() => lc().a, () => lc().b], {
995
+ ...baseOpts,
996
+ straightFirst: true,
997
+ straightLast: true,
998
+ needsRegularUpdate: true
999
+ });
1000
+ }
562
1001
  const pA = ctx.resolveRef(obj.attrs.p1);
563
1002
  const pB = ctx.resolveRef(obj.attrs.p2);
564
1003
  return view.create("line3d", [pA, pB], {
1004
+ ...baseOpts,
565
1005
  straightFirst: true,
566
- straightLast: true,
567
- strokeColor: obj.attrs.color ?? "#0f172a",
568
- strokeWidth: 2,
569
- visible: obj.visible
1006
+ straightLast: true
570
1007
  });
571
1008
  }
572
- });
1009
+ };
1010
+ registerKind(def3);
573
1011
  }
574
1012
  });
575
1013
 
@@ -635,33 +1073,77 @@ var init_vector3d = __esm({
635
1073
  });
636
1074
 
637
1075
  // src/core/scene/kinds/plane3d.ts
1076
+ function plane3dConstructionRefs(c) {
1077
+ switch (c.kind) {
1078
+ case "planeParallelThrough":
1079
+ return [c.point, c.refPlane];
1080
+ case "planePerpToLine":
1081
+ return [c.point, c.lineA, c.lineB];
1082
+ default: {
1083
+ return [];
1084
+ }
1085
+ }
1086
+ }
1087
+ var def4;
638
1088
  var init_plane3d = __esm({
639
1089
  "src/core/scene/kinds/plane3d.ts"() {
640
1090
  init_registry();
641
1091
  init_labelOf();
642
- registerKind({
1092
+ init_constraint3d_math();
1093
+ def4 = {
643
1094
  type: "plane3d",
644
1095
  schemaVersion: 1,
645
1096
  migrate: {},
646
1097
  validate: (a) => {
647
- if (!a?.p1 || !a?.p2 || !a?.p3) throw new Error("plane3d: c\u1EA7n 3 \u0111i\u1EC3m");
1098
+ if (a?.construction) return;
1099
+ if (!a?.p1 || !a?.p2 || !a?.p3) throw new Error("plane3d: c\u1EA7n 3 \u0111i\u1EC3m (ho\u1EB7c construction)");
1100
+ },
1101
+ dependsOn: (a) => a.construction ? plane3dConstructionRefs(a.construction) : [a.p1, a.p2, a.p3],
1102
+ describe: (obj, state) => {
1103
+ const c = obj.attrs.construction;
1104
+ if (!c) {
1105
+ return `M\u1EB7t ${obj.label} qua ${labelOf(obj.attrs.p1, state)}, ${labelOf(obj.attrs.p2, state)}, ${labelOf(obj.attrs.p3, state)}`;
1106
+ }
1107
+ switch (c.kind) {
1108
+ case "planeParallelThrough":
1109
+ return `${obj.label} \u2225 ${labelOf(c.refPlane, state)} qua ${labelOf(c.point, state)}`;
1110
+ case "planePerpToLine":
1111
+ return `${obj.label} \u22A5 ${labelOf(c.lineA, state)}${labelOf(c.lineB, state)} qua ${labelOf(c.point, state)}`;
1112
+ default:
1113
+ return obj.label;
1114
+ }
648
1115
  },
649
- dependsOn: (a) => [a.p1, a.p2, a.p3],
650
- describe: (obj, state) => `M\u1EB7t ${obj.label} qua ${labelOf(obj.attrs.p1, state)}, ${labelOf(obj.attrs.p2, state)}, ${labelOf(obj.attrs.p3, state)}`,
651
1116
  render: (obj, ctx) => {
652
1117
  const view = ctx.jxg;
653
- return view.create("plane3d", [
654
- ctx.resolveRef(obj.attrs.p1),
655
- ctx.resolveRef(obj.attrs.p2),
656
- ctx.resolveRef(obj.attrs.p3)
657
- ], {
1118
+ const opts = {
658
1119
  fillOpacity: 0.15,
659
1120
  fillColor: obj.attrs.color ?? "#60a5fa",
660
1121
  strokeColor: obj.attrs.color ?? "#60a5fa",
661
1122
  visible: obj.visible
662
- });
1123
+ };
1124
+ const c = obj.attrs.construction;
1125
+ if (c) {
1126
+ const getState = ctx.getState;
1127
+ if (!getState) throw new Error("plane3d construction: c\u1EA7n ctx.getState");
1128
+ const pc = () => planeConstructionWorld(c, getState());
1129
+ const diff = (which) => {
1130
+ const r = pc();
1131
+ const q = r[which];
1132
+ return [q[0] - r.p1[0], q[1] - r.p1[1], q[2] - r.p1[2]];
1133
+ };
1134
+ return view.create("plane3d", [() => pc().p1, () => diff("p2"), () => diff("p3")], {
1135
+ ...opts,
1136
+ needsRegularUpdate: true
1137
+ });
1138
+ }
1139
+ return view.create("plane3d", [
1140
+ ctx.resolveRef(obj.attrs.p1),
1141
+ ctx.resolveRef(obj.attrs.p2),
1142
+ ctx.resolveRef(obj.attrs.p3)
1143
+ ], opts);
663
1144
  }
664
- });
1145
+ };
1146
+ registerKind(def4);
665
1147
  }
666
1148
  });
667
1149
 
@@ -760,11 +1242,37 @@ var init_polyhedron3d = __esm({
760
1242
  }
761
1243
  });
762
1244
 
1245
+ // src/core/scene/kinds/_ringBasis.ts
1246
+ function perpBasis(axis) {
1247
+ const n = Math.hypot(axis[0], axis[1], axis[2]);
1248
+ if (n < 1e-12) return [[1, 0, 0], [0, 1, 0]];
1249
+ const a = [axis[0] / n, axis[1] / n, axis[2] / n];
1250
+ const seed = Math.abs(a[2]) < 0.9 ? [0, 0, 1] : [1, 0, 0];
1251
+ let u = [
1252
+ seed[1] * a[2] - seed[2] * a[1],
1253
+ seed[2] * a[0] - seed[0] * a[2],
1254
+ seed[0] * a[1] - seed[1] * a[0]
1255
+ ];
1256
+ const un = Math.hypot(u[0], u[1], u[2]) || 1;
1257
+ u = [u[0] / un, u[1] / un, u[2] / un];
1258
+ const v = [
1259
+ a[1] * u[2] - a[2] * u[1],
1260
+ a[2] * u[0] - a[0] * u[2],
1261
+ a[0] * u[1] - a[1] * u[0]
1262
+ ];
1263
+ return [u, v];
1264
+ }
1265
+ var init_ringBasis = __esm({
1266
+ "src/core/scene/kinds/_ringBasis.ts"() {
1267
+ }
1268
+ });
1269
+
763
1270
  // src/core/scene/kinds/cylinder3d.ts
764
1271
  var CURVED_SEGMENTS;
765
1272
  var init_cylinder3d = __esm({
766
1273
  "src/core/scene/kinds/cylinder3d.ts"() {
767
1274
  init_registry();
1275
+ init_ringBasis();
768
1276
  CURVED_SEGMENTS = 16;
769
1277
  registerKind({
770
1278
  type: "cylinder3d",
@@ -783,14 +1291,17 @@ var init_cylinder3d = __esm({
783
1291
  const r = obj.attrs.radius;
784
1292
  const ax = a.X?.() ?? 0, ay = a.Y?.() ?? 0, az = a.Z?.() ?? 0;
785
1293
  const bx = b.X?.() ?? 0, by = b.Y?.() ?? 0, bz = b.Z?.() ?? 0;
1294
+ const [u, v] = perpBasis([bx - ax, by - ay, bz - az]);
786
1295
  const baseRing = [];
787
1296
  const topRing = [];
788
1297
  for (let i = 0; i < CURVED_SEGMENTS; i++) {
789
1298
  const theta = i / CURVED_SEGMENTS * Math.PI * 2;
790
- const dx = r * Math.cos(theta);
791
- const dy = r * Math.sin(theta);
792
- baseRing.push([ax + dx, ay + dy, az]);
793
- topRing.push([bx + dx, by + dy, bz]);
1299
+ const c = Math.cos(theta), s = Math.sin(theta);
1300
+ const ox = r * (c * u[0] + s * v[0]);
1301
+ const oy = r * (c * u[1] + s * v[1]);
1302
+ const oz = r * (c * u[2] + s * v[2]);
1303
+ baseRing.push([ax + ox, ay + oy, az + oz]);
1304
+ topRing.push([bx + ox, by + oy, bz + oz]);
794
1305
  }
795
1306
  const vertices = [...baseRing, ...topRing];
796
1307
  const faces = [];
@@ -801,7 +1312,7 @@ var init_cylinder3d = __esm({
801
1312
  faces.push([i, next, CURVED_SEGMENTS + next, CURVED_SEGMENTS + i]);
802
1313
  }
803
1314
  const vertJxgs = vertices.map(
804
- (v, i) => view.create("point3d", v, {
1315
+ (v2, i) => view.create("point3d", v2, {
805
1316
  id: `${obj.id}.v${i}`,
806
1317
  visible: false,
807
1318
  fixed: true,
@@ -829,6 +1340,7 @@ var CURVED_SEGMENTS2;
829
1340
  var init_cone3d = __esm({
830
1341
  "src/core/scene/kinds/cone3d.ts"() {
831
1342
  init_registry();
1343
+ init_ringBasis();
832
1344
  CURVED_SEGMENTS2 = 16;
833
1345
  registerKind({
834
1346
  type: "cone3d",
@@ -851,10 +1363,16 @@ var init_cone3d = __esm({
851
1363
  apexPt.Y?.() ?? 0,
852
1364
  apexPt.Z?.() ?? 0
853
1365
  ];
1366
+ const [u, v] = perpBasis([apexCoords[0] - bx, apexCoords[1] - by, apexCoords[2] - bz]);
854
1367
  const baseRing = [];
855
1368
  for (let i = 0; i < CURVED_SEGMENTS2; i++) {
856
1369
  const theta = i / CURVED_SEGMENTS2 * Math.PI * 2;
857
- baseRing.push([bx + r * Math.cos(theta), by + r * Math.sin(theta), bz]);
1370
+ const c = Math.cos(theta), s = Math.sin(theta);
1371
+ baseRing.push([
1372
+ bx + r * (c * u[0] + s * v[0]),
1373
+ by + r * (c * u[1] + s * v[1]),
1374
+ bz + r * (c * u[2] + s * v[2])
1375
+ ]);
858
1376
  }
859
1377
  const apexIdx = baseRing.length;
860
1378
  const vertices = [...baseRing, apexCoords];
@@ -863,7 +1381,7 @@ var init_cone3d = __esm({
863
1381
  faces.push([i, (i + 1) % CURVED_SEGMENTS2, apexIdx]);
864
1382
  }
865
1383
  const vertJxgs = vertices.map(
866
- (v, i) => view.create("point3d", v, {
1384
+ (v2, i) => view.create("point3d", v2, {
867
1385
  id: `${obj.id}.v${i}`,
868
1386
  visible: false,
869
1387
  fixed: true,
@@ -939,8 +1457,12 @@ function constraintRefs2D(c) {
939
1457
  return [c.line, c.circle, c.other];
940
1458
  case "tangencyPoint":
941
1459
  return [c.circle, c.onLine];
942
- case "arcMidpoint":
943
- return [c.circle, c.a, c.b, c.notContaining];
1460
+ case "arcMidpoint": {
1461
+ const containment = c.notContaining ?? c.containing;
1462
+ return containment ? [c.circle, c.a, c.b, containment] : [c.circle, c.a, c.b];
1463
+ }
1464
+ case "mixtilinearPoint":
1465
+ return [c.vertices[0], c.vertices[1], c.vertices[2]];
944
1466
  case "pointAtDistance": {
945
1467
  const d = c.distance;
946
1468
  const extra = d.kind === "circleRadius" ? [d.circle] : d.kind === "segmentLength" ? [d.p1, d.p2] : [];
@@ -948,8 +1470,16 @@ function constraintRefs2D(c) {
948
1470
  }
949
1471
  case "excenter":
950
1472
  return [c.vertices[0], c.vertices[1], c.vertices[2]];
951
- default:
1473
+ case "commonTangentPoint":
1474
+ return [c.circles[0], c.circles[1]];
1475
+ // Các kind KHÔNG có ref scene-object (toạ độ literal): liệt kê TƯỜNG MINH để
1476
+ // exhaustive never-guard bên dưới buộc khai báo khi thêm constraint kind mới.
1477
+ case "free":
1478
+ case "onAxis":
1479
+ return [];
1480
+ default: {
952
1481
  return [];
1482
+ }
953
1483
  }
954
1484
  }
955
1485
  var init_d_constraint2 = __esm({
@@ -1227,7 +1757,7 @@ function dist(p, q) {
1227
1757
  function sideOf(a, b, p) {
1228
1758
  return (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
1229
1759
  }
1230
- function arcMidpoint(center, radius, a, b, notContaining) {
1760
+ function arcMidpoint(center, radius, a, b, reference, sameSide = false) {
1231
1761
  const mcx = (a[0] + b[0]) / 2;
1232
1762
  const mcy = (a[1] + b[1]) / 2;
1233
1763
  let ux = mcx - center[0];
@@ -1242,12 +1772,18 @@ function arcMidpoint(center, radius, a, b, notContaining) {
1242
1772
  uy /= len;
1243
1773
  const cand1 = [center[0] + radius * ux, center[1] + radius * uy];
1244
1774
  const cand2 = [center[0] - radius * ux, center[1] - radius * uy];
1775
+ if (!reference) return cand1[1] >= cand2[1] ? cand1 : cand2;
1776
+ const notContaining = reference;
1245
1777
  const sN = sideOf(a, b, notContaining);
1246
1778
  if (Math.abs(sN) < 1e-9) {
1247
- return dist(cand1, notContaining) >= dist(cand2, notContaining) ? cand1 : cand2;
1779
+ const far = dist(cand1, notContaining) >= dist(cand2, notContaining) ? cand1 : cand2;
1780
+ const near = far === cand1 ? cand2 : cand1;
1781
+ return sameSide ? near : far;
1248
1782
  }
1249
1783
  const s1 = sideOf(a, b, cand1);
1250
- return s1 * sN < 0 ? cand1 : cand2;
1784
+ const opp = s1 * sN < 0 ? cand1 : cand2;
1785
+ const same = opp === cand1 ? cand2 : cand1;
1786
+ return sameSide ? same : opp;
1251
1787
  }
1252
1788
  function excenter(vertices, oppositeIndex) {
1253
1789
  const [A, B, C] = vertices;
@@ -1263,6 +1799,35 @@ function excenter(vertices, oppositeIndex) {
1263
1799
  (w[0] * A[1] + w[1] * B[1] + w[2] * C[1]) / sum
1264
1800
  ];
1265
1801
  }
1802
+ function circumcenterXY(a, b, c) {
1803
+ const ax = a[0], ay = a[1], bx = b[0], by = b[1], cx = c[0], cy = c[1];
1804
+ const d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
1805
+ if (Math.abs(d) < 1e-12) return [(ax + bx + cx) / 3, (ay + by + cy) / 3];
1806
+ const ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay) + (cx * cx + cy * cy) * (ay - by)) / d;
1807
+ const uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx) + (cx * cx + cy * cy) * (bx - ax)) / d;
1808
+ return [ux, uy];
1809
+ }
1810
+ function mixtilinearPoint(a, b, c, which) {
1811
+ const O = circumcenterXY(a, b, c);
1812
+ const R = Math.hypot(O[0] - a[0], O[1] - a[1]);
1813
+ const ux1 = (b[0] - a[0]) / (Math.hypot(b[0] - a[0], b[1] - a[1]) || 1);
1814
+ const uy1 = (b[1] - a[1]) / (Math.hypot(b[0] - a[0], b[1] - a[1]) || 1);
1815
+ const ux2 = (c[0] - a[0]) / (Math.hypot(c[0] - a[0], c[1] - a[1]) || 1);
1816
+ const uy2 = (c[1] - a[1]) / (Math.hypot(c[0] - a[0], c[1] - a[1]) || 1);
1817
+ let bx = ux1 + ux2, by = uy1 + uy2;
1818
+ const bl = Math.hypot(bx, by) || 1;
1819
+ bx /= bl;
1820
+ by /= bl;
1821
+ const cosA = ux1 * ux2 + uy1 * uy2;
1822
+ const sinHalf = Math.sqrt(Math.max(0, (1 - cosA) / 2));
1823
+ const cos2Half = Math.max(1e-9, (1 + cosA) / 2);
1824
+ const dotAO = bx * (O[0] - a[0]) + by * (O[1] - a[1]);
1825
+ const d = 2 * (dotAO - R * sinHalf) / cos2Half;
1826
+ const K = [a[0] + d * bx, a[1] + d * by];
1827
+ if (which === "center") return K;
1828
+ const kl = Math.hypot(K[0] - O[0], K[1] - O[1]) || 1;
1829
+ return [O[0] + R * (K[0] - O[0]) / kl, O[1] + R * (K[1] - O[1]) / kl];
1830
+ }
1266
1831
  function pointAtDistanceCoord(from, through, d) {
1267
1832
  const dx = through[0] - from[0];
1268
1833
  const dy = through[1] - from[1];
@@ -1290,29 +1855,38 @@ var init_arcMidpoint = __esm({
1290
1855
  arcMidpointConstraint = definePointConstraint({
1291
1856
  kind: "arcMidpoint",
1292
1857
  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");
1858
+ if (!c.circle || !c.a || !c.b) {
1859
+ throw new Error("point.arcMidpoint: circle, a, b b\u1EAFt bu\u1ED9c");
1860
+ }
1861
+ if (c.notContaining && c.containing) {
1862
+ throw new Error("point.arcMidpoint: kh\xF4ng th\u1EC3 v\u1EEBa notContaining v\u1EEBa containing");
1295
1863
  }
1296
1864
  },
1297
1865
  describe: (obj, state, c) => {
1298
1866
  const al = state?.objects[c.a]?.label ?? c.a;
1299
1867
  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})`;
1868
+ const ref = c.containing ?? c.notContaining;
1869
+ if (!ref) return `${obj.label} = trung \u0111i\u1EC3m cung ${al}${bl}`;
1870
+ const rl = state?.objects[ref]?.label ?? ref;
1871
+ const rel = c.containing ? "ch\u1EE9a" : "kh\xF4ng ch\u1EE9a";
1872
+ return `${obj.label} = trung \u0111i\u1EC3m cung ${al}${bl} (${rel} ${rl})`;
1302
1873
  },
1303
1874
  render: (obj, ctx, c, opts) => {
1304
1875
  const board = ctx.jxg;
1305
1876
  const circle = ctx.resolveRef(c.circle);
1306
1877
  const A = ctx.resolveRef(c.a);
1307
1878
  const B = ctx.resolveRef(c.b);
1308
- const N = ctx.resolveRef(c.notContaining);
1879
+ const refName = c.containing ?? c.notContaining;
1880
+ const ref = refName ? ctx.resolveRef(refName) : void 0;
1881
+ const sameSide = !!c.containing;
1309
1882
  const O = circle?.center ?? circle?.midpoint ?? circle;
1310
1883
  const am = () => arcMidpoint(
1311
1884
  [O.X(), O.Y()],
1312
1885
  circle.Radius(),
1313
1886
  [A.X(), A.Y()],
1314
1887
  [B.X(), B.Y()],
1315
- [N.X(), N.Y()]
1888
+ ref ? [ref.X(), ref.Y()] : void 0,
1889
+ sameSide
1316
1890
  );
1317
1891
  return board.create("point", [() => am()[0], () => am()[1]], opts);
1318
1892
  }
@@ -1362,6 +1936,59 @@ var init_excenter = __esm({
1362
1936
  }
1363
1937
  });
1364
1938
 
1939
+ // src/core/scene/kinds/point-constraints/mixtilinearPoint.ts
1940
+ var mixtilinearPointConstraint;
1941
+ var init_mixtilinearPoint = __esm({
1942
+ "src/core/scene/kinds/point-constraints/mixtilinearPoint.ts"() {
1943
+ init_pointConstructions();
1944
+ init_types2();
1945
+ mixtilinearPointConstraint = definePointConstraint({
1946
+ kind: "mixtilinearPoint",
1947
+ validate: (c) => {
1948
+ if (!Array.isArray(c.vertices) || c.vertices.length !== 3 || !c.vertices.every(Boolean)) {
1949
+ throw new Error("point.mixtilinearPoint: vertices ph\u1EA3i l\xE0 tuple 3 id non-empty");
1950
+ }
1951
+ if (c.which !== "center" && c.which !== "touch") {
1952
+ throw new Error("point.mixtilinearPoint: which ph\u1EA3i 'center' | 'touch'");
1953
+ }
1954
+ },
1955
+ describe: (obj, state, c) => {
1956
+ const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
1957
+ 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)`;
1958
+ },
1959
+ render: (obj, ctx, c, opts) => {
1960
+ const board = ctx.jxg;
1961
+ const a = ctx.resolveRef(c.vertices[0]);
1962
+ const b = ctx.resolveRef(c.vertices[1]);
1963
+ const d = ctx.resolveRef(c.vertices[2]);
1964
+ const mp = () => mixtilinearPoint(
1965
+ [a.X(), a.Y()],
1966
+ [b.X(), b.Y()],
1967
+ [d.X(), d.Y()],
1968
+ c.which
1969
+ );
1970
+ return board.create("point", [() => mp()[0], () => mp()[1]], opts);
1971
+ }
1972
+ });
1973
+ }
1974
+ });
1975
+
1976
+ // src/core/scene/kinds/_label.ts
1977
+ function labelOpts(labelOffset, dflt) {
1978
+ const offset = labelOffset ?? dflt;
1979
+ return { label: { fixed: false, ...offset ? { offset } : {} } };
1980
+ }
1981
+ function readLabelOffset(label) {
1982
+ const off = label.evalVisProp?.("offset") ?? label.visProp?.offset;
1983
+ const rel = label.relativeCoords?.scrCoords;
1984
+ if (!off || !rel || off.length < 2 || rel.length < 3) return null;
1985
+ return [Math.round(off[0] + rel[1]), Math.round(off[1] - rel[2])];
1986
+ }
1987
+ var init_label = __esm({
1988
+ "src/core/scene/kinds/_label.ts"() {
1989
+ }
1990
+ });
1991
+
1365
1992
  // src/core/scene/kinds/point-constraints/shared.ts
1366
1993
  function buildJxgTransforms(board, ctx, t) {
1367
1994
  switch (t.kind) {
@@ -1390,19 +2017,19 @@ function buildJxgTransforms(board, ctx, t) {
1390
2017
  }
1391
2018
  }
1392
2019
  function makeDistanceFn(ctx, d) {
1393
- const scale = d.scale ?? 1;
2020
+ const scale2 = d.scale ?? 1;
1394
2021
  const offset = d.offset ?? 0;
1395
2022
  if (d.kind === "literal") {
1396
2023
  const v = d.value;
1397
- return () => scale * v + offset;
2024
+ return () => scale2 * v + offset;
1398
2025
  }
1399
2026
  if (d.kind === "segmentLength") {
1400
2027
  const p = ctx.resolveRef(d.p1);
1401
2028
  const q = ctx.resolveRef(d.p2);
1402
- return () => scale * Math.hypot(p.X() - q.X(), p.Y() - q.Y()) + offset;
2029
+ return () => scale2 * Math.hypot(p.X() - q.X(), p.Y() - q.Y()) + offset;
1403
2030
  }
1404
2031
  const circle = ctx.resolveRef(d.circle);
1405
- return () => scale * circle.Radius() + offset;
2032
+ return () => scale2 * circle.Radius() + offset;
1406
2033
  }
1407
2034
  function buildPointOpts(obj) {
1408
2035
  return {
@@ -1413,11 +2040,13 @@ function buildPointOpts(obj) {
1413
2040
  strokeColor: obj.attrs.color ?? "#1e40af",
1414
2041
  fillColor: obj.attrs.color ?? "#1e40af",
1415
2042
  face: obj.attrs.face ?? "o",
1416
- size: obj.attrs.size ?? 4
2043
+ size: obj.attrs.size ?? 4,
2044
+ ...labelOpts(obj.attrs.labelOffset, [10, 10])
1417
2045
  };
1418
2046
  }
1419
2047
  var init_shared = __esm({
1420
2048
  "src/core/scene/kinds/point-constraints/shared.ts"() {
2049
+ init_label();
1421
2050
  }
1422
2051
  });
1423
2052
 
@@ -1449,6 +2078,73 @@ var init_pointAtDistance = __esm({
1449
2078
  }
1450
2079
  });
1451
2080
 
2081
+ // src/stamps/geometry-2d/geometry/commonTangent.ts
2082
+ function computeCommonTangentPoint(O1, r1, O2, r2, on, variant, side) {
2083
+ const dx = O2[0] - O1[0];
2084
+ const dy = O2[1] - O1[1];
2085
+ const d = Math.hypot(dx, dy);
2086
+ if (d < EPS) return null;
2087
+ const beta = Math.atan2(dy, dx);
2088
+ const ratio = variant === "external" ? (r1 - r2) / d : (r1 + r2) / d;
2089
+ if (Math.abs(ratio) > 1) return null;
2090
+ const gamma = Math.acos(ratio);
2091
+ const s = side === 0 ? 1 : -1;
2092
+ const angle = beta + s * gamma;
2093
+ const nx = Math.cos(angle);
2094
+ const ny = Math.sin(angle);
2095
+ if (on === 0) {
2096
+ return [O1[0] + r1 * nx, O1[1] + r1 * ny];
2097
+ }
2098
+ if (variant === "external") {
2099
+ return [O2[0] + r2 * nx, O2[1] + r2 * ny];
2100
+ }
2101
+ return [O2[0] - r2 * nx, O2[1] - r2 * ny];
2102
+ }
2103
+ var EPS;
2104
+ var init_commonTangent = __esm({
2105
+ "src/stamps/geometry-2d/geometry/commonTangent.ts"() {
2106
+ EPS = 1e-9;
2107
+ }
2108
+ });
2109
+
2110
+ // src/core/scene/kinds/point-constraints/commonTangentPoint.ts
2111
+ var commonTangentPointConstraint;
2112
+ var init_commonTangentPoint = __esm({
2113
+ "src/core/scene/kinds/point-constraints/commonTangentPoint.ts"() {
2114
+ init_commonTangent();
2115
+ init_types2();
2116
+ commonTangentPointConstraint = definePointConstraint({
2117
+ kind: "commonTangentPoint",
2118
+ describe: (obj, state, c) => {
2119
+ const c1 = state?.objects[c.circles[0]]?.label ?? c.circles[0];
2120
+ const c2 = state?.objects[c.circles[1]]?.label ?? c.circles[1];
2121
+ const v = c.variant === "internal" ? "trong" : "ngo\xE0i";
2122
+ return `${obj.label} = ti\u1EBFp \u0111i\u1EC3m ti\u1EBFp tuy\u1EBFn chung ${v} ${c1},${c2} (tr\xEAn ${c.on === 0 ? c1 : c2})`;
2123
+ },
2124
+ render: (obj, ctx, c, opts) => {
2125
+ const board = ctx.jxg;
2126
+ const k1 = ctx.resolveRef(c.circles[0]);
2127
+ const k2 = ctx.resolveRef(c.circles[1]);
2128
+ const O1 = k1?.center ?? k1?.midpoint ?? k1;
2129
+ const O2 = k2?.center ?? k2?.midpoint ?? k2;
2130
+ const compute = () => {
2131
+ const t = computeCommonTangentPoint(
2132
+ [O1.X(), O1.Y()],
2133
+ k1.Radius(),
2134
+ [O2.X(), O2.Y()],
2135
+ k2.Radius(),
2136
+ c.on,
2137
+ c.variant,
2138
+ c.side
2139
+ );
2140
+ return t ?? [0, 0];
2141
+ };
2142
+ return board.create("point", [() => compute()[0], () => compute()[1]], opts);
2143
+ }
2144
+ });
2145
+ }
2146
+ });
2147
+
1452
2148
  // src/core/scene/kinds/point-constraints/circleIntersection.ts
1453
2149
  var circleIntersectionConstraint;
1454
2150
  var init_circleIntersection = __esm({
@@ -1726,7 +2422,9 @@ var init_registry2 = __esm({
1726
2422
  init_centroid();
1727
2423
  init_arcMidpoint();
1728
2424
  init_excenter();
2425
+ init_mixtilinearPoint();
1729
2426
  init_pointAtDistance();
2427
+ init_commonTangentPoint();
1730
2428
  init_circleIntersection();
1731
2429
  init_circleSecondIntersection();
1732
2430
  init_secondIntersection();
@@ -1751,7 +2449,9 @@ var init_registry2 = __esm({
1751
2449
  centroidConstraint,
1752
2450
  arcMidpointConstraint,
1753
2451
  excenterConstraint,
2452
+ mixtilinearPointConstraint,
1754
2453
  pointAtDistanceConstraint,
2454
+ commonTangentPointConstraint,
1755
2455
  circleIntersectionConstraint,
1756
2456
  circleSecondIntersectionConstraint,
1757
2457
  secondIntersectionConstraint,
@@ -1768,14 +2468,15 @@ var init_registry2 = __esm({
1768
2468
  });
1769
2469
 
1770
2470
  // src/core/scene/kinds/point.ts
1771
- var def3;
2471
+ var def5;
1772
2472
  var init_point = __esm({
1773
2473
  "src/core/scene/kinds/point.ts"() {
1774
2474
  init_registry();
1775
2475
  init_d_constraint2();
1776
2476
  init_registry2();
1777
2477
  init_shared();
1778
- def3 = {
2478
+ init_label();
2479
+ def5 = {
1779
2480
  type: "point",
1780
2481
  schemaVersion: 1,
1781
2482
  migrate: {},
@@ -1842,7 +2543,8 @@ var init_point = __esm({
1842
2543
  strokeColor: obj.attrs.color ?? "#1e40af",
1843
2544
  fillColor: obj.attrs.color ?? "#1e40af",
1844
2545
  face: obj.attrs.face ?? "o",
1845
- size: obj.attrs.size ?? 4
2546
+ size: obj.attrs.size ?? 4,
2547
+ ...labelOpts(obj.attrs.labelOffset, [10, 10])
1846
2548
  });
1847
2549
  } catch {
1848
2550
  }
@@ -1852,17 +2554,18 @@ var init_point = __esm({
1852
2554
  throw new Error("point: constraint kind changed \u2014 recreate");
1853
2555
  }
1854
2556
  };
1855
- registerKind(def3);
2557
+ registerKind(def5);
1856
2558
  }
1857
2559
  });
1858
2560
 
1859
2561
  // src/core/scene/kinds/segment.ts
1860
- var def4;
2562
+ var def6;
1861
2563
  var init_segment = __esm({
1862
2564
  "src/core/scene/kinds/segment.ts"() {
1863
2565
  init_registry();
1864
2566
  init_labelOf();
1865
- def4 = {
2567
+ init_label();
2568
+ def6 = {
1866
2569
  type: "segment",
1867
2570
  schemaVersion: 1,
1868
2571
  migrate: {},
@@ -1893,11 +2596,12 @@ var init_segment = __esm({
1893
2596
  strokeWidth: obj.attrs.width ?? 2,
1894
2597
  dash: obj.attrs.dash ?? 0,
1895
2598
  visible: obj.visible,
1896
- fixed: obj.locked
2599
+ fixed: obj.locked,
2600
+ ...labelOpts(obj.attrs.labelOffset)
1897
2601
  });
1898
2602
  }
1899
2603
  };
1900
- registerKind(def4);
2604
+ registerKind(def6);
1901
2605
  }
1902
2606
  });
1903
2607
 
@@ -1925,13 +2629,14 @@ function constructionRefs(c) {
1925
2629
  return [c.throughPoint, c.toCircle];
1926
2630
  }
1927
2631
  }
1928
- var def5;
2632
+ var def7;
1929
2633
  var init_line = __esm({
1930
2634
  "src/core/scene/kinds/line.ts"() {
1931
2635
  init_registry();
1932
2636
  init_labelOf();
2637
+ init_label();
1933
2638
  init_pointConstructions();
1934
- def5 = {
2639
+ def7 = {
1935
2640
  type: "line",
1936
2641
  schemaVersion: 1,
1937
2642
  migrate: {},
@@ -1972,7 +2677,8 @@ var init_line = __esm({
1972
2677
  strokeWidth: obj.attrs.width ?? 2,
1973
2678
  dash: obj.attrs.dash ?? 0,
1974
2679
  visible: obj.visible,
1975
- fixed: obj.locked
2680
+ fixed: obj.locked,
2681
+ ...labelOpts(obj.attrs.labelOffset)
1976
2682
  };
1977
2683
  const c = obj.attrs.construction;
1978
2684
  if (!c) {
@@ -2126,17 +2832,17 @@ var init_line = __esm({
2126
2832
  }
2127
2833
  }
2128
2834
  };
2129
- registerKind(def5);
2835
+ registerKind(def7);
2130
2836
  }
2131
2837
  });
2132
2838
 
2133
2839
  // src/core/scene/kinds/ray.ts
2134
- var def6;
2840
+ var def8;
2135
2841
  var init_ray = __esm({
2136
2842
  "src/core/scene/kinds/ray.ts"() {
2137
2843
  init_registry();
2138
2844
  init_labelOf();
2139
- def6 = {
2845
+ def8 = {
2140
2846
  type: "ray",
2141
2847
  schemaVersion: 1,
2142
2848
  migrate: {},
@@ -2161,17 +2867,17 @@ var init_ray = __esm({
2161
2867
  });
2162
2868
  }
2163
2869
  };
2164
- registerKind(def6);
2870
+ registerKind(def8);
2165
2871
  }
2166
2872
  });
2167
2873
 
2168
2874
  // src/core/scene/kinds/vector.ts
2169
- var def7;
2875
+ var def9;
2170
2876
  var init_vector = __esm({
2171
2877
  "src/core/scene/kinds/vector.ts"() {
2172
2878
  init_registry();
2173
2879
  init_labelOf();
2174
- def7 = {
2880
+ def9 = {
2175
2881
  type: "vector",
2176
2882
  schemaVersion: 1,
2177
2883
  migrate: {},
@@ -2193,7 +2899,7 @@ var init_vector = __esm({
2193
2899
  });
2194
2900
  }
2195
2901
  };
2196
- registerKind(def7);
2902
+ registerKind(def9);
2197
2903
  }
2198
2904
  });
2199
2905
 
@@ -2223,13 +2929,14 @@ function constructionRefs2(c) {
2223
2929
  return [c.p1, c.p2];
2224
2930
  }
2225
2931
  }
2226
- var def8;
2932
+ var def10;
2227
2933
  var init_circle = __esm({
2228
2934
  "src/core/scene/kinds/circle.ts"() {
2229
2935
  init_registry();
2230
2936
  init_labelOf();
2937
+ init_label();
2231
2938
  init_pointConstructions();
2232
- def8 = {
2939
+ def10 = {
2233
2940
  type: "circle",
2234
2941
  schemaVersion: 1,
2235
2942
  migrate: {},
@@ -2289,15 +2996,17 @@ var init_circle = __esm({
2289
2996
  const board = ctx.jxg;
2290
2997
  const isCenterLabel = (l) => /^[A-Z]['′]?\d*$/u.test(l);
2291
2998
  const isCenter = isCenterLabel(obj.label);
2999
+ const isRenamedCircle = /_c$/.test(obj.label);
2292
3000
  const baseOpts = {
2293
3001
  name: obj.label,
2294
- withLabel: isCenter ? obj.attrs.showLabel ?? false : true,
3002
+ withLabel: isCenter ? obj.attrs.showLabel ?? false : isRenamedCircle ? false : true,
2295
3003
  strokeColor: obj.attrs.color ?? "#0f172a",
2296
3004
  strokeWidth: obj.attrs.width ?? 2,
2297
3005
  dash: obj.attrs.dash ?? 0,
2298
3006
  fillColor: "none",
2299
3007
  visible: obj.visible,
2300
- fixed: obj.locked
3008
+ fixed: obj.locked,
3009
+ ...labelOpts(obj.attrs.labelOffset)
2301
3010
  };
2302
3011
  const c = asConstruction(obj.attrs);
2303
3012
  if (c?.kind === "circumscribed") {
@@ -2379,7 +3088,7 @@ var init_circle = __esm({
2379
3088
  return board.create("circle", [center, surface], baseOpts);
2380
3089
  }
2381
3090
  };
2382
- registerKind(def8);
3091
+ registerKind(def10);
2383
3092
  }
2384
3093
  });
2385
3094
 
@@ -2394,12 +3103,12 @@ function constructionRefs3(c) {
2394
3103
  return [c.p1, c.p2, c.p3];
2395
3104
  }
2396
3105
  }
2397
- var def9;
3106
+ var def11;
2398
3107
  var init_arc = __esm({
2399
3108
  "src/core/scene/kinds/arc.ts"() {
2400
3109
  init_registry();
2401
3110
  init_labelOf();
2402
- def9 = {
3111
+ def11 = {
2403
3112
  type: "arc",
2404
3113
  schemaVersion: 1,
2405
3114
  migrate: {},
@@ -2457,17 +3166,17 @@ var init_arc = __esm({
2457
3166
  return board.create("circumcirclearc", [A, B, C], baseOpts);
2458
3167
  }
2459
3168
  };
2460
- registerKind(def9);
3169
+ registerKind(def11);
2461
3170
  }
2462
3171
  });
2463
3172
 
2464
3173
  // src/core/scene/kinds/sector.ts
2465
- var def10;
3174
+ var def12;
2466
3175
  var init_sector = __esm({
2467
3176
  "src/core/scene/kinds/sector.ts"() {
2468
3177
  init_registry();
2469
3178
  init_labelOf();
2470
- def10 = {
3179
+ def12 = {
2471
3180
  type: "sector",
2472
3181
  schemaVersion: 1,
2473
3182
  migrate: {},
@@ -2507,7 +3216,7 @@ var init_sector = __esm({
2507
3216
  });
2508
3217
  }
2509
3218
  };
2510
- registerKind(def10);
3219
+ registerKind(def12);
2511
3220
  }
2512
3221
  });
2513
3222
 
@@ -2553,12 +3262,12 @@ function specialShapeName(kind) {
2553
3262
  return "";
2554
3263
  }
2555
3264
  }
2556
- var def11;
3265
+ var def13;
2557
3266
  var init_polygon = __esm({
2558
3267
  "src/core/scene/kinds/polygon.ts"() {
2559
3268
  init_registry();
2560
3269
  init_labelOf();
2561
- def11 = {
3270
+ def13 = {
2562
3271
  type: "polygon",
2563
3272
  schemaVersion: 1,
2564
3273
  migrate: {},
@@ -2745,16 +3454,16 @@ var init_polygon = __esm({
2745
3454
  return poly;
2746
3455
  }
2747
3456
  };
2748
- registerKind(def11);
3457
+ registerKind(def13);
2749
3458
  }
2750
3459
  });
2751
3460
 
2752
3461
  // src/core/scene/kinds/intersection.ts
2753
- var def12;
3462
+ var def14;
2754
3463
  var init_intersection = __esm({
2755
3464
  "src/core/scene/kinds/intersection.ts"() {
2756
3465
  init_registry();
2757
- def12 = {
3466
+ def14 = {
2758
3467
  type: "intersection",
2759
3468
  schemaVersion: 1,
2760
3469
  migrate: {},
@@ -2782,8 +3491,11 @@ var init_intersection = __esm({
2782
3491
  const opts = {
2783
3492
  name: obj.label,
2784
3493
  withLabel: true,
2785
- strokeColor: obj.attrs.color ?? "#dc2626",
2786
- fillColor: obj.attrs.color ?? "#dc2626",
3494
+ // Cùng màu xanh với mọi điểm khác (buildPointOpts dùng '#1e40af').
3495
+ // Trước đây điểm giao tô đỏ '#dc2626' → tách biệt thị giác nhưng gây
3496
+ // khó hiểu ("vì sao điểm này khác màu?"). Giữ chung một màu.
3497
+ strokeColor: obj.attrs.color ?? "#1e40af",
3498
+ fillColor: obj.attrs.color ?? "#1e40af",
2787
3499
  visible: obj.visible,
2788
3500
  fixed: obj.locked
2789
3501
  };
@@ -2792,19 +3504,51 @@ var init_intersection = __esm({
2792
3504
  }
2793
3505
  const branch = obj.attrs.branch ?? 0;
2794
3506
  return board.create("intersection", [a, b, branch], opts);
3507
+ },
3508
+ /**
3509
+ * Cập nhật TẠI CHỖ các thuộc tính "trang trí" (tên/màu/ẩn-hiện/khoá) qua
3510
+ * setAttribute — giữ nguyên JxgObj identity nên các object phụ thuộc điểm
3511
+ * giao (đường thẳng qua nó, …) KHÔNG bị stale parent ref. Mô phỏng update
3512
+ * hook của point.ts.
3513
+ *
3514
+ * Nếu định nghĩa hình học đổi (kind/ref1/ref2/branch) thì throw → renderer
3515
+ * fallback remove + create để JSXGraph dựng lại phép giao đúng.
3516
+ */
3517
+ update: (obj, prev, ctx, existing) => {
3518
+ const a = obj.attrs;
3519
+ const p = prev.attrs;
3520
+ const branchA = a.branch;
3521
+ const branchP = p.branch;
3522
+ if (a.kind !== p.kind || a.ref1 !== p.ref1 || a.ref2 !== p.ref2 || branchA !== branchP) {
3523
+ throw new Error("intersection: \u0111\u1ECBnh ngh\u0129a h\xECnh h\u1ECDc \u0111\u1ED5i \u2014 recreate");
3524
+ }
3525
+ const el = existing;
3526
+ if (typeof el.setAttribute === "function") {
3527
+ try {
3528
+ el.setAttribute({
3529
+ name: obj.label,
3530
+ withLabel: true,
3531
+ strokeColor: a.color ?? "#1e40af",
3532
+ fillColor: a.color ?? "#1e40af",
3533
+ visible: obj.visible,
3534
+ fixed: obj.locked
3535
+ });
3536
+ } catch {
3537
+ }
3538
+ }
2795
3539
  }
2796
3540
  };
2797
- registerKind(def12);
3541
+ registerKind(def14);
2798
3542
  }
2799
3543
  });
2800
3544
 
2801
3545
  // src/core/scene/kinds/angle.ts
2802
- var def13;
3546
+ var def15;
2803
3547
  var init_angle = __esm({
2804
3548
  "src/core/scene/kinds/angle.ts"() {
2805
3549
  init_registry();
2806
3550
  init_labelOf();
2807
- def13 = {
3551
+ def15 = {
2808
3552
  type: "angle",
2809
3553
  schemaVersion: 1,
2810
3554
  migrate: {},
@@ -2840,17 +3584,17 @@ var init_angle = __esm({
2840
3584
  });
2841
3585
  }
2842
3586
  };
2843
- registerKind(def13);
3587
+ registerKind(def15);
2844
3588
  }
2845
3589
  });
2846
3590
 
2847
3591
  // src/core/scene/kinds/distance.ts
2848
- var def14;
3592
+ var def16;
2849
3593
  var init_distance = __esm({
2850
3594
  "src/core/scene/kinds/distance.ts"() {
2851
3595
  init_registry();
2852
3596
  init_labelOf();
2853
- def14 = {
3597
+ def16 = {
2854
3598
  type: "distance",
2855
3599
  schemaVersion: 1,
2856
3600
  migrate: {},
@@ -2879,7 +3623,7 @@ var init_distance = __esm({
2879
3623
  });
2880
3624
  }
2881
3625
  };
2882
- registerKind(def14);
3626
+ registerKind(def16);
2883
3627
  }
2884
3628
  });
2885
3629
 
@@ -3008,12 +3752,12 @@ var init_parser = __esm({
3008
3752
  });
3009
3753
 
3010
3754
  // src/core/scene/kinds/function2d.ts
3011
- var def15;
3755
+ var def17;
3012
3756
  var init_function2d = __esm({
3013
3757
  "src/core/scene/kinds/function2d.ts"() {
3014
3758
  init_registry();
3015
3759
  init_parser();
3016
- def15 = {
3760
+ def17 = {
3017
3761
  type: "function2d",
3018
3762
  schemaVersion: 1,
3019
3763
  migrate: {},
@@ -3052,16 +3796,16 @@ var init_function2d = __esm({
3052
3796
  });
3053
3797
  }
3054
3798
  };
3055
- registerKind(def15);
3799
+ registerKind(def17);
3056
3800
  }
3057
3801
  });
3058
3802
 
3059
3803
  // src/core/scene/kinds/parameter.ts
3060
- var def16;
3804
+ var def18;
3061
3805
  var init_parameter = __esm({
3062
3806
  "src/core/scene/kinds/parameter.ts"() {
3063
3807
  init_registry();
3064
- def16 = {
3808
+ def18 = {
3065
3809
  type: "parameter",
3066
3810
  schemaVersion: 1,
3067
3811
  migrate: {},
@@ -3079,16 +3823,16 @@ var init_parameter = __esm({
3079
3823
  render: () => null
3080
3824
  // Không render lên board
3081
3825
  };
3082
- registerKind(def16);
3826
+ registerKind(def18);
3083
3827
  }
3084
3828
  });
3085
3829
 
3086
3830
  // src/core/scene/kinds/pointOnCurve.ts
3087
- var def17;
3831
+ var def19;
3088
3832
  var init_pointOnCurve = __esm({
3089
3833
  "src/core/scene/kinds/pointOnCurve.ts"() {
3090
3834
  init_registry();
3091
- def17 = {
3835
+ def19 = {
3092
3836
  type: "pointOnCurve",
3093
3837
  schemaVersion: 1,
3094
3838
  migrate: {},
@@ -3115,16 +3859,16 @@ var init_pointOnCurve = __esm({
3115
3859
  });
3116
3860
  }
3117
3861
  };
3118
- registerKind(def17);
3862
+ registerKind(def19);
3119
3863
  }
3120
3864
  });
3121
3865
 
3122
3866
  // src/core/scene/kinds/tangent2d.ts
3123
- var def18;
3867
+ var def20;
3124
3868
  var init_tangent2d = __esm({
3125
3869
  "src/core/scene/kinds/tangent2d.ts"() {
3126
3870
  init_registry();
3127
- def18 = {
3871
+ def20 = {
3128
3872
  type: "tangent2d",
3129
3873
  schemaVersion: 1,
3130
3874
  migrate: {},
@@ -3147,7 +3891,7 @@ var init_tangent2d = __esm({
3147
3891
  });
3148
3892
  }
3149
3893
  };
3150
- registerKind(def18);
3894
+ registerKind(def20);
3151
3895
  }
3152
3896
  });
3153
3897
 
@@ -3223,13 +3967,13 @@ var init_evaluator = __esm({
3223
3967
  });
3224
3968
 
3225
3969
  // src/core/scene/kinds/extremum2d.ts
3226
- var def19;
3970
+ var def21;
3227
3971
  var init_extremum2d = __esm({
3228
3972
  "src/core/scene/kinds/extremum2d.ts"() {
3229
3973
  init_registry();
3230
3974
  init_evaluator();
3231
3975
  init_parser();
3232
- def19 = {
3976
+ def21 = {
3233
3977
  type: "extremum2d",
3234
3978
  schemaVersion: 1,
3235
3979
  migrate: {},
@@ -3262,18 +4006,18 @@ var init_extremum2d = __esm({
3262
4006
  }));
3263
4007
  }
3264
4008
  };
3265
- registerKind(def19);
4009
+ registerKind(def21);
3266
4010
  }
3267
4011
  });
3268
4012
 
3269
4013
  // src/core/scene/kinds/root2d.ts
3270
- var def20;
4014
+ var def22;
3271
4015
  var init_root2d = __esm({
3272
4016
  "src/core/scene/kinds/root2d.ts"() {
3273
4017
  init_registry();
3274
4018
  init_evaluator();
3275
4019
  init_parser();
3276
- def20 = {
4020
+ def22 = {
3277
4021
  type: "root2d",
3278
4022
  schemaVersion: 1,
3279
4023
  migrate: {},
@@ -3303,16 +4047,16 @@ var init_root2d = __esm({
3303
4047
  }));
3304
4048
  }
3305
4049
  };
3306
- registerKind(def20);
4050
+ registerKind(def22);
3307
4051
  }
3308
4052
  });
3309
4053
 
3310
4054
  // src/core/scene/kinds/slope2d.ts
3311
- var def21;
4055
+ var def23;
3312
4056
  var init_slope2d = __esm({
3313
4057
  "src/core/scene/kinds/slope2d.ts"() {
3314
4058
  init_registry();
3315
- def21 = {
4059
+ def23 = {
3316
4060
  type: "slope2d",
3317
4061
  schemaVersion: 1,
3318
4062
  migrate: {},
@@ -3336,7 +4080,7 @@ var init_slope2d = __esm({
3336
4080
  });
3337
4081
  }
3338
4082
  };
3339
- registerKind(def21);
4083
+ registerKind(def23);
3340
4084
  }
3341
4085
  });
3342
4086
 
@@ -3478,6 +4222,7 @@ var init_JxgRenderer = __esm({
3478
4222
  init_registry();
3479
4223
  init_types2d();
3480
4224
  init_parser();
4225
+ init_label();
3481
4226
  JxgRenderer = class {
3482
4227
  constructor(store, board, options = {}) {
3483
4228
  this.elements = /* @__PURE__ */ new Map();
@@ -3541,11 +4286,12 @@ var init_JxgRenderer = __esm({
3541
4286
  }
3542
4287
  create(obj) {
3543
4288
  try {
3544
- const def22 = getKind(obj.kind);
3545
- const el = def22.render(obj, this.ctx());
4289
+ const def24 = getKind(obj.kind);
4290
+ const el = def24.render(obj, this.ctx());
3546
4291
  this.elements.set(obj.id, el);
3547
4292
  this.attachFreePointDragSync(obj, el);
3548
4293
  this.attachGliderDragSync(obj, el);
4294
+ this.attachLabelDragSync(obj, el);
3549
4295
  } catch (err) {
3550
4296
  console.warn(`[scene/render/2d] kh\xF4ng render \u0111\u01B0\u1EE3c ${obj.kind} id="${obj.id}":`, err);
3551
4297
  }
@@ -3660,6 +4406,54 @@ var init_JxgRenderer = __esm({
3660
4406
  });
3661
4407
  });
3662
4408
  }
4409
+ /**
4410
+ * Cho phép kéo NHÃN của bất kỳ object nào có label (point/line/segment/
4411
+ * circle...). Khi user kéo nhãn, JSXGraph cộng dồn vào label.relativeCoords
4412
+ * (drag-delta screen px) nhưng KHÔNG đổi attribute offset → vị trí cuối =
4413
+ * offset + relativeCoords. Ta gộp lại thành offset thuần (readLabelOffset),
4414
+ * zero relativeCoords để khỏi double-count khi update-hook/recreate áp lại
4415
+ * offset, rồi dispatch UPDATE_ATTRS { labelOffset }. Right-click → reset.
4416
+ */
4417
+ attachLabelDragSync(obj, el) {
4418
+ const label = el?.label;
4419
+ if (!label || typeof label.on !== "function") return;
4420
+ const sceneId = obj.id;
4421
+ label.on("up", () => {
4422
+ if (this.disposed) return;
4423
+ const off = readLabelOffset(label);
4424
+ if (!off) return;
4425
+ const cur = this.store.getState().objects[sceneId];
4426
+ if (!cur) return;
4427
+ const prev = cur.attrs.labelOffset;
4428
+ if (prev && prev[0] === off[0] && prev[1] === off[1]) return;
4429
+ try {
4430
+ label.setAttribute({ offset: off });
4431
+ if (label.relativeCoords?.scrCoords) {
4432
+ label.relativeCoords.scrCoords[1] = 0;
4433
+ label.relativeCoords.scrCoords[2] = 0;
4434
+ }
4435
+ } catch {
4436
+ }
4437
+ this.store.dispatch({
4438
+ type: "UPDATE_ATTRS",
4439
+ payload: { id: sceneId, patch: { labelOffset: off } }
4440
+ });
4441
+ });
4442
+ const node = label.rendNode;
4443
+ if (node?.addEventListener) {
4444
+ node.addEventListener("contextmenu", (ev) => {
4445
+ if (this.disposed) return;
4446
+ ev.preventDefault();
4447
+ const c = this.store.getState().objects[sceneId];
4448
+ if (!c) return;
4449
+ if (c.attrs.labelOffset === void 0) return;
4450
+ this.store.dispatch({
4451
+ type: "UPDATE_ATTRS",
4452
+ payload: { id: sceneId, patch: { labelOffset: void 0 } }
4453
+ });
4454
+ });
4455
+ }
4456
+ }
3663
4457
  remove(id) {
3664
4458
  this.removeHalo(id);
3665
4459
  this.selectedIds.delete(id);
@@ -3697,11 +4491,11 @@ var init_JxgRenderer = __esm({
3697
4491
  continue;
3698
4492
  }
3699
4493
  if (Object.is(old, cur)) continue;
3700
- const def22 = getKind(cur.kind);
4494
+ const def24 = getKind(cur.kind);
3701
4495
  const existing = this.elements.get(id);
3702
- if (def22.update && existing) {
4496
+ if (def24.update && existing) {
3703
4497
  try {
3704
- def22.update(cur, old, this.ctx(), existing);
4498
+ def24.update(cur, old, this.ctx(), existing);
3705
4499
  continue;
3706
4500
  } catch (err) {
3707
4501
  console.warn(`[scene/render/2d] update fail, recreate:`, err);
@@ -3796,88 +4590,89 @@ var init_JxgRenderer = __esm({
3796
4590
  layer: 4,
3797
4591
  needsRegularUpdate: true
3798
4592
  };
4593
+ const elementClass = el.elementClass;
4594
+ const elType = el.elType;
4595
+ const isPointLike = elementClass === 1 || elType === "point" || elType === "glider" || elType === "intersection";
4596
+ const isPolygonLike = Array.isArray(el.vertices) && el.vertices.length >= 3;
4597
+ const isCircleLike = elementClass === 3 || elType === "circle" || elType === "circumcircle" || elType === "incircle";
4598
+ const isSegment = elType === "segment";
4599
+ const isLineLike = elementClass === 2 || elType === "line" || elType === "arrow" || elType === "ray" || elType === "vector" || elType === "tangent" || elType === "normal" || elType === "parallel" || elType === "perpendicular" || elType === "bisector";
3799
4600
  const halos = [];
3800
4601
  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);
4602
+ if (isPointLike) {
4603
+ const baseSize = el.getAttribute?.("size") ?? 4;
4604
+ const halo = board.create("point", [
4605
+ () => el.X?.() ?? 0,
4606
+ () => el.Y?.() ?? 0
4607
+ ], {
4608
+ ...haloBase,
4609
+ size: baseSize + 6,
4610
+ face: "o",
4611
+ strokeWidth: 2,
4612
+ strokeOpacity: 0.75,
4613
+ fillOpacity: 0.25
4614
+ });
4615
+ halos.push(halo);
4616
+ } else if (isPolygonLike) {
4617
+ const verts = el.vertices;
4618
+ const last = verts.length - 1;
4619
+ const trimmed = verts[last] === verts[0] ? verts.slice(0, last) : verts.slice();
4620
+ const halo = board.create("polygon", trimmed, {
4621
+ ...haloBase,
4622
+ fillOpacity: 0.2,
4623
+ borders: {
4624
+ strokeColor: SEL_STROKE,
4625
+ strokeWidth: 7,
4626
+ strokeOpacity: 0.55,
4627
+ highlight: false
3876
4628
  }
3877
- break;
4629
+ });
4630
+ halos.push(halo);
4631
+ } else if (isCircleLike && el.center && typeof el.Radius === "function") {
4632
+ const halo = board.create("circle", [el.center, () => el.Radius?.() ?? 0], {
4633
+ ...haloBase,
4634
+ strokeWidth: 9,
4635
+ fillOpacity: 0
4636
+ });
4637
+ halos.push(halo);
4638
+ } else if (isSegment && el.point1 && el.point2) {
4639
+ const halo = board.create("segment", [el.point1, el.point2], {
4640
+ ...haloBase,
4641
+ strokeWidth: 9,
4642
+ straightFirst: false,
4643
+ straightLast: false
4644
+ });
4645
+ halos.push(halo);
4646
+ } else if (isLineLike && el.point1 && el.point2) {
4647
+ const halo = board.create("line", [el.point1, el.point2], {
4648
+ ...haloBase,
4649
+ strokeWidth: 9
4650
+ });
4651
+ halos.push(halo);
4652
+ } else if (el.center && (el.radiuspoint ?? el.radiusPoint) && (el.anglepoint ?? el.anglePoint) && (elType === "arc" || elType === "semicircle" || elType === "circumcirclearc" || elType === "sector" || elType === "angle")) {
4653
+ const rp = el.radiuspoint ?? el.radiusPoint;
4654
+ const ap = el.anglepoint ?? el.anglePoint;
4655
+ if (elType === "sector") {
4656
+ halos.push(board.create("sector", [el.center, rp, ap], {
4657
+ ...haloBase,
4658
+ strokeWidth: 7,
4659
+ fillOpacity: 0.2
4660
+ }));
4661
+ } else if (elType === "angle") {
4662
+ halos.push(board.create("angle", [rp, el.center, ap], {
4663
+ ...haloBase,
4664
+ strokeWidth: 7,
4665
+ fillOpacity: 0.2,
4666
+ radius: el.getAttribute?.("radius") ?? 1
4667
+ }));
4668
+ } else {
4669
+ halos.push(board.create("arc", [el.center, rp, ap], {
4670
+ ...haloBase,
4671
+ strokeWidth: 9,
4672
+ fillColor: "none",
4673
+ fillOpacity: 0
4674
+ }));
3878
4675
  }
3879
- default:
3880
- break;
3881
4676
  }
3882
4677
  } catch (err) {
3883
4678
  console.warn("[scene/render/2d] halo create fail:", err);
@@ -3977,15 +4772,18 @@ function computeAutoFitBbox(points, circles, aspect, padPct = 0.12) {
3977
4772
  const xs = points.map((p) => p[0]).filter(Number.isFinite);
3978
4773
  const ys = points.map((p) => p[1]).filter(Number.isFinite);
3979
4774
  let xmin = Infinity, xmax = -Infinity, ymin = Infinity, ymax = -Infinity;
3980
- let clusterDiag = 0;
4775
+ let fullDiag = 0;
3981
4776
  if (xs.length >= 2 && ys.length >= 2) {
3982
4777
  [xmin, xmax] = robustRange(xs);
3983
4778
  [ymin, ymax] = robustRange(ys);
3984
- clusterDiag = Math.hypot(xmax - xmin, ymax - ymin);
4779
+ fullDiag = Math.hypot(
4780
+ Math.max(...xs) - Math.min(...xs),
4781
+ Math.max(...ys) - Math.min(...ys)
4782
+ );
3985
4783
  }
3986
4784
  for (const c of circles) {
3987
4785
  if (!Number.isFinite(c.cx) || !Number.isFinite(c.cy) || !Number.isFinite(c.r)) continue;
3988
- if (clusterDiag > 0 && c.r > CIRCLE_MAX_FACTOR * clusterDiag) continue;
4786
+ if (fullDiag > 0 && c.r > CIRCLE_MAX_FACTOR * fullDiag) continue;
3989
4787
  xmin = Math.min(xmin, c.cx - c.r);
3990
4788
  xmax = Math.max(xmax, c.cx + c.r);
3991
4789
  ymin = Math.min(ymin, c.cy - c.r);
@@ -4037,6 +4835,49 @@ var init_autoFitBbox = __esm({
4037
4835
  }
4038
4836
  });
4039
4837
 
4838
+ // src/stamps/geometry-2d/autoFitBoard.ts
4839
+ function isDefaultBbox(bbox) {
4840
+ const d = DEFAULT_VIEW_2D.bbox;
4841
+ return bbox.length === 4 && bbox[0] === d[0] && bbox[1] === d[1] && bbox[2] === d[2] && bbox[3] === d[3];
4842
+ }
4843
+ function fittedBboxFromBoard(board, aspect) {
4844
+ const objs = board?.objectsList;
4845
+ if (!Array.isArray(objs)) return null;
4846
+ const points = [];
4847
+ const circles = [];
4848
+ for (const raw of objs) {
4849
+ const o = raw;
4850
+ if (o?.elementClass === 1 && typeof o.X === "function" && typeof o.Y === "function") {
4851
+ const x = o.X(), y = o.Y();
4852
+ if (Number.isFinite(x) && Number.isFinite(y)) points.push([x, y]);
4853
+ } else if (o?.elementClass === 3 && o.center?.X && typeof o.Radius === "function") {
4854
+ const cx = o.center.X(), cy = o.center.Y(), r = o.Radius();
4855
+ if (Number.isFinite(cx) && Number.isFinite(cy) && Number.isFinite(r)) {
4856
+ circles.push({ cx, cy, r });
4857
+ }
4858
+ }
4859
+ }
4860
+ const safeAspect = Number.isFinite(aspect) && aspect > 0 ? aspect : 1;
4861
+ return computeAutoFitBbox(points, circles, safeAspect);
4862
+ }
4863
+ function autoFitBoardToContent(board, aspect) {
4864
+ const bbox = fittedBboxFromBoard(board, aspect);
4865
+ if (!bbox) return;
4866
+ const b = board;
4867
+ try {
4868
+ b.setBoundingBox?.(bbox);
4869
+ if (typeof b.update === "function") b.update();
4870
+ if (typeof b.fullUpdate === "function") b.fullUpdate();
4871
+ } catch {
4872
+ }
4873
+ }
4874
+ var init_autoFitBoard = __esm({
4875
+ "src/stamps/geometry-2d/autoFitBoard.ts"() {
4876
+ init_types();
4877
+ init_autoFitBbox();
4878
+ }
4879
+ });
4880
+
4040
4881
  // src/stamps/geometry-2d/labelLayout.ts
4041
4882
  function radialLabelOffsets(points, R = 14) {
4042
4883
  const out = /* @__PURE__ */ new Map();
@@ -4071,8 +4912,9 @@ function containerDimsForBbox(bbox) {
4071
4912
  if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) {
4072
4913
  return { width: FALLBACK_W, height: FALLBACK_H };
4073
4914
  }
4074
- let width = w * PIXELS_PER_UNIT;
4075
- let height = h * PIXELS_PER_UNIT;
4915
+ const pxPerUnit = DEFAULT_VIEW_PX / Math.max(w, h);
4916
+ let width = w * pxPerUnit;
4917
+ let height = h * pxPerUnit;
4076
4918
  const maxAxis = Math.max(width, height);
4077
4919
  if (maxAxis > MAX_DIM) {
4078
4920
  const ratio = MAX_DIM / maxAxis;
@@ -4087,31 +4929,6 @@ function containerDimsForBbox(bbox) {
4087
4929
  }
4088
4930
  return { width: Math.round(width), height: Math.round(height) };
4089
4931
  }
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
4932
  function applyRadialLabelOffsets(board) {
4116
4933
  const objs = board?.objectsList;
4117
4934
  if (!Array.isArray(objs)) return;
@@ -4137,10 +4954,6 @@ function applyRadialLabelOffsets(board) {
4137
4954
  } catch {
4138
4955
  }
4139
4956
  }
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
4957
  async function renderGeometrySvgFromState(jsonState) {
4145
4958
  const state = deserializeBoard(jsonState);
4146
4959
  const view = state.meta.domain === "2d" ? state.meta.view : DEFAULT_VIEW_2D;
@@ -4175,7 +4988,7 @@ async function renderGeometrySvgFromState(jsonState) {
4175
4988
  const store = createStore(state);
4176
4989
  const renderer = new JxgRenderer(store, board);
4177
4990
  if (shouldAutoFit) {
4178
- autoFitBboxFromBoard(board, dims.width / dims.height);
4991
+ autoFitBoardToContent(board, dims.width / dims.height);
4179
4992
  applyRadialLabelOffsets(board);
4180
4993
  }
4181
4994
  return renderer;
@@ -4183,7 +4996,7 @@ async function renderGeometrySvgFromState(jsonState) {
4183
4996
  });
4184
4997
  return svgString;
4185
4998
  }
4186
- var PIXELS_PER_UNIT, MIN_DIM, MAX_DIM, FALLBACK_W, FALLBACK_H;
4999
+ var DEFAULT_VIEW_PX, MIN_DIM, MAX_DIM, FALLBACK_W, FALLBACK_H;
4187
5000
  var init_render = __esm({
4188
5001
  "src/stamps/geometry-2d/render.ts"() {
4189
5002
  init_serialize();
@@ -4192,9 +5005,9 @@ var init_render = __esm({
4192
5005
  init_types();
4193
5006
  init_JxgRenderer();
4194
5007
  init_jxgOffscreenRender();
4195
- init_autoFitBbox();
5008
+ init_autoFitBoard();
4196
5009
  init_labelLayout();
4197
- PIXELS_PER_UNIT = 20;
5010
+ DEFAULT_VIEW_PX = 500;
4198
5011
  MIN_DIM = 100;
4199
5012
  MAX_DIM = 1200;
4200
5013
  FALLBACK_W = 400;
@@ -4833,7 +5646,7 @@ var init_useToolHoverTooltip = __esm({
4833
5646
  init_types4();
4834
5647
  }
4835
5648
  });
4836
- function normalize(s) {
5649
+ function normalize2(s) {
4837
5650
  return s.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/đ/g, "d").replace(/Đ/g, "d");
4838
5651
  }
4839
5652
  function SearchIcon() {
@@ -4880,12 +5693,12 @@ function ToolGrid(props) {
4880
5693
  const { tools, groupOrder, groupLabels, activeTool, onToolChange, chord } = props;
4881
5694
  const { hover, portalReady, showHover, hideHover } = useToolHoverTooltip();
4882
5695
  const [query, setQuery] = React2.useState("");
4883
- const normalizedQuery = React2.useMemo(() => normalize(query.trim()), [query]);
5696
+ const normalizedQuery = React2.useMemo(() => normalize2(query.trim()), [query]);
4884
5697
  const filteredTools = React2.useMemo(() => {
4885
5698
  if (!normalizedQuery) return tools;
4886
5699
  return tools.filter((t) => {
4887
- if (normalize(t.label).includes(normalizedQuery)) return true;
4888
- if (t.hint && normalize(t.hint).includes(normalizedQuery)) return true;
5700
+ if (normalize2(t.label).includes(normalizedQuery)) return true;
5701
+ if (t.hint && normalize2(t.hint).includes(normalizedQuery)) return true;
4889
5702
  return false;
4890
5703
  });
4891
5704
  }, [tools, normalizedQuery]);
@@ -5754,41 +6567,48 @@ var init_icons = __esm({
5754
6567
  /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4", cy: "4", r: "1.7", fill: C_POINT })
5755
6568
  ] }),
5756
6569
  // ===== 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 })
6570
+ // Hệ màu đồng bộ: XANH = input click sẵn, ĐỎ = output suy ra. Toạ độ tính để
6571
+ // điểm nằm ĐÚNG trên đường tròn/cung/tiếp tuyến (không trôi nổi).
6572
+ excenter: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", strokeLinejoin: "round", children: [
6573
+ /* @__PURE__ */ jsxRuntime.jsx("polygon", { points: "12,3 15,15 9,15", stroke: "currentColor", strokeWidth: "1.4", fill: "none" }),
6574
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "9", y1: "15", x2: "8", y2: "18.9", stroke: "#94a3b8", strokeWidth: "1", strokeDasharray: "1.6 1.6" }),
6575
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "15", y1: "15", x2: "16", y2: "18.9", stroke: "#94a3b8", strokeWidth: "1", strokeDasharray: "1.6 1.6" }),
6576
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "18.9", r: "3.8", stroke: "currentColor", strokeWidth: "1.3" }),
6577
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "3", r: "1.6", fill: C_POINT }),
6578
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9", cy: "15", r: "1.6", fill: C_POINT }),
6579
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "15", cy: "15", r: "1.6", fill: C_POINT }),
6580
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "18.9", r: "2.2", fill: C_CONSTRUCT })
5761
6581
  ] }),
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 })
6582
+ tangencyPoint: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
6583
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9.5", cy: "12", r: "7", stroke: "currentColor", strokeWidth: "1.4" }),
6584
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "16.5", y1: "3.5", x2: "16.5", y2: "20.5", stroke: "currentColor", strokeWidth: "1.4" }),
6585
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "16.5", cy: "12", r: "2.3", fill: C_CONSTRUCT })
5766
6586
  ] }),
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 })
6587
+ secondIntersection: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
6588
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "11", cy: "12", r: "6.5", stroke: "currentColor", strokeWidth: "1.4" }),
6589
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "2.4", y1: "8.2", x2: "20.4", y2: "11.4", stroke: "currentColor", strokeWidth: "1.4" }),
6590
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "5.37", cy: "8.75", r: "1.8", fill: C_POINT }),
6591
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "17.4", cy: "10.87", r: "2.4", fill: C_CONSTRUCT })
5772
6592
  ] }),
5773
6593
  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 })
6594
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 17 A 8.5 8.5 0 0 1 20 17", stroke: "currentColor", strokeWidth: "1.6", fill: "none" }),
6595
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4", cy: "17", r: "1.8", fill: C_POINT }),
6596
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "20", cy: "17", r: "1.8", fill: C_POINT }),
6597
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "11.4", r: "2.3", fill: C_CONSTRUCT })
5778
6598
  ] }),
5779
6599
  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 })
6600
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9", cy: "12", r: "6", stroke: "currentColor", strokeWidth: "1.4" }),
6601
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "15", cy: "12", r: "6", stroke: "currentColor", strokeWidth: "1.4" }),
6602
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "6.8", r: "2", fill: C_CONSTRUCT }),
6603
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "17.2", r: "2", fill: C_CONSTRUCT })
5784
6604
  ] }),
5785
6605
  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 })
6606
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "13", cy: "12", r: "6", stroke: "currentColor", strokeWidth: "1.4" }),
6607
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "12", x2: "11.8", y2: "18.6", stroke: "currentColor", strokeWidth: "1.3" }),
6608
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "12", x2: "11.8", y2: "5.4", stroke: "currentColor", strokeWidth: "1.3" }),
6609
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "3", cy: "12", r: "2", fill: C_POINT }),
6610
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9.4", cy: "16.8", r: "1.8", fill: C_CONSTRUCT }),
6611
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9.4", cy: "7.2", r: "1.8", fill: C_CONSTRUCT })
5792
6612
  ] }),
5793
6613
  circleCR: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
5794
6614
  /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "7.5", stroke: "currentColor", strokeWidth: "1.3" }),
@@ -5799,9 +6619,11 @@ var init_icons = __esm({
5799
6619
  /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 19 L12 4 L21 19 Z", stroke: "currentColor", strokeWidth: "1.3", strokeLinejoin: "round" }),
5800
6620
  /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "14", r: "4.2", fill: "none", stroke: C_CONSTRUCT, strokeWidth: "1.3" })
5801
6621
  ] }),
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" })
6622
+ excircle: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", strokeLinejoin: "round", children: [
6623
+ /* @__PURE__ */ jsxRuntime.jsx("polygon", { points: "12,3 15,15 9,15", stroke: "currentColor", strokeWidth: "1.4", fill: "none" }),
6624
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "9", y1: "15", x2: "8", y2: "18.9", stroke: "#94a3b8", strokeWidth: "1", strokeDasharray: "1.6 1.6" }),
6625
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "15", y1: "15", x2: "16", y2: "18.9", stroke: "#94a3b8", strokeWidth: "1", strokeDasharray: "1.6 1.6" }),
6626
+ /* @__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
6627
  ] }),
5806
6628
  pointOn: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
5807
6629
  /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "7.5", stroke: "currentColor", strokeWidth: "1.3" }),
@@ -6584,8 +7406,8 @@ var init_circles = __esm({
6584
7406
  const ax = picks[0].X(), ay = picks[0].Y();
6585
7407
  const bx = picks[1].X(), by = picks[1].Y();
6586
7408
  const cx = picks[2].X(), cy = picks[2].Y();
6587
- const cross = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
6588
- if (Math.abs(cross) < 1e-6) {
7409
+ const cross2 = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
7410
+ if (Math.abs(cross2) < 1e-6) {
6589
7411
  ctx.toast?.("Kh\xF4ng v\u1EBD \u0111\u01B0\u1EE3c cung qua 3 \u0111i\u1EC3m th\u1EB3ng h\xE0ng", {
6590
7412
  variant: "warning",
6591
7413
  id: "arc3-collinear"
@@ -7811,11 +8633,11 @@ function buildObjectSnapshot(state, id, anchorScreen) {
7811
8633
  const obj = state.objects[id];
7812
8634
  if (!obj) return null;
7813
8635
  const k = obj.kind;
7814
- if (k !== "point" && k !== "line" && k !== "circle" && k !== "segment" && k !== "ray" && k !== "vector") {
8636
+ if (k !== "point" && k !== "line" && k !== "circle" && k !== "segment" && k !== "ray" && k !== "vector" && k !== "intersection") {
7815
8637
  return null;
7816
8638
  }
7817
8639
  const a = obj.attrs;
7818
- const jKind = k === "point" ? "point" : k === "circle" ? "circle" : "line";
8640
+ const jKind = k === "point" || k === "intersection" ? "point" : k === "circle" ? "circle" : "line";
7819
8641
  return {
7820
8642
  id,
7821
8643
  kind: jKind,
@@ -8223,6 +9045,7 @@ var init_MiniBoard = __esm({
8223
9045
  init_safeJsx();
8224
9046
  init_attachJxgWheelZoom();
8225
9047
  init_initJxgBoard();
9048
+ init_autoFitBoard();
8226
9049
  init_snapshot();
8227
9050
  init_previewActions();
8228
9051
  init_buildHandle();
@@ -8462,6 +9285,11 @@ var init_MiniBoard = __esm({
8462
9285
  rendererRef.current = new JxgRenderer(store, board, {
8463
9286
  theme: paletteFor(isDarkRef.current)
8464
9287
  });
9288
+ if (isDefaultBbox(initialView.bbox)) {
9289
+ const el = containerRef.current;
9290
+ const aspect = el && el.clientHeight > 0 ? el.clientWidth / el.clientHeight : 1;
9291
+ safeJsx("MiniBoard.autoFit", () => autoFitBoardToContent(board, aspect));
9292
+ }
8465
9293
  if (containerRef.current) {
8466
9294
  wheelCleanup = attachJxgWheelZoom(containerRef.current, board, "MiniBoard.2d");
8467
9295
  }
@@ -9259,6 +10087,7 @@ function useAiFigure(generator) {
9259
10087
  const [prompt, setPrompt] = React2.useState("");
9260
10088
  const [isLoading, setIsLoading] = React2.useState(false);
9261
10089
  const [error, setError] = React2.useState(null);
10090
+ const [notice, setNotice] = React2.useState(null);
9262
10091
  const [tokens, setTokens] = React2.useState(0);
9263
10092
  const abortRef = React2.useRef(null);
9264
10093
  const requestIdRef = React2.useRef(0);
@@ -9279,6 +10108,7 @@ function useAiFigure(generator) {
9279
10108
  abortRef.current = controller;
9280
10109
  setIsLoading(true);
9281
10110
  setError(null);
10111
+ setNotice(null);
9282
10112
  setTokens(0);
9283
10113
  try {
9284
10114
  const generated = await generator(problem, {
@@ -9292,6 +10122,7 @@ function useAiFigure(generator) {
9292
10122
  setError(generated.message);
9293
10123
  return null;
9294
10124
  }
10125
+ if (generated.partial) setNotice(generated.partial.message);
9295
10126
  return generated.state;
9296
10127
  } catch (caught) {
9297
10128
  if (controller.signal.aborted || caught instanceof DOMException && caught.name === "AbortError") {
@@ -9318,6 +10149,7 @@ function useAiFigure(generator) {
9318
10149
  setPrompt,
9319
10150
  isLoading,
9320
10151
  error,
10152
+ notice,
9321
10153
  submit,
9322
10154
  cancel,
9323
10155
  tokens
@@ -9354,6 +10186,141 @@ var init_tesseract = __esm({
9354
10186
  }
9355
10187
  });
9356
10188
 
10189
+ // src/stamps/geometry-2d/ai/vision/repairOcrSymbols.ts
10190
+ function repairOcrSymbols(text) {
10191
+ let t = text;
10192
+ t = t.replace(PERP_RE, "$1 \u22A5 $3");
10193
+ t = t.replace(TRI_STRICT_RE, "tam gi\xE1c $1 $2");
10194
+ t = t.replace(TRI_DOUBLE_RE, "tam gi\xE1c A$1");
10195
+ t = t.replace(CIRCLE_O_RE, "(O)");
10196
+ t = t.replace(ELEM_RE, "$1 \u2208 (O)");
10197
+ t = t.replace(INTERSECT_RE, "$1 \u2229 $2 = {");
10198
+ t = t.replace(SQUARE_RE, "$1\xB2");
10199
+ t = t.replace(HORN_O_C_RE, "C");
10200
+ t = t.replace(DUONG_DBAR_RE, "\u0110\u01B0\u1EDDng");
10201
+ t = t.replace(DUONG_LOWER_RE, "\u0111\u01B0\u1EDDng");
10202
+ t = t.replace(DUONG_TRON_RE, "$1tr\xF2n");
10203
+ t = t.replace(FLORIN_F_RE, "F");
10204
+ t = t.replace(TAI_TAI_RE, "t\u1EA1i$1");
10205
+ t = t.replace(TAM_TAM_RE, "t\xE2m$1");
10206
+ t = t.replace(EURO_ELEM_RE, "\u2208");
10207
+ t = t.replace(PARALLEL_RE, "\u2225");
10208
+ t = t.replace(CENT_C_RE, "c");
10209
+ t = t.replace(ETH_D_RE, "D");
10210
+ t = t.replace(SLASH_O_RE, "O");
10211
+ t = t.replace(GOI_RE, "G\u1ECDi");
10212
+ t = t.replace(LAY_RE, "L\u1EA5y");
10213
+ t = t.replace(DIQUA_RE, "\u0111i qua");
10214
+ t = t.replace(VA_VAND_RE, "v\xE0 ");
10215
+ t = t.replace(TREN_RE, "$1r\xEAn");
10216
+ t = t.replace(DAY_RE, "d\xE2y");
10217
+ t = t.replace(CHUNG_MINH_RE, "$1minh");
10218
+ t = t.replace(THANG_HANG_RE, "th\u1EB3ng h\xE0ng");
10219
+ t = t.replace(TINH_RE, "T\xEDnh");
10220
+ t = t.replace(DO_DAI_RE, "\u0111\u1ED9 d\xE0i");
10221
+ t = t.replace(NIM_RE, "n\u1EB1m");
10222
+ t = t.replace(NAM_GIUA_RE, "n\u1EB1m");
10223
+ t = t.replace(ANGLE_Z_RE, "\u2220$1");
10224
+ t = t.replace(DE_PAREN_RE, "(\u0110\u1EC1 ");
10225
+ t = t.replace(DE_VERB_RE, "$1xu\u1EA5t$2");
10226
+ t = t.replace(DE_THI_LOWER_RE, "\u0111\u1EC1");
10227
+ t = t.replace(DI_DONG_RE, "di \u0111\u1ED9ng");
10228
+ t = t.replace(POUND_E_RE, "E");
10229
+ t = t.replace(TRI_SPACE_2_1, "$1$2$3$4");
10230
+ t = t.replace(TRI_SPACE_1_2, "$1$2$3$4");
10231
+ t = t.replace(DIAERESIS_O_RE, "O");
10232
+ t = t.replace(INTERSECT_PAREN_RE, "$1 \u2229 $2$3");
10233
+ return t;
10234
+ }
10235
+ var PT, PERP_RE, TRI_STRICT_RE, TRI_DOUBLE_RE, CIRCLE_O_RE, ELEM_RE, INTERSECT_RE, SQUARE_RE, HORN_O_C_RE, DUONG_DBAR_RE, DUONG_TRON_RE, FLORIN_F_RE, TAI_TAI_RE, TAM_TAM_RE, EURO_ELEM_RE, PARALLEL_RE, CENT_C_RE, ETH_D_RE, SLASH_O_RE, GOI_RE, LAY_RE, DIQUA_RE, VA_VAND_RE, TREN_RE, DUONG_LOWER_RE, DAY_RE, CHUNG_MINH_RE, THANG_HANG_RE, TINH_RE, DO_DAI_RE, NIM_RE, NAM_GIUA_RE, ANGLE_Z_RE, DE_PAREN_RE, DE_VERB_RE, DE_THI_LOWER_RE, DI_DONG_RE, POUND_E_RE, TRI_SPACE_2_1, TRI_SPACE_1_2, DIAERESIS_O_RE, INTERSECT_PAREN_RE;
10236
+ var init_repairOcrSymbols = __esm({
10237
+ "src/stamps/geometry-2d/ai/vision/repairOcrSymbols.ts"() {
10238
+ PT = `[A-Z]{2,3}['\u2032]?`;
10239
+ PERP_RE = new RegExp(
10240
+ `(?<![\\p{L}\\d])(${PT})\\s+([1|L])\\s+(${PT})(?![\\p{L}\\d])`,
10241
+ "gu"
10242
+ );
10243
+ TRI_STRICT_RE = /(?<!giác )(?<!thang )(?<![\p{L}\d])A([A-Z]{3})\s+(đều|cân|nhọn|vuông)(?![\p{L}])/gu;
10244
+ TRI_DOUBLE_RE = /(?<![\p{L}\d])AA([A-Z]{2})(?![A-Z])/gu;
10245
+ CIRCLE_O_RE = /\(0\)/gu;
10246
+ ELEM_RE = /(?<![\p{L}\d])([A-Z](?:\s*,\s*[A-Z])+)\s*e\s*\((?:O|0)\)/gu;
10247
+ INTERSECT_RE = /([A-Z]{2,3})N\s+([A-Z]\S{0,2})\s*=\s*\{/gu;
10248
+ SQUARE_RE = /([A-Z])\?(?=\s*[-=+)])/gu;
10249
+ HORN_O_C_RE = /(?<!\p{Ll})Ơ(?!\p{Ll})/gu;
10250
+ DUONG_DBAR_RE = /D(?:ường|uong)(?= (?:tròn|tron|thẳng|kính|cao|chéo|trung|phân|vuông|nối|gấp|tâm))/gu;
10251
+ DUONG_TRON_RE = /([Đđ]ường\s+)tron(?![\p{L}])/gu;
10252
+ FLORIN_F_RE = /Ƒ/gu;
10253
+ TAI_TAI_RE = /(?<![\p{L}])tai(\s+)(?=[A-Z(]|điểm)/gu;
10254
+ TAM_TAM_RE = /(?<![\p{L}])tam(\s+)(?=đường|\(|đối xứng)/gu;
10255
+ EURO_ELEM_RE = /€/gu;
10256
+ PARALLEL_RE = /\|\|/gu;
10257
+ CENT_C_RE = /¢/gu;
10258
+ ETH_D_RE = /(?<!\p{Ll})Ð(?!\p{Ll})/gu;
10259
+ SLASH_O_RE = /(?<!\p{Ll})Ø(?!\p{Ll})/gu;
10260
+ GOI_RE = /(?<!\p{L})Goi(?!\p{L})/gu;
10261
+ LAY_RE = /(?<!\p{L})Lay(?!\p{L})/gu;
10262
+ DIQUA_RE = /(?<!\p{L})di qua(?!\p{L})/gu;
10263
+ VA_VAND_RE = /(?<!\p{L})va (?=[A-Z(]|dây|đường)/gu;
10264
+ TREN_RE = /([Tt])rén(?![\p{L}])/gu;
10265
+ DUONG_LOWER_RE = /(?<![\p{L}])dường(?= (?:tròn|tron|thẳng|kính|cao|chéo|trung|phân|vuông|nối|gấp|tâm))/gu;
10266
+ DAY_RE = /(?<![\p{L}])day(?=\s+(?:cung(?![\p{L}])|[A-Z]{2}))/gu;
10267
+ CHUNG_MINH_RE = /([Cc]hứng )mình(?![\p{L}])/gu;
10268
+ THANG_HANG_RE = /thẳng hang(?![\p{L}])/gu;
10269
+ TINH_RE = /(?<![\p{L}])Tinh(?= (?:[A-Z]|do(?![\p{L}])|độ|diện|bán|chu|số|giá))/gu;
10270
+ DO_DAI_RE = /(?<![\p{L}])do dai(?![\p{L}])/gu;
10271
+ NIM_RE = /(?<![\p{L}])nim(?= (?:giữa|trên|trong)(?![\p{L}]))/gu;
10272
+ NAM_GIUA_RE = /(?<![\p{L}])năm(?= (?:giữa|trên) [A-Z])/gu;
10273
+ ANGLE_Z_RE = /(?<![\p{L}\d])4?Z([A-Z]{2,3})(?![A-Z])/gu;
10274
+ DE_PAREN_RE = /\(Dé /gu;
10275
+ DE_VERB_RE = /(\(Đề )\S+( bởi)/gu;
10276
+ DE_THI_LOWER_RE = /(?<![\p{L}])dé(?= thi)/gu;
10277
+ DI_DONG_RE = /(?<![\p{L}])di dong(?![\p{L}])/gu;
10278
+ POUND_E_RE = /£/gu;
10279
+ TRI_SPACE_2_1 = /(tam\s*giác\s+)([A-Z])([A-Z])\s+([A-Z])(?![A-Z\p{Ll}])/gu;
10280
+ TRI_SPACE_1_2 = /(tam\s*giác\s+)([A-Z])\s+([A-Z])([A-Z])(?![A-Z\p{Ll}])/gu;
10281
+ DIAERESIS_O_RE = /(?<!\p{Ll})Ö(?!\p{Ll})/gu;
10282
+ INTERSECT_PAREN_RE = /(\([A-Z]{3}\))\s+N\s+([A-Z]{2})(\s*=\s*\{)/gu;
10283
+ }
10284
+ });
10285
+
10286
+ // src/stamps/geometry-2d/ai/vision/detectFormulaRisk.ts
10287
+ function hasManglingLtGt(text) {
10288
+ const re = /[<>]/gu;
10289
+ let m;
10290
+ while ((m = re.exec(text)) !== null) {
10291
+ const prev = text.slice(0, m.index).replace(/\s+$/u, "").slice(-1);
10292
+ const next = text.slice(m.index + 1).replace(/^\s+/u, "").slice(0, 1);
10293
+ const prevOperand = /[A-Za-z0-9]/.test(prev);
10294
+ const nextOperand = /[A-Za-z0-9]/.test(next);
10295
+ if (!(prevOperand && nextOperand)) return true;
10296
+ }
10297
+ return false;
10298
+ }
10299
+ function detectFormulaRisk(text) {
10300
+ const reasons = [];
10301
+ if (hasManglingLtGt(text)) {
10302
+ reasons.push('ph\xE2n s\u1ED1/bi\u1EC3u th\u1EE9c nghi b\u1ECB nu\u1ED1t (k\xFD hi\u1EC7u "<"/">")');
10303
+ }
10304
+ if (SUP_RESIDUE.test(text)) {
10305
+ reasons.push('s\u1ED1 m\u0169 ho\u1EB7c \u0111\u1ED9 nghi b\u1ECB m\u1EA5t (d\u1EA5u "?")');
10306
+ }
10307
+ for (const m of text.matchAll(ANGLE_DEG)) {
10308
+ const tok = m[1];
10309
+ if (/[A-Z]/.test(tok) && /[^\x00-\x7F]/u.test(tok)) {
10310
+ reasons.push("t\xEAn g\xF3c nghi b\u1ECB nh\u1EADn d\u1EA1ng sai (k\xFD t\u1EF1 c\xF3 d\u1EA5u)");
10311
+ break;
10312
+ }
10313
+ }
10314
+ return reasons;
10315
+ }
10316
+ var SUP_RESIDUE, ANGLE_DEG;
10317
+ var init_detectFormulaRisk = __esm({
10318
+ "src/stamps/geometry-2d/ai/vision/detectFormulaRisk.ts"() {
10319
+ SUP_RESIDUE = /[A-Z0-9]\?(?=\s*[-=+)(])/u;
10320
+ ANGLE_DEG = /([\p{L}]{1,6})\s*=\s*\d+\s*°/gu;
10321
+ }
10322
+ });
10323
+
9357
10324
  // src/stamps/geometry-2d/ai/vision/extractProblem.ts
9358
10325
  async function extractProblemFromImage(image, opts = {}) {
9359
10326
  let raw;
@@ -9381,6 +10348,7 @@ async function extractProblemFromImage(image, opts = {}) {
9381
10348
  ok: true,
9382
10349
  text,
9383
10350
  confidence,
10351
+ warnings: detectFormulaRisk(text),
9384
10352
  usage: { inputTokens: 0, outputTokens: 0 }
9385
10353
  };
9386
10354
  }
@@ -9392,6 +10360,7 @@ function postProcess(raw) {
9392
10360
  t = t.replace(/```[\s\S]*?```/g, "").replace(/`([^`]+)`/g, "$1");
9393
10361
  t = t.replace(/\s+/g, " ").trim();
9394
10362
  t = t.normalize("NFC");
10363
+ t = repairOcrSymbols(t);
9395
10364
  if (t.length > MAX_TEXT_CHARS) t = t.slice(0, MAX_TEXT_CHARS);
9396
10365
  return t;
9397
10366
  }
@@ -9399,6 +10368,8 @@ var MIN_HIGH_CONFIDENCE_CHARS, MAX_TEXT_CHARS, TESSERACT_HIGH_CONFIDENCE_THRESHO
9399
10368
  var init_extractProblem = __esm({
9400
10369
  "src/stamps/geometry-2d/ai/vision/extractProblem.ts"() {
9401
10370
  init_tesseract();
10371
+ init_repairOcrSymbols();
10372
+ init_detectFormulaRisk();
9402
10373
  MIN_HIGH_CONFIDENCE_CHARS = 10;
9403
10374
  MAX_TEXT_CHARS = 2e3;
9404
10375
  TESSERACT_HIGH_CONFIDENCE_THRESHOLD = 70;
@@ -9410,6 +10381,14 @@ async function handleExtractProblem(image, opts = {}) {
9410
10381
  try {
9411
10382
  const r = await extractProblemFromImage(image, opts);
9412
10383
  if (r.ok) {
10384
+ if (r.warnings.length > 0) {
10385
+ return {
10386
+ kind: "low-confidence",
10387
+ text: r.text,
10388
+ warning: `C\xF3 th\u1EC3 c\xF3 c\xF4ng th\u1EE9c/k\xFD hi\u1EC7u b\u1ECB nh\u1EADn d\u1EA1ng sai (${r.warnings.join("; ")}). Ki\u1EC3m tra k\u1EF9 tr\u01B0\u1EDBc khi v\u1EBD.`,
10389
+ usage: r.usage
10390
+ };
10391
+ }
9413
10392
  if (r.confidence === "low") {
9414
10393
  return {
9415
10394
  kind: "low-confidence",
@@ -9474,9 +10453,9 @@ async function fileToImagePart(file) {
9474
10453
  const bitmap = await createImageBitmap(file);
9475
10454
  const { width, height } = bitmap;
9476
10455
  const maxEdge = Math.max(width, height);
9477
- const scale = maxEdge > MAX_EDGE_PX ? MAX_EDGE_PX / maxEdge : 1;
9478
- const targetW = Math.round(width * scale);
9479
- const targetH = Math.round(height * scale);
10456
+ const scale2 = maxEdge > MAX_EDGE_PX ? MAX_EDGE_PX / maxEdge : 1;
10457
+ const targetW = Math.round(width * scale2);
10458
+ const targetH = Math.round(height * scale2);
9480
10459
  const canvas = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(targetW, targetH) : Object.assign(document.createElement("canvas"), { width: targetW, height: targetH });
9481
10460
  const ctx = canvas.getContext("2d");
9482
10461
  if (!ctx) throw new Error("Kh\xF4ng t\u1EA1o \u0111\u01B0\u1EE3c canvas 2D context");
@@ -9528,6 +10507,7 @@ function AiFigurePrompt({ generator, onGenerated }) {
9528
10507
  setPrompt,
9529
10508
  isLoading,
9530
10509
  error,
10510
+ notice,
9531
10511
  submit,
9532
10512
  cancel,
9533
10513
  tokens
@@ -9541,6 +10521,10 @@ function AiFigurePrompt({ generator, onGenerated }) {
9541
10521
  const id = setInterval(() => setElapsed((s) => s + 1), 1e3);
9542
10522
  return () => clearInterval(id);
9543
10523
  }, [isLoading]);
10524
+ const [noticeDismissed, setNoticeDismissed] = React2.useState(false);
10525
+ React2.useEffect(() => {
10526
+ setNoticeDismissed(false);
10527
+ }, [notice]);
9544
10528
  const [image, setImage] = React2.useState(null);
9545
10529
  const [ocrLoading, setOcrLoading] = React2.useState(false);
9546
10530
  const [ocrError, setOcrError] = React2.useState(null);
@@ -9756,7 +10740,30 @@ function AiFigurePrompt({ generator, onGenerated }) {
9756
10740
  }
9757
10741
  ),
9758
10742
  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 })
10743
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "mt-1 px-1 text-xs text-red-600", children: error }),
10744
+ notice && !noticeDismissed && /* @__PURE__ */ jsxRuntime.jsxs(
10745
+ "div",
10746
+ {
10747
+ role: "status",
10748
+ "data-testid": "geometry-ai-partial-notice",
10749
+ 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",
10750
+ children: [
10751
+ notice,
10752
+ /* @__PURE__ */ jsxRuntime.jsx(
10753
+ "button",
10754
+ {
10755
+ type: "button",
10756
+ onClick: () => setNoticeDismissed(true),
10757
+ "aria-label": "\u0110\xF3ng th\xF4ng b\xE1o",
10758
+ title: "\u0110\xF3ng th\xF4ng b\xE1o",
10759
+ "data-testid": "geometry-ai-partial-dismiss",
10760
+ 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",
10761
+ children: "\xD7"
10762
+ }
10763
+ )
10764
+ ]
10765
+ }
10766
+ )
9760
10767
  ] });
9761
10768
  }
9762
10769
  var ArrowUpIcon, StopIcon, PaperclipIcon;
@@ -10381,11 +11388,17 @@ async function insertStampImage(api, opts) {
10381
11388
  const elements = api.getSceneElements();
10382
11389
  const editingId = opts.editingElementId ?? null;
10383
11390
  if (editingId) {
11391
+ const old = elements.find((e) => e.id === editingId) ;
11392
+ const oldLongest = old ? Math.max(old.width ?? 0, old.height ?? 0) : 0;
11393
+ const newLongest = Math.max(width, height);
11394
+ const scale2 = oldLongest > 0 && newLongest > 0 ? oldLongest / newLongest : 1;
11395
+ const w = width * scale2;
11396
+ const h = height * scale2;
10384
11397
  const updated = elements.map(
10385
- (e) => e.id === editingId ? { ...e, fileId, customData, width, height } : e
11398
+ (e) => e.id === editingId ? { ...e, fileId, customData, width: w, height: h } : e
10386
11399
  );
10387
11400
  api.updateScene({ elements: updated, appState: clearAppStateAfterInsert() });
10388
- return { fileId, width, height, elementId: editingId };
11401
+ return { fileId, width: w, height: h, elementId: editingId };
10389
11402
  }
10390
11403
  const newElement = buildStampImageElement(
10391
11404
  api,
@@ -10539,8 +11552,11 @@ function serializePoint(obj, state) {
10539
11552
  };
10540
11553
  }
10541
11554
  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}`);
11555
+ const containment = c.containing ?? c.notContaining;
11556
+ const baseRefs = [c.circle, c.a, c.b];
11557
+ if (containment) baseRefs.push(containment);
11558
+ const refs = resolveRefs(baseRefs, state);
11559
+ if (!refs) return fail("unresolved-ref", baseRefs.join(","));
10544
11560
  return {
10545
11561
  ok: true,
10546
11562
  entity: {
@@ -10549,7 +11565,7 @@ function serializePoint(obj, state) {
10549
11565
  circle: refs[0],
10550
11566
  a: refs[1],
10551
11567
  b: refs[2],
10552
- notContaining: refs[3]
11568
+ ...c.containing ? { containing: refs[3] } : c.notContaining ? { notContaining: refs[3] } : {}
10553
11569
  }
10554
11570
  };
10555
11571
  }
@@ -10586,6 +11602,21 @@ function serializePoint(obj, state) {
10586
11602
  entity: { name: obj.label, kind: "pointAtDistance", from: refs[0], through: refs[1], distance }
10587
11603
  };
10588
11604
  }
11605
+ case "commonTangentPoint": {
11606
+ const refs = resolveRefs([c.circles[0], c.circles[1]], state);
11607
+ if (!refs) return fail("unresolved-ref", `${c.circles[0]},${c.circles[1]}`);
11608
+ return {
11609
+ ok: true,
11610
+ entity: {
11611
+ name: obj.label,
11612
+ kind: "commonTangentPoint",
11613
+ circles: [refs[0], refs[1]],
11614
+ on: c.on,
11615
+ variant: c.variant,
11616
+ side: c.side
11617
+ }
11618
+ };
11619
+ }
10589
11620
  // Out of DSL v1:
10590
11621
  case "onAxis":
10591
11622
  case "onPolygon":
@@ -10593,6 +11624,7 @@ function serializePoint(obj, state) {
10593
11624
  case "onPerpendicular":
10594
11625
  case "onPerpBisector":
10595
11626
  case "onCircleAroundPoint":
11627
+ case "mixtilinearPoint":
10596
11628
  return fail("unsupported-constraint", c.kind);
10597
11629
  default: {
10598
11630
  return fail("unsupported-constraint");
@@ -10895,9 +11927,11 @@ function describeEntity(e) {
10895
11927
  return `${e.name} = \u0111\u01B0\u1EDDng tr\xF2n b\xE0ng ti\u1EBFp ${e.vertices.join("")} \u0111\u1ED1i di\u1EC7n ${e.opposite}`;
10896
11928
  // Cụm A
10897
11929
  case "arcMidpoint":
10898
- return `${e.name} = trung \u0111i\u1EC3m cung ${e.a}${e.b} (kh\xF4ng ch\u1EE9a ${e.notContaining}) tr\xEAn ${e.circle}`;
11930
+ 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
11931
  case "excenter":
10900
11932
  return `${e.name} = t\xE2m b\xE0ng ti\u1EBFp ${e.vertices.join("")} \u0111\u1ED1i di\u1EC7n ${e.opposite}`;
11933
+ case "mixtilinearPoint":
11934
+ return `${e.name} = ${e.which === "center" ? "t\xE2m" : "ti\u1EBFp \u0111i\u1EC3m"} mixtilinear ${e.vertices.join("")}`;
10901
11935
  case "reflectPoint":
10902
11936
  return `${e.name} = \u0111\u1ED1i x\u1EE9ng ${e.of} qua \u0111i\u1EC3m ${e.through}`;
10903
11937
  case "reflectLine":
@@ -10908,6 +11942,12 @@ function describeEntity(e) {
10908
11942
  const distStr = d.kind === "circleRadius" ? `r(${d.circle})` : d.kind === "segmentLength" ? `|${d.p1}${d.p2}|` : `${d.value}`;
10909
11943
  return `${e.name} = \u0111i\u1EC3m tr\xEAn tia ${e.from}${e.through} c\xE1ch ${e.through} m\u1ED9t kho\u1EA3ng ${distStr}`;
10910
11944
  }
11945
+ case "onPerpBisector":
11946
+ return `${e.name} = \u0111i\u1EC3m tr\xEAn trung tr\u1EF1c ${e.p1}${e.p2}`;
11947
+ case "commonTangentPoint": {
11948
+ const v = e.variant === "internal" ? "trong" : "ngo\xE0i";
11949
+ return `${e.name} = ti\u1EBFp \u0111i\u1EC3m ti\u1EBFp tuy\u1EBFn chung ${v} ${e.circles[0]},${e.circles[1]} (tr\xEAn ${e.circles[e.on]})`;
11950
+ }
10911
11951
  default: {
10912
11952
  return "";
10913
11953
  }
@@ -11030,7 +12070,8 @@ var init_host = __esm({
11030
12070
  version: 1,
11031
12071
  jsonState
11032
12072
  }),
11033
- editingElementId: editingElement?.id ?? null
12073
+ editingElementId: editingElement?.id ?? null,
12074
+ preserveExistingSize: true
11034
12075
  });
11035
12076
  } catch (err) {
11036
12077
  console.error("Geometry insert failed:", err);