@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
package/dist/graph-2d.js CHANGED
@@ -37,16 +37,16 @@ var __export = (target, all) => {
37
37
  };
38
38
 
39
39
  // src/core/scene/registry.ts
40
- function registerKind(def22) {
41
- if (registry.has(def22.type)) {
42
- 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`);
40
+ function registerKind(def24) {
41
+ if (registry.has(def24.type)) {
42
+ 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`);
43
43
  }
44
- registry.set(def22.type, def22);
44
+ registry.set(def24.type, def24);
45
45
  }
46
46
  function getKind(type) {
47
- const def22 = registry.get(type);
48
- if (!def22) throw new Error(`[scene] unknown kind: ${type}`);
49
- return def22;
47
+ const def24 = registry.get(type);
48
+ if (!def24) throw new Error(`[scene] unknown kind: ${type}`);
49
+ return def24;
50
50
  }
51
51
  var registry;
52
52
  var init_registry = __esm({
@@ -132,8 +132,8 @@ function reduce(draft, action) {
132
132
  return;
133
133
  }
134
134
  case "TRANSACTION": {
135
- for (const sub of action.payload.actions) {
136
- reduce(draft, sub);
135
+ for (const sub2 of action.payload.actions) {
136
+ reduce(draft, sub2);
137
137
  }
138
138
  return;
139
139
  }
@@ -392,6 +392,22 @@ var init_parser = __esm({
392
392
  }
393
393
  });
394
394
 
395
+ // src/core/scene/kinds/_label.ts
396
+ function labelOpts(labelOffset, dflt) {
397
+ const offset = labelOffset ?? dflt;
398
+ return { label: { fixed: false, ...offset ? { offset } : {} } };
399
+ }
400
+ function readLabelOffset(label) {
401
+ const off = label.evalVisProp?.("offset") ?? label.visProp?.offset;
402
+ const rel = label.relativeCoords?.scrCoords;
403
+ if (!off || !rel || off.length < 2 || rel.length < 3) return null;
404
+ return [Math.round(off[0] + rel[1]), Math.round(off[1] - rel[2])];
405
+ }
406
+ var init_label = __esm({
407
+ "src/core/scene/kinds/_label.ts"() {
408
+ }
409
+ });
410
+
395
411
  // src/core/scene/render/JxgRenderer.ts
396
412
  var JxgRenderer;
397
413
  var init_JxgRenderer = __esm({
@@ -399,6 +415,7 @@ var init_JxgRenderer = __esm({
399
415
  init_registry();
400
416
  init_types2d();
401
417
  init_parser();
418
+ init_label();
402
419
  JxgRenderer = class {
403
420
  constructor(store, board, options = {}) {
404
421
  this.elements = /* @__PURE__ */ new Map();
@@ -462,11 +479,12 @@ var init_JxgRenderer = __esm({
462
479
  }
463
480
  create(obj) {
464
481
  try {
465
- const def22 = getKind(obj.kind);
466
- const el = def22.render(obj, this.ctx());
482
+ const def24 = getKind(obj.kind);
483
+ const el = def24.render(obj, this.ctx());
467
484
  this.elements.set(obj.id, el);
468
485
  this.attachFreePointDragSync(obj, el);
469
486
  this.attachGliderDragSync(obj, el);
487
+ this.attachLabelDragSync(obj, el);
470
488
  } catch (err) {
471
489
  console.warn(`[scene/render/2d] kh\xF4ng render \u0111\u01B0\u1EE3c ${obj.kind} id="${obj.id}":`, err);
472
490
  }
@@ -581,6 +599,54 @@ var init_JxgRenderer = __esm({
581
599
  });
582
600
  });
583
601
  }
602
+ /**
603
+ * Cho phép kéo NHÃN của bất kỳ object nào có label (point/line/segment/
604
+ * circle...). Khi user kéo nhãn, JSXGraph cộng dồn vào label.relativeCoords
605
+ * (drag-delta screen px) nhưng KHÔNG đổi attribute offset → vị trí cuối =
606
+ * offset + relativeCoords. Ta gộp lại thành offset thuần (readLabelOffset),
607
+ * zero relativeCoords để khỏi double-count khi update-hook/recreate áp lại
608
+ * offset, rồi dispatch UPDATE_ATTRS { labelOffset }. Right-click → reset.
609
+ */
610
+ attachLabelDragSync(obj, el) {
611
+ const label = el?.label;
612
+ if (!label || typeof label.on !== "function") return;
613
+ const sceneId = obj.id;
614
+ label.on("up", () => {
615
+ if (this.disposed) return;
616
+ const off = readLabelOffset(label);
617
+ if (!off) return;
618
+ const cur = this.store.getState().objects[sceneId];
619
+ if (!cur) return;
620
+ const prev = cur.attrs.labelOffset;
621
+ if (prev && prev[0] === off[0] && prev[1] === off[1]) return;
622
+ try {
623
+ label.setAttribute({ offset: off });
624
+ if (label.relativeCoords?.scrCoords) {
625
+ label.relativeCoords.scrCoords[1] = 0;
626
+ label.relativeCoords.scrCoords[2] = 0;
627
+ }
628
+ } catch {
629
+ }
630
+ this.store.dispatch({
631
+ type: "UPDATE_ATTRS",
632
+ payload: { id: sceneId, patch: { labelOffset: off } }
633
+ });
634
+ });
635
+ const node = label.rendNode;
636
+ if (node?.addEventListener) {
637
+ node.addEventListener("contextmenu", (ev) => {
638
+ if (this.disposed) return;
639
+ ev.preventDefault();
640
+ const c = this.store.getState().objects[sceneId];
641
+ if (!c) return;
642
+ if (c.attrs.labelOffset === void 0) return;
643
+ this.store.dispatch({
644
+ type: "UPDATE_ATTRS",
645
+ payload: { id: sceneId, patch: { labelOffset: void 0 } }
646
+ });
647
+ });
648
+ }
649
+ }
584
650
  remove(id) {
585
651
  this.removeHalo(id);
586
652
  this.selectedIds.delete(id);
@@ -618,11 +684,11 @@ var init_JxgRenderer = __esm({
618
684
  continue;
619
685
  }
620
686
  if (Object.is(old, cur)) continue;
621
- const def22 = getKind(cur.kind);
687
+ const def24 = getKind(cur.kind);
622
688
  const existing = this.elements.get(id);
623
- if (def22.update && existing) {
689
+ if (def24.update && existing) {
624
690
  try {
625
- def22.update(cur, old, this.ctx(), existing);
691
+ def24.update(cur, old, this.ctx(), existing);
626
692
  continue;
627
693
  } catch (err) {
628
694
  console.warn(`[scene/render/2d] update fail, recreate:`, err);
@@ -717,88 +783,89 @@ var init_JxgRenderer = __esm({
717
783
  layer: 4,
718
784
  needsRegularUpdate: true
719
785
  };
786
+ const elementClass = el.elementClass;
787
+ const elType = el.elType;
788
+ const isPointLike = elementClass === 1 || elType === "point" || elType === "glider" || elType === "intersection";
789
+ const isPolygonLike = Array.isArray(el.vertices) && el.vertices.length >= 3;
790
+ const isCircleLike = elementClass === 3 || elType === "circle" || elType === "circumcircle" || elType === "incircle";
791
+ const isSegment = elType === "segment";
792
+ const isLineLike = elementClass === 2 || elType === "line" || elType === "arrow" || elType === "ray" || elType === "vector" || elType === "tangent" || elType === "normal" || elType === "parallel" || elType === "perpendicular" || elType === "bisector";
720
793
  const halos = [];
721
794
  try {
722
- switch (el.elType) {
723
- case "point":
724
- case "glider":
725
- case "intersection": {
726
- const baseSize = el.getAttribute?.("size") ?? 4;
727
- const halo = board.create("point", [
728
- () => el.X?.() ?? 0,
729
- () => el.Y?.() ?? 0
730
- ], {
731
- ...haloBase,
732
- size: baseSize + 6,
733
- face: "o",
734
- strokeWidth: 2,
735
- strokeOpacity: 0.75,
736
- fillOpacity: 0.25
737
- });
738
- halos.push(halo);
739
- break;
740
- }
741
- case "segment": {
742
- if (el.point1 && el.point2) {
743
- const halo = board.create("segment", [el.point1, el.point2], {
744
- ...haloBase,
745
- strokeWidth: 9,
746
- straightFirst: false,
747
- straightLast: false
748
- });
749
- halos.push(halo);
750
- }
751
- break;
752
- }
753
- case "line":
754
- case "arrow":
755
- case "ray":
756
- case "vector":
757
- case "tangent":
758
- case "normal":
759
- case "parallel":
760
- case "perpendicular":
761
- case "bisector": {
762
- if (el.point1 && el.point2) {
763
- const halo = board.create("line", [el.point1, el.point2], {
764
- ...haloBase,
765
- strokeWidth: 9
766
- });
767
- halos.push(halo);
768
- }
769
- break;
770
- }
771
- case "circle": {
772
- if (el.center && typeof el.Radius === "function") {
773
- const halo = board.create("circle", [el.center, () => el.Radius?.() ?? 0], {
774
- ...haloBase,
775
- strokeWidth: 9,
776
- fillOpacity: 0
777
- });
778
- halos.push(halo);
779
- }
780
- break;
781
- }
782
- case "polygon": {
783
- if (Array.isArray(el.vertices) && el.vertices.length >= 3) {
784
- const last = el.vertices.length - 1;
785
- const verts = el.vertices[last] === el.vertices[0] ? el.vertices.slice(0, last) : el.vertices.slice();
786
- const halo = board.create("polygon", verts, {
787
- ...haloBase,
788
- fillOpacity: 0.2,
789
- borders: {
790
- strokeColor: SEL_STROKE,
791
- strokeWidth: 7,
792
- strokeOpacity: 0.55,
793
- highlight: false
794
- }
795
- });
796
- halos.push(halo);
795
+ if (isPointLike) {
796
+ const baseSize = el.getAttribute?.("size") ?? 4;
797
+ const halo = board.create("point", [
798
+ () => el.X?.() ?? 0,
799
+ () => el.Y?.() ?? 0
800
+ ], {
801
+ ...haloBase,
802
+ size: baseSize + 6,
803
+ face: "o",
804
+ strokeWidth: 2,
805
+ strokeOpacity: 0.75,
806
+ fillOpacity: 0.25
807
+ });
808
+ halos.push(halo);
809
+ } else if (isPolygonLike) {
810
+ const verts = el.vertices;
811
+ const last = verts.length - 1;
812
+ const trimmed = verts[last] === verts[0] ? verts.slice(0, last) : verts.slice();
813
+ const halo = board.create("polygon", trimmed, {
814
+ ...haloBase,
815
+ fillOpacity: 0.2,
816
+ borders: {
817
+ strokeColor: SEL_STROKE,
818
+ strokeWidth: 7,
819
+ strokeOpacity: 0.55,
820
+ highlight: false
797
821
  }
798
- break;
822
+ });
823
+ halos.push(halo);
824
+ } else if (isCircleLike && el.center && typeof el.Radius === "function") {
825
+ const halo = board.create("circle", [el.center, () => el.Radius?.() ?? 0], {
826
+ ...haloBase,
827
+ strokeWidth: 9,
828
+ fillOpacity: 0
829
+ });
830
+ halos.push(halo);
831
+ } else if (isSegment && el.point1 && el.point2) {
832
+ const halo = board.create("segment", [el.point1, el.point2], {
833
+ ...haloBase,
834
+ strokeWidth: 9,
835
+ straightFirst: false,
836
+ straightLast: false
837
+ });
838
+ halos.push(halo);
839
+ } else if (isLineLike && el.point1 && el.point2) {
840
+ const halo = board.create("line", [el.point1, el.point2], {
841
+ ...haloBase,
842
+ strokeWidth: 9
843
+ });
844
+ halos.push(halo);
845
+ } else if (el.center && (el.radiuspoint ?? el.radiusPoint) && (el.anglepoint ?? el.anglePoint) && (elType === "arc" || elType === "semicircle" || elType === "circumcirclearc" || elType === "sector" || elType === "angle")) {
846
+ const rp = el.radiuspoint ?? el.radiusPoint;
847
+ const ap = el.anglepoint ?? el.anglePoint;
848
+ if (elType === "sector") {
849
+ halos.push(board.create("sector", [el.center, rp, ap], {
850
+ ...haloBase,
851
+ strokeWidth: 7,
852
+ fillOpacity: 0.2
853
+ }));
854
+ } else if (elType === "angle") {
855
+ halos.push(board.create("angle", [rp, el.center, ap], {
856
+ ...haloBase,
857
+ strokeWidth: 7,
858
+ fillOpacity: 0.2,
859
+ radius: el.getAttribute?.("radius") ?? 1
860
+ }));
861
+ } else {
862
+ halos.push(board.create("arc", [el.center, rp, ap], {
863
+ ...haloBase,
864
+ strokeWidth: 9,
865
+ fillColor: "none",
866
+ fillOpacity: 0
867
+ }));
799
868
  }
800
- default:
801
- break;
802
869
  }
803
870
  } catch (err) {
804
871
  console.warn("[scene/render/2d] halo create fail:", err);
@@ -1090,8 +1157,36 @@ function constraintRefs(c) {
1090
1157
  return [c.polygonId];
1091
1158
  case "onSphere":
1092
1159
  return [c.sphereId];
1093
- default:
1160
+ case "midpoint":
1161
+ return [c.p1, c.p2];
1162
+ case "centroid":
1163
+ return [...c.vertices];
1164
+ case "intersectionLines":
1165
+ return [c.a1, c.b1, c.a2, c.b2];
1166
+ case "intersectionLinePlane":
1167
+ return [c.a, c.b, c.plane];
1168
+ case "perpFootLine":
1169
+ return [c.from, c.a, c.b];
1170
+ case "perpFootPlane":
1171
+ return [c.from, c.plane];
1172
+ case "circumsphereCenter":
1173
+ return [...c.vertices];
1174
+ case "pyramidInsphereCenter":
1175
+ return [c.apex, ...c.vertices];
1176
+ case "faceCircumcenter":
1177
+ return [...c.vertices];
1178
+ case "pointAboveFace":
1179
+ return [c.base, c.apex, ...c.vertices];
1180
+ // Kind KHÔNG có ref scene-object (toạ độ literal): liệt kê TƯỜNG MINH để
1181
+ // exhaustive never-guard bên dưới buộc khai báo khi thêm constraint kind mới
1182
+ // (mô phỏng constraintRefs2D — quên case = cascade-delete/deps sai âm thầm).
1183
+ case "free":
1184
+ case "onGround":
1185
+ case "onAxis":
1094
1186
  return [];
1187
+ default: {
1188
+ return [];
1189
+ }
1095
1190
  }
1096
1191
  }
1097
1192
  var init_d_constraint = __esm({
@@ -1099,12 +1194,359 @@ var init_d_constraint = __esm({
1099
1194
  }
1100
1195
  });
1101
1196
 
1197
+ // src/core/scene/kinds/constraint3d-math.ts
1198
+ function sub(a, b) {
1199
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
1200
+ }
1201
+ function add(a, b) {
1202
+ return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
1203
+ }
1204
+ function scale(a, k) {
1205
+ return [a[0] * k, a[1] * k, a[2] * k];
1206
+ }
1207
+ function dot(a, b) {
1208
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
1209
+ }
1210
+ function cross(a, b) {
1211
+ 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]];
1212
+ }
1213
+ function norm(a) {
1214
+ return Math.sqrt(dot(a, a));
1215
+ }
1216
+ function normalize(a) {
1217
+ const n = norm(a);
1218
+ return n === 0 ? a : scale(a, 1 / n);
1219
+ }
1220
+ function getPointWorld(id, state) {
1221
+ const obj = state.objects[id];
1222
+ if (!obj || obj.kind !== "point3d") {
1223
+ throw new Error(`constraintMath: point ${id} not found`);
1224
+ }
1225
+ const attrs = obj.attrs;
1226
+ return constraintToWorld(attrs.constraint, state);
1227
+ }
1228
+ function getPlaneBasis(planeObj, state) {
1229
+ const a = planeObj.attrs;
1230
+ let p1, p2, p3;
1231
+ if (a.construction) {
1232
+ const r = planeConstructionWorld(a.construction, state);
1233
+ p1 = r.p1;
1234
+ p2 = r.p2;
1235
+ p3 = r.p3;
1236
+ } else {
1237
+ p1 = getPointWorld(a.p1, state);
1238
+ p2 = getPointWorld(a.p2, state);
1239
+ p3 = getPointWorld(a.p3, state);
1240
+ }
1241
+ const basis1 = sub(p2, p1);
1242
+ const tmp = sub(p3, p1);
1243
+ const normal = normalize(cross(basis1, tmp));
1244
+ const basis2 = cross(normal, basis1);
1245
+ return { origin: p1, basis1, basis2, normal };
1246
+ }
1247
+ function orthoBasisFromNormal(n) {
1248
+ const seed = Math.abs(n[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0];
1249
+ const basis1 = normalize(cross(n, seed));
1250
+ const basis2 = cross(n, basis1);
1251
+ return { basis1, basis2 };
1252
+ }
1253
+ function planeConstructionWorld(c, state) {
1254
+ 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?");
1255
+ _recDepth++;
1256
+ try {
1257
+ return planeConstructionWorldInner(c, state);
1258
+ } finally {
1259
+ _recDepth--;
1260
+ }
1261
+ }
1262
+ function planeConstructionWorldInner(c, state) {
1263
+ switch (c.kind) {
1264
+ case "planeParallelThrough": {
1265
+ const P = getPointWorld(c.point, state);
1266
+ const ref = state.objects[c.refPlane];
1267
+ if (!ref || ref.kind !== "plane3d") throw new Error("planeParallelThrough: m\u1EB7t tham chi\u1EBFu thi\u1EBFu");
1268
+ const { basis1, basis2 } = getPlaneBasis(ref, state);
1269
+ if (norm(basis1) < 1e-12 || norm(basis2) < 1e-12) {
1270
+ return { p1: P, p2: add(P, [1, 0, 0]), p3: add(P, [0, 1, 0]) };
1271
+ }
1272
+ return { p1: P, p2: add(P, basis1), p3: add(P, basis2) };
1273
+ }
1274
+ case "planePerpToLine": {
1275
+ const P = getPointWorld(c.point, state);
1276
+ const dir = sub(getPointWorld(c.lineB, state), getPointWorld(c.lineA, state));
1277
+ const dn = norm(dir);
1278
+ if (dn < 1e-12) return { p1: P, p2: add(P, [1, 0, 0]), p3: add(P, [0, 1, 0]) };
1279
+ const { basis1, basis2 } = orthoBasisFromNormal(scale(dir, 1 / dn));
1280
+ return { p1: P, p2: add(P, basis1), p3: add(P, basis2) };
1281
+ }
1282
+ default: {
1283
+ throw new Error("planeConstructionWorld: kind kh\xF4ng h\u1ED7 tr\u1EE3");
1284
+ }
1285
+ }
1286
+ }
1287
+ function lineDefiningPointIds(line) {
1288
+ if (line.kind === "line3d" || line.kind === "segment3d") {
1289
+ const a = line.attrs;
1290
+ return a.p1 && a.p2 ? [a.p1, a.p2] : null;
1291
+ }
1292
+ if (line.kind === "ray3d") {
1293
+ const a = line.attrs;
1294
+ return [a.origin, a.through];
1295
+ }
1296
+ if (line.kind === "vector3d") {
1297
+ const a = line.attrs;
1298
+ return [a.from, a.to];
1299
+ }
1300
+ return null;
1301
+ }
1302
+ function lineEndpointsWorld(lineId, state, ctx) {
1303
+ const line = state.objects[lineId];
1304
+ if (!line) throw new Error(`${ctx}: \u0111\u01B0\u1EDDng ${lineId} kh\xF4ng t\u1ED3n t\u1EA1i`);
1305
+ const ids = lineDefiningPointIds(line);
1306
+ if (!ids) throw new Error(`${ctx}: kind ${line.kind} kh\xF4ng ph\u1EA3i \u0111\u01B0\u1EDDng`);
1307
+ return { a: getPointWorld(ids[0], state), b: getPointWorld(ids[1], state) };
1308
+ }
1309
+ function lineLineClosestMidpoint(A, B, C, D) {
1310
+ const u = sub(B, A), v = sub(D, C), w0 = sub(A, C);
1311
+ const a = dot(u, u), b = dot(u, v), cc = dot(v, v), d = dot(u, w0), e = dot(v, w0);
1312
+ const denom = a * cc - b * b;
1313
+ if (Math.abs(denom) < 1e-12) return scale(add(A, C), 0.5);
1314
+ const sc = (b * e - cc * d) / denom;
1315
+ const tc = (a * e - b * d) / denom;
1316
+ return scale(add(add(A, scale(u, sc)), add(C, scale(v, tc))), 0.5);
1317
+ }
1318
+ function planeOriginNormal(planeId, state, ctx) {
1319
+ const plane = state.objects[planeId];
1320
+ if (!plane || plane.kind !== "plane3d") throw new Error(`${ctx}: m\u1EB7t ph\u1EB3ng ${planeId} thi\u1EBFu`);
1321
+ const { origin, normal } = getPlaneBasis(plane, state);
1322
+ return { origin, normal };
1323
+ }
1324
+ function lineConstructionWorld(c, state) {
1325
+ switch (c.kind) {
1326
+ case "planePlaneIntersection": {
1327
+ const P1 = planeOriginNormal(c.plane1, state, "planePlaneIntersection");
1328
+ const P2 = planeOriginNormal(c.plane2, state, "planePlaneIntersection");
1329
+ const d = cross(P1.normal, P2.normal);
1330
+ const dd = dot(d, d);
1331
+ if (dd < 1e-12) {
1332
+ return { a: P1.origin, b: add(P1.origin, P2.normal) };
1333
+ }
1334
+ const c1 = dot(P1.normal, P1.origin);
1335
+ const c2 = dot(P2.normal, P2.origin);
1336
+ const p0 = scale(add(scale(cross(P2.normal, d), c1), scale(cross(d, P1.normal), c2)), 1 / dd);
1337
+ return { a: p0, b: add(p0, d) };
1338
+ }
1339
+ case "lineParallelThrough": {
1340
+ const P = getPointWorld(c.point, state);
1341
+ const dir = sub(getPointWorld(c.dirB, state), getPointWorld(c.dirA, state));
1342
+ if (norm(dir) < 1e-12) return { a: P, b: add(P, [1, 0, 0]) };
1343
+ return { a: P, b: add(P, dir) };
1344
+ }
1345
+ case "linePerpToPlane": {
1346
+ const P = getPointWorld(c.point, state);
1347
+ const { normal } = planeOriginNormal(c.plane, state, "linePerpToPlane");
1348
+ return { a: P, b: add(P, normal) };
1349
+ }
1350
+ default: {
1351
+ throw new Error("lineConstructionWorld: kind kh\xF4ng h\u1ED7 tr\u1EE3");
1352
+ }
1353
+ }
1354
+ }
1355
+ function solve3(M, b) {
1356
+ 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]);
1357
+ const det = det3(M);
1358
+ if (Math.abs(det) < 1e-9) return null;
1359
+ const colDet = (j) => {
1360
+ const A = M.map((r) => r.slice());
1361
+ for (let i = 0; i < 3; i++) A[i][j] = b[i];
1362
+ return det3(A);
1363
+ };
1364
+ return [colDet(0) / det, colDet(1) / det, colDet(2) / det];
1365
+ }
1366
+ function constraintToWorld(c, state) {
1367
+ if (_recDepth >= MAX_REC_DEPTH) throw new Error("constraintToWorld: \u0111\u1EC7 quy qu\xE1 s\xE2u \u2014 chu tr\xECnh tham chi\u1EBFu?");
1368
+ _recDepth++;
1369
+ try {
1370
+ return constraintToWorldInner(c, state);
1371
+ } finally {
1372
+ _recDepth--;
1373
+ }
1374
+ }
1375
+ function constraintToWorldInner(c, state) {
1376
+ switch (c.kind) {
1377
+ case "free":
1378
+ return [c.x, c.y, c.z];
1379
+ case "onGround":
1380
+ return [c.x, c.y, 0];
1381
+ case "onAxis": {
1382
+ if (c.axis === "x") return [c.t, 0, 0];
1383
+ if (c.axis === "y") return [0, c.t, 0];
1384
+ return [0, 0, c.t];
1385
+ }
1386
+ case "onPlane": {
1387
+ const plane = state.objects[c.planeId];
1388
+ if (!plane || plane.kind !== "plane3d") throw new Error("onPlane: plane missing");
1389
+ const { origin, basis1, basis2 } = getPlaneBasis(plane, state);
1390
+ return add(add(origin, scale(basis1, c.u)), scale(basis2, c.v));
1391
+ }
1392
+ case "onLine": {
1393
+ const { a, b } = lineEndpointsWorld(c.lineId, state, "onLine");
1394
+ return add(a, scale(sub(b, a), c.t));
1395
+ }
1396
+ case "onPolygon": {
1397
+ const pg = state.objects[c.polygonId];
1398
+ if (!pg || pg.kind !== "polygon3d") throw new Error("onPolygon: parent missing");
1399
+ const v = pg.attrs.vertices;
1400
+ if (v.length < 3) throw new Error("onPolygon: < 3 vertices");
1401
+ const p1 = getPointWorld(v[0], state);
1402
+ const p2 = getPointWorld(v[1], state);
1403
+ const p3 = getPointWorld(v[2], state);
1404
+ const basis1 = sub(p2, p1);
1405
+ const tmp = sub(p3, p1);
1406
+ const normal = normalize(cross(basis1, tmp));
1407
+ const basis2 = cross(normal, basis1);
1408
+ return add(add(p1, scale(basis1, c.u)), scale(basis2, c.v));
1409
+ }
1410
+ case "onSphere": {
1411
+ const sph = state.objects[c.sphereId];
1412
+ if (!sph || sph.kind !== "sphere3d") throw new Error("onSphere: parent missing");
1413
+ const a = sph.attrs;
1414
+ const center = getPointWorld(a.center, state);
1415
+ const surface = getPointWorld(a.surfacePoint, state);
1416
+ const radius = norm(sub(surface, center));
1417
+ const x = center[0] + radius * Math.sin(c.phi) * Math.cos(c.theta);
1418
+ const y = center[1] + radius * Math.sin(c.phi) * Math.sin(c.theta);
1419
+ const z = center[2] + radius * Math.cos(c.phi);
1420
+ return [x, y, z];
1421
+ }
1422
+ // ───── Điểm phái sinh (v1) ─────
1423
+ case "midpoint": {
1424
+ const p1 = getPointWorld(c.p1, state);
1425
+ const p2 = getPointWorld(c.p2, state);
1426
+ return scale(add(p1, p2), 0.5);
1427
+ }
1428
+ case "centroid": {
1429
+ const n = c.vertices.length;
1430
+ if (n === 0) return [0, 0, 0];
1431
+ let acc = [0, 0, 0];
1432
+ for (const id of c.vertices) acc = add(acc, getPointWorld(id, state));
1433
+ return scale(acc, 1 / n);
1434
+ }
1435
+ case "circumsphereCenter": {
1436
+ const P = c.vertices.map((id) => getPointWorld(id, state));
1437
+ if (P.length < 4) return P.length ? P[0] : [0, 0, 0];
1438
+ const p0 = P[0];
1439
+ const M = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
1440
+ const rhs = [0, 0, 0];
1441
+ for (let i = 1; i < P.length; i++) {
1442
+ const r = [2 * (P[i][0] - p0[0]), 2 * (P[i][1] - p0[1]), 2 * (P[i][2] - p0[2])];
1443
+ const bi = dot(P[i], P[i]) - dot(p0, p0);
1444
+ for (let a = 0; a < 3; a++) {
1445
+ for (let bcol = 0; bcol < 3; bcol++) M[a][bcol] += r[a] * r[bcol];
1446
+ rhs[a] += r[a] * bi;
1447
+ }
1448
+ }
1449
+ const sol = solve3(M, rhs);
1450
+ if (sol) return sol;
1451
+ let acc = [0, 0, 0];
1452
+ for (const p of P) acc = add(acc, p);
1453
+ return scale(acc, 1 / P.length);
1454
+ }
1455
+ case "pyramidInsphereCenter": {
1456
+ const S = getPointWorld(c.apex, state);
1457
+ const base = c.vertices.map((id) => getPointWorld(id, state));
1458
+ const n = base.length;
1459
+ if (n < 3) return S;
1460
+ let G = [0, 0, 0];
1461
+ for (const p of base) G = add(G, p);
1462
+ G = scale(G, 1 / n);
1463
+ let nb = normalize(cross(sub(base[1], base[0]), sub(base[2], base[0])));
1464
+ if (dot(nb, sub(S, G)) < 0) nb = scale(nb, -1);
1465
+ let m = normalize(cross(sub(base[0], S), sub(base[1], S)));
1466
+ if (dot(m, sub(G, base[0])) < 0) m = scale(m, -1);
1467
+ const denom = 1 - dot(m, nb);
1468
+ const s = Math.abs(denom) < 1e-9 ? NaN : dot(m, sub(G, base[0])) / denom;
1469
+ if (!Number.isFinite(s) || s <= 0) {
1470
+ return scale(add(S, scale(G, n)), 1 / (n + 1));
1471
+ }
1472
+ return add(G, scale(nb, s));
1473
+ }
1474
+ case "faceCircumcenter": {
1475
+ const P = c.vertices.map((id) => getPointWorld(id, state));
1476
+ if (P.length < 3) return P.length ? P[0] : [0, 0, 0];
1477
+ const [p0, p1, p2] = P;
1478
+ const e1 = sub(p1, p0), e2 = sub(p2, p0);
1479
+ const nrm = cross(e1, e2);
1480
+ const M = [
1481
+ [2 * e1[0], 2 * e1[1], 2 * e1[2]],
1482
+ [2 * e2[0], 2 * e2[1], 2 * e2[2]],
1483
+ [nrm[0], nrm[1], nrm[2]]
1484
+ ];
1485
+ const rhs = [dot(p1, p1) - dot(p0, p0), dot(p2, p2) - dot(p0, p0), dot(nrm, p0)];
1486
+ const sol = solve3(M, rhs);
1487
+ if (sol) return sol;
1488
+ return scale(add(add(p0, p1), p2), 1 / 3);
1489
+ }
1490
+ case "pointAboveFace": {
1491
+ const G = getPointWorld(c.base, state);
1492
+ const P = c.vertices.map((id) => getPointWorld(id, state));
1493
+ if (P.length < 3) return G;
1494
+ let n = normalize(cross(sub(P[1], P[0]), sub(P[2], P[0])));
1495
+ const S = getPointWorld(c.apex, state);
1496
+ if (dot(n, sub(S, P[0])) < 0) n = scale(n, -1);
1497
+ const h = dot(sub(S, P[0]), n);
1498
+ if (!Number.isFinite(h) || h <= 1e-9) return G;
1499
+ return add(G, scale(n, h));
1500
+ }
1501
+ case "intersectionLines": {
1502
+ const A = getPointWorld(c.a1, state), B = getPointWorld(c.b1, state);
1503
+ const C = getPointWorld(c.a2, state), D = getPointWorld(c.b2, state);
1504
+ return lineLineClosestMidpoint(A, B, C, D);
1505
+ }
1506
+ case "intersectionLinePlane": {
1507
+ const A = getPointWorld(c.a, state), B = getPointWorld(c.b, state);
1508
+ const plane = state.objects[c.plane];
1509
+ if (!plane || plane.kind !== "plane3d") throw new Error("intersectionLinePlane: m\u1EB7t ph\u1EB3ng thi\u1EBFu");
1510
+ const { origin, normal } = getPlaneBasis(plane, state);
1511
+ const dir = sub(B, A);
1512
+ const dn = dot(dir, normal);
1513
+ if (Math.abs(dn) < 1e-12) return A;
1514
+ const t = dot(sub(origin, A), normal) / dn;
1515
+ return add(A, scale(dir, t));
1516
+ }
1517
+ case "perpFootLine": {
1518
+ const P = getPointWorld(c.from, state);
1519
+ const A = getPointWorld(c.a, state), B = getPointWorld(c.b, state);
1520
+ const u = sub(B, A);
1521
+ const uu = dot(u, u);
1522
+ const t = uu === 0 ? 0 : dot(sub(P, A), u) / uu;
1523
+ return add(A, scale(u, t));
1524
+ }
1525
+ case "perpFootPlane": {
1526
+ const P = getPointWorld(c.from, state);
1527
+ const plane = state.objects[c.plane];
1528
+ if (!plane || plane.kind !== "plane3d") throw new Error("perpFootPlane: m\u1EB7t ph\u1EB3ng thi\u1EBFu");
1529
+ const { origin, normal } = getPlaneBasis(plane, state);
1530
+ const dist2 = dot(sub(P, origin), normal);
1531
+ return sub(P, scale(normal, dist2));
1532
+ }
1533
+ }
1534
+ }
1535
+ var MAX_REC_DEPTH, _recDepth;
1536
+ var init_constraint3d_math = __esm({
1537
+ "src/core/scene/kinds/constraint3d-math.ts"() {
1538
+ MAX_REC_DEPTH = 512;
1539
+ _recDepth = 0;
1540
+ }
1541
+ });
1542
+
1102
1543
  // src/core/scene/kinds/point3d.ts
1103
1544
  var def;
1104
1545
  var init_point3d = __esm({
1105
1546
  "src/core/scene/kinds/point3d.ts"() {
1106
1547
  init_registry();
1107
1548
  init_d_constraint();
1549
+ init_constraint3d_math();
1108
1550
  def = {
1109
1551
  type: "point3d",
1110
1552
  schemaVersion: 1,
@@ -1142,6 +1584,15 @@ var init_point3d = __esm({
1142
1584
  if (c.kind === "onLine") return `${obj.label} tr\xEAn \u0111\u01B0\u1EDDng ${c.lineId}`;
1143
1585
  if (c.kind === "onPolygon") return `${obj.label} tr\xEAn \u0111a gi\xE1c ${c.polygonId}`;
1144
1586
  if (c.kind === "onSphere") return `${obj.label} tr\xEAn m\u1EB7t c\u1EA7u ${c.sphereId}`;
1587
+ if (c.kind === "midpoint") return `${obj.label} = trung \u0111i\u1EC3m ${c.p1}${c.p2}`;
1588
+ if (c.kind === "centroid") return `${obj.label} = tr\u1ECDng t\xE2m ${c.vertices.join("")}`;
1589
+ if (c.kind === "intersectionLines") return `${obj.label} = giao 2 \u0111\u01B0\u1EDDng (${c.a1}${c.b1}, ${c.a2}${c.b2})`;
1590
+ if (c.kind === "intersectionLinePlane") return `${obj.label} = giao ${c.a}${c.b} \u2229 ${c.plane}`;
1591
+ if (c.kind === "perpFootLine") return `${obj.label} = ch\xE2n \u22A5 t\u1EEB ${c.from} xu\u1ED1ng ${c.a}${c.b}`;
1592
+ if (c.kind === "perpFootPlane") return `${obj.label} = ch\xE2n \u22A5 t\u1EEB ${c.from} xu\u1ED1ng ${c.plane}`;
1593
+ if (c.kind === "pyramidInsphereCenter") return `${obj.label} = t\xE2m c\u1EA7u n\u1ED9i ti\u1EBFp ch\xF3p ${c.apex}.${c.vertices.join("")}`;
1594
+ if (c.kind === "faceCircumcenter") return `${obj.label} = t\xE2m ngo\u1EA1i ti\u1EBFp ${c.vertices.join("")}`;
1595
+ if (c.kind === "pointAboveFace") return `${obj.label} = \u0111\u1EC9nh tr\u1EE5c tr\u1EE5 \u22A5 m\u1EB7t ${c.vertices.join("")}`;
1145
1596
  return obj.label;
1146
1597
  },
1147
1598
  render: (obj, ctx) => {
@@ -1191,6 +1642,15 @@ var init_point3d = __esm({
1191
1642
  () => sph.F(c.theta, c.phi)[2]
1192
1643
  ], opts);
1193
1644
  }
1645
+ const getState = ctx.getState;
1646
+ if (getState) {
1647
+ const cw = () => constraintToWorld(c, getState());
1648
+ return view.create("point3d", [
1649
+ () => cw()[0],
1650
+ () => cw()[1],
1651
+ () => cw()[2]
1652
+ ], { ...opts, needsRegularUpdate: true });
1653
+ }
1194
1654
  return view.create("point3d", [0, 0, 0], opts);
1195
1655
  }
1196
1656
  };
@@ -1252,32 +1712,77 @@ var init_segment3d = __esm({
1252
1712
  });
1253
1713
 
1254
1714
  // src/core/scene/kinds/line3d.ts
1715
+ function line3dConstructionRefs(c) {
1716
+ switch (c.kind) {
1717
+ case "planePlaneIntersection":
1718
+ return [c.plane1, c.plane2];
1719
+ case "lineParallelThrough":
1720
+ return [c.point, c.dirA, c.dirB];
1721
+ case "linePerpToPlane":
1722
+ return [c.point, c.plane];
1723
+ default: {
1724
+ return [];
1725
+ }
1726
+ }
1727
+ }
1728
+ var def3;
1255
1729
  var init_line3d = __esm({
1256
1730
  "src/core/scene/kinds/line3d.ts"() {
1257
1731
  init_registry();
1258
1732
  init_labelOf();
1259
- registerKind({
1733
+ init_constraint3d_math();
1734
+ def3 = {
1260
1735
  type: "line3d",
1261
1736
  schemaVersion: 1,
1262
1737
  migrate: {},
1263
1738
  validate: (a) => {
1264
- if (!a?.p1 || !a?.p2) throw new Error("line3d: p1/p2 required");
1739
+ if (a?.construction) return;
1740
+ if (!a?.p1 || !a?.p2) throw new Error("line3d: p1/p2 required (ho\u1EB7c construction)");
1741
+ },
1742
+ dependsOn: (a) => a.construction ? line3dConstructionRefs(a.construction) : [a.p1, a.p2],
1743
+ describe: (obj, state) => {
1744
+ const c = obj.attrs.construction;
1745
+ if (!c) return `\u0110\u01B0\u1EDDng ${obj.label} qua ${labelOf(obj.attrs.p1, state)}, ${labelOf(obj.attrs.p2, state)}`;
1746
+ switch (c.kind) {
1747
+ case "planePlaneIntersection":
1748
+ return `Giao tuy\u1EBFn ${obj.label} = ${labelOf(c.plane1, state)} \u2229 ${labelOf(c.plane2, state)}`;
1749
+ case "lineParallelThrough":
1750
+ return `${obj.label} \u2225 ${labelOf(c.dirA, state)}${labelOf(c.dirB, state)} qua ${labelOf(c.point, state)}`;
1751
+ case "linePerpToPlane":
1752
+ return `${obj.label} \u22A5 ${labelOf(c.plane, state)} qua ${labelOf(c.point, state)}`;
1753
+ default:
1754
+ return obj.label;
1755
+ }
1265
1756
  },
1266
- dependsOn: (a) => [a.p1, a.p2],
1267
- describe: (obj, state) => `\u0110\u01B0\u1EDDng ${obj.label} qua ${labelOf(obj.attrs.p1, state)}, ${labelOf(obj.attrs.p2, state)}`,
1268
1757
  render: (obj, ctx) => {
1269
1758
  const view = ctx.jxg;
1759
+ const baseOpts = {
1760
+ strokeColor: obj.attrs.color ?? "#0f172a",
1761
+ strokeWidth: 2,
1762
+ visible: obj.visible
1763
+ };
1764
+ const c = obj.attrs.construction;
1765
+ if (c) {
1766
+ const getState = ctx.getState;
1767
+ if (!getState) throw new Error("line3d construction: c\u1EA7n ctx.getState");
1768
+ const lc = () => lineConstructionWorld(c, getState());
1769
+ return view.create("line3d", [() => lc().a, () => lc().b], {
1770
+ ...baseOpts,
1771
+ straightFirst: true,
1772
+ straightLast: true,
1773
+ needsRegularUpdate: true
1774
+ });
1775
+ }
1270
1776
  const pA = ctx.resolveRef(obj.attrs.p1);
1271
1777
  const pB = ctx.resolveRef(obj.attrs.p2);
1272
1778
  return view.create("line3d", [pA, pB], {
1779
+ ...baseOpts,
1273
1780
  straightFirst: true,
1274
- straightLast: true,
1275
- strokeColor: obj.attrs.color ?? "#0f172a",
1276
- strokeWidth: 2,
1277
- visible: obj.visible
1781
+ straightLast: true
1278
1782
  });
1279
1783
  }
1280
- });
1784
+ };
1785
+ registerKind(def3);
1281
1786
  }
1282
1787
  });
1283
1788
 
@@ -1343,33 +1848,77 @@ var init_vector3d = __esm({
1343
1848
  });
1344
1849
 
1345
1850
  // src/core/scene/kinds/plane3d.ts
1851
+ function plane3dConstructionRefs(c) {
1852
+ switch (c.kind) {
1853
+ case "planeParallelThrough":
1854
+ return [c.point, c.refPlane];
1855
+ case "planePerpToLine":
1856
+ return [c.point, c.lineA, c.lineB];
1857
+ default: {
1858
+ return [];
1859
+ }
1860
+ }
1861
+ }
1862
+ var def4;
1346
1863
  var init_plane3d = __esm({
1347
1864
  "src/core/scene/kinds/plane3d.ts"() {
1348
1865
  init_registry();
1349
1866
  init_labelOf();
1350
- registerKind({
1867
+ init_constraint3d_math();
1868
+ def4 = {
1351
1869
  type: "plane3d",
1352
1870
  schemaVersion: 1,
1353
1871
  migrate: {},
1354
1872
  validate: (a) => {
1355
- if (!a?.p1 || !a?.p2 || !a?.p3) throw new Error("plane3d: c\u1EA7n 3 \u0111i\u1EC3m");
1873
+ if (a?.construction) return;
1874
+ if (!a?.p1 || !a?.p2 || !a?.p3) throw new Error("plane3d: c\u1EA7n 3 \u0111i\u1EC3m (ho\u1EB7c construction)");
1875
+ },
1876
+ dependsOn: (a) => a.construction ? plane3dConstructionRefs(a.construction) : [a.p1, a.p2, a.p3],
1877
+ describe: (obj, state) => {
1878
+ const c = obj.attrs.construction;
1879
+ if (!c) {
1880
+ return `M\u1EB7t ${obj.label} qua ${labelOf(obj.attrs.p1, state)}, ${labelOf(obj.attrs.p2, state)}, ${labelOf(obj.attrs.p3, state)}`;
1881
+ }
1882
+ switch (c.kind) {
1883
+ case "planeParallelThrough":
1884
+ return `${obj.label} \u2225 ${labelOf(c.refPlane, state)} qua ${labelOf(c.point, state)}`;
1885
+ case "planePerpToLine":
1886
+ return `${obj.label} \u22A5 ${labelOf(c.lineA, state)}${labelOf(c.lineB, state)} qua ${labelOf(c.point, state)}`;
1887
+ default:
1888
+ return obj.label;
1889
+ }
1356
1890
  },
1357
- dependsOn: (a) => [a.p1, a.p2, a.p3],
1358
- describe: (obj, state) => `M\u1EB7t ${obj.label} qua ${labelOf(obj.attrs.p1, state)}, ${labelOf(obj.attrs.p2, state)}, ${labelOf(obj.attrs.p3, state)}`,
1359
1891
  render: (obj, ctx) => {
1360
1892
  const view = ctx.jxg;
1361
- return view.create("plane3d", [
1362
- ctx.resolveRef(obj.attrs.p1),
1363
- ctx.resolveRef(obj.attrs.p2),
1364
- ctx.resolveRef(obj.attrs.p3)
1365
- ], {
1893
+ const opts = {
1366
1894
  fillOpacity: 0.15,
1367
1895
  fillColor: obj.attrs.color ?? "#60a5fa",
1368
1896
  strokeColor: obj.attrs.color ?? "#60a5fa",
1369
1897
  visible: obj.visible
1370
- });
1898
+ };
1899
+ const c = obj.attrs.construction;
1900
+ if (c) {
1901
+ const getState = ctx.getState;
1902
+ if (!getState) throw new Error("plane3d construction: c\u1EA7n ctx.getState");
1903
+ const pc = () => planeConstructionWorld(c, getState());
1904
+ const diff = (which) => {
1905
+ const r = pc();
1906
+ const q = r[which];
1907
+ return [q[0] - r.p1[0], q[1] - r.p1[1], q[2] - r.p1[2]];
1908
+ };
1909
+ return view.create("plane3d", [() => pc().p1, () => diff("p2"), () => diff("p3")], {
1910
+ ...opts,
1911
+ needsRegularUpdate: true
1912
+ });
1913
+ }
1914
+ return view.create("plane3d", [
1915
+ ctx.resolveRef(obj.attrs.p1),
1916
+ ctx.resolveRef(obj.attrs.p2),
1917
+ ctx.resolveRef(obj.attrs.p3)
1918
+ ], opts);
1371
1919
  }
1372
- });
1920
+ };
1921
+ registerKind(def4);
1373
1922
  }
1374
1923
  });
1375
1924
 
@@ -1468,11 +2017,37 @@ var init_polyhedron3d = __esm({
1468
2017
  }
1469
2018
  });
1470
2019
 
2020
+ // src/core/scene/kinds/_ringBasis.ts
2021
+ function perpBasis(axis) {
2022
+ const n = Math.hypot(axis[0], axis[1], axis[2]);
2023
+ if (n < 1e-12) return [[1, 0, 0], [0, 1, 0]];
2024
+ const a = [axis[0] / n, axis[1] / n, axis[2] / n];
2025
+ const seed = Math.abs(a[2]) < 0.9 ? [0, 0, 1] : [1, 0, 0];
2026
+ let u = [
2027
+ seed[1] * a[2] - seed[2] * a[1],
2028
+ seed[2] * a[0] - seed[0] * a[2],
2029
+ seed[0] * a[1] - seed[1] * a[0]
2030
+ ];
2031
+ const un = Math.hypot(u[0], u[1], u[2]) || 1;
2032
+ u = [u[0] / un, u[1] / un, u[2] / un];
2033
+ const v = [
2034
+ a[1] * u[2] - a[2] * u[1],
2035
+ a[2] * u[0] - a[0] * u[2],
2036
+ a[0] * u[1] - a[1] * u[0]
2037
+ ];
2038
+ return [u, v];
2039
+ }
2040
+ var init_ringBasis = __esm({
2041
+ "src/core/scene/kinds/_ringBasis.ts"() {
2042
+ }
2043
+ });
2044
+
1471
2045
  // src/core/scene/kinds/cylinder3d.ts
1472
2046
  var CURVED_SEGMENTS;
1473
2047
  var init_cylinder3d = __esm({
1474
2048
  "src/core/scene/kinds/cylinder3d.ts"() {
1475
2049
  init_registry();
2050
+ init_ringBasis();
1476
2051
  CURVED_SEGMENTS = 16;
1477
2052
  registerKind({
1478
2053
  type: "cylinder3d",
@@ -1491,14 +2066,17 @@ var init_cylinder3d = __esm({
1491
2066
  const r = obj.attrs.radius;
1492
2067
  const ax = a.X?.() ?? 0, ay = a.Y?.() ?? 0, az = a.Z?.() ?? 0;
1493
2068
  const bx = b.X?.() ?? 0, by = b.Y?.() ?? 0, bz = b.Z?.() ?? 0;
2069
+ const [u, v] = perpBasis([bx - ax, by - ay, bz - az]);
1494
2070
  const baseRing = [];
1495
2071
  const topRing = [];
1496
2072
  for (let i = 0; i < CURVED_SEGMENTS; i++) {
1497
2073
  const theta = i / CURVED_SEGMENTS * Math.PI * 2;
1498
- const dx = r * Math.cos(theta);
1499
- const dy = r * Math.sin(theta);
1500
- baseRing.push([ax + dx, ay + dy, az]);
1501
- topRing.push([bx + dx, by + dy, bz]);
2074
+ const c = Math.cos(theta), s = Math.sin(theta);
2075
+ const ox = r * (c * u[0] + s * v[0]);
2076
+ const oy = r * (c * u[1] + s * v[1]);
2077
+ const oz = r * (c * u[2] + s * v[2]);
2078
+ baseRing.push([ax + ox, ay + oy, az + oz]);
2079
+ topRing.push([bx + ox, by + oy, bz + oz]);
1502
2080
  }
1503
2081
  const vertices = [...baseRing, ...topRing];
1504
2082
  const faces = [];
@@ -1509,7 +2087,7 @@ var init_cylinder3d = __esm({
1509
2087
  faces.push([i, next, CURVED_SEGMENTS + next, CURVED_SEGMENTS + i]);
1510
2088
  }
1511
2089
  const vertJxgs = vertices.map(
1512
- (v, i) => view.create("point3d", v, {
2090
+ (v2, i) => view.create("point3d", v2, {
1513
2091
  id: `${obj.id}.v${i}`,
1514
2092
  visible: false,
1515
2093
  fixed: true,
@@ -1537,6 +2115,7 @@ var CURVED_SEGMENTS2;
1537
2115
  var init_cone3d = __esm({
1538
2116
  "src/core/scene/kinds/cone3d.ts"() {
1539
2117
  init_registry();
2118
+ init_ringBasis();
1540
2119
  CURVED_SEGMENTS2 = 16;
1541
2120
  registerKind({
1542
2121
  type: "cone3d",
@@ -1559,10 +2138,16 @@ var init_cone3d = __esm({
1559
2138
  apexPt.Y?.() ?? 0,
1560
2139
  apexPt.Z?.() ?? 0
1561
2140
  ];
2141
+ const [u, v] = perpBasis([apexCoords[0] - bx, apexCoords[1] - by, apexCoords[2] - bz]);
1562
2142
  const baseRing = [];
1563
2143
  for (let i = 0; i < CURVED_SEGMENTS2; i++) {
1564
2144
  const theta = i / CURVED_SEGMENTS2 * Math.PI * 2;
1565
- baseRing.push([bx + r * Math.cos(theta), by + r * Math.sin(theta), bz]);
2145
+ const c = Math.cos(theta), s = Math.sin(theta);
2146
+ baseRing.push([
2147
+ bx + r * (c * u[0] + s * v[0]),
2148
+ by + r * (c * u[1] + s * v[1]),
2149
+ bz + r * (c * u[2] + s * v[2])
2150
+ ]);
1566
2151
  }
1567
2152
  const apexIdx = baseRing.length;
1568
2153
  const vertices = [...baseRing, apexCoords];
@@ -1571,7 +2156,7 @@ var init_cone3d = __esm({
1571
2156
  faces.push([i, (i + 1) % CURVED_SEGMENTS2, apexIdx]);
1572
2157
  }
1573
2158
  const vertJxgs = vertices.map(
1574
- (v, i) => view.create("point3d", v, {
2159
+ (v2, i) => view.create("point3d", v2, {
1575
2160
  id: `${obj.id}.v${i}`,
1576
2161
  visible: false,
1577
2162
  fixed: true,
@@ -1647,8 +2232,12 @@ function constraintRefs2D(c) {
1647
2232
  return [c.line, c.circle, c.other];
1648
2233
  case "tangencyPoint":
1649
2234
  return [c.circle, c.onLine];
1650
- case "arcMidpoint":
1651
- return [c.circle, c.a, c.b, c.notContaining];
2235
+ case "arcMidpoint": {
2236
+ const containment = c.notContaining ?? c.containing;
2237
+ return containment ? [c.circle, c.a, c.b, containment] : [c.circle, c.a, c.b];
2238
+ }
2239
+ case "mixtilinearPoint":
2240
+ return [c.vertices[0], c.vertices[1], c.vertices[2]];
1652
2241
  case "pointAtDistance": {
1653
2242
  const d = c.distance;
1654
2243
  const extra = d.kind === "circleRadius" ? [d.circle] : d.kind === "segmentLength" ? [d.p1, d.p2] : [];
@@ -1656,8 +2245,16 @@ function constraintRefs2D(c) {
1656
2245
  }
1657
2246
  case "excenter":
1658
2247
  return [c.vertices[0], c.vertices[1], c.vertices[2]];
1659
- default:
2248
+ case "commonTangentPoint":
2249
+ return [c.circles[0], c.circles[1]];
2250
+ // Các kind KHÔNG có ref scene-object (toạ độ literal): liệt kê TƯỜNG MINH để
2251
+ // exhaustive never-guard bên dưới buộc khai báo khi thêm constraint kind mới.
2252
+ case "free":
2253
+ case "onAxis":
2254
+ return [];
2255
+ default: {
1660
2256
  return [];
2257
+ }
1661
2258
  }
1662
2259
  }
1663
2260
  var init_d_constraint2 = __esm({
@@ -1935,7 +2532,7 @@ function dist(p, q) {
1935
2532
  function sideOf(a, b, p) {
1936
2533
  return (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
1937
2534
  }
1938
- function arcMidpoint(center, radius, a, b, notContaining) {
2535
+ function arcMidpoint(center, radius, a, b, reference, sameSide = false) {
1939
2536
  const mcx = (a[0] + b[0]) / 2;
1940
2537
  const mcy = (a[1] + b[1]) / 2;
1941
2538
  let ux = mcx - center[0];
@@ -1950,12 +2547,18 @@ function arcMidpoint(center, radius, a, b, notContaining) {
1950
2547
  uy /= len;
1951
2548
  const cand1 = [center[0] + radius * ux, center[1] + radius * uy];
1952
2549
  const cand2 = [center[0] - radius * ux, center[1] - radius * uy];
2550
+ if (!reference) return cand1[1] >= cand2[1] ? cand1 : cand2;
2551
+ const notContaining = reference;
1953
2552
  const sN = sideOf(a, b, notContaining);
1954
2553
  if (Math.abs(sN) < 1e-9) {
1955
- return dist(cand1, notContaining) >= dist(cand2, notContaining) ? cand1 : cand2;
2554
+ const far = dist(cand1, notContaining) >= dist(cand2, notContaining) ? cand1 : cand2;
2555
+ const near = far === cand1 ? cand2 : cand1;
2556
+ return sameSide ? near : far;
1956
2557
  }
1957
2558
  const s1 = sideOf(a, b, cand1);
1958
- return s1 * sN < 0 ? cand1 : cand2;
2559
+ const opp = s1 * sN < 0 ? cand1 : cand2;
2560
+ const same = opp === cand1 ? cand2 : cand1;
2561
+ return sameSide ? same : opp;
1959
2562
  }
1960
2563
  function excenter(vertices, oppositeIndex) {
1961
2564
  const [A, B, C] = vertices;
@@ -1971,6 +2574,35 @@ function excenter(vertices, oppositeIndex) {
1971
2574
  (w[0] * A[1] + w[1] * B[1] + w[2] * C[1]) / sum
1972
2575
  ];
1973
2576
  }
2577
+ function circumcenterXY(a, b, c) {
2578
+ const ax = a[0], ay = a[1], bx = b[0], by = b[1], cx = c[0], cy = c[1];
2579
+ const d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
2580
+ if (Math.abs(d) < 1e-12) return [(ax + bx + cx) / 3, (ay + by + cy) / 3];
2581
+ const ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay) + (cx * cx + cy * cy) * (ay - by)) / d;
2582
+ const uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx) + (cx * cx + cy * cy) * (bx - ax)) / d;
2583
+ return [ux, uy];
2584
+ }
2585
+ function mixtilinearPoint(a, b, c, which) {
2586
+ const O = circumcenterXY(a, b, c);
2587
+ const R = Math.hypot(O[0] - a[0], O[1] - a[1]);
2588
+ const ux1 = (b[0] - a[0]) / (Math.hypot(b[0] - a[0], b[1] - a[1]) || 1);
2589
+ const uy1 = (b[1] - a[1]) / (Math.hypot(b[0] - a[0], b[1] - a[1]) || 1);
2590
+ const ux2 = (c[0] - a[0]) / (Math.hypot(c[0] - a[0], c[1] - a[1]) || 1);
2591
+ const uy2 = (c[1] - a[1]) / (Math.hypot(c[0] - a[0], c[1] - a[1]) || 1);
2592
+ let bx = ux1 + ux2, by = uy1 + uy2;
2593
+ const bl = Math.hypot(bx, by) || 1;
2594
+ bx /= bl;
2595
+ by /= bl;
2596
+ const cosA = ux1 * ux2 + uy1 * uy2;
2597
+ const sinHalf = Math.sqrt(Math.max(0, (1 - cosA) / 2));
2598
+ const cos2Half = Math.max(1e-9, (1 + cosA) / 2);
2599
+ const dotAO = bx * (O[0] - a[0]) + by * (O[1] - a[1]);
2600
+ const d = 2 * (dotAO - R * sinHalf) / cos2Half;
2601
+ const K = [a[0] + d * bx, a[1] + d * by];
2602
+ if (which === "center") return K;
2603
+ const kl = Math.hypot(K[0] - O[0], K[1] - O[1]) || 1;
2604
+ return [O[0] + R * (K[0] - O[0]) / kl, O[1] + R * (K[1] - O[1]) / kl];
2605
+ }
1974
2606
  function pointAtDistanceCoord(from, through, d) {
1975
2607
  const dx = through[0] - from[0];
1976
2608
  const dy = through[1] - from[1];
@@ -1998,29 +2630,38 @@ var init_arcMidpoint = __esm({
1998
2630
  arcMidpointConstraint = definePointConstraint({
1999
2631
  kind: "arcMidpoint",
2000
2632
  validate: (c) => {
2001
- if (!c.circle || !c.a || !c.b || !c.notContaining) {
2002
- throw new Error("point.arcMidpoint: circle, a, b, notContaining b\u1EAFt bu\u1ED9c");
2633
+ if (!c.circle || !c.a || !c.b) {
2634
+ throw new Error("point.arcMidpoint: circle, a, b b\u1EAFt bu\u1ED9c");
2635
+ }
2636
+ if (c.notContaining && c.containing) {
2637
+ throw new Error("point.arcMidpoint: kh\xF4ng th\u1EC3 v\u1EEBa notContaining v\u1EEBa containing");
2003
2638
  }
2004
2639
  },
2005
2640
  describe: (obj, state, c) => {
2006
2641
  const al = state?.objects[c.a]?.label ?? c.a;
2007
2642
  const bl = state?.objects[c.b]?.label ?? c.b;
2008
- const nl = state?.objects[c.notContaining]?.label ?? c.notContaining;
2009
- return `${obj.label} = trung \u0111i\u1EC3m cung ${al}${bl} (kh\xF4ng ch\u1EE9a ${nl})`;
2643
+ const ref = c.containing ?? c.notContaining;
2644
+ if (!ref) return `${obj.label} = trung \u0111i\u1EC3m cung ${al}${bl}`;
2645
+ const rl = state?.objects[ref]?.label ?? ref;
2646
+ const rel = c.containing ? "ch\u1EE9a" : "kh\xF4ng ch\u1EE9a";
2647
+ return `${obj.label} = trung \u0111i\u1EC3m cung ${al}${bl} (${rel} ${rl})`;
2010
2648
  },
2011
2649
  render: (obj, ctx, c, opts) => {
2012
2650
  const board = ctx.jxg;
2013
2651
  const circle = ctx.resolveRef(c.circle);
2014
2652
  const A = ctx.resolveRef(c.a);
2015
2653
  const B = ctx.resolveRef(c.b);
2016
- const N = ctx.resolveRef(c.notContaining);
2654
+ const refName = c.containing ?? c.notContaining;
2655
+ const ref = refName ? ctx.resolveRef(refName) : void 0;
2656
+ const sameSide = !!c.containing;
2017
2657
  const O = circle?.center ?? circle?.midpoint ?? circle;
2018
2658
  const am = () => arcMidpoint(
2019
2659
  [O.X(), O.Y()],
2020
2660
  circle.Radius(),
2021
2661
  [A.X(), A.Y()],
2022
2662
  [B.X(), B.Y()],
2023
- [N.X(), N.Y()]
2663
+ ref ? [ref.X(), ref.Y()] : void 0,
2664
+ sameSide
2024
2665
  );
2025
2666
  return board.create("point", [() => am()[0], () => am()[1]], opts);
2026
2667
  }
@@ -2070,6 +2711,43 @@ var init_excenter = __esm({
2070
2711
  }
2071
2712
  });
2072
2713
 
2714
+ // src/core/scene/kinds/point-constraints/mixtilinearPoint.ts
2715
+ var mixtilinearPointConstraint;
2716
+ var init_mixtilinearPoint = __esm({
2717
+ "src/core/scene/kinds/point-constraints/mixtilinearPoint.ts"() {
2718
+ init_pointConstructions();
2719
+ init_types3();
2720
+ mixtilinearPointConstraint = definePointConstraint({
2721
+ kind: "mixtilinearPoint",
2722
+ validate: (c) => {
2723
+ if (!Array.isArray(c.vertices) || c.vertices.length !== 3 || !c.vertices.every(Boolean)) {
2724
+ throw new Error("point.mixtilinearPoint: vertices ph\u1EA3i l\xE0 tuple 3 id non-empty");
2725
+ }
2726
+ if (c.which !== "center" && c.which !== "touch") {
2727
+ throw new Error("point.mixtilinearPoint: which ph\u1EA3i 'center' | 'touch'");
2728
+ }
2729
+ },
2730
+ describe: (obj, state, c) => {
2731
+ const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
2732
+ 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)`;
2733
+ },
2734
+ render: (obj, ctx, c, opts) => {
2735
+ const board = ctx.jxg;
2736
+ const a = ctx.resolveRef(c.vertices[0]);
2737
+ const b = ctx.resolveRef(c.vertices[1]);
2738
+ const d = ctx.resolveRef(c.vertices[2]);
2739
+ const mp = () => mixtilinearPoint(
2740
+ [a.X(), a.Y()],
2741
+ [b.X(), b.Y()],
2742
+ [d.X(), d.Y()],
2743
+ c.which
2744
+ );
2745
+ return board.create("point", [() => mp()[0], () => mp()[1]], opts);
2746
+ }
2747
+ });
2748
+ }
2749
+ });
2750
+
2073
2751
  // src/core/scene/kinds/point-constraints/shared.ts
2074
2752
  function buildJxgTransforms(board, ctx, t) {
2075
2753
  switch (t.kind) {
@@ -2098,19 +2776,19 @@ function buildJxgTransforms(board, ctx, t) {
2098
2776
  }
2099
2777
  }
2100
2778
  function makeDistanceFn(ctx, d) {
2101
- const scale = d.scale ?? 1;
2779
+ const scale2 = d.scale ?? 1;
2102
2780
  const offset = d.offset ?? 0;
2103
2781
  if (d.kind === "literal") {
2104
2782
  const v = d.value;
2105
- return () => scale * v + offset;
2783
+ return () => scale2 * v + offset;
2106
2784
  }
2107
2785
  if (d.kind === "segmentLength") {
2108
2786
  const p = ctx.resolveRef(d.p1);
2109
2787
  const q = ctx.resolveRef(d.p2);
2110
- return () => scale * Math.hypot(p.X() - q.X(), p.Y() - q.Y()) + offset;
2788
+ return () => scale2 * Math.hypot(p.X() - q.X(), p.Y() - q.Y()) + offset;
2111
2789
  }
2112
2790
  const circle = ctx.resolveRef(d.circle);
2113
- return () => scale * circle.Radius() + offset;
2791
+ return () => scale2 * circle.Radius() + offset;
2114
2792
  }
2115
2793
  function buildPointOpts(obj) {
2116
2794
  return {
@@ -2121,11 +2799,13 @@ function buildPointOpts(obj) {
2121
2799
  strokeColor: obj.attrs.color ?? "#1e40af",
2122
2800
  fillColor: obj.attrs.color ?? "#1e40af",
2123
2801
  face: obj.attrs.face ?? "o",
2124
- size: obj.attrs.size ?? 4
2802
+ size: obj.attrs.size ?? 4,
2803
+ ...labelOpts(obj.attrs.labelOffset, [10, 10])
2125
2804
  };
2126
2805
  }
2127
2806
  var init_shared = __esm({
2128
2807
  "src/core/scene/kinds/point-constraints/shared.ts"() {
2808
+ init_label();
2129
2809
  }
2130
2810
  });
2131
2811
 
@@ -2157,6 +2837,73 @@ var init_pointAtDistance = __esm({
2157
2837
  }
2158
2838
  });
2159
2839
 
2840
+ // src/stamps/geometry-2d/geometry/commonTangent.ts
2841
+ function computeCommonTangentPoint(O1, r1, O2, r2, on, variant, side) {
2842
+ const dx = O2[0] - O1[0];
2843
+ const dy = O2[1] - O1[1];
2844
+ const d = Math.hypot(dx, dy);
2845
+ if (d < EPS) return null;
2846
+ const beta = Math.atan2(dy, dx);
2847
+ const ratio = variant === "external" ? (r1 - r2) / d : (r1 + r2) / d;
2848
+ if (Math.abs(ratio) > 1) return null;
2849
+ const gamma = Math.acos(ratio);
2850
+ const s = side === 0 ? 1 : -1;
2851
+ const angle = beta + s * gamma;
2852
+ const nx = Math.cos(angle);
2853
+ const ny = Math.sin(angle);
2854
+ if (on === 0) {
2855
+ return [O1[0] + r1 * nx, O1[1] + r1 * ny];
2856
+ }
2857
+ if (variant === "external") {
2858
+ return [O2[0] + r2 * nx, O2[1] + r2 * ny];
2859
+ }
2860
+ return [O2[0] - r2 * nx, O2[1] - r2 * ny];
2861
+ }
2862
+ var EPS;
2863
+ var init_commonTangent = __esm({
2864
+ "src/stamps/geometry-2d/geometry/commonTangent.ts"() {
2865
+ EPS = 1e-9;
2866
+ }
2867
+ });
2868
+
2869
+ // src/core/scene/kinds/point-constraints/commonTangentPoint.ts
2870
+ var commonTangentPointConstraint;
2871
+ var init_commonTangentPoint = __esm({
2872
+ "src/core/scene/kinds/point-constraints/commonTangentPoint.ts"() {
2873
+ init_commonTangent();
2874
+ init_types3();
2875
+ commonTangentPointConstraint = definePointConstraint({
2876
+ kind: "commonTangentPoint",
2877
+ describe: (obj, state, c) => {
2878
+ const c1 = state?.objects[c.circles[0]]?.label ?? c.circles[0];
2879
+ const c2 = state?.objects[c.circles[1]]?.label ?? c.circles[1];
2880
+ const v = c.variant === "internal" ? "trong" : "ngo\xE0i";
2881
+ return `${obj.label} = ti\u1EBFp \u0111i\u1EC3m ti\u1EBFp tuy\u1EBFn chung ${v} ${c1},${c2} (tr\xEAn ${c.on === 0 ? c1 : c2})`;
2882
+ },
2883
+ render: (obj, ctx, c, opts) => {
2884
+ const board = ctx.jxg;
2885
+ const k1 = ctx.resolveRef(c.circles[0]);
2886
+ const k2 = ctx.resolveRef(c.circles[1]);
2887
+ const O1 = k1?.center ?? k1?.midpoint ?? k1;
2888
+ const O2 = k2?.center ?? k2?.midpoint ?? k2;
2889
+ const compute = () => {
2890
+ const t = computeCommonTangentPoint(
2891
+ [O1.X(), O1.Y()],
2892
+ k1.Radius(),
2893
+ [O2.X(), O2.Y()],
2894
+ k2.Radius(),
2895
+ c.on,
2896
+ c.variant,
2897
+ c.side
2898
+ );
2899
+ return t ?? [0, 0];
2900
+ };
2901
+ return board.create("point", [() => compute()[0], () => compute()[1]], opts);
2902
+ }
2903
+ });
2904
+ }
2905
+ });
2906
+
2160
2907
  // src/core/scene/kinds/point-constraints/circleIntersection.ts
2161
2908
  var circleIntersectionConstraint;
2162
2909
  var init_circleIntersection = __esm({
@@ -2434,7 +3181,9 @@ var init_registry2 = __esm({
2434
3181
  init_centroid();
2435
3182
  init_arcMidpoint();
2436
3183
  init_excenter();
3184
+ init_mixtilinearPoint();
2437
3185
  init_pointAtDistance();
3186
+ init_commonTangentPoint();
2438
3187
  init_circleIntersection();
2439
3188
  init_circleSecondIntersection();
2440
3189
  init_secondIntersection();
@@ -2459,7 +3208,9 @@ var init_registry2 = __esm({
2459
3208
  centroidConstraint,
2460
3209
  arcMidpointConstraint,
2461
3210
  excenterConstraint,
3211
+ mixtilinearPointConstraint,
2462
3212
  pointAtDistanceConstraint,
3213
+ commonTangentPointConstraint,
2463
3214
  circleIntersectionConstraint,
2464
3215
  circleSecondIntersectionConstraint,
2465
3216
  secondIntersectionConstraint,
@@ -2476,14 +3227,15 @@ var init_registry2 = __esm({
2476
3227
  });
2477
3228
 
2478
3229
  // src/core/scene/kinds/point.ts
2479
- var def3;
3230
+ var def5;
2480
3231
  var init_point = __esm({
2481
3232
  "src/core/scene/kinds/point.ts"() {
2482
3233
  init_registry();
2483
3234
  init_d_constraint2();
2484
3235
  init_registry2();
2485
3236
  init_shared();
2486
- def3 = {
3237
+ init_label();
3238
+ def5 = {
2487
3239
  type: "point",
2488
3240
  schemaVersion: 1,
2489
3241
  migrate: {},
@@ -2550,7 +3302,8 @@ var init_point = __esm({
2550
3302
  strokeColor: obj.attrs.color ?? "#1e40af",
2551
3303
  fillColor: obj.attrs.color ?? "#1e40af",
2552
3304
  face: obj.attrs.face ?? "o",
2553
- size: obj.attrs.size ?? 4
3305
+ size: obj.attrs.size ?? 4,
3306
+ ...labelOpts(obj.attrs.labelOffset, [10, 10])
2554
3307
  });
2555
3308
  } catch {
2556
3309
  }
@@ -2560,17 +3313,18 @@ var init_point = __esm({
2560
3313
  throw new Error("point: constraint kind changed \u2014 recreate");
2561
3314
  }
2562
3315
  };
2563
- registerKind(def3);
3316
+ registerKind(def5);
2564
3317
  }
2565
3318
  });
2566
3319
 
2567
3320
  // src/core/scene/kinds/segment.ts
2568
- var def4;
3321
+ var def6;
2569
3322
  var init_segment = __esm({
2570
3323
  "src/core/scene/kinds/segment.ts"() {
2571
3324
  init_registry();
2572
3325
  init_labelOf();
2573
- def4 = {
3326
+ init_label();
3327
+ def6 = {
2574
3328
  type: "segment",
2575
3329
  schemaVersion: 1,
2576
3330
  migrate: {},
@@ -2601,11 +3355,12 @@ var init_segment = __esm({
2601
3355
  strokeWidth: obj.attrs.width ?? 2,
2602
3356
  dash: obj.attrs.dash ?? 0,
2603
3357
  visible: obj.visible,
2604
- fixed: obj.locked
3358
+ fixed: obj.locked,
3359
+ ...labelOpts(obj.attrs.labelOffset)
2605
3360
  });
2606
3361
  }
2607
3362
  };
2608
- registerKind(def4);
3363
+ registerKind(def6);
2609
3364
  }
2610
3365
  });
2611
3366
 
@@ -2633,13 +3388,14 @@ function constructionRefs(c) {
2633
3388
  return [c.throughPoint, c.toCircle];
2634
3389
  }
2635
3390
  }
2636
- var def5;
3391
+ var def7;
2637
3392
  var init_line = __esm({
2638
3393
  "src/core/scene/kinds/line.ts"() {
2639
3394
  init_registry();
2640
3395
  init_labelOf();
3396
+ init_label();
2641
3397
  init_pointConstructions();
2642
- def5 = {
3398
+ def7 = {
2643
3399
  type: "line",
2644
3400
  schemaVersion: 1,
2645
3401
  migrate: {},
@@ -2680,7 +3436,8 @@ var init_line = __esm({
2680
3436
  strokeWidth: obj.attrs.width ?? 2,
2681
3437
  dash: obj.attrs.dash ?? 0,
2682
3438
  visible: obj.visible,
2683
- fixed: obj.locked
3439
+ fixed: obj.locked,
3440
+ ...labelOpts(obj.attrs.labelOffset)
2684
3441
  };
2685
3442
  const c = obj.attrs.construction;
2686
3443
  if (!c) {
@@ -2834,17 +3591,17 @@ var init_line = __esm({
2834
3591
  }
2835
3592
  }
2836
3593
  };
2837
- registerKind(def5);
3594
+ registerKind(def7);
2838
3595
  }
2839
3596
  });
2840
3597
 
2841
3598
  // src/core/scene/kinds/ray.ts
2842
- var def6;
3599
+ var def8;
2843
3600
  var init_ray = __esm({
2844
3601
  "src/core/scene/kinds/ray.ts"() {
2845
3602
  init_registry();
2846
3603
  init_labelOf();
2847
- def6 = {
3604
+ def8 = {
2848
3605
  type: "ray",
2849
3606
  schemaVersion: 1,
2850
3607
  migrate: {},
@@ -2869,17 +3626,17 @@ var init_ray = __esm({
2869
3626
  });
2870
3627
  }
2871
3628
  };
2872
- registerKind(def6);
3629
+ registerKind(def8);
2873
3630
  }
2874
3631
  });
2875
3632
 
2876
3633
  // src/core/scene/kinds/vector.ts
2877
- var def7;
3634
+ var def9;
2878
3635
  var init_vector = __esm({
2879
3636
  "src/core/scene/kinds/vector.ts"() {
2880
3637
  init_registry();
2881
3638
  init_labelOf();
2882
- def7 = {
3639
+ def9 = {
2883
3640
  type: "vector",
2884
3641
  schemaVersion: 1,
2885
3642
  migrate: {},
@@ -2901,7 +3658,7 @@ var init_vector = __esm({
2901
3658
  });
2902
3659
  }
2903
3660
  };
2904
- registerKind(def7);
3661
+ registerKind(def9);
2905
3662
  }
2906
3663
  });
2907
3664
 
@@ -2931,13 +3688,14 @@ function constructionRefs2(c) {
2931
3688
  return [c.p1, c.p2];
2932
3689
  }
2933
3690
  }
2934
- var def8;
3691
+ var def10;
2935
3692
  var init_circle = __esm({
2936
3693
  "src/core/scene/kinds/circle.ts"() {
2937
3694
  init_registry();
2938
3695
  init_labelOf();
3696
+ init_label();
2939
3697
  init_pointConstructions();
2940
- def8 = {
3698
+ def10 = {
2941
3699
  type: "circle",
2942
3700
  schemaVersion: 1,
2943
3701
  migrate: {},
@@ -2997,15 +3755,17 @@ var init_circle = __esm({
2997
3755
  const board = ctx.jxg;
2998
3756
  const isCenterLabel = (l) => /^[A-Z]['′]?\d*$/u.test(l);
2999
3757
  const isCenter = isCenterLabel(obj.label);
3758
+ const isRenamedCircle = /_c$/.test(obj.label);
3000
3759
  const baseOpts = {
3001
3760
  name: obj.label,
3002
- withLabel: isCenter ? obj.attrs.showLabel ?? false : true,
3761
+ withLabel: isCenter ? obj.attrs.showLabel ?? false : isRenamedCircle ? false : true,
3003
3762
  strokeColor: obj.attrs.color ?? "#0f172a",
3004
3763
  strokeWidth: obj.attrs.width ?? 2,
3005
3764
  dash: obj.attrs.dash ?? 0,
3006
3765
  fillColor: "none",
3007
3766
  visible: obj.visible,
3008
- fixed: obj.locked
3767
+ fixed: obj.locked,
3768
+ ...labelOpts(obj.attrs.labelOffset)
3009
3769
  };
3010
3770
  const c = asConstruction(obj.attrs);
3011
3771
  if (c?.kind === "circumscribed") {
@@ -3087,7 +3847,7 @@ var init_circle = __esm({
3087
3847
  return board.create("circle", [center, surface], baseOpts);
3088
3848
  }
3089
3849
  };
3090
- registerKind(def8);
3850
+ registerKind(def10);
3091
3851
  }
3092
3852
  });
3093
3853
 
@@ -3102,12 +3862,12 @@ function constructionRefs3(c) {
3102
3862
  return [c.p1, c.p2, c.p3];
3103
3863
  }
3104
3864
  }
3105
- var def9;
3865
+ var def11;
3106
3866
  var init_arc = __esm({
3107
3867
  "src/core/scene/kinds/arc.ts"() {
3108
3868
  init_registry();
3109
3869
  init_labelOf();
3110
- def9 = {
3870
+ def11 = {
3111
3871
  type: "arc",
3112
3872
  schemaVersion: 1,
3113
3873
  migrate: {},
@@ -3165,17 +3925,17 @@ var init_arc = __esm({
3165
3925
  return board.create("circumcirclearc", [A, B, C], baseOpts);
3166
3926
  }
3167
3927
  };
3168
- registerKind(def9);
3928
+ registerKind(def11);
3169
3929
  }
3170
3930
  });
3171
3931
 
3172
3932
  // src/core/scene/kinds/sector.ts
3173
- var def10;
3933
+ var def12;
3174
3934
  var init_sector = __esm({
3175
3935
  "src/core/scene/kinds/sector.ts"() {
3176
3936
  init_registry();
3177
3937
  init_labelOf();
3178
- def10 = {
3938
+ def12 = {
3179
3939
  type: "sector",
3180
3940
  schemaVersion: 1,
3181
3941
  migrate: {},
@@ -3215,7 +3975,7 @@ var init_sector = __esm({
3215
3975
  });
3216
3976
  }
3217
3977
  };
3218
- registerKind(def10);
3978
+ registerKind(def12);
3219
3979
  }
3220
3980
  });
3221
3981
 
@@ -3261,12 +4021,12 @@ function specialShapeName(kind) {
3261
4021
  return "";
3262
4022
  }
3263
4023
  }
3264
- var def11;
4024
+ var def13;
3265
4025
  var init_polygon = __esm({
3266
4026
  "src/core/scene/kinds/polygon.ts"() {
3267
4027
  init_registry();
3268
4028
  init_labelOf();
3269
- def11 = {
4029
+ def13 = {
3270
4030
  type: "polygon",
3271
4031
  schemaVersion: 1,
3272
4032
  migrate: {},
@@ -3453,16 +4213,16 @@ var init_polygon = __esm({
3453
4213
  return poly;
3454
4214
  }
3455
4215
  };
3456
- registerKind(def11);
4216
+ registerKind(def13);
3457
4217
  }
3458
4218
  });
3459
4219
 
3460
4220
  // src/core/scene/kinds/intersection.ts
3461
- var def12;
4221
+ var def14;
3462
4222
  var init_intersection = __esm({
3463
4223
  "src/core/scene/kinds/intersection.ts"() {
3464
4224
  init_registry();
3465
- def12 = {
4225
+ def14 = {
3466
4226
  type: "intersection",
3467
4227
  schemaVersion: 1,
3468
4228
  migrate: {},
@@ -3490,8 +4250,11 @@ var init_intersection = __esm({
3490
4250
  const opts = {
3491
4251
  name: obj.label,
3492
4252
  withLabel: true,
3493
- strokeColor: obj.attrs.color ?? "#dc2626",
3494
- fillColor: obj.attrs.color ?? "#dc2626",
4253
+ // Cùng màu xanh với mọi điểm khác (buildPointOpts dùng '#1e40af').
4254
+ // Trước đây điểm giao tô đỏ '#dc2626' → tách biệt thị giác nhưng gây
4255
+ // khó hiểu ("vì sao điểm này khác màu?"). Giữ chung một màu.
4256
+ strokeColor: obj.attrs.color ?? "#1e40af",
4257
+ fillColor: obj.attrs.color ?? "#1e40af",
3495
4258
  visible: obj.visible,
3496
4259
  fixed: obj.locked
3497
4260
  };
@@ -3500,19 +4263,51 @@ var init_intersection = __esm({
3500
4263
  }
3501
4264
  const branch = obj.attrs.branch ?? 0;
3502
4265
  return board.create("intersection", [a, b, branch], opts);
4266
+ },
4267
+ /**
4268
+ * Cập nhật TẠI CHỖ các thuộc tính "trang trí" (tên/màu/ẩn-hiện/khoá) qua
4269
+ * setAttribute — giữ nguyên JxgObj identity nên các object phụ thuộc điểm
4270
+ * giao (đường thẳng qua nó, …) KHÔNG bị stale parent ref. Mô phỏng update
4271
+ * hook của point.ts.
4272
+ *
4273
+ * Nếu định nghĩa hình học đổi (kind/ref1/ref2/branch) thì throw → renderer
4274
+ * fallback remove + create để JSXGraph dựng lại phép giao đúng.
4275
+ */
4276
+ update: (obj, prev, ctx, existing) => {
4277
+ const a = obj.attrs;
4278
+ const p = prev.attrs;
4279
+ const branchA = a.branch;
4280
+ const branchP = p.branch;
4281
+ if (a.kind !== p.kind || a.ref1 !== p.ref1 || a.ref2 !== p.ref2 || branchA !== branchP) {
4282
+ throw new Error("intersection: \u0111\u1ECBnh ngh\u0129a h\xECnh h\u1ECDc \u0111\u1ED5i \u2014 recreate");
4283
+ }
4284
+ const el = existing;
4285
+ if (typeof el.setAttribute === "function") {
4286
+ try {
4287
+ el.setAttribute({
4288
+ name: obj.label,
4289
+ withLabel: true,
4290
+ strokeColor: a.color ?? "#1e40af",
4291
+ fillColor: a.color ?? "#1e40af",
4292
+ visible: obj.visible,
4293
+ fixed: obj.locked
4294
+ });
4295
+ } catch {
4296
+ }
4297
+ }
3503
4298
  }
3504
4299
  };
3505
- registerKind(def12);
4300
+ registerKind(def14);
3506
4301
  }
3507
4302
  });
3508
4303
 
3509
4304
  // src/core/scene/kinds/angle.ts
3510
- var def13;
4305
+ var def15;
3511
4306
  var init_angle = __esm({
3512
4307
  "src/core/scene/kinds/angle.ts"() {
3513
4308
  init_registry();
3514
4309
  init_labelOf();
3515
- def13 = {
4310
+ def15 = {
3516
4311
  type: "angle",
3517
4312
  schemaVersion: 1,
3518
4313
  migrate: {},
@@ -3548,17 +4343,17 @@ var init_angle = __esm({
3548
4343
  });
3549
4344
  }
3550
4345
  };
3551
- registerKind(def13);
4346
+ registerKind(def15);
3552
4347
  }
3553
4348
  });
3554
4349
 
3555
4350
  // src/core/scene/kinds/distance.ts
3556
- var def14;
4351
+ var def16;
3557
4352
  var init_distance = __esm({
3558
4353
  "src/core/scene/kinds/distance.ts"() {
3559
4354
  init_registry();
3560
4355
  init_labelOf();
3561
- def14 = {
4356
+ def16 = {
3562
4357
  type: "distance",
3563
4358
  schemaVersion: 1,
3564
4359
  migrate: {},
@@ -3587,17 +4382,17 @@ var init_distance = __esm({
3587
4382
  });
3588
4383
  }
3589
4384
  };
3590
- registerKind(def14);
4385
+ registerKind(def16);
3591
4386
  }
3592
4387
  });
3593
4388
 
3594
4389
  // src/core/scene/kinds/function2d.ts
3595
- var def15;
4390
+ var def17;
3596
4391
  var init_function2d = __esm({
3597
4392
  "src/core/scene/kinds/function2d.ts"() {
3598
4393
  init_registry();
3599
4394
  init_parser();
3600
- def15 = {
4395
+ def17 = {
3601
4396
  type: "function2d",
3602
4397
  schemaVersion: 1,
3603
4398
  migrate: {},
@@ -3636,16 +4431,16 @@ var init_function2d = __esm({
3636
4431
  });
3637
4432
  }
3638
4433
  };
3639
- registerKind(def15);
4434
+ registerKind(def17);
3640
4435
  }
3641
4436
  });
3642
4437
 
3643
4438
  // src/core/scene/kinds/parameter.ts
3644
- var def16;
4439
+ var def18;
3645
4440
  var init_parameter = __esm({
3646
4441
  "src/core/scene/kinds/parameter.ts"() {
3647
4442
  init_registry();
3648
- def16 = {
4443
+ def18 = {
3649
4444
  type: "parameter",
3650
4445
  schemaVersion: 1,
3651
4446
  migrate: {},
@@ -3663,16 +4458,16 @@ var init_parameter = __esm({
3663
4458
  render: () => null
3664
4459
  // Không render lên board
3665
4460
  };
3666
- registerKind(def16);
4461
+ registerKind(def18);
3667
4462
  }
3668
4463
  });
3669
4464
 
3670
4465
  // src/core/scene/kinds/pointOnCurve.ts
3671
- var def17;
4466
+ var def19;
3672
4467
  var init_pointOnCurve = __esm({
3673
4468
  "src/core/scene/kinds/pointOnCurve.ts"() {
3674
4469
  init_registry();
3675
- def17 = {
4470
+ def19 = {
3676
4471
  type: "pointOnCurve",
3677
4472
  schemaVersion: 1,
3678
4473
  migrate: {},
@@ -3699,16 +4494,16 @@ var init_pointOnCurve = __esm({
3699
4494
  });
3700
4495
  }
3701
4496
  };
3702
- registerKind(def17);
4497
+ registerKind(def19);
3703
4498
  }
3704
4499
  });
3705
4500
 
3706
4501
  // src/core/scene/kinds/tangent2d.ts
3707
- var def18;
4502
+ var def20;
3708
4503
  var init_tangent2d = __esm({
3709
4504
  "src/core/scene/kinds/tangent2d.ts"() {
3710
4505
  init_registry();
3711
- def18 = {
4506
+ def20 = {
3712
4507
  type: "tangent2d",
3713
4508
  schemaVersion: 1,
3714
4509
  migrate: {},
@@ -3731,7 +4526,7 @@ var init_tangent2d = __esm({
3731
4526
  });
3732
4527
  }
3733
4528
  };
3734
- registerKind(def18);
4529
+ registerKind(def20);
3735
4530
  }
3736
4531
  });
3737
4532
 
@@ -3807,13 +4602,13 @@ var init_evaluator = __esm({
3807
4602
  });
3808
4603
 
3809
4604
  // src/core/scene/kinds/extremum2d.ts
3810
- var def19;
4605
+ var def21;
3811
4606
  var init_extremum2d = __esm({
3812
4607
  "src/core/scene/kinds/extremum2d.ts"() {
3813
4608
  init_registry();
3814
4609
  init_evaluator();
3815
4610
  init_parser();
3816
- def19 = {
4611
+ def21 = {
3817
4612
  type: "extremum2d",
3818
4613
  schemaVersion: 1,
3819
4614
  migrate: {},
@@ -3846,18 +4641,18 @@ var init_extremum2d = __esm({
3846
4641
  }));
3847
4642
  }
3848
4643
  };
3849
- registerKind(def19);
4644
+ registerKind(def21);
3850
4645
  }
3851
4646
  });
3852
4647
 
3853
4648
  // src/core/scene/kinds/root2d.ts
3854
- var def20;
4649
+ var def22;
3855
4650
  var init_root2d = __esm({
3856
4651
  "src/core/scene/kinds/root2d.ts"() {
3857
4652
  init_registry();
3858
4653
  init_evaluator();
3859
4654
  init_parser();
3860
- def20 = {
4655
+ def22 = {
3861
4656
  type: "root2d",
3862
4657
  schemaVersion: 1,
3863
4658
  migrate: {},
@@ -3887,16 +4682,16 @@ var init_root2d = __esm({
3887
4682
  }));
3888
4683
  }
3889
4684
  };
3890
- registerKind(def20);
4685
+ registerKind(def22);
3891
4686
  }
3892
4687
  });
3893
4688
 
3894
4689
  // src/core/scene/kinds/slope2d.ts
3895
- var def21;
4690
+ var def23;
3896
4691
  var init_slope2d = __esm({
3897
4692
  "src/core/scene/kinds/slope2d.ts"() {
3898
4693
  init_registry();
3899
- def21 = {
4694
+ def23 = {
3900
4695
  type: "slope2d",
3901
4696
  schemaVersion: 1,
3902
4697
  migrate: {},
@@ -3920,7 +4715,7 @@ var init_slope2d = __esm({
3920
4715
  });
3921
4716
  }
3922
4717
  };
3923
- registerKind(def21);
4718
+ registerKind(def23);
3924
4719
  }
3925
4720
  });
3926
4721
 
@@ -5619,7 +6414,7 @@ var init_useToolHoverTooltip = __esm({
5619
6414
  init_types4();
5620
6415
  }
5621
6416
  });
5622
- function normalize(s) {
6417
+ function normalize2(s) {
5623
6418
  return s.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/đ/g, "d").replace(/Đ/g, "d");
5624
6419
  }
5625
6420
  function SearchIcon() {
@@ -5666,12 +6461,12 @@ function ToolGrid(props) {
5666
6461
  const { tools, groupOrder, groupLabels, activeTool, onToolChange, chord } = props;
5667
6462
  const { hover, portalReady, showHover, hideHover } = useToolHoverTooltip();
5668
6463
  const [query, setQuery] = React5.useState("");
5669
- const normalizedQuery = React5.useMemo(() => normalize(query.trim()), [query]);
6464
+ const normalizedQuery = React5.useMemo(() => normalize2(query.trim()), [query]);
5670
6465
  const filteredTools = React5.useMemo(() => {
5671
6466
  if (!normalizedQuery) return tools;
5672
6467
  return tools.filter((t) => {
5673
- if (normalize(t.label).includes(normalizedQuery)) return true;
5674
- if (t.hint && normalize(t.hint).includes(normalizedQuery)) return true;
6468
+ if (normalize2(t.label).includes(normalizedQuery)) return true;
6469
+ if (t.hint && normalize2(t.hint).includes(normalizedQuery)) return true;
5675
6470
  return false;
5676
6471
  });
5677
6472
  }, [tools, normalizedQuery]);
@@ -6258,11 +7053,17 @@ async function insertStampImage(api, opts) {
6258
7053
  const elements = api.getSceneElements();
6259
7054
  const editingId = opts.editingElementId ?? null;
6260
7055
  if (editingId) {
7056
+ const old = elements.find((e) => e.id === editingId) ;
7057
+ const oldLongest = old ? Math.max(old.width ?? 0, old.height ?? 0) : 0;
7058
+ const newLongest = Math.max(width, height);
7059
+ const scale2 = oldLongest > 0 && newLongest > 0 ? oldLongest / newLongest : 1;
7060
+ const w = width * scale2;
7061
+ const h = height * scale2;
6261
7062
  const updated = elements.map(
6262
- (e) => e.id === editingId ? { ...e, fileId, customData, width, height } : e
7063
+ (e) => e.id === editingId ? { ...e, fileId, customData, width: w, height: h } : e
6263
7064
  );
6264
7065
  api.updateScene({ elements: updated, appState: clearAppStateAfterInsert() });
6265
- return { fileId, width, height, elementId: editingId };
7066
+ return { fileId, width: w, height: h, elementId: editingId };
6266
7067
  }
6267
7068
  const newElement = buildStampImageElement(
6268
7069
  api,
@@ -6736,7 +7537,8 @@ var init_host = __esm({
6736
7537
  version: 2,
6737
7538
  jsonState
6738
7539
  }),
6739
- editingElementId: editingElement?.id ?? null
7540
+ editingElementId: editingElement?.id ?? null,
7541
+ preserveExistingSize: true
6740
7542
  });
6741
7543
  } catch (err) {
6742
7544
  console.error("Graph2D insert failed:", err);