@xom11/whiteboard 0.31.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 (64) hide show
  1. package/dist/ai.d.mts +148 -27
  2. package/dist/ai.d.ts +148 -27
  3. package/dist/ai.js +2652 -430
  4. package/dist/ai.js.map +1 -1
  5. package/dist/ai.mjs +2482 -426
  6. package/dist/ai.mjs.map +1 -1
  7. package/dist/catalog.json +4 -4
  8. package/dist/{chunk-JJ4FPCBE.mjs → chunk-4EXRNZRX.mjs} +641 -75
  9. package/dist/chunk-4EXRNZRX.mjs.map +1 -0
  10. package/dist/{chunk-BMYC2ILT.mjs → chunk-DBG6DZ2V.mjs} +4 -4
  11. package/dist/{chunk-BMYC2ILT.mjs.map → chunk-DBG6DZ2V.mjs.map} +1 -1
  12. package/dist/{chunk-T3N4BSJV.mjs → chunk-DCFZTKFE.mjs} +13 -5
  13. package/dist/chunk-DCFZTKFE.mjs.map +1 -0
  14. package/dist/{chunk-DWIEVCGK.mjs → chunk-DJM5EWTT.mjs} +53 -3
  15. package/dist/chunk-DJM5EWTT.mjs.map +1 -0
  16. package/dist/{chunk-44JY2AKC.mjs → chunk-F42MZ745.mjs} +3 -3
  17. package/dist/{chunk-44JY2AKC.mjs.map → chunk-F42MZ745.mjs.map} +1 -1
  18. package/dist/{chunk-TMRFSOM7.mjs → chunk-FI2MGX5H.mjs} +4 -4
  19. package/dist/{chunk-TMRFSOM7.mjs.map → chunk-FI2MGX5H.mjs.map} +1 -1
  20. package/dist/{chunk-KOXOC2FI.mjs → chunk-KLGPAMJS.mjs} +10 -7
  21. package/dist/chunk-KLGPAMJS.mjs.map +1 -0
  22. package/dist/{chunk-73Q7ADVL.mjs → chunk-MNHOFOM5.mjs} +2 -2
  23. package/dist/{chunk-73Q7ADVL.mjs.map → chunk-MNHOFOM5.mjs.map} +1 -1
  24. package/dist/{chunk-K5BS2H56.mjs → chunk-MOAXHVKI.mjs} +4 -4
  25. package/dist/{chunk-K5BS2H56.mjs.map → chunk-MOAXHVKI.mjs.map} +1 -1
  26. package/dist/chunk-SUXIMLYN.mjs +238 -0
  27. package/dist/chunk-SUXIMLYN.mjs.map +1 -0
  28. package/dist/{chunk-LTLLQUMN.mjs → chunk-XSTGTEGP.mjs} +4 -4
  29. package/dist/{chunk-LTLLQUMN.mjs.map → chunk-XSTGTEGP.mjs.map} +1 -1
  30. package/dist/geometry-2d.js +883 -119
  31. package/dist/geometry-2d.js.map +1 -1
  32. package/dist/geometry-2d.mjs +4 -4
  33. package/dist/geometry-3d.js +3995 -471
  34. package/dist/geometry-3d.js.map +1 -1
  35. package/dist/geometry-3d.mjs +4 -4
  36. package/dist/graph-2d.js +706 -111
  37. package/dist/graph-2d.js.map +1 -1
  38. package/dist/graph-2d.mjs +4 -4
  39. package/dist/{handleExtractProblem-BrDY9ifM.d.ts → handleExtractProblem-mtJyhEw3.d.mts} +2 -0
  40. package/dist/{handleExtractProblem-BrDY9ifM.d.mts → handleExtractProblem-mtJyhEw3.d.ts} +2 -0
  41. package/dist/{host-KMWP7KBT.mjs → host-JNLJ5BVR.mjs} +8 -8
  42. package/dist/{host-KMWP7KBT.mjs.map → host-JNLJ5BVR.mjs.map} +1 -1
  43. package/dist/{host-4FIUNIDQ.mjs → host-VRWLQSMH.mjs} +7 -7
  44. package/dist/{host-4FIUNIDQ.mjs.map → host-VRWLQSMH.mjs.map} +1 -1
  45. package/dist/host-W4U6YCSX.mjs +4973 -0
  46. package/dist/host-W4U6YCSX.mjs.map +1 -0
  47. package/dist/index.d.mts +1 -1
  48. package/dist/index.d.ts +1 -1
  49. package/dist/index.js +4090 -392
  50. package/dist/index.js.map +1 -1
  51. package/dist/index.mjs +27 -21
  52. package/dist/index.mjs.map +1 -1
  53. package/dist/serialize-7UZWQQ4I.mjs +9 -0
  54. package/dist/{serialize-PGHQZEPV.mjs.map → serialize-7UZWQQ4I.mjs.map} +1 -1
  55. package/package.json +4 -1
  56. package/dist/chunk-DWIEVCGK.mjs.map +0 -1
  57. package/dist/chunk-JJ4FPCBE.mjs.map +0 -1
  58. package/dist/chunk-KOXOC2FI.mjs.map +0 -1
  59. package/dist/chunk-QK6OVDLC.mjs +0 -103
  60. package/dist/chunk-QK6OVDLC.mjs.map +0 -1
  61. package/dist/chunk-T3N4BSJV.mjs.map +0 -1
  62. package/dist/host-4ZB4XD4S.mjs +0 -2378
  63. package/dist/host-4ZB4XD4S.mjs.map +0 -1
  64. package/dist/serialize-PGHQZEPV.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,
@@ -952,8 +1470,16 @@ function constraintRefs2D(c) {
952
1470
  }
953
1471
  case "excenter":
954
1472
  return [c.vertices[0], c.vertices[1], c.vertices[2]];
955
- 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":
956
1479
  return [];
1480
+ default: {
1481
+ return [];
1482
+ }
957
1483
  }
958
1484
  }
959
1485
  var init_d_constraint2 = __esm({
@@ -1491,19 +2017,19 @@ function buildJxgTransforms(board, ctx, t) {
1491
2017
  }
1492
2018
  }
1493
2019
  function makeDistanceFn(ctx, d) {
1494
- const scale = d.scale ?? 1;
2020
+ const scale2 = d.scale ?? 1;
1495
2021
  const offset = d.offset ?? 0;
1496
2022
  if (d.kind === "literal") {
1497
2023
  const v = d.value;
1498
- return () => scale * v + offset;
2024
+ return () => scale2 * v + offset;
1499
2025
  }
1500
2026
  if (d.kind === "segmentLength") {
1501
2027
  const p = ctx.resolveRef(d.p1);
1502
2028
  const q = ctx.resolveRef(d.p2);
1503
- 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;
1504
2030
  }
1505
2031
  const circle = ctx.resolveRef(d.circle);
1506
- return () => scale * circle.Radius() + offset;
2032
+ return () => scale2 * circle.Radius() + offset;
1507
2033
  }
1508
2034
  function buildPointOpts(obj) {
1509
2035
  return {
@@ -1552,6 +2078,73 @@ var init_pointAtDistance = __esm({
1552
2078
  }
1553
2079
  });
1554
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
+
1555
2148
  // src/core/scene/kinds/point-constraints/circleIntersection.ts
1556
2149
  var circleIntersectionConstraint;
1557
2150
  var init_circleIntersection = __esm({
@@ -1831,6 +2424,7 @@ var init_registry2 = __esm({
1831
2424
  init_excenter();
1832
2425
  init_mixtilinearPoint();
1833
2426
  init_pointAtDistance();
2427
+ init_commonTangentPoint();
1834
2428
  init_circleIntersection();
1835
2429
  init_circleSecondIntersection();
1836
2430
  init_secondIntersection();
@@ -1857,6 +2451,7 @@ var init_registry2 = __esm({
1857
2451
  excenterConstraint,
1858
2452
  mixtilinearPointConstraint,
1859
2453
  pointAtDistanceConstraint,
2454
+ commonTangentPointConstraint,
1860
2455
  circleIntersectionConstraint,
1861
2456
  circleSecondIntersectionConstraint,
1862
2457
  secondIntersectionConstraint,
@@ -1873,7 +2468,7 @@ var init_registry2 = __esm({
1873
2468
  });
1874
2469
 
1875
2470
  // src/core/scene/kinds/point.ts
1876
- var def3;
2471
+ var def5;
1877
2472
  var init_point = __esm({
1878
2473
  "src/core/scene/kinds/point.ts"() {
1879
2474
  init_registry();
@@ -1881,7 +2476,7 @@ var init_point = __esm({
1881
2476
  init_registry2();
1882
2477
  init_shared();
1883
2478
  init_label();
1884
- def3 = {
2479
+ def5 = {
1885
2480
  type: "point",
1886
2481
  schemaVersion: 1,
1887
2482
  migrate: {},
@@ -1959,18 +2554,18 @@ var init_point = __esm({
1959
2554
  throw new Error("point: constraint kind changed \u2014 recreate");
1960
2555
  }
1961
2556
  };
1962
- registerKind(def3);
2557
+ registerKind(def5);
1963
2558
  }
1964
2559
  });
1965
2560
 
1966
2561
  // src/core/scene/kinds/segment.ts
1967
- var def4;
2562
+ var def6;
1968
2563
  var init_segment = __esm({
1969
2564
  "src/core/scene/kinds/segment.ts"() {
1970
2565
  init_registry();
1971
2566
  init_labelOf();
1972
2567
  init_label();
1973
- def4 = {
2568
+ def6 = {
1974
2569
  type: "segment",
1975
2570
  schemaVersion: 1,
1976
2571
  migrate: {},
@@ -2006,7 +2601,7 @@ var init_segment = __esm({
2006
2601
  });
2007
2602
  }
2008
2603
  };
2009
- registerKind(def4);
2604
+ registerKind(def6);
2010
2605
  }
2011
2606
  });
2012
2607
 
@@ -2034,14 +2629,14 @@ function constructionRefs(c) {
2034
2629
  return [c.throughPoint, c.toCircle];
2035
2630
  }
2036
2631
  }
2037
- var def5;
2632
+ var def7;
2038
2633
  var init_line = __esm({
2039
2634
  "src/core/scene/kinds/line.ts"() {
2040
2635
  init_registry();
2041
2636
  init_labelOf();
2042
2637
  init_label();
2043
2638
  init_pointConstructions();
2044
- def5 = {
2639
+ def7 = {
2045
2640
  type: "line",
2046
2641
  schemaVersion: 1,
2047
2642
  migrate: {},
@@ -2237,17 +2832,17 @@ var init_line = __esm({
2237
2832
  }
2238
2833
  }
2239
2834
  };
2240
- registerKind(def5);
2835
+ registerKind(def7);
2241
2836
  }
2242
2837
  });
2243
2838
 
2244
2839
  // src/core/scene/kinds/ray.ts
2245
- var def6;
2840
+ var def8;
2246
2841
  var init_ray = __esm({
2247
2842
  "src/core/scene/kinds/ray.ts"() {
2248
2843
  init_registry();
2249
2844
  init_labelOf();
2250
- def6 = {
2845
+ def8 = {
2251
2846
  type: "ray",
2252
2847
  schemaVersion: 1,
2253
2848
  migrate: {},
@@ -2272,17 +2867,17 @@ var init_ray = __esm({
2272
2867
  });
2273
2868
  }
2274
2869
  };
2275
- registerKind(def6);
2870
+ registerKind(def8);
2276
2871
  }
2277
2872
  });
2278
2873
 
2279
2874
  // src/core/scene/kinds/vector.ts
2280
- var def7;
2875
+ var def9;
2281
2876
  var init_vector = __esm({
2282
2877
  "src/core/scene/kinds/vector.ts"() {
2283
2878
  init_registry();
2284
2879
  init_labelOf();
2285
- def7 = {
2880
+ def9 = {
2286
2881
  type: "vector",
2287
2882
  schemaVersion: 1,
2288
2883
  migrate: {},
@@ -2304,7 +2899,7 @@ var init_vector = __esm({
2304
2899
  });
2305
2900
  }
2306
2901
  };
2307
- registerKind(def7);
2902
+ registerKind(def9);
2308
2903
  }
2309
2904
  });
2310
2905
 
@@ -2334,14 +2929,14 @@ function constructionRefs2(c) {
2334
2929
  return [c.p1, c.p2];
2335
2930
  }
2336
2931
  }
2337
- var def8;
2932
+ var def10;
2338
2933
  var init_circle = __esm({
2339
2934
  "src/core/scene/kinds/circle.ts"() {
2340
2935
  init_registry();
2341
2936
  init_labelOf();
2342
2937
  init_label();
2343
2938
  init_pointConstructions();
2344
- def8 = {
2939
+ def10 = {
2345
2940
  type: "circle",
2346
2941
  schemaVersion: 1,
2347
2942
  migrate: {},
@@ -2493,7 +3088,7 @@ var init_circle = __esm({
2493
3088
  return board.create("circle", [center, surface], baseOpts);
2494
3089
  }
2495
3090
  };
2496
- registerKind(def8);
3091
+ registerKind(def10);
2497
3092
  }
2498
3093
  });
2499
3094
 
@@ -2508,12 +3103,12 @@ function constructionRefs3(c) {
2508
3103
  return [c.p1, c.p2, c.p3];
2509
3104
  }
2510
3105
  }
2511
- var def9;
3106
+ var def11;
2512
3107
  var init_arc = __esm({
2513
3108
  "src/core/scene/kinds/arc.ts"() {
2514
3109
  init_registry();
2515
3110
  init_labelOf();
2516
- def9 = {
3111
+ def11 = {
2517
3112
  type: "arc",
2518
3113
  schemaVersion: 1,
2519
3114
  migrate: {},
@@ -2571,17 +3166,17 @@ var init_arc = __esm({
2571
3166
  return board.create("circumcirclearc", [A, B, C], baseOpts);
2572
3167
  }
2573
3168
  };
2574
- registerKind(def9);
3169
+ registerKind(def11);
2575
3170
  }
2576
3171
  });
2577
3172
 
2578
3173
  // src/core/scene/kinds/sector.ts
2579
- var def10;
3174
+ var def12;
2580
3175
  var init_sector = __esm({
2581
3176
  "src/core/scene/kinds/sector.ts"() {
2582
3177
  init_registry();
2583
3178
  init_labelOf();
2584
- def10 = {
3179
+ def12 = {
2585
3180
  type: "sector",
2586
3181
  schemaVersion: 1,
2587
3182
  migrate: {},
@@ -2621,7 +3216,7 @@ var init_sector = __esm({
2621
3216
  });
2622
3217
  }
2623
3218
  };
2624
- registerKind(def10);
3219
+ registerKind(def12);
2625
3220
  }
2626
3221
  });
2627
3222
 
@@ -2667,12 +3262,12 @@ function specialShapeName(kind) {
2667
3262
  return "";
2668
3263
  }
2669
3264
  }
2670
- var def11;
3265
+ var def13;
2671
3266
  var init_polygon = __esm({
2672
3267
  "src/core/scene/kinds/polygon.ts"() {
2673
3268
  init_registry();
2674
3269
  init_labelOf();
2675
- def11 = {
3270
+ def13 = {
2676
3271
  type: "polygon",
2677
3272
  schemaVersion: 1,
2678
3273
  migrate: {},
@@ -2859,16 +3454,16 @@ var init_polygon = __esm({
2859
3454
  return poly;
2860
3455
  }
2861
3456
  };
2862
- registerKind(def11);
3457
+ registerKind(def13);
2863
3458
  }
2864
3459
  });
2865
3460
 
2866
3461
  // src/core/scene/kinds/intersection.ts
2867
- var def12;
3462
+ var def14;
2868
3463
  var init_intersection = __esm({
2869
3464
  "src/core/scene/kinds/intersection.ts"() {
2870
3465
  init_registry();
2871
- def12 = {
3466
+ def14 = {
2872
3467
  type: "intersection",
2873
3468
  schemaVersion: 1,
2874
3469
  migrate: {},
@@ -2943,17 +3538,17 @@ var init_intersection = __esm({
2943
3538
  }
2944
3539
  }
2945
3540
  };
2946
- registerKind(def12);
3541
+ registerKind(def14);
2947
3542
  }
2948
3543
  });
2949
3544
 
2950
3545
  // src/core/scene/kinds/angle.ts
2951
- var def13;
3546
+ var def15;
2952
3547
  var init_angle = __esm({
2953
3548
  "src/core/scene/kinds/angle.ts"() {
2954
3549
  init_registry();
2955
3550
  init_labelOf();
2956
- def13 = {
3551
+ def15 = {
2957
3552
  type: "angle",
2958
3553
  schemaVersion: 1,
2959
3554
  migrate: {},
@@ -2989,17 +3584,17 @@ var init_angle = __esm({
2989
3584
  });
2990
3585
  }
2991
3586
  };
2992
- registerKind(def13);
3587
+ registerKind(def15);
2993
3588
  }
2994
3589
  });
2995
3590
 
2996
3591
  // src/core/scene/kinds/distance.ts
2997
- var def14;
3592
+ var def16;
2998
3593
  var init_distance = __esm({
2999
3594
  "src/core/scene/kinds/distance.ts"() {
3000
3595
  init_registry();
3001
3596
  init_labelOf();
3002
- def14 = {
3597
+ def16 = {
3003
3598
  type: "distance",
3004
3599
  schemaVersion: 1,
3005
3600
  migrate: {},
@@ -3028,7 +3623,7 @@ var init_distance = __esm({
3028
3623
  });
3029
3624
  }
3030
3625
  };
3031
- registerKind(def14);
3626
+ registerKind(def16);
3032
3627
  }
3033
3628
  });
3034
3629
 
@@ -3157,12 +3752,12 @@ var init_parser = __esm({
3157
3752
  });
3158
3753
 
3159
3754
  // src/core/scene/kinds/function2d.ts
3160
- var def15;
3755
+ var def17;
3161
3756
  var init_function2d = __esm({
3162
3757
  "src/core/scene/kinds/function2d.ts"() {
3163
3758
  init_registry();
3164
3759
  init_parser();
3165
- def15 = {
3760
+ def17 = {
3166
3761
  type: "function2d",
3167
3762
  schemaVersion: 1,
3168
3763
  migrate: {},
@@ -3201,16 +3796,16 @@ var init_function2d = __esm({
3201
3796
  });
3202
3797
  }
3203
3798
  };
3204
- registerKind(def15);
3799
+ registerKind(def17);
3205
3800
  }
3206
3801
  });
3207
3802
 
3208
3803
  // src/core/scene/kinds/parameter.ts
3209
- var def16;
3804
+ var def18;
3210
3805
  var init_parameter = __esm({
3211
3806
  "src/core/scene/kinds/parameter.ts"() {
3212
3807
  init_registry();
3213
- def16 = {
3808
+ def18 = {
3214
3809
  type: "parameter",
3215
3810
  schemaVersion: 1,
3216
3811
  migrate: {},
@@ -3228,16 +3823,16 @@ var init_parameter = __esm({
3228
3823
  render: () => null
3229
3824
  // Không render lên board
3230
3825
  };
3231
- registerKind(def16);
3826
+ registerKind(def18);
3232
3827
  }
3233
3828
  });
3234
3829
 
3235
3830
  // src/core/scene/kinds/pointOnCurve.ts
3236
- var def17;
3831
+ var def19;
3237
3832
  var init_pointOnCurve = __esm({
3238
3833
  "src/core/scene/kinds/pointOnCurve.ts"() {
3239
3834
  init_registry();
3240
- def17 = {
3835
+ def19 = {
3241
3836
  type: "pointOnCurve",
3242
3837
  schemaVersion: 1,
3243
3838
  migrate: {},
@@ -3264,16 +3859,16 @@ var init_pointOnCurve = __esm({
3264
3859
  });
3265
3860
  }
3266
3861
  };
3267
- registerKind(def17);
3862
+ registerKind(def19);
3268
3863
  }
3269
3864
  });
3270
3865
 
3271
3866
  // src/core/scene/kinds/tangent2d.ts
3272
- var def18;
3867
+ var def20;
3273
3868
  var init_tangent2d = __esm({
3274
3869
  "src/core/scene/kinds/tangent2d.ts"() {
3275
3870
  init_registry();
3276
- def18 = {
3871
+ def20 = {
3277
3872
  type: "tangent2d",
3278
3873
  schemaVersion: 1,
3279
3874
  migrate: {},
@@ -3296,7 +3891,7 @@ var init_tangent2d = __esm({
3296
3891
  });
3297
3892
  }
3298
3893
  };
3299
- registerKind(def18);
3894
+ registerKind(def20);
3300
3895
  }
3301
3896
  });
3302
3897
 
@@ -3372,13 +3967,13 @@ var init_evaluator = __esm({
3372
3967
  });
3373
3968
 
3374
3969
  // src/core/scene/kinds/extremum2d.ts
3375
- var def19;
3970
+ var def21;
3376
3971
  var init_extremum2d = __esm({
3377
3972
  "src/core/scene/kinds/extremum2d.ts"() {
3378
3973
  init_registry();
3379
3974
  init_evaluator();
3380
3975
  init_parser();
3381
- def19 = {
3976
+ def21 = {
3382
3977
  type: "extremum2d",
3383
3978
  schemaVersion: 1,
3384
3979
  migrate: {},
@@ -3411,18 +4006,18 @@ var init_extremum2d = __esm({
3411
4006
  }));
3412
4007
  }
3413
4008
  };
3414
- registerKind(def19);
4009
+ registerKind(def21);
3415
4010
  }
3416
4011
  });
3417
4012
 
3418
4013
  // src/core/scene/kinds/root2d.ts
3419
- var def20;
4014
+ var def22;
3420
4015
  var init_root2d = __esm({
3421
4016
  "src/core/scene/kinds/root2d.ts"() {
3422
4017
  init_registry();
3423
4018
  init_evaluator();
3424
4019
  init_parser();
3425
- def20 = {
4020
+ def22 = {
3426
4021
  type: "root2d",
3427
4022
  schemaVersion: 1,
3428
4023
  migrate: {},
@@ -3452,16 +4047,16 @@ var init_root2d = __esm({
3452
4047
  }));
3453
4048
  }
3454
4049
  };
3455
- registerKind(def20);
4050
+ registerKind(def22);
3456
4051
  }
3457
4052
  });
3458
4053
 
3459
4054
  // src/core/scene/kinds/slope2d.ts
3460
- var def21;
4055
+ var def23;
3461
4056
  var init_slope2d = __esm({
3462
4057
  "src/core/scene/kinds/slope2d.ts"() {
3463
4058
  init_registry();
3464
- def21 = {
4059
+ def23 = {
3465
4060
  type: "slope2d",
3466
4061
  schemaVersion: 1,
3467
4062
  migrate: {},
@@ -3485,7 +4080,7 @@ var init_slope2d = __esm({
3485
4080
  });
3486
4081
  }
3487
4082
  };
3488
- registerKind(def21);
4083
+ registerKind(def23);
3489
4084
  }
3490
4085
  });
3491
4086
 
@@ -3691,8 +4286,8 @@ var init_JxgRenderer = __esm({
3691
4286
  }
3692
4287
  create(obj) {
3693
4288
  try {
3694
- const def22 = getKind(obj.kind);
3695
- const el = def22.render(obj, this.ctx());
4289
+ const def24 = getKind(obj.kind);
4290
+ const el = def24.render(obj, this.ctx());
3696
4291
  this.elements.set(obj.id, el);
3697
4292
  this.attachFreePointDragSync(obj, el);
3698
4293
  this.attachGliderDragSync(obj, el);
@@ -3896,11 +4491,11 @@ var init_JxgRenderer = __esm({
3896
4491
  continue;
3897
4492
  }
3898
4493
  if (Object.is(old, cur)) continue;
3899
- const def22 = getKind(cur.kind);
4494
+ const def24 = getKind(cur.kind);
3900
4495
  const existing = this.elements.get(id);
3901
- if (def22.update && existing) {
4496
+ if (def24.update && existing) {
3902
4497
  try {
3903
- def22.update(cur, old, this.ctx(), existing);
4498
+ def24.update(cur, old, this.ctx(), existing);
3904
4499
  continue;
3905
4500
  } catch (err) {
3906
4501
  console.warn(`[scene/render/2d] update fail, recreate:`, err);
@@ -4177,15 +4772,18 @@ function computeAutoFitBbox(points, circles, aspect, padPct = 0.12) {
4177
4772
  const xs = points.map((p) => p[0]).filter(Number.isFinite);
4178
4773
  const ys = points.map((p) => p[1]).filter(Number.isFinite);
4179
4774
  let xmin = Infinity, xmax = -Infinity, ymin = Infinity, ymax = -Infinity;
4180
- let clusterDiag = 0;
4775
+ let fullDiag = 0;
4181
4776
  if (xs.length >= 2 && ys.length >= 2) {
4182
4777
  [xmin, xmax] = robustRange(xs);
4183
4778
  [ymin, ymax] = robustRange(ys);
4184
- 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
+ );
4185
4783
  }
4186
4784
  for (const c of circles) {
4187
4785
  if (!Number.isFinite(c.cx) || !Number.isFinite(c.cy) || !Number.isFinite(c.r)) continue;
4188
- if (clusterDiag > 0 && c.r > CIRCLE_MAX_FACTOR * clusterDiag) continue;
4786
+ if (fullDiag > 0 && c.r > CIRCLE_MAX_FACTOR * fullDiag) continue;
4189
4787
  xmin = Math.min(xmin, c.cx - c.r);
4190
4788
  xmax = Math.max(xmax, c.cx + c.r);
4191
4789
  ymin = Math.min(ymin, c.cy - c.r);
@@ -5048,7 +5646,7 @@ var init_useToolHoverTooltip = __esm({
5048
5646
  init_types4();
5049
5647
  }
5050
5648
  });
5051
- function normalize(s) {
5649
+ function normalize2(s) {
5052
5650
  return s.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/đ/g, "d").replace(/Đ/g, "d");
5053
5651
  }
5054
5652
  function SearchIcon() {
@@ -5095,12 +5693,12 @@ function ToolGrid(props) {
5095
5693
  const { tools, groupOrder, groupLabels, activeTool, onToolChange, chord } = props;
5096
5694
  const { hover, portalReady, showHover, hideHover } = useToolHoverTooltip();
5097
5695
  const [query, setQuery] = React2.useState("");
5098
- const normalizedQuery = React2.useMemo(() => normalize(query.trim()), [query]);
5696
+ const normalizedQuery = React2.useMemo(() => normalize2(query.trim()), [query]);
5099
5697
  const filteredTools = React2.useMemo(() => {
5100
5698
  if (!normalizedQuery) return tools;
5101
5699
  return tools.filter((t) => {
5102
- if (normalize(t.label).includes(normalizedQuery)) return true;
5103
- 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;
5104
5702
  return false;
5105
5703
  });
5106
5704
  }, [tools, normalizedQuery]);
@@ -6808,8 +7406,8 @@ var init_circles = __esm({
6808
7406
  const ax = picks[0].X(), ay = picks[0].Y();
6809
7407
  const bx = picks[1].X(), by = picks[1].Y();
6810
7408
  const cx = picks[2].X(), cy = picks[2].Y();
6811
- const cross = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
6812
- if (Math.abs(cross) < 1e-6) {
7409
+ const cross2 = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
7410
+ if (Math.abs(cross2) < 1e-6) {
6813
7411
  ctx.toast?.("Kh\xF4ng v\u1EBD \u0111\u01B0\u1EE3c cung qua 3 \u0111i\u1EC3m th\u1EB3ng h\xE0ng", {
6814
7412
  variant: "warning",
6815
7413
  id: "arc3-collinear"
@@ -9588,6 +10186,141 @@ var init_tesseract = __esm({
9588
10186
  }
9589
10187
  });
9590
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
+
9591
10324
  // src/stamps/geometry-2d/ai/vision/extractProblem.ts
9592
10325
  async function extractProblemFromImage(image, opts = {}) {
9593
10326
  let raw;
@@ -9615,6 +10348,7 @@ async function extractProblemFromImage(image, opts = {}) {
9615
10348
  ok: true,
9616
10349
  text,
9617
10350
  confidence,
10351
+ warnings: detectFormulaRisk(text),
9618
10352
  usage: { inputTokens: 0, outputTokens: 0 }
9619
10353
  };
9620
10354
  }
@@ -9626,6 +10360,7 @@ function postProcess(raw) {
9626
10360
  t = t.replace(/```[\s\S]*?```/g, "").replace(/`([^`]+)`/g, "$1");
9627
10361
  t = t.replace(/\s+/g, " ").trim();
9628
10362
  t = t.normalize("NFC");
10363
+ t = repairOcrSymbols(t);
9629
10364
  if (t.length > MAX_TEXT_CHARS) t = t.slice(0, MAX_TEXT_CHARS);
9630
10365
  return t;
9631
10366
  }
@@ -9633,6 +10368,8 @@ var MIN_HIGH_CONFIDENCE_CHARS, MAX_TEXT_CHARS, TESSERACT_HIGH_CONFIDENCE_THRESHO
9633
10368
  var init_extractProblem = __esm({
9634
10369
  "src/stamps/geometry-2d/ai/vision/extractProblem.ts"() {
9635
10370
  init_tesseract();
10371
+ init_repairOcrSymbols();
10372
+ init_detectFormulaRisk();
9636
10373
  MIN_HIGH_CONFIDENCE_CHARS = 10;
9637
10374
  MAX_TEXT_CHARS = 2e3;
9638
10375
  TESSERACT_HIGH_CONFIDENCE_THRESHOLD = 70;
@@ -9644,6 +10381,14 @@ async function handleExtractProblem(image, opts = {}) {
9644
10381
  try {
9645
10382
  const r = await extractProblemFromImage(image, opts);
9646
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
+ }
9647
10392
  if (r.confidence === "low") {
9648
10393
  return {
9649
10394
  kind: "low-confidence",
@@ -9708,9 +10453,9 @@ async function fileToImagePart(file) {
9708
10453
  const bitmap = await createImageBitmap(file);
9709
10454
  const { width, height } = bitmap;
9710
10455
  const maxEdge = Math.max(width, height);
9711
- const scale = maxEdge > MAX_EDGE_PX ? MAX_EDGE_PX / maxEdge : 1;
9712
- const targetW = Math.round(width * scale);
9713
- 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);
9714
10459
  const canvas = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(targetW, targetH) : Object.assign(document.createElement("canvas"), { width: targetW, height: targetH });
9715
10460
  const ctx = canvas.getContext("2d");
9716
10461
  if (!ctx) throw new Error("Kh\xF4ng t\u1EA1o \u0111\u01B0\u1EE3c canvas 2D context");
@@ -10646,9 +11391,9 @@ async function insertStampImage(api, opts) {
10646
11391
  const old = elements.find((e) => e.id === editingId) ;
10647
11392
  const oldLongest = old ? Math.max(old.width ?? 0, old.height ?? 0) : 0;
10648
11393
  const newLongest = Math.max(width, height);
10649
- const scale = oldLongest > 0 && newLongest > 0 ? oldLongest / newLongest : 1;
10650
- const w = width * scale;
10651
- const h = height * scale;
11394
+ const scale2 = oldLongest > 0 && newLongest > 0 ? oldLongest / newLongest : 1;
11395
+ const w = width * scale2;
11396
+ const h = height * scale2;
10652
11397
  const updated = elements.map(
10653
11398
  (e) => e.id === editingId ? { ...e, fileId, customData, width: w, height: h } : e
10654
11399
  );
@@ -10857,6 +11602,21 @@ function serializePoint(obj, state) {
10857
11602
  entity: { name: obj.label, kind: "pointAtDistance", from: refs[0], through: refs[1], distance }
10858
11603
  };
10859
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
+ }
10860
11620
  // Out of DSL v1:
10861
11621
  case "onAxis":
10862
11622
  case "onPolygon":
@@ -11184,6 +11944,10 @@ function describeEntity(e) {
11184
11944
  }
11185
11945
  case "onPerpBisector":
11186
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
+ }
11187
11951
  default: {
11188
11952
  return "";
11189
11953
  }