@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
package/dist/ai.js CHANGED
@@ -15,6 +15,11 @@ var GEOMETRY_KEYWORDS = [
15
15
  "h\xECnh thoi",
16
16
  "h\xECnh thang",
17
17
  "t\u1EE9 gi\xE1c",
18
+ // Đa giác ≥5 đỉnh: "Cho lục giác ABCDEF …" — thiếu các từ này thì clause chỉ
19
+ // có tên đa giác (không "ngoại/nội tiếp") bị coi văn xuôi → hexagonRule miss.
20
+ "ng\u0169 gi\xE1c",
21
+ "l\u1EE5c gi\xE1c",
22
+ "\u0111a gi\xE1c",
18
23
  // Triangle variants
19
24
  "vu\xF4ng t\u1EA1i",
20
25
  "c\xE2n t\u1EA1i",
@@ -96,6 +101,13 @@ var GEOMETRY_KEYWORDS = [
96
101
  "c\u1EAFt",
97
102
  "\u0111\u01B0\u1EDDng th\u1EB3ng",
98
103
  "\u0111\u01B0\u01A1ng th\u1EB3ng",
104
+ // "thẳng hàng" — "Cho 3 điểm A, B, C thẳng hàng theo thứ tự" (C41): clause GIỚI
105
+ // THIỆU bộ điểm thẳng hàng (collinearPointsRule dựng A,B,C) NHƯNG không có keyword
106
+ // nào khác → hasGeometry=false → clause bị lọc khỏi rule engine → A,B,C KHÔNG dựng
107
+ // → cascade làm sụp tiếp tuyến/trung điểm phụ thuộc chúng. "thẳng hàng" (≥8 ký tự,
108
+ // phân biệt) đánh dấu geo-clause. Clause CHỨNG MINH ("Chứng minh A,B,C thẳng hàng")
109
+ // vẫn non-geo nhờ guard proofOnly (startsProofSection) — không bị ảnh hưởng.
110
+ "th\u1EB3ng h\xE0ng",
99
111
  // Ký hiệu (symbol-only phrasing tương đương từ chữ): "⊥" ≡ "vuông góc".
100
112
  // perpFoot rule đã nhận "⊥" ở tầng construct (PERP_DRAW), nhưng gate
101
113
  // hasGeometry trước đây CHỈ nhận chữ "vuông góc" → clause mà "⊥" là tín hiệu
@@ -211,17 +223,18 @@ function countGeometryKeywords(text) {
211
223
  var MASK_SEMI = "";
212
224
  var MASK_DOT = "";
213
225
  var MASK_COMMA = "";
226
+ var MASK_SEMI_LIST = "";
214
227
  function maskNonBoundaryPunct(s) {
215
228
  return s.replace(
216
229
  /\(([^()\n]{1,40})\)/g,
217
230
  (m) => m.replace(/;/g, MASK_SEMI).replace(/\./g, MASK_DOT).replace(/,/g, MASK_COMMA)
218
231
  ).replace(
219
- /;(?=\s*[A-Z][A-Z]?['′]?\d?\s*(?:[;,]|cắt nhau|đồng quy))/gu,
220
- MASK_SEMI
221
- );
232
+ /;(?=\s*[A-Z][A-Z]?['′]?\d?\s*(?:[;,]|lần lượt|cắt nhau|đồng quy|đôi một))/gu,
233
+ MASK_SEMI_LIST
234
+ ).replace(/;(?=\s*[A-Z][A-Z]?['′]?\d?\s*(?:[;,.]|$))/gu, MASK_SEMI_LIST);
222
235
  }
223
236
  function unmask(s) {
224
- return s.replace(/\u0001/g, ";").replace(/\u0002/g, ".").replace(/\u0003/g, ",");
237
+ return s.replace(/\u0001/g, ";").replace(/\u0002/g, ".").replace(/\u0003/g, ",").replace(/\u0004/g, ",");
225
238
  }
226
239
  function segmentClauses(problem) {
227
240
  let proofMode = false;
@@ -230,7 +243,7 @@ function segmentClauses(problem) {
230
243
  // giờ khớp trước space → split chết im lặng (bug class \b+tiếng Việt).
231
244
  /[.;\n]+|,\s*(?=(?:Gọi|Vẽ|Kẻ|Cho|Lấy|Dựng|trên|với|Let|Draw|Mark|Take|Construct|Join)(?!\p{L}))/u
232
245
  ).map((s) => unmask(s).trim()).filter((s) => s.length > 0).map((text, id) => {
233
- const hasGeometryKeyword = countGeometryKeywords(text) > 0;
246
+ const hasGeometryKeyword = countGeometryKeywords(text) > 0 || NAMED_LINE_PICK.test(text) || BARE_SEG_PICK.test(text) && !/tia\s+đối/u.test(text);
234
247
  const proofOnly = isProofOnlyClause(text, proofMode);
235
248
  if (startsProofSection(text)) proofMode = true;
236
249
  const locusOnly = LOCUS_CLAUSE.test(text) && !CONSTRUCTION_LEAD.test(text);
@@ -239,6 +252,8 @@ function segmentClauses(problem) {
239
252
  }
240
253
  var ENUM_PREFIX = "(?:[0-9]+\\s*[.)]?\\s*|[a-zA-Z]\\s*[.)]\\s*)*";
241
254
  var LOCUS_CLAUSE = /(?:di\s*chuyển|di\s*động)\s+trên\s+(?:\(|đường\s*tròn|đương\s*tròn|cung|nửa)/u;
255
+ var NAMED_LINE_PICK = /(?:[Tt]rên|[Tt]huộc)\s+(?:đường\s*thẳng\s+)?[a-z]{1,2}[0-9]?(?![\p{L}])\s+(?:[Ll]ấy|có)|[Ll]ấy\s+điểm\s+[A-Z]['′]?[0-9]?\s+(?:trên|thuộc)\s+(?:đường\s*thẳng\s+)?[a-z]{1,2}[0-9]?(?![\p{L}])/u;
256
+ var BARE_SEG_PICK = /[Ll]ấy\s+(?:điểm\s+)?[A-Z]['′]?(?:\s*,\s*[A-Z]['′]?)?\s+(?:bất\s*k[iìyỳ]\s+)?(?:trên|thuộc)\s+(?:cạnh\s+|đoạn\s+|tia\s+)?[A-Z]{2}(?![A-Z])|[Tt]rên\s+(?:cạnh\s+|đoạn\s+|tia\s+)?[A-Z]{2}(?:\s*,\s*[A-Z]{2})*\s+(?:(?:theo\s+)?thứ\s+tự\s+|lần\s*lượt\s+)?lấy\s+(?:các\s+)?(?:điểm\s+)?[A-Z]/u;
242
257
  var PROOF_SECTION_START = new RegExp(
243
258
  `^${ENUM_PREFIX}(?:[Cc]h\u1EE9ng\\s*minh|C/m|CMR|[Tt]\xEDnh|[Tt]\xECm|[Xx]\xE1c\\s*\u0111\u1ECBnh|[Hh]\xE3y\\s+x\xE1c\\s*\u0111\u1ECBnh)(?!\\p{L})`,
244
259
  "u"
@@ -307,6 +322,9 @@ function pairFromToken(token) {
307
322
  return m ? [m[1], m[2]] : [];
308
323
  }
309
324
  var SIDE_PREFIX = "(?:c\u1EA1nh(?:\\s+huy\u1EC1n)?\\s+|\u0111o\u1EA1n(?:\\s+th\u1EB3ng)?\\s+|d\xE2y(?:\\s+cung)?\\s+)?";
325
+ function escapeRe(s) {
326
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
327
+ }
310
328
  var DUONG_KW = "[\u0110\u0111]\u01B0[\u1EDD\u01A1]ng";
311
329
  var CIRCLE_KW = DUONG_KW + "\\s*tr\xF2n";
312
330
 
@@ -475,44 +493,44 @@ var QUAD = "([A-Z])([A-Z])([A-Z])([A-Z])(?![A-Z])";
475
493
  var KINDS = [
476
494
  // hình thang cân / vuông (modifier TRƯỚC đỉnh) — phải match trước thang chung
477
495
  {
478
- name: new RegExp("h\xECnh\\s+thang\\s+c\xE2n\\s+" + QUAD, "gu"),
496
+ name: new RegExp("[Hh]\xECnh\\s+thang\\s+c\xE2n\\s+" + QUAD, "gu"),
479
497
  shape: "trapezoid",
480
498
  variant: "isoceles"
481
499
  },
482
500
  {
483
- name: new RegExp("h\xECnh\\s+thang\\s+vu\xF4ng\\s+" + QUAD, "gu"),
501
+ name: new RegExp("[Hh]\xECnh\\s+thang\\s+vu\xF4ng\\s+" + QUAD, "gu"),
484
502
  shape: "trapezoid",
485
503
  variant: "right"
486
504
  },
487
505
  // hình thang chung — sau đó dò modifier 'vuông'/'cân' đứng SAU đỉnh
488
506
  // ("hình thang ABCD vuông tại A" → right; "...cân" → isoceles).
489
507
  {
490
- name: new RegExp("h\xECnh\\s+thang\\s+" + QUAD, "gu"),
508
+ name: new RegExp("[Hh]\xECnh\\s+thang\\s+" + QUAD, "gu"),
491
509
  shape: "trapezoid",
492
510
  variant: "general"
493
511
  },
494
512
  {
495
- name: new RegExp("h\xECnh\\s+vu\xF4ng\\s+" + QUAD, "gu"),
513
+ name: new RegExp("[Hh]\xECnh\\s+vu\xF4ng\\s+" + QUAD, "gu"),
496
514
  shape: "square",
497
515
  variant: "standard"
498
516
  },
499
517
  {
500
- name: new RegExp("h\xECnh\\s+ch\u1EEF\\s+nh\u1EADt\\s+" + QUAD, "gu"),
518
+ name: new RegExp("[Hh]\xECnh\\s+ch\u1EEF\\s+nh\u1EADt\\s+" + QUAD, "gu"),
501
519
  shape: "rectangle",
502
520
  variant: "wide"
503
521
  },
504
522
  {
505
- name: new RegExp("h\xECnh\\s+b\xECnh\\s+h\xE0nh\\s+" + QUAD, "gu"),
523
+ name: new RegExp("[Hh]\xECnh\\s+b\xECnh\\s+h\xE0nh\\s+" + QUAD, "gu"),
506
524
  shape: "parallelogram",
507
525
  variant: "standard"
508
526
  },
509
527
  {
510
- name: new RegExp("h\xECnh\\s+thoi\\s+" + QUAD, "gu"),
528
+ name: new RegExp("[Hh]\xECnh\\s+thoi\\s+" + QUAD, "gu"),
511
529
  shape: "rhombus",
512
530
  variant: "standard"
513
531
  },
514
532
  {
515
- name: new RegExp("t\u1EE9\\s+gi\xE1c(?:\\s+l\u1ED3i)?\\s+" + QUAD, "gu"),
533
+ name: new RegExp("[Tt]\u1EE9\\s+gi\xE1c(?:\\s+l\u1ED3i)?\\s+" + QUAD, "gu"),
516
534
  shape: "quadrilateral",
517
535
  variant: "any"
518
536
  },
@@ -562,10 +580,10 @@ var KINDS = [
562
580
  variant: "any"
563
581
  }
564
582
  ];
565
- var PREFILTER = /hình\s+(?:vuông|chữ\s+nhật|bình\s+hành|thoi|thang)|tứ\s+giác|[Ss]quare|[Rr]ectangle|[Pp]arallelogram|[Rr]hombus|[Tt]rapezoid|[Tt]rapezium|[Qq]uadrilateral/u;
583
+ var PREFILTER = /[Hh]ình\s+(?:vuông|chữ\s+nhật|bình\s+hành|thoi|thang)|[Tt]ứ\s+giác|[Ss]quare|[Rr]ectangle|[Pp]arallelogram|[Rr]hombus|[Tt]rapezoid|[Tt]rapezium|[Qq]uadrilateral/u;
566
584
  var RIGHT_AFTER = /vuông(?!\p{L})/u;
567
585
  var ISO_AFTER = /(?<!\p{L})cân(?!\p{L})/u;
568
- var NEXT_SHAPE = /hình\s|tứ\s+giác/u;
586
+ var NEXT_SHAPE = /[Hh]ình\s|[Tt]ứ\s+giác/u;
569
587
  function scanClause(text) {
570
588
  const claimed = [];
571
589
  const taken = [];
@@ -609,13 +627,17 @@ var CYCLIC_QUAD_THETAS = CYCLIC_QUAD_COORDS.map(
609
627
  );
610
628
  var CENTER = "(?:\\(\\s*([A-Z])\\s*\\)|t\xE2m\\s+([A-Z]))?";
611
629
  var QUAD_INSCRIBED_IN_CIRCLE = new RegExp(
612
- "^[\\s,]*(?:n\u1ED9i\\s*ti\u1EBFp\\s+(?:trong\\s+)?(?:m\u1ED9t\\s+)?\u0111\u01B0\u1EDDng\\s*tr\xF2n|(?:is\\s+|are\\s+)?inscribed\\s+in\\s+(?:a\\s+|the\\s+)?circle)\\s*" + CENTER,
630
+ "^[\\s,]*(?:n\u1ED9i\\s*ti\u1EBFp\\s+(?:trong\\s+)?(?:m\u1ED9t\\s+)?\u0111\u01B0\u1EDDng\\s*tr\xF2n\\s*" + CENTER + "|n\u1ED9i\\s*ti\u1EBFp\\s+(?:trong\\s+)?\\(\\s*([A-Z])\\s*\\)|(?:is\\s+|are\\s+)?inscribed\\s+in\\s+(?:a\\s+|the\\s+)?circle\\s*" + CENTER + ")",
613
631
  "iu"
614
632
  );
615
633
  var CIRCLE_CIRCUMSCRIBES_QUAD = new RegExp(
616
634
  "(?:\u0111\u01B0\u1EDDng\\s*tr\xF2n|circle)\\s*" + CENTER + "\\s*(?:ngo\u1EA1i\\s*ti\u1EBFp|(?:is\\s+|are\\s+)?circumscrib(?:es|ed)(?:\\s+(?:about|around))?)\\s+$",
617
635
  "iu"
618
636
  );
637
+ var TUGIAC_NOITIEP = new RegExp(
638
+ "[Tt]\u1EE9\\s+gi\xE1c(?:\\s+l\u1ED3i)?\\s+n\u1ED9i\\s*ti\u1EBFp\\s+(?:(?:trong\\s+)?(?:m\u1ED9t\\s+)?\u0111\u01B0\u1EDDng\\s*tr\xF2n\\s+)?(?:\\(\\s*([A-Z])\\s*\\)\\s+)?" + QUAD,
639
+ "gu"
640
+ );
619
641
  var TRIANGLE_DECL = /(?:tam\s*giác|[Tt]riangle)\s+([A-Z])([A-Z])([A-Z])(?![A-Z])/gu;
620
642
  function ownedByOthers(ctx, selfLabels) {
621
643
  const owned = /* @__PURE__ */ new Set();
@@ -634,9 +656,26 @@ function ownedByOthers(ctx, selfLabels) {
634
656
  }
635
657
  return owned;
636
658
  }
659
+ function conflictingOwners(ctx, quadLabels) {
660
+ const quadSet = new Set(quadLabels);
661
+ const owned = /* @__PURE__ */ new Set();
662
+ const addIfOutside = (verts) => {
663
+ if (verts.some((v) => !quadSet.has(v))) for (const v of verts) owned.add(v);
664
+ };
665
+ TRIANGLE_DECL.lastIndex = 0;
666
+ for (const m of ctx.problem.matchAll(TRIANGLE_DECL)) addIfOutside([m[1], m[2], m[3]]);
667
+ const selfKey = quadLabels.join("");
668
+ for (const c of ctx.clauses) {
669
+ for (const h of scanClause(c.text)) {
670
+ if (h.labels.join("") === selfKey) continue;
671
+ addIfOutside(h.labels);
672
+ }
673
+ }
674
+ return owned;
675
+ }
637
676
  function detectCyclic(text, hit) {
638
677
  const a = QUAD_INSCRIBED_IN_CIRCLE.exec(text.slice(hit.afterEnd));
639
- if (a) return a[1] ?? a[2] ?? "";
678
+ if (a) return a[1] ?? a[2] ?? a[3] ?? a[4] ?? a[5] ?? "";
640
679
  const b = CIRCLE_CIRCUMSCRIBES_QUAD.exec(text.slice(0, hit.index));
641
680
  if (b) return b[1] ?? b[2] ?? "";
642
681
  return void 0;
@@ -649,12 +688,30 @@ var quadRule = {
649
688
  match(ctx) {
650
689
  const out = [];
651
690
  for (const c of ctx.clauses) {
691
+ TUGIAC_NOITIEP.lastIndex = 0;
692
+ for (const m of c.text.matchAll(TUGIAC_NOITIEP)) {
693
+ const labels = [m[2], m[3], m[4], m[5]];
694
+ const owned = ownedByOthers(ctx, labels);
695
+ if (labels.some((lbl) => owned.has(lbl))) continue;
696
+ const centerName = m[1] || "O";
697
+ out.push({
698
+ ruleId: "quad",
699
+ clauseIds: [c.id],
700
+ intents: [
701
+ drawCircle(centerName, "centerRadius", { center: centerName, radius: CYCLIC_RADIUS }),
702
+ ...labels.map(
703
+ (lbl, i) => addPoint(lbl, { kind: "onCircle", circle: centerName, theta: CYCLIC_QUAD_THETAS[i] })
704
+ ),
705
+ markShape("quadrilateral", labels)
706
+ ]
707
+ });
708
+ }
652
709
  for (const hit of scanClause(c.text)) {
653
710
  let intents = [drawShape(hit.shape, hit.labels, hit.variant)];
654
711
  if (hit.shape === "quadrilateral") {
655
712
  const center = detectCyclic(c.text, hit);
656
713
  if (center !== void 0) {
657
- const owned = ownedByOthers(ctx, hit.labels);
714
+ const owned = conflictingOwners(ctx, hit.labels);
658
715
  const shared = hit.labels.some((lbl) => owned.has(lbl));
659
716
  if (!shared) {
660
717
  const centerName = center || "O";
@@ -764,11 +821,11 @@ var NAME_AFTER_G = new RegExp(
764
821
  "gu"
765
822
  );
766
823
  var DISTRIB = new RegExp(
767
- `((?:[A-Z](?:['\u2032]?)\\s*(?:,|v\xE0)\\s*)+[A-Z](?:['\u2032]?))\\s+(?:l\u1EA7n\\s*l\u01B0\u1EE3t\\s+|theo\\s+th\u1EE9\\s+t\u1EF1\\s+)?(?:l\xE0\\s+)?(?:\u0111i\u1EC3m\\s+)?trung\\s*\u0111i\u1EC3m\\s+(?:c\u1EE7a\\s+)?(?:c\xE1c\\s+)?${SIDE_PREFIX}((?:[A-Z][A-Z]\\s*,\\s*)*(?:[A-Z][A-Z]\\s*,\\s*)?[A-Z][A-Z](?:\\s*v\xE0\\s*[A-Z][A-Z])?)`,
824
+ `((?:[A-Z](?:['\u2032]?)\\s*(?:,|v\xE0)\\s*)+[A-Z](?:['\u2032]?))\\s+(?:l\u1EA7n\\s*l\u01B0\u1EE3t\\s+|(?:theo\\s+)?th\u1EE9\\s+t\u1EF1\\s+)?(?:l\xE0\\s+)?(?:\u0111i\u1EC3m\\s+)?trung\\s*\u0111i\u1EC3m\\s+(?:c\u1EE7a\\s+)?(?:c\xE1c\\s+)?${SIDE_PREFIX}((?:[A-Z][A-Z]\\s*,\\s*)*(?:[A-Z][A-Z]\\s*,\\s*)?[A-Z][A-Z](?:\\s*v\xE0\\s*[A-Z][A-Z])?)`,
768
825
  "u"
769
826
  );
770
827
  var DISTRIB_AFTER = new RegExp(
771
- `trung\\s*\u0111i\u1EC3m\\s+(?:c\u1EE7a\\s+)?(?:c\xE1c\\s+)?${SIDE_PREFIX}((?:[A-Z][A-Z]\\s*(?:,|v\xE0)\\s*)+[A-Z][A-Z])(?![A-Z])\\s+(?:l\u1EA7n\\s*l\u01B0\u1EE3t\\s+|theo\\s+th\u1EE9\\s+t\u1EF1\\s+)?l\xE0\\s+((?:[A-Z](?:['\u2032]?)\\s*(?:,|v\xE0)\\s*)+[A-Z](?:['\u2032]?))(?!\\p{L})`,
828
+ `trung\\s*\u0111i\u1EC3m\\s+(?:c\u1EE7a\\s+)?(?:c\xE1c\\s+)?${SIDE_PREFIX}((?:[A-Z][A-Z]\\s*(?:,|v\xE0)\\s*)+[A-Z][A-Z])(?![A-Z])\\s+(?:l\u1EA7n\\s*l\u01B0\u1EE3t\\s+|(?:theo\\s+)?th\u1EE9\\s+t\u1EF1\\s+)?l\xE0\\s+((?:[A-Z](?:['\u2032]?)\\s*(?:,|v\xE0)\\s*)+[A-Z](?:['\u2032]?))(?!\\p{L})`,
772
829
  "u"
773
830
  );
774
831
  function nameToken(raw) {
@@ -1134,8 +1191,8 @@ var PREFILTER4 = [
1134
1191
  /[Bb]isector\s+of\s+(?:the\s+)?(?:[Aa]ngle|∠)/u
1135
1192
  // Form A EN
1136
1193
  ];
1137
- var ANGLE3 = /phân\s*giác\s+(?:trong\s+)?(?:(?:của|cho)\s+)?góc\s+([A-Z])([A-Z])([A-Z])(?![A-Z])/giu;
1138
- var ANGLE1 = /phân\s*giác\s+(?:trong\s+)?(?:(?:của|cho)\s+)?góc\s+([A-Z])(?![A-Z])/giu;
1194
+ var ANGLE3 = /phân\s*giác\s+(?:trong\s+)?(?:(?:của|cho)\s+)?(?:góc\s+|∠\s*)([A-Z])([A-Z])([A-Z])(?![A-Z])/giu;
1195
+ var ANGLE1 = /phân\s*giác\s+(?:trong\s+)?(?:(?:của|cho)\s+)?(?:góc\s+|∠\s*)([A-Z])(?![A-Z])/giu;
1139
1196
  var CHAN_BEFORE = /chân\s+(?:của\s+)?(?:đường\s*|tia\s+)?$/u;
1140
1197
  var ANGLE3_EN_A = /[Bb]isector\s+of\s+(?:the\s+)?(?:[Aa]ngle|∠)\s*([A-Z])([A-Z])([A-Z])(?![A-Z])/gu;
1141
1198
  var ANGLE3_EN_B = /[Aa]ngle\s+bisector\s+of\s+([A-Z])([A-Z])([A-Z])(?![A-Z])/gu;
@@ -1313,6 +1370,13 @@ function resolveTriangle(clauseText, kwIndex, uniqueProblemTri) {
1313
1370
  }
1314
1371
  return uniqueProblemTri;
1315
1372
  }
1373
+ function dropDegenerate(intents) {
1374
+ return intents.filter((i) => {
1375
+ if (i.op !== "add-point") return true;
1376
+ const of = i.constraint.of;
1377
+ return !(Array.isArray(of) && of.includes(i.name));
1378
+ });
1379
+ }
1316
1380
  var centersRule = {
1317
1381
  id: "centers",
1318
1382
  priority: 70,
@@ -1345,7 +1409,8 @@ var centersRule = {
1345
1409
  if (of) {
1346
1410
  intents.push(addPoint(dc[1], { kind: centerTypeToKind(dc[3]), of }));
1347
1411
  intents.push(addPoint(dc[2], { kind: centerTypeToKind(dc[4]), of }));
1348
- out.push({ ruleId: "centers", clauseIds: [c.id], intents });
1412
+ const kept2 = dropDegenerate(intents);
1413
+ if (kept2.length) out.push({ ruleId: "centers", clauseIds: [c.id], intents: kept2 });
1349
1414
  continue;
1350
1415
  }
1351
1416
  }
@@ -1394,8 +1459,9 @@ var centersRule = {
1394
1459
  const of = ofFor(c.text, INCENTER_KW_EN);
1395
1460
  if (name && of) intents.push(addPoint(name, { kind: "incenter", of }));
1396
1461
  }
1397
- if (intents.length > 0) {
1398
- out.push({ ruleId: "centers", clauseIds: [c.id], intents });
1462
+ const kept = dropDegenerate(intents);
1463
+ if (kept.length > 0) {
1464
+ out.push({ ruleId: "centers", clauseIds: [c.id], intents: kept });
1399
1465
  }
1400
1466
  }
1401
1467
  return out;
@@ -1408,9 +1474,11 @@ var PREFILTER5 = new RegExp(
1408
1474
  "u"
1409
1475
  );
1410
1476
  var PREFILTER_EN = /projection|perpendicular|foot\s+of/i;
1411
- var LINE = "(?:" + DUONG_KW + "\\s*th\u1EB3ng\\s+|c\u1EA1nh\\s+|\u0111o\u1EA1n(?:\\s+th\u1EB3ng)?\\s+)?([A-Z]{1,2})(?![A-Z])";
1477
+ var LINE_PREFIX = "(?:" + DUONG_KW + "\\s*th\u1EB3ng\\s+|ti\u1EBFp\\s*tuy\u1EBFn\\s+|c\u1EA1nh\\s+|\u0111o\u1EA1n(?:\\s+th\u1EB3ng)?\\s+)?";
1478
+ var LINE_TOK = "([A-Z]{1,2}(?![A-Za-z])|[A-Z]?[a-z][a-z]?[0-9]?(?![A-Za-z]))";
1479
+ var LINE = LINE_PREFIX + LINE_TOK;
1412
1480
  var PREP = "(?:tr\xEAn|l\xEAn|xu\u1ED1ng|\u0111\u1EBFn|t\u1EDBi)";
1413
- var LANLUOT_OPT = "(?:(?:l\u1EA7n\\s*l\u01B0\u1EE3t|theo\\s+th\u1EE9\\s+t\u1EF1|th\u1EE9\\s+t\u1EF1)\\s+)?";
1481
+ var LANLUOT_OPT = "(?:(?:l\u1EA7n\\s*l\u01B0\u1EE3t|theo\\s+th\u1EE9\\s+t\u1EF1|th\u1EE9\\s+t\u1EF1|t\u01B0\u01A1ng\\s+\u1EE9ng)\\s+)?";
1414
1482
  var PROJ_CORE = `h\xECnh\\s*chi\u1EBFu\\s+(?:vu\xF4ng\\s*g\xF3c\\s+)?(?:c\u1EE7a\\s+)?(?:\u0111i\u1EC3m\\s+)?([A-Z])\\s+${PREP}\\s+${LINE}`;
1415
1483
  var FOOT_CORE = `ch\xE2n\\s+(?:c\u1EE7a\\s+)?${DUONG_KW}\\s+(?:vu\xF4ng\\s*g\xF3c|cao)\\s*(?:h\u1EA1\\s+|k\u1EBB\\s+|v\u1EBD\\s+|d\u1EF1ng\\s+)?(?:t\u1EEB\\s+)?(?:\u0111i\u1EC3m\\s+)?([A-Z])\\s+${PREP}\\s+${LINE}`;
1416
1484
  var LANLUOT = new RegExp(
@@ -1421,14 +1489,27 @@ var SINGLE = new RegExp(`(?:${PROJ_CORE})|(?:${FOOT_CORE})`, "gu");
1421
1489
  var NAMES_BLOB = "((?:[A-Z](?:['\u2032]?)\\s*(?:,|v\xE0)\\s*)+[A-Z](?:['\u2032]?))";
1422
1490
  var LINES_BLOB = "((?:[A-Z]{1,2}\\s*(?:,|v\xE0)\\s*)+[A-Z]{1,2})(?!\\p{L})";
1423
1491
  var LINES_PREFIX = "(?:c\xE1c\\s+)?(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|c\u1EA1nh\\s+|\u0111o\u1EA1n\\s+)?";
1492
+ var NO_HOA_BEFORE = "(?<![A-Z])";
1424
1493
  var DISTRIB_PROJ = new RegExp(
1425
- NAMES_BLOB + "\\s+" + LANLUOT_OPT + "(?:l\xE0\\s+)?(?:c\xE1c\\s+)?h\xECnh\\s*chi\u1EBFu\\s+(?:vu\xF4ng\\s*g\xF3c\\s+)?(?:c\u1EE7a\\s+)?(?:\u0111i\u1EC3m\\s+)?([A-Z])(?!\\p{L})\\s+(?:tr\xEAn|l\xEAn|xu\u1ED1ng|\u0111\u1EBFn)\\s+" + LINES_PREFIX + LINES_BLOB,
1494
+ NO_HOA_BEFORE + NAMES_BLOB + "\\s+" + LANLUOT_OPT + "(?:l\xE0\\s+)?(?:c\xE1c\\s+)?h\xECnh\\s*chi\u1EBFu\\s+(?:vu\xF4ng\\s*g\xF3c\\s+)?(?:c\u1EE7a\\s+)?(?:\u0111i\u1EC3m\\s+)?([A-Z])(?!\\p{L})\\s+(?:tr\xEAn|l\xEAn|xu\u1ED1ng|\u0111\u1EBFn)\\s+" + LINES_PREFIX + LINES_BLOB,
1426
1495
  "gu"
1427
1496
  );
1428
1497
  var DISTRIB_FOOT = new RegExp(
1429
- NAMES_BLOB + "\\s+" + LANLUOT_OPT + "(?:l\xE0\\s+)?(?:c\xE1c\\s+)?ch\xE2n\\s+(?:c\u1EE7a\\s+)?(?:c\xE1c\\s+)?\u0111\u01B0\u1EDDng\\s+(?:vu\xF4ng\\s*g\xF3c|cao)\\s*(?:h\u1EA1\\s+|k\u1EBB\\s+|v\u1EBD\\s+|d\u1EF1ng\\s+)?(?:t\u1EEB\\s+)?(?:\u0111i\u1EC3m\\s+)?([A-Z])(?!\\p{L})\\s+(?:\u0111\u1EBFn|xu\u1ED1ng|tr\xEAn|t\u1EDBi)\\s+" + LINES_PREFIX + LINES_BLOB,
1498
+ NO_HOA_BEFORE + NAMES_BLOB + "\\s+" + LANLUOT_OPT + "(?:l\xE0\\s+)?(?:c\xE1c\\s+)?ch\xE2n\\s+(?:c\u1EE7a\\s+)?(?:c\xE1c\\s+)?\u0111\u01B0\u1EDDng\\s+(?:vu\xF4ng\\s*g\xF3c|cao)\\s*(?:h\u1EA1\\s+|k\u1EBB\\s+|v\u1EBD\\s+|d\u1EF1ng\\s+)?(?:t\u1EEB\\s+)?(?:\u0111i\u1EC3m\\s+)?([A-Z])(?!\\p{L})\\s+(?:\u0111\u1EBFn|xu\u1ED1ng|tr\xEAn|t\u1EDBi)\\s+" + LINES_PREFIX + LINES_BLOB,
1499
+ "gu"
1500
+ );
1501
+ var NAMES_BLOB_IDX = "((?:[A-Z][0-9]?(?:['\u2032]?)\\s*(?:,|v\xE0)\\s*)+[A-Z][0-9]?(?:['\u2032]?))";
1502
+ var FROMS_BLOB_IDX = "((?:[A-Z][0-9]?(?:['\u2032]?)\\s*(?:,|v\xE0)\\s*)+[A-Z][0-9]?(?:['\u2032]?))(?!\\p{L})";
1503
+ var ZIP_FROM_PROJ = new RegExp(
1504
+ NO_HOA_BEFORE + NAMES_BLOB_IDX + "\\s+" + LANLUOT_OPT + "(?:l\xE0\\s+)?(?:c\xE1c\\s+)?h\xECnh\\s*chi\u1EBFu\\s+(?:vu\xF4ng\\s*g\xF3c\\s+)?(?:c\u1EE7a\\s+)?(?:\u0111i\u1EC3m\\s+)?" + FROMS_BLOB_IDX + "\\s+(?:tr\xEAn|l\xEAn|xu\u1ED1ng|\u0111\u1EBFn)\\s+" + LINES_PREFIX + LINES_BLOB,
1430
1505
  "gu"
1431
1506
  );
1507
+ function splitNamesIdx(blob) {
1508
+ return blob.split(/,|và/).map((s) => {
1509
+ const mm = /^([A-Z][0-9]?)(['′]?)/u.exec(s.trim());
1510
+ return mm ? mm[2] ? `${mm[1]}'` : mm[1] : "";
1511
+ }).filter(Boolean);
1512
+ }
1432
1513
  var TWO_LINES_PREP = "(?:tr\xEAn|l\xEAn|xu\u1ED1ng|\u0111\u1EBFn|t\u1EDBi)";
1433
1514
  var TWO_LINES_LINE = "(?:c\xE1c\\s+)?(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|c\u1EA1nh\\s+|\u0111o\u1EA1n\\s+)?([A-Z]{1,2})(?![A-Z])";
1434
1515
  var SHARED_FROM_TWO = new RegExp(
@@ -1436,21 +1517,44 @@ var SHARED_FROM_TWO = new RegExp(
1436
1517
  "gu"
1437
1518
  );
1438
1519
  var FEET_FROM_SHARED_LINE = new RegExp(
1439
- "(?:[Hh]ai\\s+\u0111i\u1EC3m\\s+|[Cc]\xE1c\\s+\u0111i\u1EC3m\\s+)?([A-Z])(?:['\u2032])?\\s*(?:,|v\xE0)\\s*([A-Z])(?:['\u2032])?\\s+l\u1EA7n\\s*l\u01B0\u1EE3t\\s+l\xE0\\s+ch\xE2n\\s+(?:c\xE1c\\s+)?(?:\u0111\u01B0\u1EDDng\\s+)?(?:vu\xF4ng\\s*g\xF3c|cao)\\s+(?:k\u1EBB\\s+|h\u1EA1\\s+|v\u1EBD\\s+|d\u1EF1ng\\s+)?(?:t\u1EEB\\s+)?([A-Z](?:['\u2032])?)\\s*(?:,|v\xE0)\\s*([A-Z](?:['\u2032])?)\\s+(?:\u0111\u1EBFn|xu\u1ED1ng|tr\xEAn|t\u1EDBi)\\s+(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|c\u1EA1nh\\s+|\u0111o\u1EA1n\\s+)?([A-Z]{1,2}(?:['\u2032])?)(?![A-Z])",
1520
+ "(?:[Hh]ai\\s+\u0111i\u1EC3m\\s+|[Cc]\xE1c\\s+\u0111i\u1EC3m\\s+)?([A-Z])(?:['\u2032])?\\s*(?:,|v\xE0)\\s*([A-Z])(?:['\u2032])?\\s+" + // "lần lượt"/"theo thứ tự"/"thứ tự" = phân phối 1-1, OPTIONAL (LANLUOT_OPT).
1521
+ LANLUOT_OPT + "l\xE0\\s+ch\xE2n\\s+(?:c\xE1c\\s+)?(?:\u0111\u01B0\u1EDDng\\s+)?(?:vu\xF4ng\\s*g\xF3c|cao)\\s+(?:k\u1EBB\\s+|h\u1EA1\\s+|v\u1EBD\\s+|d\u1EF1ng\\s+)?(?:t\u1EEB\\s+)?([A-Z](?:['\u2032])?)\\s*(?:,|v\xE0)\\s*([A-Z](?:['\u2032])?)\\s+(?:\u0111\u1EBFn|xu\u1ED1ng|tr\xEAn|t\u1EDBi)\\s+(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|\u0111\u01B0\u1EDDng\\s*k\xEDnh\\s+|c\u1EA1nh\\s+|\u0111o\u1EA1n\\s+)?([A-Z]{1,2}(?:['\u2032])?)(?![A-Z])",
1440
1522
  "gu"
1441
1523
  );
1524
+ var PERP_LINE_PREFIX = "(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|ti\u1EBFp\\s*tuy\u1EBFn\\s+|c\u1EA1nh\\s+|\u0111o\u1EA1n\\s+)?";
1525
+ var PERP_LINE_TOK = "([A-Z]{1,2}(?![A-Za-z])|[A-Z]?[a-z][a-z]?[0-9]?(?![A-Za-z]))";
1442
1526
  var PERP_DRAW_DISTRIB = new RegExp(
1443
1527
  "(?:[Kk]\u1EBB|[Vv]\u1EBD|[Dd]\u1EF1ng|[Hh]\u1EA1)\\s+([A-Z])([A-Z])\\s*,\\s*([A-Z])([A-Z])(?![A-Z])\\s+(?:l\u1EA7n\\s*l\u01B0\u1EE3t\\s+)?(?:\u22A5|vu\xF4ng\\s*g\xF3c(?:\\s+v\u1EDBi)?)\\s+(?:v\u1EDBi\\s+)?(?:c\xE1c\\s+)?(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|c\u1EA1nh\\s+|\u0111o\u1EA1n\\s+)?([A-Z]{2})\\s*,\\s*([A-Z]{2})(?![A-Z])",
1444
1528
  "gu"
1445
1529
  );
1530
+ var PAIRS_BLOB = "((?:[A-Z]{2}\\s*,\\s*)+[A-Z]{2})(?![A-Z])";
1531
+ var PERP_DRAW_ARE = new RegExp(
1532
+ "(?:[Kk]\u1EBB|[Vv]\u1EBD|[Dd]\u1EF1ng|[Hh]\u1EA1)\\s+" + PAIRS_BLOB + "\\s+l\xE0\\s+(?:c\xE1c\\s+)?(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|\u0111\u01B0\u1EDDng\\s+)?vu\xF4ng\\s*g\xF3c\\s+(?:t\u1EDBi|\u0111\u1EBFn|xu\u1ED1ng|v\u1EDBi)\\s+(?:c\xE1c\\s+)?(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|c\u1EA1nh\\s+|\u0111o\u1EA1n\\s+)?((?:[A-Z]{2}\\s*(?:,|v\xE0)\\s*)+[A-Z]{2})(?![A-Z])",
1533
+ "gu"
1534
+ );
1446
1535
  var PERP_SHARED_CUNG = new RegExp(
1447
- "(?:[Kk]\u1EBB|[Vv]\u1EBD|[Dd]\u1EF1ng|[Hh]\u1EA1)\\s+([A-Z])([A-Z])(?![A-Z])\\s+v\xE0\\s+([A-Z])([A-Z])(?![A-Z])\\s+c\xF9ng\\s+vu\xF4ng\\s*g\xF3c\\s+(?:v\u1EDBi\\s+)?(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|c\u1EA1nh\\s+|\u0111o\u1EA1n\\s+)?([A-Z]{1,2})(?![A-Z])",
1536
+ "(?:[Kk]\u1EBB|[Vv]\u1EBD|[Dd]\u1EF1ng|[Hh]\u1EA1)\\s+([A-Z])([A-Z])(?![A-Z])\\s+v\xE0\\s+([A-Z])([A-Z])(?![A-Z])\\s+(?:c\xF9ng\\s+)?vu\xF4ng\\s*g\xF3c\\s+(?:v\u1EDBi\\s+)?" + PERP_LINE_PREFIX + PERP_LINE_TOK,
1448
1537
  "gu"
1449
1538
  );
1450
1539
  var FROM_DRAW_DISTRIB = new RegExp(
1451
1540
  "t\u1EEB\\s+([A-Z])(?!\\p{L})\\s+k\u1EBB\\s+([A-Z]{2})\\s*,\\s*([A-Z]{2})\\s+vu\xF4ng\\s*g\xF3c\\s+v\u1EDBi\\s+c\xE1c\\s+c\u1EA1nh\\s+([A-Z]{2})\\s*,\\s*([A-Z]{2})(?![A-Z])",
1452
1541
  "gu"
1453
1542
  );
1543
+ var DECLARE_LINE = new RegExp(
1544
+ "(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng|ti\u1EBFp\\s*tuy\u1EBFn|c\xE1t\\s*tuy\u1EBFn|tia)\\s+([A-Z]?[a-z][a-z]?[0-9]?)(?![A-Za-z])",
1545
+ "gu"
1546
+ );
1547
+ function declaredNamedLines(problem) {
1548
+ const out = /* @__PURE__ */ new Set();
1549
+ DECLARE_LINE.lastIndex = 0;
1550
+ let m;
1551
+ while ((m = DECLARE_LINE.exec(problem)) !== null) out.add(m[1]);
1552
+ return out;
1553
+ }
1554
+ function isAllowedLine(line, declared) {
1555
+ if (/^[A-Z]{1,2}$/u.test(line)) return true;
1556
+ return declared.has(line);
1557
+ }
1454
1558
  function splitNames(blob) {
1455
1559
  return blob.split(/,|và/).map((s) => {
1456
1560
  const mm = /^([A-Z])(['′]?)/u.exec(s.trim());
@@ -1458,11 +1562,11 @@ function splitNames(blob) {
1458
1562
  }).filter(Boolean);
1459
1563
  }
1460
1564
  var PERP_DRAW = new RegExp(
1461
- `(?:[Kk]\u1EBB|[Vv]\u1EBD|[Dd]\u1EF1ng|[Hh]\u1EA1)\\s+([A-Z])([A-Z])(?![A-Z])\\s+(?:\u22A5\\s*|vu\xF4ng\\s*g\xF3c(?:\\s+v\u1EDBi)?\\s+)(?:v\u1EDBi\\s+)?(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|c\u1EA1nh\\s+|\u0111o\u1EA1n\\s+)?([A-Z]{1,2})(?![A-Z])(?:\\s+t\u1EA1i\\s+([A-Z]))?`,
1565
+ `(?:[Kk]\u1EBB|[Vv]\u1EBD|[Dd]\u1EF1ng|[Hh]\u1EA1)\\s+([A-Z])([A-Z])(?![A-Z])\\s+(?:\u22A5\\s*|vu\xF4ng\\s*g\xF3c(?:\\s+v\u1EDBi)?\\s+)(?:v\u1EDBi\\s+)?${PERP_LINE_PREFIX}${PERP_LINE_TOK}(?:\\s+t\u1EA1i\\s+([A-Z]))?`,
1462
1566
  "gu"
1463
1567
  );
1464
1568
  var PERP_BARE = new RegExp(
1465
- `([A-Z])([A-Z])(?![A-Z])\\s+(?:\u22A5\\s*|vu\xF4ng\\s*g\xF3c(?:\\s+v\u1EDBi)?\\s+)(?:v\u1EDBi\\s+)?(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|c\u1EA1nh\\s+|\u0111o\u1EA1n\\s+)?([A-Z]{1,2})(?![A-Z])(?:\\s+t\u1EA1i\\s+([A-Z]))?`,
1569
+ `([A-Z])([A-Z])(?![A-Z])\\s+(?:\u22A5\\s*|vu\xF4ng\\s*g\xF3c(?:\\s+v\u1EDBi)?\\s+)(?:v\u1EDBi\\s+)?${PERP_LINE_PREFIX}${PERP_LINE_TOK}(?:\\s+t\u1EA1i\\s+([A-Z]))?`,
1466
1570
  "gu"
1467
1571
  );
1468
1572
  var DRAW_VERB = /[Kk]ẻ|[Vv]ẽ|[Dd]ựng/u;
@@ -1531,7 +1635,7 @@ function parseAltitudeBundle(text, fallbackTri) {
1531
1635
  }
1532
1636
  return out;
1533
1637
  }
1534
- function parseFeet(text, fallbackTri) {
1638
+ function parseFeet(text, fallbackTri, declared = /* @__PURE__ */ new Set()) {
1535
1639
  const out = [];
1536
1640
  const consumed = [];
1537
1641
  const bundled = parseAltitudeBundle(text, fallbackTri);
@@ -1573,6 +1677,24 @@ function parseFeet(text, fallbackTri) {
1573
1677
  }
1574
1678
  if (ok) return local;
1575
1679
  }
1680
+ ZIP_FROM_PROJ.lastIndex = 0;
1681
+ for (const zm of text.matchAll(ZIP_FROM_PROJ)) {
1682
+ const names = splitNamesIdx(zm[1]);
1683
+ const froms = splitNamesIdx(zm[2]);
1684
+ const lines = zm[3].split(/,|và/).map((s) => s.trim()).filter(Boolean);
1685
+ if (names.length < 2) continue;
1686
+ if (names.length !== froms.length || names.length !== lines.length) continue;
1687
+ const local = [];
1688
+ let ok = true;
1689
+ for (let i = 0; i < names.length; i++) {
1690
+ if (lines[i].includes(names[i]) || names[i] === froms[i]) {
1691
+ ok = false;
1692
+ break;
1693
+ }
1694
+ local.push({ name: names[i], from: froms[i], onLine: lines[i], withSegment: true });
1695
+ }
1696
+ if (ok && local.length > 0) return local;
1697
+ }
1576
1698
  for (const re of [DISTRIB_PROJ, DISTRIB_FOOT]) {
1577
1699
  re.lastIndex = 0;
1578
1700
  for (const dm2 of text.matchAll(re)) {
@@ -1613,8 +1735,8 @@ function parseFeet(text, fallbackTri) {
1613
1735
  for (const fm of text.matchAll(FEET_FROM_SHARED_LINE)) {
1614
1736
  const start = fm.index ?? 0;
1615
1737
  if (consumed.some(([a, b]) => start >= a && start < b)) continue;
1616
- const norm2 = (s) => s.replace(/′/g, "'");
1617
- const [n1, n2, f1, f2, line] = [fm[1], fm[2], norm2(fm[3]), norm2(fm[4]), norm2(fm[5])];
1738
+ const norm5 = (s) => s.replace(/′/g, "'");
1739
+ const [n1, n2, f1, f2, line] = [fm[1], fm[2], norm5(fm[3]), norm5(fm[4]), norm5(fm[5])];
1618
1740
  if (n1 === n2 || f1 === f2 || line.includes(n1) || line.includes(n2)) continue;
1619
1741
  out.push({ name: n1, from: f1, onLine: line, withSegment: true });
1620
1742
  out.push({ name: n2, from: f2, onLine: line, withSegment: true });
@@ -1639,7 +1761,9 @@ function parseFeet(text, fallbackTri) {
1639
1761
  if (MID_BEFORE.test(before)) continue;
1640
1762
  const nm = NAME_BEFORE.exec(before);
1641
1763
  if (!nm) continue;
1642
- out.push({ name: nm[1], from: m[1] ?? m[3], onLine: m[2] ?? m[4] });
1764
+ const onLine = m[2] ?? m[4];
1765
+ if (!isAllowedLine(onLine, declared)) continue;
1766
+ out.push({ name: nm[1], from: m[1] ?? m[3], onLine });
1643
1767
  }
1644
1768
  PERP_DRAW_DISTRIB.lastIndex = 0;
1645
1769
  for (const ddm of text.matchAll(PERP_DRAW_DISTRIB)) {
@@ -1650,10 +1774,36 @@ function parseFeet(text, fallbackTri) {
1650
1774
  out.push({ name: foot2, from: from2, onLine: line2, withSegment: true });
1651
1775
  consumed.push([ddm.index ?? 0, (ddm.index ?? 0) + ddm[0].length]);
1652
1776
  }
1777
+ PERP_DRAW_ARE.lastIndex = 0;
1778
+ for (const am of text.matchAll(PERP_DRAW_ARE)) {
1779
+ const start = am.index ?? 0;
1780
+ if (consumed.some(([a, b]) => start >= a && start < b)) continue;
1781
+ const pairs = am[1].split(",").map((s) => s.trim()).filter((s) => /^[A-Z]{2}$/.test(s));
1782
+ const lines = am[2].split(/,|và/).map((s) => s.trim()).filter((s) => /^[A-Z]{2}$/.test(s));
1783
+ if (pairs.length < 2 || pairs.length !== lines.length) continue;
1784
+ const from = pairs[0][0];
1785
+ if (!pairs.every((p) => p[0] === from)) continue;
1786
+ const local = [];
1787
+ let ok = true;
1788
+ for (let i = 0; i < pairs.length; i++) {
1789
+ const foot = pairs[i][1];
1790
+ const onLine = lines[i];
1791
+ if (onLine.includes(foot)) {
1792
+ ok = false;
1793
+ break;
1794
+ }
1795
+ local.push({ name: foot, from, onLine, withSegment: true });
1796
+ }
1797
+ if (ok && local.length > 0) {
1798
+ out.push(...local);
1799
+ consumed.push([start, start + am[0].length]);
1800
+ }
1801
+ }
1653
1802
  PERP_SHARED_CUNG.lastIndex = 0;
1654
1803
  for (const sm of text.matchAll(PERP_SHARED_CUNG)) {
1655
1804
  const [f1, t1, f2, t2, line] = [sm[1], sm[2], sm[3], sm[4], sm[5]];
1656
1805
  if (t1 === t2 || line.includes(t1) || line.includes(t2)) continue;
1806
+ if (!isAllowedLine(line, declared)) continue;
1657
1807
  out.push({ name: t1, from: f1, onLine: line, withSegment: true });
1658
1808
  out.push({ name: t2, from: f2, onLine: line, withSegment: true });
1659
1809
  consumed.push([sm.index ?? 0, (sm.index ?? 0) + sm[0].length]);
@@ -1667,6 +1817,7 @@ function parseFeet(text, fallbackTri) {
1667
1817
  const at = pm[4];
1668
1818
  if (at && at !== foot) continue;
1669
1819
  if (onLine.includes(foot)) continue;
1820
+ if (!isAllowedLine(onLine, declared)) continue;
1670
1821
  out.push({ name: foot, from, onLine });
1671
1822
  }
1672
1823
  if (DRAW_VERB.test(text)) {
@@ -1679,6 +1830,7 @@ function parseFeet(text, fallbackTri) {
1679
1830
  const at = bm[4];
1680
1831
  if (at && at !== foot) continue;
1681
1832
  if (onLine.includes(foot)) continue;
1833
+ if (!isAllowedLine(onLine, declared)) continue;
1682
1834
  if (out.some((o) => o.name === foot)) continue;
1683
1835
  out.push({ name: foot, from, onLine });
1684
1836
  }
@@ -1714,13 +1866,14 @@ var perpFootRule = {
1714
1866
  match(ctx) {
1715
1867
  const out = [];
1716
1868
  const fallbackTri = uniqueTriangle(ctx.problem);
1869
+ const declared = declaredNamedLines(ctx.problem);
1717
1870
  for (const c of ctx.clauses) {
1718
- const feet = parseFeet(c.text, fallbackTri);
1871
+ const feet = parseFeet(c.text, fallbackTri, declared);
1719
1872
  if (feet.length === 0) continue;
1720
1873
  const intents = feet.flatMap((f) => {
1721
1874
  if (f.from === "") return [addPoint(f.name, { kind: "orthocenter", of: f.onLine.split("") })];
1722
- const add = addPoint(f.name, { kind: "perpFoot", from: f.from, onLine: f.onLine });
1723
- return f.withSegment ? [add, connect(f.from, f.name, "segment")] : [add];
1875
+ const add2 = addPoint(f.name, { kind: "perpFoot", from: f.from, onLine: f.onLine });
1876
+ return f.withSegment ? [add2, connect(f.from, f.name, "segment")] : [add2];
1724
1877
  });
1725
1878
  out.push({ ruleId: "perpFoot", clauseIds: [c.id], intents });
1726
1879
  }
@@ -1767,28 +1920,28 @@ function parseNum(raw) {
1767
1920
  function isInscribedCircumscribed(problem, center) {
1768
1921
  const REL = "(?:(?:n\u1ED9i|ngo\u1EA1i)\\s*ti\u1EBFp|[Ii]nscribed|[Cc]ircumscrib(?:es|ed))";
1769
1922
  const after = new RegExp(
1770
- `${REL}[^.]{0,30}?(?:\u0111\u01B0\u1EDDng\\s*tr\xF2n\\s*|[Cc]ircle\\s*)?\\(?\\s*${center}(?![A-Z])`,
1923
+ `${REL}[^.]{0,30}?(?:\u0111\u01B0\u1EDDng\\s*tr\xF2n\\s*|[Cc]ircle\\s*)?\\(?\\s*${escapeRe(center)}(?![A-Z])`,
1771
1924
  "u"
1772
1925
  );
1773
1926
  const before = new RegExp(
1774
- `\\(\\s*${center}\\s*[;,][^()]*\\)[^.]{0,30}?${REL}`,
1927
+ `\\(\\s*${escapeRe(center)}\\s*[;,][^()]*\\)[^.]{0,30}?${REL}`,
1775
1928
  "u"
1776
1929
  );
1777
1930
  return after.test(problem) || before.test(problem);
1778
1931
  }
1779
1932
  function isDiameterCircle(problem, center) {
1780
1933
  const parenThenDiameter = new RegExp(
1781
- `\\(\\s*${center}\\s*[;,][^()]*\\)[^.]{0,40}?${DUONG_KW}\\s*k\xEDnh\\s+[A-Z]{2}(?![A-Z])`,
1934
+ `\\(\\s*${escapeRe(center)}\\s*[;,][^()]*\\)[^.]{0,40}?${DUONG_KW}\\s*k\xEDnh\\s+[A-Z]{2}(?![A-Z])`,
1782
1935
  "u"
1783
1936
  );
1784
1937
  const namedThenDiameter = new RegExp(
1785
- `${CIRCLE_KW}\\s*\\(\\s*${center}\\s*\\)[^.]{0,40}?${DUONG_KW}\\s*k\xEDnh\\s+[A-Z]{2}(?![A-Z])`,
1938
+ `${CIRCLE_KW}\\s*\\(\\s*${escapeRe(center)}\\s*\\)[^.]{0,40}?${DUONG_KW}\\s*k\xEDnh\\s+[A-Z]{2}(?![A-Z])`,
1786
1939
  "u"
1787
1940
  );
1788
1941
  return parenThenDiameter.test(problem) || namedThenDiameter.test(problem);
1789
1942
  }
1790
1943
  function findParenClauseId(clauses, center) {
1791
- const frag = new RegExp(`\\(\\s*${center}(?![A-Z])`, "u");
1944
+ const frag = new RegExp(`\\(\\s*${escapeRe(center)}(?![A-Z])`, "u");
1792
1945
  for (const c of clauses) if (frag.test(c.text)) return c.id;
1793
1946
  return void 0;
1794
1947
  }
@@ -2326,8 +2479,52 @@ var externalPointRule = {
2326
2479
  }
2327
2480
  };
2328
2481
 
2482
+ // src/stamps/geometry-2d/ai/rules/externalPointAtRadius.ts
2483
+ var METRIC = /([A-Z])(?:['′]?)([A-Z])(?:['′]?)\s*=\s*([2-9])\s*[Rr](?![A-Za-z])/gu;
2484
+ function isCenter(problem, c) {
2485
+ const e = escapeRe(c);
2486
+ return new RegExp(
2487
+ String.raw`\(\s*${e}[\s);,]|tâm\s*${e}(?![A-Za-z])|đường\s*tròn\s*\(?\s*${e}(?![A-Za-z])`,
2488
+ "u"
2489
+ ).test(problem);
2490
+ }
2491
+ var externalPointAtRadiusRule = {
2492
+ id: "externalPointAtRadius",
2493
+ priority: 67,
2494
+ languages: ["vi"],
2495
+ patterns: [/[A-Z](?:['′]?)[A-Z](?:['′]?)\s*=\s*[2-9]\s*[Rr](?![A-Za-z])/u],
2496
+ match(ctx) {
2497
+ const out = [];
2498
+ for (const c of ctx.clauses) {
2499
+ METRIC.lastIndex = 0;
2500
+ for (const m of c.text.matchAll(METRIC)) {
2501
+ const a = m[1];
2502
+ const b = m[2];
2503
+ let center;
2504
+ let point;
2505
+ const aC = isCenter(ctx.problem, a);
2506
+ const bC = isCenter(ctx.problem, b);
2507
+ if (aC && !bC) {
2508
+ center = a;
2509
+ point = b;
2510
+ } else if (bC && !aC) {
2511
+ center = b;
2512
+ point = a;
2513
+ }
2514
+ if (!center || !point || center === point) continue;
2515
+ out.push({
2516
+ ruleId: "externalPointAtRadius",
2517
+ clauseIds: [c.id],
2518
+ intents: [addPoint(point, { kind: "externalToCircle", circle: center })]
2519
+ });
2520
+ }
2521
+ }
2522
+ return out;
2523
+ }
2524
+ };
2525
+
2329
2526
  // src/stamps/geometry-2d/ai/rules/arcMidpoint.ts
2330
- var ARC_MID = /(?:chính\s+giữa|trung\s*điểm)\s+(?:của\s+)?cung/u;
2527
+ var ARC_MID = /(?:chính\s+giữa|trung\s*điểm)\s+(?:của\s+|các\s+)*cung/u;
2331
2528
  var ARC_PAIR = /(?:chính\s+giữa|trung\s*điểm)\s+(?:của\s+)?cung\s+(nhỏ\s+|lớn\s+)?([A-Z])([A-Z])/u;
2332
2529
  var NAME_BEFORE2 = /([A-Z])(?:['′]?)\s+(?:là\s+)?(?:điểm\s+)?(?:chính\s+giữa|trung\s*điểm)\s+(?:của\s+)?cung/u;
2333
2530
  var TRI3 = /tam giác\s+([A-Z])([A-Z])([A-Z])/u;
@@ -2363,6 +2560,10 @@ var DISTRIB_TWO_ARCS = new RegExp(
2363
2560
  "([A-Z])(?:['\u2032]?)\\s*(?:,|v\xE0)\\s*([A-Z])(?:['\u2032]?)\\s+l\u1EA7n\\s*l\u01B0\u1EE3t\\s+(?:l\xE0\\s+)?(?:\u0111i\u1EC3m\\s+)?(?:ch\xEDnh\\s+gi\u1EEFa|trung\\s*\u0111i\u1EC3m)\\s+(?:c\u1EE7a\\s+)?cung\\s+(?:nh\u1ECF\\s+|l\u1EDBn\\s+)?([A-Z])([A-Z])(?![A-Z])\\s+v\xE0\\s+(?:\u0111i\u1EC3m\\s+)?(?:(?:ch\xEDnh\\s+gi\u1EEFa|trung\\s*\u0111i\u1EC3m)\\s+)?(?:c\u1EE7a\\s+)?cung\\s+(?:nh\u1ECF\\s+|l\u1EDBn\\s+)?([A-Z])([A-Z])(?![A-Z])",
2364
2561
  "u"
2365
2562
  );
2563
+ var DISTRIB_TWO_ARCS3 = new RegExp(
2564
+ "([A-Z])(?:['\u2032]?)\\s*(?:,|v\xE0)\\s*([A-Z])(?:['\u2032]?)\\s+l\u1EA7n\\s*l\u01B0\u1EE3t\\s+(?:l\xE0\\s+)?(?:\u0111i\u1EC3m\\s+)?(?:ch\xEDnh\\s+gi\u1EEFa|trung\\s*\u0111i\u1EC3m)\\s+(?:c\u1EE7a\\s+|c\xE1c\\s+)*cung\\s+([A-Z])([A-Z])([A-Z])(?![A-Z])\\s+v\xE0\\s+(?:\u0111i\u1EC3m\\s+)?(?:(?:ch\xEDnh\\s+gi\u1EEFa|trung\\s*\u0111i\u1EC3m)\\s+)?(?:c\u1EE7a\\s+)?(?:cung\\s+)?([A-Z])([A-Z])([A-Z])(?![A-Z])",
2565
+ "u"
2566
+ );
2366
2567
  function resolveCircle(problem) {
2367
2568
  const w = CIRCLE_WORDS.exec(problem);
2368
2569
  if (w) return w[1];
@@ -2410,6 +2611,27 @@ var arcMidpointRule = {
2410
2611
  };
2411
2612
  for (const c of ctx.clauses) {
2412
2613
  if (!ARC_MID.test(c.text)) continue;
2614
+ const twoArcs3 = DISTRIB_TWO_ARCS3.exec(c.text);
2615
+ if (twoArcs3) {
2616
+ const [, n1, n2, x1, y1, z1, x2, y2, z210] = twoArcs3;
2617
+ const triples = [
2618
+ [n1, x1, z1, y1],
2619
+ [n2, x2, z210, y2]
2620
+ ];
2621
+ const ok = triples.every(
2622
+ ([n, a2, b2, ct]) => arcOnCircum(a2, b2) && (/* @__PURE__ */ new Set([a2, b2, ct])).size === 3 && n !== a2 && n !== b2 && n !== ct
2623
+ );
2624
+ if (ok) {
2625
+ const mark = out.length;
2626
+ let good = true;
2627
+ for (const [n, a2, b2, ct] of triples) {
2628
+ good = pushArc(c, n, a2, b2, { rel: "in", point: ct });
2629
+ if (!good) break;
2630
+ }
2631
+ if (!good) out.length = mark;
2632
+ }
2633
+ continue;
2634
+ }
2413
2635
  const twoArcs = DISTRIB_TWO_ARCS.exec(c.text);
2414
2636
  if (twoArcs) {
2415
2637
  const [, n1, n2, a1, b1, a2, b2] = twoArcs;
@@ -2433,7 +2655,7 @@ var arcMidpointRule = {
2433
2655
  }
2434
2656
  const pairM = ARC_PAIR.exec(c.text);
2435
2657
  if (!pairM) continue;
2436
- if (pairM[1] && /lớn/u.test(pairM[1])) continue;
2658
+ const isMajor = !!pairM[1] && /lớn/u.test(pairM[1]);
2437
2659
  const pair = pairFromToken(pairM[2] + pairM[3]);
2438
2660
  if (pair.length !== 2) continue;
2439
2661
  const [a, b] = pair;
@@ -2458,7 +2680,9 @@ var arcMidpointRule = {
2458
2680
  pushArc(c, name, a, b, conts[0]);
2459
2681
  } else if (tri) {
2460
2682
  const third = [tri[1], tri[2], tri[3]].find((v) => v !== a && v !== b);
2461
- if (third) pushArc(c, name, a, b, { rel: "not", point: third });
2683
+ if (third) pushArc(c, name, a, b, isMajor ? { rel: "in", point: third } : { rel: "not", point: third });
2684
+ } else if (isMajor && explicitCircle) {
2685
+ pushArc(c, name, a, b, { rel: "in", point: explicitCircle });
2462
2686
  } else {
2463
2687
  out.push({
2464
2688
  ruleId: "arcMidpoint",
@@ -2554,6 +2778,14 @@ var RE_CUT = new RegExp(
2554
2778
  THROUGH_LINE + "\\s+c\u1EAFt\\s+(?:\u0111\u01B0\u1EDDng\\s*(?:th\u1EB3ng\\s*)?)?([A-Z])([A-Z])(?!\\p{L})\\s+(?:t\u1EA1i|\u1EDF)\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?![A-Z])",
2555
2779
  "gu"
2556
2780
  );
2781
+ var FOOT2 = "([A-Z])([A-Z])\\s*,\\s*([A-Z])([A-Z])";
2782
+ var FOOT3 = "([A-Z])([A-Z])\\s*,\\s*([A-Z])([A-Z])\\s*,\\s*([A-Z])([A-Z])";
2783
+ var TARGET2 = "([A-Z]{2})\\s*,\\s*([A-Z]{2})(?![A-Z])";
2784
+ var TARGET3 = "([A-Z]{2})\\s*,\\s*([A-Z]{2})\\s*,\\s*([A-Z]{2})(?![A-Z])";
2785
+ var PERP_KW = "(?:l\u1EA7n\\s*l\u01B0\u1EE3t\\s+|theo\\s+th\u1EE9\\s+t\u1EF1\\s+)?(?:\u22A5|vu\xF4ng\\s*g\xF3c)\\s+(?:v\u1EDBi\\s+)?(?:c\xE1c\\s+)?(?:c\u1EA1nh\\s+|\u0111o\u1EA1n(?:\\s+th\u1EB3ng)?\\s+|\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+)?";
2786
+ var FROM = "(?:Qua|qua|T\u1EEB|t\u1EEB)\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?:['\u2032]?)(?!\\p{L})[^.]{0,16}?(?:k\u1EBB|v\u1EBD|d\u1EF1ng)\\s+";
2787
+ var PERP_NAMED_DISTRIB3 = new RegExp(FROM + FOOT3 + "\\s+" + PERP_KW + TARGET3, "gu");
2788
+ var PERP_NAMED_DISTRIB2 = new RegExp(FROM + FOOT2 + "\\s+" + PERP_KW + TARGET2, "gu");
2557
2789
  var PREFILTER6 = /(?:Qua|qua|Từ|từ)\s+(?:điểm\s+)?[A-Z][^.]{0,24}?(?:song\s*song|vuông\s*góc)|đường\s*thẳng\s+qua\s+[A-Z][^.]{0,24}?(?:song\s*song|vuông\s*góc)/u;
2558
2790
  function lineNameOf(isParallel, through) {
2559
2791
  return (isParallel ? "par" : "prp") + through;
@@ -2583,6 +2815,43 @@ var parallelPerpRule = {
2583
2815
  });
2584
2816
  cutSpans.push([start, end]);
2585
2817
  };
2818
+ const emitNamedPerp = (from, foot, target, start, end) => {
2819
+ if (foot === from || target.includes(foot)) return;
2820
+ const lineName = lineNameOf(false, from) + foot;
2821
+ out.push({
2822
+ ruleId: "parallelPerp",
2823
+ clauseIds: [c.id],
2824
+ intents: [
2825
+ drawLine(lineName, "perpThrough", { through: from, to: target }),
2826
+ addPoint(foot, { kind: "intersection", of: [lineName, target] })
2827
+ ]
2828
+ });
2829
+ cutSpans.push([start, end]);
2830
+ };
2831
+ PERP_NAMED_DISTRIB3.lastIndex = 0;
2832
+ for (const m of c.text.matchAll(PERP_NAMED_DISTRIB3)) {
2833
+ const from = m[1];
2834
+ const triples = [
2835
+ [m[3], m[8]],
2836
+ [m[5], m[9]],
2837
+ [m[7], m[10]]
2838
+ ];
2839
+ if ([m[2], m[4], m[6]].every((f) => f === from)) {
2840
+ const s = m.index ?? 0;
2841
+ const e = s + m[0].length;
2842
+ for (const [foot, target] of triples) emitNamedPerp(from, foot, target, s, e);
2843
+ }
2844
+ }
2845
+ PERP_NAMED_DISTRIB2.lastIndex = 0;
2846
+ for (const m of c.text.matchAll(PERP_NAMED_DISTRIB2)) {
2847
+ const s = m.index ?? 0;
2848
+ if (cutSpans.some(([a, b]) => s >= a && s < b)) continue;
2849
+ const from = m[1];
2850
+ if (m[2] !== from || m[4] !== from) continue;
2851
+ const pairs = [[m[3], m[6]], [m[5], m[7]]];
2852
+ const e = s + m[0].length;
2853
+ for (const [foot, target] of pairs) emitNamedPerp(from, foot, target, s, e);
2854
+ }
2586
2855
  RE_GIAO.lastIndex = 0;
2587
2856
  for (const m of c.text.matchAll(RE_GIAO)) {
2588
2857
  emitCut(m[2], m[3], m[4], m[5], m[6] + m[7], m[1], m.index ?? 0, (m.index ?? 0) + m[0].length);
@@ -2616,12 +2885,23 @@ var REFLECT = /đối\s*xứng/u;
2616
2885
  var PT = `[A-Z](?:['\u2032])?`;
2617
2886
  var NAME_BEFORE4 = new RegExp(
2618
2887
  `(${PT})\\s+(?:l\xE0\\s+)?(?:\u0111i\u1EC3m\\s+)?\u0111\u1ED1i\\s*x\u1EE9ng\\s+(?:c\u1EE7a\\s+|v\u1EDBi\\s+)?(?:\u0111i\u1EC3m\\s+)?(${PT})\\s+qua\\s+(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|c\u1EA1nh\\s+|\u0111o\u1EA1n\\s+|tr\u1EE5c\\s+|trung\\s*\u0111i\u1EC3m\\s+|t\xE2m\\s+|\u0111i\u1EC3m\\s+)?([A-Za-z][A-Za-z\u2032']?)`,
2619
- "u"
2888
+ "gu"
2620
2889
  );
2621
2890
  var DISTRIB2 = new RegExp(
2622
2891
  `(${PT})\\s*,\\s*(${PT})\\s+l\u1EA7n\\s*l\u01B0\u1EE3t\\s+(?:l\xE0\\s+)?(?:\u0111i\u1EC3m\\s+)?\u0111\u1ED1i\\s*x\u1EE9ng\\s+(?:c\u1EE7a\\s+|v\u1EDBi\\s+)?(?:\u0111i\u1EC3m\\s+)?(${PT})\\s+qua\\s+([A-Za-z]{1,2})(?:['\u2032]?)\\s*(?:,|v\xE0)\\s*([A-Za-z]{1,2})(?:['\u2032]?)`,
2623
2892
  "u"
2624
2893
  );
2894
+ var LL = `(?:l\u1EA7n\\s*l\u01B0\u1EE3t|t\u01B0\u01A1ng\\s*\u1EE9ng|(?:theo\\s+)?th\u1EE9\\s+t\u1EF1)`;
2895
+ var NAMES_BLOB2 = `(${PT}(?:\\s*(?:,|v\xE0)\\s*${PT})+)`;
2896
+ var SRCS_BLOB = `(${PT}(?:\\s*(?:,|v\xE0)\\s*${PT})+)`;
2897
+ var MIRRORS_BLOB = `([A-Za-z]{1,2}(?:['\u2032]?)(?:\\s*(?:,|v\xE0)\\s*[A-Za-z]{1,2}(?:['\u2032]?))+)`;
2898
+ var DISTRIB_MULTI = new RegExp(
2899
+ `${NAMES_BLOB2}\\s+(?:${LL}\\s+)?(?:l\xE0\\s+)?(?:c\xE1c\\s+)?(?:\u0111i\u1EC3m\\s+)?\u0111\u1ED1i\\s*x\u1EE9ng\\s+(?:c\u1EE7a\\s+|v\u1EDBi\\s+)?(?:\u0111i\u1EC3m\\s+)?${SRCS_BLOB}\\s+(?:${LL}\\s+)?qua\\s+${MIRRORS_BLOB}`,
2900
+ "u"
2901
+ );
2902
+ function splitList(blob) {
2903
+ return blob.split(/\s*,\s*|\s+và\s+/u).map((s) => s.trim()).filter(Boolean);
2904
+ }
2625
2905
  var NAME_AFTER = new RegExp(
2626
2906
  `(?:\u0111i\u1EC3m\\s+)?\u0111\u1ED1i\\s*x\u1EE9ng\\s+(?:c\u1EE7a\\s+|v\u1EDBi\\s+)?(?:\u0111i\u1EC3m\\s+)?(${PT})\\s+qua\\s+(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|c\u1EA1nh\\s+|\u0111o\u1EA1n\\s+|tr\u1EE5c\\s+|trung\\s*\u0111i\u1EC3m\\s+|t\xE2m\\s+|\u0111i\u1EC3m\\s+)?([A-Za-z][A-Za-z\u2032']?)`,
2627
2907
  "u"
@@ -2632,10 +2912,10 @@ function normalizePointName(name) {
2632
2912
  var REFLECT_EN = /(?<![A-Za-z])([A-Z])(?:['′]?)\s+(?:is|be)\s+the\s+reflection\s+of\s+([A-Z])(?:['′]?)\s+(?:over|across|in|about|through)\s+(?:the\s+)?(?:line\s+|point\s+|segment\s+)?([A-Za-z][A-Za-z′']?)/u;
2633
2913
  var REFLECT_EN_PRE = /[Rr]eflection/u;
2634
2914
  function classifyThrough(raw) {
2635
- const z46 = raw.replace(/['′]/gu, "").trim();
2636
- if (/^[A-Z]$/u.test(z46)) return { kind: "point", value: z46 };
2637
- if (/^[A-Z][A-Z]$/u.test(z46)) return { kind: "line", value: z46 };
2638
- if (/^[a-z][A-Za-z0-9]*$/u.test(z46)) return { kind: "line", value: z46 };
2915
+ const z47 = raw.replace(/['′]/gu, "").trim();
2916
+ if (/^[A-Z]$/u.test(z47)) return { kind: "point", value: z47 };
2917
+ if (/^[A-Z][A-Z]$/u.test(z47)) return { kind: "line", value: z47 };
2918
+ if (/^[a-z][A-Za-z0-9]*$/u.test(z47)) return { kind: "line", value: z47 };
2639
2919
  return void 0;
2640
2920
  }
2641
2921
  var reflectionRule = {
@@ -2647,6 +2927,28 @@ var reflectionRule = {
2647
2927
  const out = [];
2648
2928
  for (const c of ctx.clauses) {
2649
2929
  if (!REFLECT.test(c.text) && !REFLECT_EN_PRE.test(c.text)) continue;
2930
+ const dmm = DISTRIB_MULTI.exec(c.text);
2931
+ if (dmm) {
2932
+ const names = splitList(dmm[1]).map(normalizePointName);
2933
+ const srcs = splitList(dmm[2]).map(normalizePointName);
2934
+ const mirrors = splitList(dmm[3]);
2935
+ if (names.length >= 2 && names.length === srcs.length && names.length === mirrors.length) {
2936
+ const through2 = mirrors.map(classifyThrough);
2937
+ if (through2.every((t) => !!t)) {
2938
+ out.push({
2939
+ ruleId: "reflection",
2940
+ clauseIds: [c.id],
2941
+ intents: names.map(
2942
+ (nm, i) => addPoint(
2943
+ nm,
2944
+ through2[i].kind === "point" ? { kind: "reflectPoint", of: srcs[i], through: through2[i].value } : { kind: "reflectLine", of: srcs[i], through: through2[i].value }
2945
+ )
2946
+ )
2947
+ });
2948
+ continue;
2949
+ }
2950
+ }
2951
+ }
2650
2952
  const dm = DISTRIB2.exec(c.text);
2651
2953
  if (dm) {
2652
2954
  const [n1, n2, ofPt, raw1, raw2] = [
@@ -2668,15 +2970,31 @@ var reflectionRule = {
2668
2970
  continue;
2669
2971
  }
2670
2972
  }
2973
+ NAME_BEFORE4.lastIndex = 0;
2974
+ const befores = [...c.text.matchAll(NAME_BEFORE4)];
2975
+ if (befores.length) {
2976
+ for (const b of befores) {
2977
+ const nm = normalizePointName(b[1]);
2978
+ const ofP = normalizePointName(b[2]);
2979
+ const thr = classifyThrough(b[3]);
2980
+ if (!thr) continue;
2981
+ out.push({
2982
+ ruleId: "reflection",
2983
+ clauseIds: [c.id],
2984
+ intents: [
2985
+ addPoint(
2986
+ nm,
2987
+ thr.kind === "point" ? { kind: "reflectPoint", of: ofP, through: thr.value } : { kind: "reflectLine", of: ofP, through: thr.value }
2988
+ )
2989
+ ]
2990
+ });
2991
+ }
2992
+ continue;
2993
+ }
2671
2994
  let name;
2672
2995
  let of;
2673
2996
  let throughRaw;
2674
- const before = NAME_BEFORE4.exec(c.text);
2675
- if (before) {
2676
- name = normalizePointName(before[1]);
2677
- of = normalizePointName(before[2]);
2678
- throughRaw = before[3];
2679
- } else {
2997
+ {
2680
2998
  const after = NAME_AFTER.exec(c.text);
2681
2999
  if (after) {
2682
3000
  name = extractPointName(c.text);
@@ -3179,7 +3497,7 @@ var DISTRIB_TWO = new RegExp(
3179
3497
  "gu"
3180
3498
  );
3181
3499
  var CAT_ONE_TWO = new RegExp(
3182
- `${REF}\\s+c\u1EAFt\\s+${REF}\\s*(?:,|v\xE0)\\s*${REF}\\s+(?:theo\\s+th\u1EE9\\s+t\u1EF1\\s+|l\u1EA7n\\s*l\u01B0\u1EE3t\\s+)?t\u1EA1i\\s+([A-Z])\\s*(?:,|v\xE0)\\s*(?:t\u1EA1i\\s+)?([A-Z])(?![A-Z])`,
3500
+ `${REF}\\s+(?:l\u1EA7n\\s*l\u01B0\u1EE3t\\s+|theo\\s+th\u1EE9\\s+t\u1EF1\\s+)?c\u1EAFt\\s+${REF}\\s*(?:,|v\xE0)\\s*${REF}\\s+(?:theo\\s+th\u1EE9\\s+t\u1EF1\\s+|l\u1EA7n\\s*l\u01B0\u1EE3t\\s+)?t\u1EA1i\\s+([A-Z])\\s*(?:,|v\xE0)\\s*(?:t\u1EA1i\\s+)?([A-Z])(?![A-Z])`,
3183
3501
  "gu"
3184
3502
  );
3185
3503
  var CAT_TWO_ONE = new RegExp(
@@ -3194,12 +3512,22 @@ var CAT_ONE_RAY = new RegExp(
3194
3512
  `${REF}\\s+c\u1EAFt\\s+(?:tia\\s+)?(?:ti\u1EBFp\\s*tuy\u1EBFn\\s+)?(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+)?([A-Z][a-z]|[a-z])(?![\\p{L}\\d])[^.;,]{0,40}?(?:t\u1EA1i|\u1EDF)\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?![A-Z])`,
3195
3513
  "gu"
3196
3514
  );
3515
+ var NAMED_CAT_TAI = new RegExp(
3516
+ `(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|tia\\s+)?\\b([a-z]{1,2}[0-9]?)(?![\\p{L}\\d])\\s+(?:k\xE9o\\s+d\xE0i\\s+)?c\u1EAFt\\s+${REF}\\s+(?:k\xE9o\\s+d\xE0i\\s+)?(?:t\u1EA1i|\u1EDF)\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?![A-Z])`,
3517
+ "gu"
3518
+ );
3197
3519
  var GIAO_TWO_ONE_LA = new RegExp(
3198
3520
  `giao\\s*\u0111i\u1EC3m\\s+(?:c\u1EE7a\\s+)?${REF}\\s*(?:,|v\xE0)\\s*${REF}\\s+v\u1EDBi\\s+${REF}\\s+(?:l\u1EA7n\\s*l\u01B0\u1EE3t\\s+|theo\\s+th\u1EE9\\s+t\u1EF1\\s+)?l\xE0\\s+([A-Z])\\s*(?:,|v\xE0)\\s*([A-Z])(?![A-Z])`,
3199
3521
  "gu"
3200
3522
  );
3523
+ var ZIP_MK = `(?:l\u1EA7n\\s*l\u01B0\u1EE3t|theo\\s+th\u1EE9\\s+t\u1EF1|t\u01B0\u01A1ng\\s+\u1EE9ng)`;
3524
+ var ZIP_SIDE = `(?:\u0111o\u1EA1n(?:\\s+th\u1EB3ng)?\\s+|c\u1EA1nh\\s+|tia\\s+|${DUONG_KW}\\s*th\u1EB3ng\\s+)?`;
3201
3525
  var CAT_ZIP = new RegExp(
3202
- `${REF}\\s*,\\s*${REF}\\s+(?:l\u1EA7n\\s*l\u01B0\u1EE3t\\s+|theo\\s+th\u1EE9\\s+t\u1EF1\\s+)c\u1EAFt\\s+(?:c\xE1c\\s+)?(?:\u0111o\u1EA1n(?:\\s+th\u1EB3ng)?\\s+|c\u1EA1nh\\s+|${DUONG_KW}\\s*th\u1EB3ng\\s+)?${REF}\\s*,\\s*${REF}\\s+(?:t\u1EA1i|\u1EDF)\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])\\s*(?:,|v\xE0)\\s*([A-Z])(?![A-Z])`,
3526
+ `${REF}\\s*,\\s*${REF}\\s+${ZIP_MK}\\s+c\u1EAFt\\s+(?:c\xE1c\\s+)?${ZIP_SIDE}${REF}\\s*,\\s*${REF}\\s+(?:t\u1EA1i|\u1EDF)\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])\\s*(?:,|v\xE0)\\s*([A-Z])(?![A-Z])`,
3527
+ "gu"
3528
+ );
3529
+ var CAT_ZIP_POST = new RegExp(
3530
+ `${REF}\\s*,\\s*${REF}\\s+c\u1EAFt\\s+(?:c\xE1c\\s+)?${ZIP_SIDE}${REF}\\s*,\\s*${REF}\\s+${ZIP_MK}\\s+(?:t\u1EA1i|\u1EDF)\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])\\s*(?:,|v\xE0)\\s*([A-Z])(?![A-Z])`,
3203
3531
  "gu"
3204
3532
  );
3205
3533
  var NAME_BEFORE6 = /([A-Z])(?:['′]?)\s+là\s+$/u;
@@ -3214,6 +3542,10 @@ var CAP_SYMBOL = new RegExp(
3214
3542
  `(${PT2})\\s*=\\s*(${PT2})\\s*(${PT2})\\s*\u2229\\s*(${PT2})\\s*(${PT2})(?!${PT2})`,
3215
3543
  "gu"
3216
3544
  );
3545
+ var CAP_SYMBOL_POST = new RegExp(
3546
+ `(${PT2})\\s*(${PT2})\\s*\u2229\\s*(${PT2})\\s*(${PT2})\\s*=\\s*\\{?\\s*(${PT2})\\s*\\}?`,
3547
+ "gu"
3548
+ );
3217
3549
  var PTP = "[A-Z](?:['\u2032])?";
3218
3550
  var REFP = `(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|\u0111o\u1EA1n(?:\\s+th\u1EB3ng)?\\s+|tia\\s+|c\u1EA1nh\\s+)?(${PTP}\\s*${PTP})(?![A-Za-z])`;
3219
3551
  var GIAO_PRIMED = new RegExp(
@@ -3238,6 +3570,12 @@ function makeIntent(name, ref1, ref2) {
3238
3570
  if (ends.includes(name)) return null;
3239
3571
  return addPoint(name, { kind: "intersection", of: [ref1, ref2] });
3240
3572
  }
3573
+ function declaredNamedLines2(problem) {
3574
+ const out = /* @__PURE__ */ new Set();
3575
+ const RE31 = /(?:đường\s*thẳng|tiếp\s*tuyến|cát\s*tuyến)\s+([a-z]{1,2}[0-9]?)(?![\p{L}\d])/gu;
3576
+ for (const m of problem.matchAll(RE31)) out.add(m[1]);
3577
+ return out;
3578
+ }
3241
3579
  function makeIntentEnds(name, e1, e2, e3, e4) {
3242
3580
  const ends = [e1, e2, e3, e4];
3243
3581
  if (new Set(ends).size !== 4) return null;
@@ -3255,6 +3593,7 @@ var intersectionRule = {
3255
3593
  patterns: [PREFILTER8],
3256
3594
  match(ctx) {
3257
3595
  const out = [];
3596
+ const namedLines = declaredNamedLines2(ctx.problem);
3258
3597
  for (const c of ctx.clauses) {
3259
3598
  const seen = /* @__PURE__ */ new Set();
3260
3599
  const emit = (name, ref1, ref2) => {
@@ -3278,6 +3617,15 @@ var intersectionRule = {
3278
3617
  seen.add(name);
3279
3618
  out.push({ ruleId: "intersection", clauseIds: [c.id], intents: [intent] });
3280
3619
  }
3620
+ CAP_SYMBOL_POST.lastIndex = 0;
3621
+ for (const m of c.text.matchAll(CAP_SYMBOL_POST)) {
3622
+ const name = m[5];
3623
+ if (seen.has(name)) continue;
3624
+ const intent = makeIntentEnds(name, m[1], m[2], m[3], m[4]);
3625
+ if (!intent) continue;
3626
+ seen.add(name);
3627
+ out.push({ ruleId: "intersection", clauseIds: [c.id], intents: [intent] });
3628
+ }
3281
3629
  CAT_ONE_TWO.lastIndex = 0;
3282
3630
  for (const m of c.text.matchAll(CAT_ONE_TWO)) {
3283
3631
  emit(m[4], m[1], m[2]);
@@ -3309,6 +3657,12 @@ var intersectionRule = {
3309
3657
  for (const m of c.text.matchAll(CAT_ONE_RAY)) {
3310
3658
  emitShape(m[3], m[1], m[2]);
3311
3659
  }
3660
+ if (namedLines.size > 0) {
3661
+ NAMED_CAT_TAI.lastIndex = 0;
3662
+ for (const m of c.text.matchAll(NAMED_CAT_TAI)) {
3663
+ if (namedLines.has(m[1])) emitShape(m[3], m[2], m[1]);
3664
+ }
3665
+ }
3312
3666
  GIAO_TWO_ONE_LA.lastIndex = 0;
3313
3667
  for (const m of c.text.matchAll(GIAO_TWO_ONE_LA)) {
3314
3668
  emit(m[4], m[1], m[3]);
@@ -3319,6 +3673,11 @@ var intersectionRule = {
3319
3673
  emit(m[5], m[1], m[3]);
3320
3674
  emit(m[6], m[2], m[4]);
3321
3675
  }
3676
+ CAT_ZIP_POST.lastIndex = 0;
3677
+ for (const m of c.text.matchAll(CAT_ZIP_POST)) {
3678
+ emit(m[5], m[1], m[3]);
3679
+ emit(m[6], m[2], m[4]);
3680
+ }
3322
3681
  GIAO_LA.lastIndex = 0;
3323
3682
  for (const m of c.text.matchAll(GIAO_LA)) emit(m[3], m[1], m[2]);
3324
3683
  GIAO_DIEM.lastIndex = 0;
@@ -3402,9 +3761,6 @@ var THETA_A = 2.3;
3402
3761
  var THETA_B = 0.7;
3403
3762
  var THETA_STEP = 0.9;
3404
3763
  var PREFILTER9 = /[Dd]ây/u;
3405
- function escapeRe(s) {
3406
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3407
- }
3408
3764
  function findCircleClauseId(clauses, circle) {
3409
3765
  const frag = new RegExp(CIRCLE_KW + `|\\(\\s*${escapeRe(circle)}\\s*\\)`, "u");
3410
3766
  for (const c of clauses) if (frag.test(c.text)) return c.id;
@@ -3417,6 +3773,29 @@ function resolveCircle2(problem) {
3417
3773
  if (p) return p[1];
3418
3774
  return void 0;
3419
3775
  }
3776
+ function inscribedTriangleVertices(problem, circle) {
3777
+ const out = /* @__PURE__ */ new Set();
3778
+ const NAMED_O = "(?:\\(\\s*([^\\s;,).:]+)\\s*\\)|t\xE2m\\s+([A-Z]))?";
3779
+ const TRI7 = "tam\\s*gi\xE1c\\s+(?:(?:nh\u1ECDn|c\xE2n|\u0111\u1EC1u|vu\xF4ng|t\xF9)\\s+)?([A-Z])([A-Z])([A-Z])(?![A-Z])";
3780
+ const RE_A = new RegExp(TRI7 + "[^.]{0,40}?n\u1ED9i\\s*ti\u1EBFp\\s+(?:trong\\s+)?(?:" + CIRCLE_KW + ")?\\s*" + NAMED_O, "gu");
3781
+ for (const m of problem.matchAll(RE_A)) {
3782
+ const named = m[4] ?? m[5];
3783
+ if (named && named !== circle) continue;
3784
+ out.add(m[1]);
3785
+ out.add(m[2]);
3786
+ out.add(m[3]);
3787
+ }
3788
+ const NAMED_O_B = "(?:\\(\\s*([^\\s;,).:]+)\\s*\\)|t\xE2m\\s+([A-Z])|([A-Z]))?";
3789
+ const RE_B = new RegExp(CIRCLE_KW + "\\s*" + NAMED_O_B + "\\s*ngo\u1EA1i\\s*ti\u1EBFp\\s+" + TRI7, "gu");
3790
+ for (const m of problem.matchAll(RE_B)) {
3791
+ const named = m[1] ?? m[2] ?? m[3];
3792
+ if (named && named !== circle) continue;
3793
+ out.add(m[4]);
3794
+ out.add(m[5]);
3795
+ out.add(m[6]);
3796
+ }
3797
+ return out;
3798
+ }
3420
3799
  var chordRule = {
3421
3800
  id: "chord",
3422
3801
  // Dưới circleRadius (75) — nếu circle O có bán kính cụ thể, định nghĩa đó thắng.
@@ -3430,6 +3809,7 @@ var chordRule = {
3430
3809
  match(ctx) {
3431
3810
  const circle = resolveCircle2(ctx.problem);
3432
3811
  if (!circle) return [];
3812
+ const inscribedVerts = inscribedTriangleVertices(ctx.problem, circle);
3433
3813
  const chords = [];
3434
3814
  const seen = /* @__PURE__ */ new Set();
3435
3815
  for (const c of ctx.clauses) {
@@ -3464,15 +3844,11 @@ var chordRule = {
3464
3844
  });
3465
3845
  chords.forEach((ch, k) => {
3466
3846
  const offset = k * THETA_STEP;
3467
- out.push({
3468
- ruleId: "chord",
3469
- clauseIds: [ch.clauseId],
3470
- intents: [
3471
- addPoint(ch.a, { kind: "onCircle", circle, theta: THETA_A + offset }),
3472
- addPoint(ch.b, { kind: "onCircle", circle, theta: THETA_B + offset }),
3473
- connect(ch.a, ch.b, "segment")
3474
- ]
3475
- });
3847
+ const intents = [];
3848
+ if (!inscribedVerts.has(ch.a)) intents.push(addPoint(ch.a, { kind: "onCircle", circle, theta: THETA_A + offset }));
3849
+ if (!inscribedVerts.has(ch.b)) intents.push(addPoint(ch.b, { kind: "onCircle", circle, theta: THETA_B + offset }));
3850
+ intents.push(connect(ch.a, ch.b, "segment"));
3851
+ out.push({ ruleId: "chord", clauseIds: [ch.clauseId], intents });
3476
3852
  });
3477
3853
  return out;
3478
3854
  }
@@ -3482,9 +3858,13 @@ var chordRule = {
3482
3858
  var PREFILTER10 = /tiếp\s*xúc|tiếp\s*điểm/u;
3483
3859
  var TANGENT_NAMED_FWD = /tiếp\s*điểm\s+(?:của\s+)?(?:đường\s*tròn\s*)?\(\s*([A-Z])\s*\)\s+với\s+(?:các\s+)?(?:(?:cạnh|đoạn)\s+)?([A-Z]{2}(?:\s*(?:,|và)\s*[A-Z]{2})*)\s+(?:lần\s*lượt\s+|theo\s+thứ\s+tự\s+)?là\s+(?:các\s+)?(?:điểm\s+)?([A-Z](?:\s*(?:,|và)\s*[A-Z])*)(?![A-Za-z])/iu;
3484
3860
  var TANGENT_NAMED_REV = /tiếp\s*điểm\s+(?:của\s+)?(?:các\s+)?(?:(?:cạnh|đoạn)\s+)?([A-Z]{2}(?:\s*(?:,|và)\s*[A-Z]{2})*)\s+với\s+(?:đường\s*tròn\s*)?\(\s*([A-Z])\s*\)\s+(?:lần\s*lượt\s+|theo\s+thứ\s+tự\s+)?là\s+(?:các\s+)?(?:điểm\s+)?([A-Z](?:\s*(?:,|và)\s*[A-Z])*)(?![A-Za-z])/iu;
3861
+ var TANGENT_NAMED_BEFORE = /([A-Z](?:\s*(?:,|và)\s*[A-Z])*)\s+(?:lần\s*lượt\s+|theo\s+thứ\s+tự\s+)?là\s+(?:các\s+)?(?:điểm\s+)?tiếp\s*điểm\s+(?:của\s+)?(?:các\s+)?(?:(?:cạnh|đoạn)\s+)?([A-Z]{2}(?:\s*(?:,|và)\s*[A-Z]{2})*)\s+với\s+(?:đường\s*tròn\s*)?\(\s*([A-Z])\s*\)/iu;
3485
3862
  var INCIRCLE_IN_CLAUSE = /đường\s*tròn\s*(?:\(\s*([A-Z])\s*\)|tâm\s+([A-Z]))?\s*nội\s*tiếp\s+tam\s*giác(?:\s+([A-Z])([A-Z])([A-Z])(?![A-Z]))?/iu;
3486
3863
  var SIDE_POINT_LIST = /tiếp\s*xúc\s+(?:với\s+)?(?:các\s+|ba\s+|hai\s+|bốn\s+)?(?:(?:cạnh|đoạn)\s+)?([A-Z]{2}(?:\s*(?:,|và)\s*[A-Z]{2})*)\s+(?:lần\s*lượt\s+|tương\s*ứng\s+)?tại\s+(?:các\s+|ba\s+|hai\s+|bốn\s+)?(?:điểm\s+)?([A-Z]'?(?:\s*(?:,|và)\s*[A-Z]'?)*)(?![A-Za-z])/iu;
3487
3864
  var REVERSED_SIDE_POINT = /(?:các\s+)?(?:cạnh|đoạn)\s+([A-Z]{2}(?:\s*(?:,|và)\s*[A-Z]{2})*)\s+tiếp\s*xúc\s+(?:với\s+)?(?:đường\s*tròn\s*)?\(\s*([A-Z])\s*\)\s+(?:lần\s*lượt\s+|tương\s*ứng\s+)?tại\s+(?:các\s+)?(?:điểm\s+)?([A-Z]'?(?:\s*(?:,|và)\s*[A-Z]'?)*)(?![A-Za-z])/iu;
3865
+ var INTERLEAVED_CIRCLE = /(?:đường\s*tròn\s*)?\(\s*([A-Z])\s*[,;]?\s*[Rr]?\s*\)\s*tiếp\s*xúc\s+(?:với\s+)?(?:(?:cạnh|đoạn)\s+)?([A-Z]{2}\s+tại\s+[A-Z]'?(?:\s*,\s*[A-Z]{2}\s+tại\s+[A-Z]'?)+)(?![A-Za-z])/iu;
3866
+ var PAIR_RE = /([A-Z]{2})\s+tại\s+([A-Z]'?)/giu;
3867
+ var REVERSED_BARE = /(?<![A-Za-z])([A-Z]{2}(?:\s*(?:,|và)\s*[A-Z]{2})*)\s+tiếp\s*xúc\s+(?:với\s+)?(?:đường\s*tròn\s*)?\(\s*([A-Z])\s*\)\s+(?:lần\s*lượt\s+|tương\s*ứng\s+)?tại\s+(?:các\s+)?(?:điểm\s+)?([A-Z]'?(?:\s*(?:,|và)\s*[A-Z]'?)*)(?![A-Za-z])/iu;
3488
3868
  function splitCsv(blob) {
3489
3869
  return blob.split(/,|và/iu).map((s) => s.trim()).filter(Boolean);
3490
3870
  }
@@ -3522,6 +3902,22 @@ var incircleTangencyRule = {
3522
3902
  match(ctx) {
3523
3903
  const out = [];
3524
3904
  for (const chunk of logicalChunks(ctx.problem, ctx.clauses)) {
3905
+ const tb = TANGENT_NAMED_BEFORE.exec(chunk.text);
3906
+ if (tb) {
3907
+ const names = splitCsv(tb[1]);
3908
+ const sides2 = splitCsv(tb[2]);
3909
+ const circle2 = tb[3];
3910
+ if (sides2.length >= 1 && sides2.length === names.length) {
3911
+ const verts2 = [...new Set(sides2.join("").split(""))];
3912
+ const intents2 = [];
3913
+ if (verts2.length === 3) intents2.push(drawCircle(circle2, "inscribedIn", { triangle: verts2 }));
3914
+ for (let i = 0; i < names.length; i++) {
3915
+ intents2.push(addPoint(names[i], { kind: "tangencyPoint", circle: circle2, onLine: sides2[i] }));
3916
+ }
3917
+ out.push({ ruleId: "incircleTangency", clauseIds: chunk.clauseIds, intents: intents2 });
3918
+ continue;
3919
+ }
3920
+ }
3525
3921
  const tn = TANGENT_NAMED_FWD.exec(chunk.text) ?? TANGENT_NAMED_REV.exec(chunk.text);
3526
3922
  if (tn) {
3527
3923
  const fwd = TANGENT_NAMED_FWD.test(chunk.text);
@@ -3539,6 +3935,20 @@ var incircleTangencyRule = {
3539
3935
  continue;
3540
3936
  }
3541
3937
  }
3938
+ const il = INTERLEAVED_CIRCLE.exec(chunk.text);
3939
+ if (il) {
3940
+ const circle2 = il[1];
3941
+ const pairs = [];
3942
+ PAIR_RE.lastIndex = 0;
3943
+ for (const pm of il[2].matchAll(PAIR_RE)) pairs.push([pm[1], pm[2]]);
3944
+ if (pairs.length >= 1) {
3945
+ const intents2 = pairs.map(
3946
+ ([side, name]) => addPoint(name, { kind: "tangencyPoint", circle: circle2, onLine: side })
3947
+ );
3948
+ out.push({ ruleId: "incircleTangency", clauseIds: chunk.clauseIds, intents: intents2 });
3949
+ continue;
3950
+ }
3951
+ }
3542
3952
  const rev = REVERSED_SIDE_POINT.exec(chunk.text);
3543
3953
  if (rev) {
3544
3954
  const sides2 = splitCsv(rev[1]);
@@ -3557,6 +3967,19 @@ var incircleTangencyRule = {
3557
3967
  continue;
3558
3968
  }
3559
3969
  }
3970
+ const rb = REVERSED_BARE.exec(chunk.text);
3971
+ if (rb) {
3972
+ const sides2 = splitCsv(rb[1]);
3973
+ const circle2 = rb[2];
3974
+ const names = splitCsv(rb[3]);
3975
+ if (sides2.length >= 1 && sides2.length === names.length) {
3976
+ const intents2 = sides2.map(
3977
+ (side, i) => addPoint(names[i], { kind: "tangencyPoint", circle: circle2, onLine: side })
3978
+ );
3979
+ out.push({ ruleId: "incircleTangency", clauseIds: chunk.clauseIds, intents: intents2 });
3980
+ continue;
3981
+ }
3982
+ }
3560
3983
  let circle = parseIncircleName(chunk.text);
3561
3984
  if (!circle) {
3562
3985
  const fm = FORWARD_CIRCLE.exec(chunk.text);
@@ -3671,16 +4094,17 @@ var altitudeDiameterCirclesRule = {
3671
4094
 
3672
4095
  // src/stamps/geometry-2d/ai/rules/diameterCircleCutsSides.ts
3673
4096
  var PATTERN = new RegExp(
3674
- // Chỉ-báo đường tròn: "đường tròn (tâm X|(X))?" HOẶC "(X)" trần (httcd:2 "(O)
3675
- // đường kính BC" KHÔNG chữ "đường tròn"). Bắt buộc có 1 trong 2.
3676
- "(?:" + CIRCLE_KW + "\\s+(?:(?:\\(\\s*[A-Z]\\s*\\)|t\xE2m\\s+[A-Z])\\s+)?|\\(\\s*[A-Z]\\s*\\)\\s+)" + DUONG_KW + // optional phẩy + đại từ "nó" giữa diameter và "cắt" (vao10:81 "AH, cắt…").
3677
- // "theo thứ tự" = "lần lượt". Anchor điểm = "tại" (dạng "" tách sang
3678
- // PATTERN_O lazy-region dừng "ở" đầu tiên thể nuốt thiếu vao10:155).
3679
- "\\s*k\xEDnh\\s+([A-Z])([A-Z])(?![A-Z])\\s*,?\\s*(?:n\xF3\\s+)?\\s*c\u1EAFt\\s+([^.;\\n]*?)\\s+(?:(?:l\u1EA7n\\s*l\u01B0\u1EE3t|theo\\s+th\u1EE9\\s+t\u1EF1)\\s+)?t\u1EA1i\\s+([A-Z](?!\\p{L})(?:\\s*(?:,|v\xE0)\\s*[A-Z](?!\\p{L}))*)",
4097
+ // Chỉ-báo đường tròn: "đường tròn ((X)|tâm X|X-trần)?" HOẶC "(X)" trần (httcd:2
4098
+ // "(O) đường kính BC"). "X-trần" = tâm 1-HOA không ngoặc ("đường tròn O đường
4099
+ // kính BC" vxhung:14). Bắt buộc 1 trong 2 nhánh.
4100
+ "(?:" + CIRCLE_KW + "\\s+(?:(?:\\(\\s*[A-Z]\\s*\\)|t\xE2m\\s+[A-Z]|[A-Z])\\s+)?|\\(\\s*[A-Z]\\s*\\)\\s+)" + DUONG_KW + // optional phẩy + đại từ "nó" giữa diameter "cắt" (vao10:81 "AH, cắt…").
4101
+ // "theo thứ tự" = "lần lượt". Anchor điểm = "tại"; "(các)? (điểm)?" trước danh
4102
+ // sách tên ("tại các điểm D và E" — vxhung:6).
4103
+ "\\s*k\xEDnh\\s+([A-Z])([A-Z])(?![A-Z])\\s*,?\\s*(?:n\xF3\\s+)?\\s*c\u1EAFt\\s+([^.;\\n]*?)\\s+(?:(?:l\u1EA7n\\s*l\u01B0\u1EE3t|theo\\s+th\u1EE9\\s+t\u1EF1)\\s+)?t\u1EA1i\\s+(?:c\xE1c\\s+)?(?:\u0111i\u1EC3m\\s+)?([A-Z](?!\\p{L})(?:\\s*(?:,|v\xE0)\\s*[A-Z](?!\\p{L}))*)",
3680
4104
  "gu"
3681
4105
  );
3682
4106
  var PATTERN_O = new RegExp(
3683
- "(?:" + CIRCLE_KW + "\\s+(?:(?:\\(\\s*[A-Z]\\s*\\)|t\xE2m\\s+[A-Z])\\s+)?|\\(\\s*[A-Z]\\s*\\)\\s+)" + DUONG_KW + "\\s*k\xEDnh\\s+([A-Z])([A-Z])(?![A-Z])\\s*,?\\s*(?:n\xF3\\s+)?\\s*c\u1EAFt\\s+([^.;\\n]*?)\\s+(?:(?:l\u1EA7n\\s*l\u01B0\u1EE3t|theo\\s+th\u1EE9\\s+t\u1EF1)\\s+)?\u1EDF\\s+([A-Z](?!\\p{L})(?:\\s*(?:,|v\xE0)\\s*[A-Z](?!\\p{L}))+)",
4107
+ "(?:" + CIRCLE_KW + "\\s+(?:(?:\\(\\s*[A-Z]\\s*\\)|t\xE2m\\s+[A-Z]|[A-Z])\\s+)?|\\(\\s*[A-Z]\\s*\\)\\s+)" + DUONG_KW + "\\s*k\xEDnh\\s+([A-Z])([A-Z])(?![A-Z])\\s*,?\\s*(?:n\xF3\\s+)?\\s*c\u1EAFt\\s+([^.;\\n]*?)\\s+(?:(?:l\u1EA7n\\s*l\u01B0\u1EE3t|theo\\s+th\u1EE9\\s+t\u1EF1)\\s+)?\u1EDF\\s+(?:c\xE1c\\s+)?(?:\u0111i\u1EC3m\\s+)?([A-Z](?!\\p{L})(?:\\s*(?:,|v\xE0)\\s*[A-Z](?!\\p{L}))+)",
3684
4108
  "gu"
3685
4109
  );
3686
4110
  var PATTERN_INTERLEAVED = new RegExp(
@@ -3779,6 +4203,10 @@ var GIVEN_BARE = new RegExp(
3779
4203
  String.raw`[Cc]ho\s+\(\s*(${CTR})\s*\)\s*,?\s*(?:có\s+)?` + DUONG_KW + String.raw`\s*kính\s+([A-Z])([A-Z])(?![A-Z])`,
3780
4204
  "gu"
3781
4205
  );
4206
+ var CHO_BARE = new RegExp(
4207
+ String.raw`[Cc]ho\s+(${CTR})(?![A-Z\p{L}])\s+` + DUONG_KW + String.raw`\s*kính\s+([A-Z])([A-Z])(?![A-Z])`,
4208
+ "gu"
4209
+ );
3782
4210
  var BARE_PAREN = new RegExp(
3783
4211
  String.raw`\(\s*(${CTR})\s*\)\s*(?:có\s+)?` + DUONG_KW + String.raw`\s*kính\s+([A-Z])([A-Z])(?![A-Z])`,
3784
4212
  "gu"
@@ -3869,6 +4297,22 @@ var circleDiameterRule = {
3869
4297
  intents: intentsFor(p)
3870
4298
  });
3871
4299
  }
4300
+ CHO_BARE.lastIndex = 0;
4301
+ let cm;
4302
+ while ((cm = CHO_BARE.exec(ctx.problem)) !== null) {
4303
+ const p = { center: cm[1], a: cm[2], b: cm[3] };
4304
+ if (!p.center || p.center === p.a || p.center === p.b || p.a === p.b) continue;
4305
+ const key = `${p.center}|${p.a}${p.b}`;
4306
+ if (emitted.has(key)) continue;
4307
+ emitted.add(key);
4308
+ const matched = cm[0];
4309
+ const claim = ctx.clauses.filter((c) => c.text.includes(matched.replace(/^[Cc]ho\s+/u, ""))).map((c) => c.id);
4310
+ out.push({
4311
+ ruleId: "circle-diameter",
4312
+ clauseIds: claim.length > 0 ? claim : [ctx.clauses[0]?.id ?? 0],
4313
+ intents: intentsFor(p)
4314
+ });
4315
+ }
3872
4316
  BARE_PAREN.lastIndex = 0;
3873
4317
  let bm;
3874
4318
  while ((bm = BARE_PAREN.exec(ctx.problem)) !== null) {
@@ -3879,7 +4323,7 @@ var circleDiameterRule = {
3879
4323
  const key = `${center}|${a}${b}`;
3880
4324
  if (emitted.has(key)) continue;
3881
4325
  const before = ctx.problem.slice(0, bm.index);
3882
- const known = (p) => new RegExp(`(?<![a-z])${p}(?![a-z])`, "u").test(before);
4326
+ const known = (p) => new RegExp(`(?<![a-z])${escapeRe(p)}(?![a-z])`, "u").test(before);
3883
4327
  if (!known(a) || !known(b)) continue;
3884
4328
  emitted.add(key);
3885
4329
  const matched = bm[0];
@@ -3901,7 +4345,7 @@ var circleDiameterRule = {
3901
4345
  var PREFILTER12 = /(?:nằm|thuộc|lấy\s+điểm|trên\s+(?:nửa\s+)?(?:đường\s*tròn|cung))/iu;
3902
4346
  var NAMED_CIRCLE = /(?:nửa\s+)?đường\s*tròn\s*(?:tâm\s+)?\(?\s*([A-Z])(?:\s*[;,]\s*[Rr])?\s*\)?/u;
3903
4347
  var COMPACT_CIRCLE = /\(\s*([A-Z])\s*[;,]\s*[Rr]\s*\)/u;
3904
- var ON_SUFFIX = "(?:cung\\s+(?:nh\u1ECF\\s+|l\u1EDBn\\s+)?[A-Z]{2}(?:\\s*(?:nh\u1ECF|l\u1EDBn))?|(?:n\u1EEDa\\s+)?\u0111\u01B0\u1EDDng\\s*tr\xF2n|\\(\\s*[A-Z]\\s*\\))";
4348
+ var ON_SUFFIX = "(?:cung\\s+(?:nh\u1ECF\\s+|l\u1EDBn\\s+)?[A-Z]{2}(?:\\s*(?:nh\u1ECF|l\u1EDBn))?|(?:n\u1EEDa\\s+)?\u0111\u01B0\u1EDDng\\s*tr\xF2n|(?:\u0111\u01B0\u1EDDng\\s+)?\\(\\s*[A-Z]\\s*\\))";
3905
4349
  var POINT_ON = new RegExp(
3906
4350
  // "nằm" optional → bắt "M (bất kì)? trên (nửa)? đường tròn" / "điểm A trên (O)"
3907
4351
  // (chỉ "trên" trần, không "nằm trên"). ON_SUFFIX neo circle/cung nên "trên"
@@ -3922,18 +4366,21 @@ var TAKE_ON_CUNG = /[Tt]rên\s+cung\s+(?:nhỏ\s+|lớn\s+)?[A-Z]{2}(?:\s*(?:nh
3922
4366
  var TAKE_ON_REV = /[Tt]rên\s+(?:nửa\s+)?(?:đường\s*tròn|cung)\s*\(\s*([A-Z])(?:\s*[;,]\s*[Rr])?\s*\)[^.]{0,16}?lấy\s+điểm\s+([A-Z])(?![A-Z])/u;
3923
4367
  var TWO_ON = /(?:lấy\s+)?hai\s*điểm\s+([A-Z])(?![A-Za-z])\s+và\s+([A-Z])(?![A-Za-z])(?:\s+bất\s*k[ìiyỳ])?\s+thuộc\s+(?:nửa\s+)?(?:đường\s*tròn|cung)/u;
3924
4368
  var TWO_ON_COMMA = /(?:[CcNn]ác\s+|[Nn]hững\s+)?điểm\s+([A-Z])(?![A-Za-z])\s*,\s*([A-Z])(?![A-Za-z])\s+thuộc\s+(?:nửa\s+)?(?:đường\s*tròn|cung)/u;
3925
- var TWO_ON_NAMES = /([A-Z])(?![A-Za-z])\s*,\s*([A-Z])(?![A-Za-z])\s+là\s+(?:hai|các)\s+điểm\s+(?:thuộc|nằm\s+trên|trên)\s+(?:(?:nửa\s+)?(?:đường\s*tròn|cung)|\(\s*[A-Z]\s*\))/u;
4369
+ var TWO_ON_NAMES = /([A-Z])(?![A-Za-z])\s*(?:,|và)\s*([A-Z])(?![A-Za-z])\s+là\s+(?:hai|các)\s+điểm\s+(?:thuộc|nằm\s+trên|trên)\s+(?:(?:nửa\s+)?(?:đường\s*tròn|cung)|\(\s*[A-Z]\s*\))/u;
3926
4370
  var TWO_ON_CUNG_REV = /[Tt]rên\s+cung\s+(?:nhỏ\s+|lớn\s+)?[A-Z]{2}(?:\s*(?:nhỏ|lớn))?\s+lấy\s+hai\s+điểm\s+([A-Z])(?![A-Za-z])\s*(?:,|và)\s*([A-Z])(?![A-Za-z])/u;
3927
4371
  var BARE_CIRCLE = /\(\s*([A-Z])\s*\)/u;
3928
4372
  var UNNAMED_DIAMETER = /(?:nửa\s+)?đường\s*tròn\s+đường\s*kính\s+([A-Z])([A-Z])(?![A-Z])/u;
4373
+ var BARE_CENTER_DIAMETER = /(?<![\p{L}\d(])([A-Z])\s+đường\s*kính/u;
3929
4374
  function resolveCircle3(problem) {
3930
4375
  const m = NAMED_CIRCLE.exec(problem) ?? COMPACT_CIRCLE.exec(problem) ?? BARE_CIRCLE.exec(problem);
3931
4376
  if (!m) {
3932
4377
  const dm = UNNAMED_DIAMETER.exec(problem);
3933
- return dm ? `k${dm[1]}${dm[2]}` : void 0;
4378
+ if (dm) return `k${dm[1]}${dm[2]}`;
4379
+ const bd = BARE_CENTER_DIAMETER.exec(problem);
4380
+ return bd ? `${bd[1]}_c` : void 0;
3934
4381
  }
3935
4382
  const center = m[1];
3936
- const c = center.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4383
+ const c = escapeRe(center);
3937
4384
  const diameterOfThisCenter = new RegExp(`(?:\\(\\s*${c}\\s*[;,)]|t\xE2m\\s+${c}(?![A-Za-z]))[^.]{0,25}?\u0111\u01B0\u1EDDng\\s*k\xEDnh`, "u").test(problem) || new RegExp(`\u0111\u01B0\u1EDDng\\s*k\xEDnh[^.]{0,25}?\\(\\s*${c}\\s*\\)`, "u").test(problem);
3938
4385
  return diameterOfThisCenter ? `${center}_c` : center;
3939
4386
  }
@@ -3978,10 +4425,12 @@ var onCirclePointRule = {
3978
4425
  if (!m) continue;
3979
4426
  const name = m[1];
3980
4427
  if (name.length !== 1) continue;
4428
+ const major = /cung\s+lớn|lớn\s+[A-Z]{2}/u.test(c.text);
4429
+ const ptTheta = major ? -1.6 : theta;
3981
4430
  out.push({
3982
4431
  ruleId: "on-circle-point",
3983
4432
  clauseIds: [c.id],
3984
- intents: [addPoint(name, { kind: "onCircle", circle, theta })]
4433
+ intents: [addPoint(name, { kind: "onCircle", circle, theta: ptTheta })]
3985
4434
  });
3986
4435
  theta += 0.8;
3987
4436
  }
@@ -3990,10 +4439,10 @@ var onCirclePointRule = {
3990
4439
  };
3991
4440
 
3992
4441
  // src/stamps/geometry-2d/ai/rules/tangentAt.ts
3993
- var PREFILTER13 = /[Tt]iếp\s*tuyến\s+(?:với\s+|của\s+)?(?:đường\s*tròn\s*)?(?:\([^)]*\)\s*)?tại/u;
4442
+ var PREFILTER13 = /[Tt]iếp\s*tuyến\s+(?:với\s+|của\s+)?(?:đường\s*tròn\s*)?(?:\([^)]*\)\s*)?(?:tại|qua)/u;
3994
4443
  var CIRCLE_REF = /đường\s*tròn\s*\(\s*([A-Z])(?:\s*[;,]\s*[Rr])?\s*\)|\(\s*([A-Z])(?:\s*[;,]\s*[Rr])?\s*\)/u;
3995
4444
  var DISTRIB3 = /[Tt]iếp\s*tuyến\s+tại\s+([A-Z])\s+cắt\s+tiếp\s*tuyến\s+tại\s+([A-Z])\s+và\s+([A-Z])[^.]{0,60}?lần\s*lượt\s+tại\s+([A-Z])\s+và\s+([A-Z])/u;
3996
- var TWO_MEET = /[Tt]iếp\s*tuyến\s+(?:với\s+|của\s+)?(?:đường\s*tròn\s*)?(?:\(\s*[A-Z]['′]?\s*\)\s*)?tại\s+([A-Z])\s*(?:,|và)\s*([A-Z])(?![A-Z])[^.]{0,30}?cắt\s+nhau\s+(?:ở|tại)\s+([A-Z])(?![A-Z])/u;
4445
+ var TWO_MEET = /[Tt]iếp\s*tuyến\s+(?:với\s+|của\s+)?(?:đường\s*tròn\s*)?(?:\(\s*[A-Z]['′]?\s*\)\s*)?(?:tại|qua)\s+([A-Z])\s*(?:,|và)\s*([A-Z])(?![A-Z])[^.]{0,30}?cắt\s+nhau\s+(?:ở|tại)\s+([A-Z])(?![A-Z])/u;
3997
4446
  var SINGLE2 = /[Tt]iếp\s*tuyến\s+tại\s+(?:điểm\s+)?([A-Z])(?:\s+với|\s+của|\s+đường|\s*$)/u;
3998
4447
  function circleName(problem) {
3999
4448
  const m = CIRCLE_REF.exec(problem);
@@ -4064,17 +4513,37 @@ var tangentAtRule = {
4064
4513
  };
4065
4514
 
4066
4515
  // src/stamps/geometry-2d/ai/rules/onSegmentPoint.ts
4067
- var PREFILTER14 = /(?:thuộc\s+(?:các\s+|hai\s+|ba\s+)?(?:cạnh|đoạn|đáy|tia|bán\s*kính|đường\s*kính|dây|[A-Z]{2})|[Tt]rên\s+(?:các\s+|hai\s+|ba\s+)?(?:cạnh|đoạn|đáy|tia|bán\s*kính|đường\s*kính|đường\s*thẳng|dây|[A-Z]{2})|(?:di\s*chuyển|di\s*động)\s+trên|nằm\s+giữa)/u;
4516
+ var PREFILTER14 = /(?:thuộc\s+(?:các\s+|hai\s+|ba\s+)?(?:cạnh|đoạn|đáy|tia|bán\s*kính|đường\s*kính|dây|[A-Za-z]{1,2}[0-9]?(?![\p{L}\d]))|[Tt]rên\s+(?:các\s+|hai\s+|ba\s+)?(?:cạnh|đoạn|đáy|tia|bán\s*kính|đường\s*kính|đường\s*thẳng|dây|[A-Za-z]{1,2}[0-9]?(?![\p{L}\d]))|(?:di\s*chuyển|di\s*động)\s+trên|nằm\s+giữa)/u;
4068
4517
  var ON_NAMED_LINE = new RegExp(
4069
4518
  String.raw`[Tt]rên\s+đường\s*thẳng\s+([a-z][0-9]?)(?!\p{L})[^.]{0,20}?(?:lấy\s+)?(?:một\s+)?(?:điểm\s+)?([A-Z](?:['′])?)(?![A-Z])`,
4070
4519
  "gu"
4071
4520
  );
4521
+ var ON_NAMED_LINE_BARE = new RegExp(
4522
+ String.raw`[Tt]rên\s+([a-z]{1,2}[0-9]?)(?![\p{L}\d])\s+(?:lấy\s+)?(?:một\s+)?(?:điểm\s+)?([A-Z](?:['′])?)(?![A-Z])`,
4523
+ "gu"
4524
+ );
4525
+ var POINT_THUOC_NAMED_BARE = new RegExp(
4526
+ String.raw`(?:điểm\s+)?([A-Z](?:['′])?)(?![A-Z])\s+(?:là\s+(?:một\s+)?điểm\s+(?:bất\s*k[iìyỳ]\s+)?)?thuộc\s+([a-z]{1,2}[0-9]?)(?![\p{L}\d])`,
4527
+ "gu"
4528
+ );
4072
4529
  var SEG = "([A-Z]{2})(?![A-Z])";
4073
4530
  var POINT = "([A-Z](?:['\u2032])?)(?![A-Z])";
4531
+ var TAKE_POINT_ON_SEG = new RegExp(
4532
+ String.raw`[Ll]ấy\s+(?:điểm\s+)?${POINT}\s+(?:bất\s*k[iìyỳ]\s+)?(?:trên|thuộc)\s+(?:cạnh\s+|đáy\s+|đoạn(?:\s+thẳng)?\s+|tia\s+|bán\s*kính\s+|đường\s*kính\s+)?${SEG}`,
4533
+ "gu"
4534
+ );
4535
+ var TAKE_TWO_ON_TWO_SEG = new RegExp(
4536
+ String.raw`[Ll]ấy\s+(?:điểm\s+)?([A-Z](?:['′])?)\s*,\s*([A-Z](?:['′])?)(?![A-Z])\s+(?:bất\s*k[iìyỳ]\s+)?(?:trên|thuộc)\s+(?:các\s+)?(?:cạnh\s+|đoạn\s+|tia\s+)?([A-Z]{2})\s*(?:,|và)\s*([A-Z]{2})(?![A-Z])`,
4537
+ "gu"
4538
+ );
4074
4539
  var ON_SEG_THEN_POINT = new RegExp(
4075
4540
  String.raw`[Tt]rên\s+(?:cạnh|đáy|đoạn(?:\s+thẳng)?|bán\s*kính|đường\s*kính|dây\s*(?:cung)?)\s+${SEG}[^.]{0,30}?(?:lấy\s+)?(?:một\s+)?(?:điểm\s+)?${POINT}`,
4076
4541
  "gu"
4077
4542
  );
4543
+ var ON_BARE_SEG_THEN_POINT = new RegExp(
4544
+ String.raw`[Tt]rên\s+${SEG}[^.]{0,14}?\blấy\s+(?:một\s+)?(?:điểm\s+)?${POINT}`,
4545
+ "gu"
4546
+ );
4078
4547
  var ON_SEG_MULTI_POINT = new RegExp(
4079
4548
  String.raw`[Tt]rên\s+(?:cạnh\s+|đáy\s+|đoạn(?:\s+thẳng)?\s+)?${SEG}\s+(?:lấy\s+)?(?:các\s+)?(?:điểm\s+)?((?:[A-Z](?:['′])?\s*,\s*)+[A-Z](?:['′])?)(?![A-Z])`,
4080
4549
  "gu"
@@ -4108,13 +4577,17 @@ var ZIP_SEG = new RegExp(
4108
4577
  "gu"
4109
4578
  );
4110
4579
  var SEGS_THEN_POINTS = new RegExp(
4111
- String.raw`[Tt]rên\s+(?:các\s+)?(?:cạnh\s+|đoạn(?:\s+thẳng)?\s+|đáy\s+|bán\s*kính\s+)?((?:[A-Z]{2}\s*,\s*)+[A-Z]{2})(?![A-Z])(?:\s+của\s+(?:tam\s*giác\s+|tứ\s*giác\s+)?[A-Z]{2,4})?\s+(?:(?:theo\s+)?thứ\s+tự\s+|lần\s*lượt\s+)?lấy\s+(?:các\s+)iểm\s+((?:[A-Z]\s*,\s*)+[A-Z])(?![A-Z])`,
4580
+ String.raw`[Tt]rên\s+(?:các\s+)?(?:cạnh\s+|đoạn(?:\s+thẳng)?\s+|đáy\s+|bán\s*kính\s+)?((?:[A-Z]{2}\s*,\s*)+[A-Z]{2})(?![A-Z])(?:\s+của\s+(?:tam\s*giác\s+|tứ\s*giác\s+)?[A-Z]{2,4})?\s+(?:(?:theo\s+)?thứ\s+tự\s+|lần\s*lượt\s+)?lấy\s+(?:các\s+)?(?:điểm\s+)?((?:[A-Z]\s*,\s*)+[A-Z])(?![A-Z])`,
4112
4581
  "gu"
4113
4582
  );
4114
4583
  var TWO_SEG_LAY = new RegExp(
4115
4584
  String.raw`[Tt]rên\s+(?:cạnh|đáy|đoạn(?:\s+thẳng)?)\s+${SEG}\s+(?:lấy\s+)?(?:một\s+)?(?:điểm\s+)?${POINT}\s+và\s+(?:trên\s+)?(?:cạnh|đáy|đoạn(?:\s+thẳng)?)\s+${SEG}\s+(?:lấy\s+)?(?:một\s+)?(?:điểm\s+)?${POINT}`,
4116
4585
  "gu"
4117
4586
  );
4587
+ var TWO_ON_NAMED_RAY = new RegExp(
4588
+ String.raw`[Tt]rên\s+(?:tia\s+)?([A-Z][xyzt])(?!\p{L})\s+(?:lấy\s+)?(?:hai\s+|2\s+)?(?:điểm\s+)?([A-Z](?:['′])?)(?![A-Z])\s*(?:,|và)\s*([A-Z](?:['′])?)(?![A-Z])`,
4589
+ "gu"
4590
+ );
4118
4591
  function normalizePoint(name) {
4119
4592
  return name.replace("\u2032", "'");
4120
4593
  }
@@ -4124,6 +4597,18 @@ function validOnSegment(name, segment) {
4124
4597
  function hasMetricConstraint(text) {
4125
4598
  return /sao\s+cho[^.]{0,40}(?:=|>|<)/u.test(text);
4126
4599
  }
4600
+ function declaredNamedLines3(problem) {
4601
+ const out = /* @__PURE__ */ new Set();
4602
+ const RE31 = /(?:đường\s*thẳng|tiếp\s*tuyến|cát\s*tuyến)\s+([a-z]{1,2}[0-9]?)(?![\p{L}\d])/gu;
4603
+ for (const m of problem.matchAll(RE31)) out.add(m[1]);
4604
+ return out;
4605
+ }
4606
+ function declaredNamedRays(problem) {
4607
+ const out = /* @__PURE__ */ new Set();
4608
+ const RE31 = /tiếp\s*tuyến\s+([A-Z][xyzt])(?![A-Za-z])/gu;
4609
+ for (const m of problem.matchAll(RE31)) out.add(m[1]);
4610
+ return out;
4611
+ }
4127
4612
  var onSegmentPointRule = {
4128
4613
  id: "on-segment-point",
4129
4614
  priority: 62,
@@ -4131,8 +4616,73 @@ var onSegmentPointRule = {
4131
4616
  patterns: [PREFILTER14],
4132
4617
  match(ctx) {
4133
4618
  const out = [];
4619
+ const namedLines = declaredNamedLines3(ctx.problem);
4620
+ const namedRays = declaredNamedRays(ctx.problem);
4134
4621
  for (const c of ctx.clauses) {
4135
4622
  const intents = [];
4623
+ ON_NAMED_LINE.lastIndex = 0;
4624
+ for (const m of c.text.matchAll(ON_NAMED_LINE)) {
4625
+ const line = m[1];
4626
+ const name = normalizePoint(m[2]);
4627
+ if (/^[A-Z]['′]?$/u.test(name)) intents.push(addPoint(name, { kind: "onSegment", of: line }));
4628
+ }
4629
+ if (namedLines.size > 0) {
4630
+ ON_NAMED_LINE_BARE.lastIndex = 0;
4631
+ for (const m of c.text.matchAll(ON_NAMED_LINE_BARE)) {
4632
+ const line = m[1];
4633
+ const name = normalizePoint(m[2]);
4634
+ if (namedLines.has(line) && /^[A-Z]['′]?$/u.test(name)) {
4635
+ intents.push(addPoint(name, { kind: "onSegment", of: line }));
4636
+ }
4637
+ }
4638
+ POINT_THUOC_NAMED_BARE.lastIndex = 0;
4639
+ for (const m of c.text.matchAll(POINT_THUOC_NAMED_BARE)) {
4640
+ const name = normalizePoint(m[1]);
4641
+ const line = m[2];
4642
+ if (namedLines.has(line) && /^[A-Z]['′]?$/u.test(name)) {
4643
+ intents.push(addPoint(name, { kind: "onSegment", of: line }));
4644
+ }
4645
+ }
4646
+ }
4647
+ if (namedRays.size > 0) {
4648
+ TWO_ON_NAMED_RAY.lastIndex = 0;
4649
+ for (const m of c.text.matchAll(TWO_ON_NAMED_RAY)) {
4650
+ const ray = m[1];
4651
+ if (!namedRays.has(ray)) continue;
4652
+ for (const raw of [m[2], m[3]]) {
4653
+ const name = normalizePoint(raw);
4654
+ if (/^[A-Z]['′]?$/u.test(name) && name[0] !== ray[0]) {
4655
+ intents.push(addPoint(name, { kind: "onSegment", of: ray }));
4656
+ }
4657
+ }
4658
+ }
4659
+ }
4660
+ if (!/tia\s+đối/u.test(c.text)) {
4661
+ TAKE_TWO_ON_TWO_SEG.lastIndex = 0;
4662
+ for (const m of c.text.matchAll(TAKE_TWO_ON_TWO_SEG)) {
4663
+ const [n1, n2, s1, s2] = [normalizePoint(m[1]), normalizePoint(m[2]), m[3], m[4]];
4664
+ if (validOnSegment(n1, s1) && validOnSegment(n2, s2)) {
4665
+ intents.push(addPoint(n1, { kind: "onSegment", of: s1 }));
4666
+ intents.push(addPoint(n2, { kind: "onSegment", of: s2 }));
4667
+ }
4668
+ }
4669
+ TAKE_POINT_ON_SEG.lastIndex = 0;
4670
+ for (const m of c.text.matchAll(TAKE_POINT_ON_SEG)) {
4671
+ const name = normalizePoint(m[1]);
4672
+ const segment = m[2];
4673
+ if (validOnSegment(name, segment)) {
4674
+ intents.push(addPoint(name, { kind: "onSegment", of: segment }));
4675
+ }
4676
+ }
4677
+ ON_BARE_SEG_THEN_POINT.lastIndex = 0;
4678
+ for (const m of c.text.matchAll(ON_BARE_SEG_THEN_POINT)) {
4679
+ const segment = m[1];
4680
+ const name = normalizePoint(m[2]);
4681
+ if (validOnSegment(name, segment)) {
4682
+ intents.push(addPoint(name, { kind: "onSegment", of: segment }));
4683
+ }
4684
+ }
4685
+ }
4136
4686
  SEGS_THEN_POINTS.lastIndex = 0;
4137
4687
  for (const m of c.text.matchAll(SEGS_THEN_POINTS)) {
4138
4688
  const segs = m[1].split(",").map((s) => s.trim());
@@ -4192,12 +4742,6 @@ var onSegmentPointRule = {
4192
4742
  if (intents.length > 0) out.push({ ruleId: "on-segment-point", clauseIds: [c.id], intents });
4193
4743
  continue;
4194
4744
  }
4195
- ON_NAMED_LINE.lastIndex = 0;
4196
- for (const m of c.text.matchAll(ON_NAMED_LINE)) {
4197
- const line = m[1];
4198
- const name = normalizePoint(m[2]);
4199
- if (/^[A-Z]['′]?$/u.test(name)) intents.push(addPoint(name, { kind: "onSegment", of: line }));
4200
- }
4201
4745
  ON_SEG_THEN_POINT.lastIndex = 0;
4202
4746
  for (const m of c.text.matchAll(ON_SEG_THEN_POINT)) {
4203
4747
  const segment = m[1];
@@ -4249,7 +4793,7 @@ var onSegmentPointRule = {
4249
4793
  };
4250
4794
 
4251
4795
  // src/stamps/geometry-2d/ai/rules/lineCircleIntersection.ts
4252
- var PREFILTER15 = /cắt\s+(?:lại\s+)?(?:(?:nửa\s+)?đường\s*tròn\s*)?\(|cắt\s+(?:lại\s+)?(?:nửa\s+)?đường\s*tròn\s+(?:ở|tại)|cắt\s+[A-Z]{2}\s+và\s+\(|giao\s*điểm\s+(?:thứ\s+hai\s+)?(?:của\s+|khác\s+[A-Z]\s+của\s+)?(?:đường\s*thẳng\s+)?[A-Z]{2}\s+(?:và|với)\s+(?:(?:nửa\s+)?đường\s*tròn|\(|ngoại\s*tiếp\s+tam\s*giác)/u;
4796
+ var PREFILTER15 = /cắt\s+(?:lại\s+)?(?:(?:nửa\s+)?đường\s*tròn\s*)?\(|cắt\s+(?:lại\s+)?(?:nửa\s+)?đường\s*tròn\s+(?:tâm\s+[A-Z](?:['′])?\s+)?(?:ở|tại)|cắt\s+[A-Z]{2}\s+và\s+\(|\(\s*[A-Z](?:['′]?)\s*(?:[;,]\s*[Rr]\s*)?\)\s+cắt\s+(?:lại\s+)?[A-Z]{2}\s*(?:,|và)?\s*(?:[A-Z]{2})?\s+(?:lần\s*lượt\s+|theo\s+thứ\s+tự\s+|tương\s+ứng\s+)?(?:ở|tại)|\(\s*[A-Z]{3}\s*\)\s*(?:,|và)\s*\(\s*[A-Z]{3}\s*\)\s+cắt|giao\s*điểm\s+(?:thứ\s+hai\s+)?(?:của\s+|khác\s+[A-Z]\s+của\s+)?(?:đường\s*thẳng\s+)?[A-Z]{2}(?:\s*,\s*[A-Z]{2})?\s+(?:và|với)\s+(?:(?:nửa\s+)?đường\s*tròn|\(|ngoại\s*tiếp\s+tam\s*giác)/u;
4253
4797
  var CIRCLE = String.raw`(?:(?:nửa\s+)?đường\s*tròn\s*)?\(\s*([A-Z])(?:['′]?)\s*(?:[;,]\s*[Rr]\s*)?\)`;
4254
4798
  var CIRCLE_P = String.raw`(?:(?:nửa\s+)?đường\s*tròn\s*)?\(\s*([A-Z](?:['′])?)\s*(?:[;,]\s*[Rr]\s*)?\)`;
4255
4799
  var PERP_CUTS_CIRCLE = new RegExp(
@@ -4265,13 +4809,21 @@ var DOUBLE_DISTRIB = new RegExp(
4265
4809
  "gu"
4266
4810
  );
4267
4811
  var SINGLE3 = new RegExp(
4268
- String.raw`([A-Z]{2})(?![A-Z])\s+cắt\s+(?:lại\s+)?` + CIRCLE + String.raw`\s+(?:ở|tại)\s+(?:điểm\s+(?:thứ\s+hai\s+)?)?(?:là\s+)?([A-Z])(?![A-Z])(?:\s+khác\s+([A-Z])(?![A-Z]))?`,
4812
+ String.raw`([A-Z]{2})(?![A-Z])\s+(?:kéo\s+dài\s+)?cắt\s+(?:lại\s+)?` + CIRCLE + String.raw`\s+(?:ở|tại)\s+(?:điểm\s+(?:thứ\s+hai\s+)?)?(?:là\s+)?([A-Z])(?![A-Z])(?:\s+khác\s+([A-Z])(?![A-Z]))?`,
4813
+ "gu"
4814
+ );
4815
+ var SINGLE_NAMED_CENTER = new RegExp(
4816
+ String.raw`([A-Z]{2})(?![A-Z])\s+(?:kéo\s+dài\s+)?cắt\s+(?:lại\s+)?(?:nửa\s+)?đường\s*tròn\s+tâm\s+([A-Z](?:['′])?)(?![A-Za-z])` + String.raw`\s+(?:ở|tại)\s+(?:điểm\s+(?:thứ\s+hai\s+)?)?(?:là\s+)?([A-Z])(?![A-Z])`,
4269
4817
  "gu"
4270
4818
  );
4271
4819
  var BOTH = new RegExp(
4272
4820
  String.raw`([A-Z]{2})(?![A-Z])\s+cắt\s+` + CIRCLE + String.raw`\s+(?:ở|tại)\s+(?:hai\s+|các\s+)?điểm\s+(?:phân\s*biệt\s+)?([A-Z])\s*(?:,|và)\s*([A-Z])(?![A-Z])`,
4273
4821
  "gu"
4274
4822
  );
4823
+ var TWO_NAMED = new RegExp(
4824
+ String.raw`([A-Z]{2})(?![A-Z])\s+(?:kéo\s+dài\s+)?cắt\s+(?:lại\s+)?` + CIRCLE + String.raw`\s+(?:ở|tại)\s+([A-Z])\s+và\s+([A-Z])(?![A-Z])`,
4825
+ "gu"
4826
+ );
4275
4827
  var LINE_AND_CIRCLE = new RegExp(
4276
4828
  String.raw`([A-Z]{2})(?![A-Z])\s+cắt\s+([A-Z]{2})(?![A-Z])\s+và\s+` + CIRCLE + String.raw`\s+(?:lần\s*lượt\s+|theo\s+thứ\s+tự\s+)?(?:tại|ở)\s+([A-Z])\s*(?:,|và)\s*([A-Z])(?![A-Z])`,
4277
4829
  "gu"
@@ -4288,12 +4840,16 @@ var CIRCLE_SUBJECT = new RegExp(
4288
4840
  CIRCLE + String.raw`\s+cắt\s+(?:lại\s+)?([A-Z]{2})(?![A-Z])\s+(?:ở|tại)\s+(?:điểm\s+(?:thứ\s+hai\s+)?)?(?:là\s+)?([A-Z])(?![A-Z])`,
4289
4841
  "gu"
4290
4842
  );
4843
+ var CIRCLE_SUBJECT_TWO = new RegExp(
4844
+ CIRCLE + String.raw`\s+cắt\s+(?:lại\s+)?([A-Z]{2})\s*(?:,|và)\s*([A-Z]{2})(?![A-Z])\s+(?:lần\s*lượt\s+|theo\s+thứ\s+tự\s+|tương\s+ứng\s+)?(?:ở|tại)\s+([A-Z])\s*(?:,|và)\s*([A-Z])(?![A-Z])`,
4845
+ "gu"
4846
+ );
4291
4847
  var GIAO_CIRCLE = new RegExp(
4292
4848
  String.raw`giao\s*điểm\s+(?:thứ\s+hai\s+)?(?:của\s+)?([A-Z]{2})(?![A-Z])\s+(?:và|với)\s+` + CIRCLE + String.raw`\s+là\s+([A-Z])(?![A-Z])(?:\s+khác\s+([A-Z])(?![A-Z]))?`,
4293
4849
  "gu"
4294
4850
  );
4295
4851
  var NAME_2ND_CUA = new RegExp(
4296
- String.raw`([A-Z])(?![A-Z])\s+là\s+giao\s*điểm\s+thứ\s+hai\s+của\s+([A-Z]{2})(?![A-Z])\s+(?:và|với)\s+` + CIRCLE,
4852
+ String.raw`([A-Z])(?![A-Z])\s+là\s+giao\s*điểm\s+thứ\s+hai\s+của\s+(?:đường\s*thẳng\s+)?([A-Z]{2})(?![A-Z])\s+(?:và|với)\s+` + CIRCLE,
4297
4853
  "gu"
4298
4854
  );
4299
4855
  var NAME_2ND_BARE = new RegExp(
@@ -4309,13 +4865,28 @@ var NAME_KHAC_CUA = new RegExp(
4309
4865
  String.raw`([A-Z])(?![A-Z])\s+là\s+giao\s*điểm\s+(?:thứ\s+hai\s+)?khác\s+([A-Z])(?![A-Z])\s+của\s+([A-Z]{2})(?![A-Z])\s+(?:và|với)\s+` + CIRCLE,
4310
4866
  "gu"
4311
4867
  );
4868
+ var NAME_CUA = new RegExp(
4869
+ String.raw`([A-Z])(?![A-Z])\s+là\s+giao\s*điểm\s+của\s+(?!thứ\s+hai)(?!khác)(?:đường\s*thẳng\s+)?([A-Z]{2})(?![A-Z])\s+(?:và|với)\s+` + CIRCLE,
4870
+ "gu"
4871
+ );
4312
4872
  var NAME_2ND_CIRCUM = new RegExp(
4313
4873
  String.raw`giao\s*điểm\s+thứ\s+hai\s+của\s+([A-Z]{2})(?![A-Z])\s+(?:và|với)\s+` + String.raw`(?:nửa\s+)?` + CIRCLE_KW + String.raw`\s+ngoại\s*tiếp\s+tam\s*giác\s+[A-Z]{3}(?![A-Z])` + String.raw`\s+là\s+(?:điểm\s+)?([A-Z])(?![A-Z])(?:\s+khác\s+([A-Z])(?![A-Z]))?`,
4314
4874
  "gu"
4315
4875
  );
4876
+ var LINE_CUTS_PAREN_PAIR = new RegExp(
4877
+ String.raw`([A-Z]{2})(?![A-Z])\s+(?:lần\s*lượt\s+|theo\s+thứ\s+tự\s+|tương\s+ứng\s+)?cắt\s+(?:lại\s+)?` + String.raw`\(\s*([A-Z]{3})\s*\)\s*(?:,|và)\s*\(\s*([A-Z]{3})\s*\)` + String.raw`\s+(?:lần\s*lượt\s+|theo\s+thứ\s+tự\s+)?(?:ở|tại)\s+(?:điểm\s+(?:thứ\s+hai\s+)?(?:là\s+)?)?([A-Z])\s*(?:,|và)\s*([A-Z])(?![A-Z])`,
4878
+ "gu"
4879
+ );
4880
+ var PAREN_PAIR_CUTS = new RegExp(
4881
+ String.raw`\(\s*([A-Z]{3})\s*\)\s*(?:,|và)\s*\(\s*([A-Z]{3})\s*\)\s+cắt\s+(?:lại\s+)?` + String.raw`([A-Z]{2})\s*(?:,|và)\s*([A-Z]{2})(?![A-Z])\s+(?:lần\s*lượt\s+|theo\s+thứ\s+tự\s+)?(?:ở|tại)\s+([A-Z])\s*(?:,|và)\s*([A-Z])(?![A-Z])`,
4882
+ "gu"
4883
+ );
4316
4884
  function secondIntersection(name, line, circle, other) {
4317
4885
  return addPoint(name, { kind: "secondIntersection", line, circle, other: other ?? line[0] });
4318
4886
  }
4887
+ function vertexOnLine(verts, line) {
4888
+ return verts.split("").find((v) => line.includes(v));
4889
+ }
4319
4890
  function valid(name, line) {
4320
4891
  return /^[A-Z]$/u.test(name) && /^[A-Z]{2}$/u.test(line) && !line.includes(name);
4321
4892
  }
@@ -4352,6 +4923,13 @@ var lineCircleIntersectionRule = {
4352
4923
  const other = m[4];
4353
4924
  if (valid(name, line)) intents.push(secondIntersection(name, line, circle, other));
4354
4925
  }
4926
+ SINGLE_NAMED_CENTER.lastIndex = 0;
4927
+ for (const m of c.text.matchAll(SINGLE_NAMED_CENTER)) {
4928
+ const line = m[1];
4929
+ const circle = m[2].replace(/′/g, "'");
4930
+ const name = m[3];
4931
+ if (valid(name, line)) intents.push(secondIntersection(name, line, circle, line[0]));
4932
+ }
4355
4933
  LINE_AND_CIRCLE.lastIndex = 0;
4356
4934
  for (const m of c.text.matchAll(LINE_AND_CIRCLE)) {
4357
4935
  const [line, line2, circle, n1, n2] = [m[1], m[2], m[3], m[4], m[5]];
@@ -4372,7 +4950,14 @@ var lineCircleIntersectionRule = {
4372
4950
  addPoint(y, { kind: "intersection", of: [line, circle], branch: 1 })
4373
4951
  );
4374
4952
  }
4375
- NAME_2ND_BARE.lastIndex = 0;
4953
+ TWO_NAMED.lastIndex = 0;
4954
+ for (const m of c.text.matchAll(TWO_NAMED)) {
4955
+ const [line, circle, p, q] = [m[1], m[2], m[3], m[4]];
4956
+ if (p === q) continue;
4957
+ if (line.includes(p) && !line.includes(q)) intents.push(secondIntersection(q, line, circle, p));
4958
+ else if (line.includes(q) && !line.includes(p)) intents.push(secondIntersection(p, line, circle, q));
4959
+ }
4960
+ NAME_2ND_BARE.lastIndex = 0;
4376
4961
  for (const m of c.text.matchAll(NAME_2ND_BARE)) {
4377
4962
  const rc = RESOLVE_CIRCLE_BARE.exec(ctx.problem);
4378
4963
  const circle = rc?.[1] ?? rc?.[2];
@@ -4409,6 +4994,13 @@ var lineCircleIntersectionRule = {
4409
4994
  const [line, name] = [m[1], m[2]];
4410
4995
  if (circle && valid(name, line)) intents.push(secondIntersection(name, line, circle, line[0]));
4411
4996
  }
4997
+ CIRCLE_SUBJECT_TWO.lastIndex = 0;
4998
+ for (const m of c.text.matchAll(CIRCLE_SUBJECT_TWO)) {
4999
+ const [circle, l1, l2, n1, n2] = [m[1], m[2], m[3], m[4], m[5]];
5000
+ if (n1 === n2) continue;
5001
+ if (valid(n1, l1)) intents.push(secondIntersection(n1, l1, circle));
5002
+ if (valid(n2, l2)) intents.push(secondIntersection(n2, l2, circle));
5003
+ }
4412
5004
  CIRCLE_SUBJECT.lastIndex = 0;
4413
5005
  for (const m of c.text.matchAll(CIRCLE_SUBJECT)) {
4414
5006
  const [circle, line, name] = [m[1], m[2], m[3]];
@@ -4430,6 +5022,11 @@ var lineCircleIntersectionRule = {
4430
5022
  const circle = m[4];
4431
5023
  if (valid(name, line)) intents.push(secondIntersection(name, line, circle, other));
4432
5024
  }
5025
+ NAME_CUA.lastIndex = 0;
5026
+ for (const m of c.text.matchAll(NAME_CUA)) {
5027
+ const [name, line, circle] = [m[1], m[2], m[3]];
5028
+ if (valid(name, line)) intents.push(secondIntersection(name, line, circle));
5029
+ }
4433
5030
  NAME_2ND_CIRCUM.lastIndex = 0;
4434
5031
  for (const m of c.text.matchAll(NAME_2ND_CIRCUM)) {
4435
5032
  const line = m[1];
@@ -4445,6 +5042,34 @@ var lineCircleIntersectionRule = {
4445
5042
  if (line.includes(name)) continue;
4446
5043
  intents.push(addPoint(name, { kind: "intersection", of: [line, circle], branch: 0 }));
4447
5044
  }
5045
+ LINE_CUTS_PAREN_PAIR.lastIndex = 0;
5046
+ for (const m of c.text.matchAll(LINE_CUTS_PAREN_PAIR)) {
5047
+ const [line, t1, t2, n1, n2] = [m[1], m[2], m[3], m[4], m[5]];
5048
+ const o1 = vertexOnLine(t1, line);
5049
+ const o2 = vertexOnLine(t2, line);
5050
+ if (!o1 || !o2 || n1 === n2) continue;
5051
+ if (line.includes(n1) || line.includes(n2)) continue;
5052
+ intents.push(
5053
+ drawCircle(`w${t1}`, "through3", { points: t1.split("") }),
5054
+ drawCircle(`w${t2}`, "through3", { points: t2.split("") }),
5055
+ secondIntersection(n1, line, `w${t1}`, o1),
5056
+ secondIntersection(n2, line, `w${t2}`, o2)
5057
+ );
5058
+ }
5059
+ PAREN_PAIR_CUTS.lastIndex = 0;
5060
+ for (const m of c.text.matchAll(PAREN_PAIR_CUTS)) {
5061
+ const [t1, t2, l1, l2, n1, n2] = [m[1], m[2], m[3], m[4], m[5], m[6]];
5062
+ const o1 = vertexOnLine(t1, l1);
5063
+ const o2 = vertexOnLine(t2, l2);
5064
+ if (!o1 || !o2 || n1 === n2) continue;
5065
+ if (l1.includes(n1) || l2.includes(n2)) continue;
5066
+ intents.push(
5067
+ drawCircle(`w${t1}`, "through3", { points: t1.split("") }),
5068
+ drawCircle(`w${t2}`, "through3", { points: t2.split("") }),
5069
+ secondIntersection(n1, l1, `w${t1}`, o1),
5070
+ secondIntersection(n2, l2, `w${t2}`, o2)
5071
+ );
5072
+ }
4448
5073
  if (intents.length > 0) out.push({ ruleId: "line-circle-intersection", clauseIds: [c.id], intents });
4449
5074
  }
4450
5075
  return out;
@@ -4505,8 +5130,9 @@ var incenterNamedTriangleRule = {
4505
5130
  };
4506
5131
 
4507
5132
  // src/stamps/geometry-2d/ai/rules/perpThroughCutsLines.ts
5133
+ var LTOK = "(?:[A-Z]{2}|[A-Z][a-z][0-9]?)";
4508
5134
  var RE2 = new RegExp(
4509
- "(?:Qua|qua|T\u1EEB|t\u1EEB)\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?:['\u2032]?)(?!\\p{L})[^.]{0,24}?(song\\s*song|vu\xF4ng\\s*g\xF3c)\\s+(?:v\u1EDBi\\s+)?(?:c\u1EA1nh\\s+|\u0111o\u1EA1n(?:\\s+th\u1EB3ng)?\\s+)?([A-Z])([A-Z])(?!\\p{L})[^.]{0,40}?c\u1EAFt\\s+(?:c\xE1c\\s+)?(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+)?([A-Z])([A-Z])\\s*(?:,|v\xE0)\\s*([A-Z])([A-Z])(?![A-Z])[^.]{0,30}?(?:\u1EDF|t\u1EA1i)\\s+([A-Z])\\s*(?:,|v\xE0)\\s*([A-Z])(?![A-Z])",
5135
+ `(?:Qua|qua|T\u1EEB|t\u1EEB)\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?:['\u2032]?)(?!\\p{L})[^.]{0,24}?(song\\s*song|vu\xF4ng\\s*g\xF3c)\\s+(?:v\u1EDBi\\s+)?(?:c\u1EA1nh\\s+|\u0111o\u1EA1n(?:\\s+th\u1EB3ng)?\\s+)?([A-Z])([A-Z])(?!\\p{L})[^.]{0,40}?c\u1EAFt\\s+(?:c\xE1c\\s+)?(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+)?(${LTOK})\\s*(?:,|v\xE0)\\s*(${LTOK})(?![A-Z])[^.]{0,30}?(?:\u1EDF|t\u1EA1i)\\s+([A-Z])\\s*(?:,|v\xE0)\\s*([A-Z])(?![A-Z])`,
4510
5136
  "gu"
4511
5137
  );
4512
5138
  var PREFILTER16 = /(?:Qua|qua|Từ|từ)\s+(?:điểm\s+)?[A-Z][^.]{0,24}?(?:song\s*song|vuông\s*góc)[^.]{0,80}?cắt/u;
@@ -4529,10 +5155,10 @@ var perpThroughCutsLinesRule = {
4529
5155
  const through = m[1];
4530
5156
  const isParallel = /song/.test(m[2]);
4531
5157
  const to = m[3] + m[4];
4532
- const line1 = m[5] + m[6];
4533
- const line2 = m[7] + m[8];
4534
- const h = m[9];
4535
- const k = m[10];
5158
+ const line1 = m[5];
5159
+ const line2 = m[6];
5160
+ const h = m[7];
5161
+ const k = m[8];
4536
5162
  if (isParallel && to.includes(through)) continue;
4537
5163
  if (h === k) continue;
4538
5164
  const kind = isParallel ? "parallelThrough" : "perpThrough";
@@ -4578,6 +5204,40 @@ var CIRCLE_CENTER3 = /\(\s*([A-Z])(?:\s*[;,]\s*[Rr])?\s*\)|tâm\s+([A-Z])(?![A-Z
4578
5204
  var DIAMETER_ENDS = /đường\s*kính\s+([A-Z])([A-Z])(?![A-Z])/u;
4579
5205
  var AFTER_TANGENT = /tiếp\s*tuyến\s+((?:[A-Z][xyzt](?![A-Za-z])\s*[,và\s]*)+)/gu;
4580
5206
  var RAY_TOKEN = /([A-Z])([xyzt])(?![A-Za-z])/gu;
5207
+ var SHAPE_VERTICES = /[Cc]ho\s+(?:tam\s*giác|tứ\s*giác|hình\s+(?:vuông|chữ\s*nhật|thang|bình\s*hành|thoi))\s+([A-Z]{3,})/gu;
5208
+ function matchFreeTangentPoint(ctx, cm) {
5209
+ const center = cm ? cm[1] ?? cm[2] : void 0;
5210
+ if (!center) return [];
5211
+ const known = /* @__PURE__ */ new Set();
5212
+ SHAPE_VERTICES.lastIndex = 0;
5213
+ for (const m of ctx.problem.matchAll(SHAPE_VERTICES)) {
5214
+ for (const ch of m[1]) known.add(ch);
5215
+ }
5216
+ const out = [];
5217
+ for (const c of ctx.clauses) {
5218
+ const intents = [];
5219
+ const seen = /* @__PURE__ */ new Set();
5220
+ AFTER_TANGENT.lastIndex = 0;
5221
+ for (const grp of c.text.matchAll(AFTER_TANGENT)) {
5222
+ RAY_TOKEN.lastIndex = 0;
5223
+ for (const m of grp[1].matchAll(RAY_TOKEN)) {
5224
+ const endpoint = m[1];
5225
+ const token = `${m[1]}${m[2]}`;
5226
+ if (known.has(endpoint)) continue;
5227
+ if (seen.has(token)) continue;
5228
+ seen.add(token);
5229
+ intents.push(
5230
+ addPoint(endpoint, { kind: "onCircle", circle: center, theta: 0.7 }),
5231
+ drawLine(token, "tangentAt", { through: endpoint, circle: center })
5232
+ );
5233
+ }
5234
+ }
5235
+ if (intents.length > 0) {
5236
+ out.push({ ruleId: "tangent-ray", clauseIds: [c.id], intents });
5237
+ }
5238
+ }
5239
+ return out;
5240
+ }
4581
5241
  var tangentRayRule = {
4582
5242
  id: "tangent-ray",
4583
5243
  // Cao hơn point-on-ray (55) + intersect-ray (48): token tia phải được dựng
@@ -4589,7 +5249,7 @@ var tangentRayRule = {
4589
5249
  match(ctx) {
4590
5250
  const cm = CIRCLE_CENTER3.exec(ctx.problem);
4591
5251
  const dm = DIAMETER_ENDS.exec(ctx.problem);
4592
- if (!dm) return [];
5252
+ if (!dm) return matchFreeTangentPoint(ctx, cm);
4593
5253
  const center = cm ? cm[1] ?? cm[2] : `k${dm[1]}${dm[2]}`;
4594
5254
  const ends = /* @__PURE__ */ new Set([dm[1], dm[2]]);
4595
5255
  const out = [];
@@ -5238,7 +5898,7 @@ var PREFILTER30 = /[Tt]iếp\s*tuyến[^.]{0,30}?tại\s+[A-Z][^.]{0,40}?cắt|Q
5238
5898
  var CIRCLE_REF4 = /đường\s*tròn\s*(?:tâm\s+)?\(?\s*([A-Z])(?:\s*[;,]\s*[Rr])?\s*\)?/u;
5239
5899
  var UNNAMED_DIAMETER2 = /(?:nửa\s+)?đường\s*tròn\s+đường\s*kính\s+([A-Z])([A-Z])(?![A-Z])/u;
5240
5900
  var QUA_RE = new RegExp(
5241
- "Qua\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?!\\p{L})[^.]{0,40}?ti\u1EBFp\\s*tuy\u1EBFn\\s+(?:th\u1EE9\\s+(?:ba|3|hai|2)\\s+)?c\u1EAFt\\s+(?:c\xE1c\\s+)?(?:(?:c\xE1c\\s+)?ti\u1EBFp\\s*tuy\u1EBFn\\s+|\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|c\u1EA1nh\\s+)?([A-Z](?:[A-Z]|[xyzt]))\\s*(?:,|v\xE0)\\s*([A-Z](?:[A-Z]|[xyzt]))(?!\\p{L})\\s+(?:l\u1EA7n\\s*l\u01B0\u1EE3t\\s+)?(?:\u1EDF|t\u1EA1i)\\s+([A-Z])\\s*(?:,|v\xE0)\\s*([A-Z])(?![A-Z])",
5901
+ "Qua\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?!\\p{L})[^.]{0,40}?ti\u1EBFp\\s*tuy\u1EBFn\\s+(?:th\u1EE9\\s+(?:ba|3|hai|2)\\s+)?c\u1EAFt\\s+(?:c\xE1c\\s+)?(?:(?:c\xE1c\\s+)?ti\u1EBFp\\s*tuy\u1EBFn\\s+|\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|c\u1EA1nh\\s+)?([A-Z](?:[A-Z]|[xyzt]))\\s*(?:,|v\xE0)\\s*([A-Z](?:[A-Z]|[xyzt]))(?!\\p{L})(?:\\s+k\xE9o\\s+d\xE0i)?\\s+(?:l\u1EA7n\\s*l\u01B0\u1EE3t\\s+)?(?:\u1EDF|t\u1EA1i)\\s+([A-Z])\\s*(?:,|v\xE0)\\s*(?:(?:t\u1EA1i|\u1EDF)\\s+)?([A-Z])(?![A-Z])",
5242
5902
  "gu"
5243
5903
  );
5244
5904
  var SINGLE_RE = new RegExp(
@@ -5246,7 +5906,7 @@ var SINGLE_RE = new RegExp(
5246
5906
  "gu"
5247
5907
  );
5248
5908
  var RE6 = new RegExp(
5249
- "[Tt]i\u1EBFp\\s*tuy\u1EBFn\\s+(?:c\u1EE7a\\s+[^.]{0,14}?\\s+)?t\u1EA1i\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?!\\p{L})(?:\\s+c\u1EE7a\\s+[^.]{0,14}?)?\\s+c\u1EAFt\\s+(?:c\xE1c\\s+)?(?:(?:c\xE1c\\s+)?ti\u1EBFp\\s*tuy\u1EBFn\\s+|\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|c\u1EA1nh\\s+)?([A-Z](?:[A-Z]|[xyzt]))\\s*(?:,|v\xE0)\\s*([A-Z](?:[A-Z]|[xyzt]))(?!\\p{L})\\s+(?:l\u1EA7n\\s*l\u01B0\u1EE3t\\s+)?(?:t\u1EA1i|\u1EDF)\\s+([A-Z])\\s*(?:,|v\xE0)\\s*([A-Z])(?![A-Z])",
5909
+ "[Tt]i\u1EBFp\\s*tuy\u1EBFn\\s+(?:(?:c\u1EE7a|v\u1EDBi)\\s+[^.]{0,14}?\\s+)?t\u1EA1i\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?!\\p{L})(?:\\s+(?:c\u1EE7a|v\u1EDBi)\\s+[^.]{0,14}?)?\\s+c\u1EAFt\\s+(?:c\xE1c\\s+)?(?:(?:c\xE1c\\s+)?ti\u1EBFp\\s*tuy\u1EBFn\\s+|\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|c\u1EA1nh\\s+)?([A-Z](?:[A-Z]|[xyzt]))\\s*(?:,|v\xE0)\\s*([A-Z](?:[A-Z]|[xyzt]))(?!\\p{L})(?:\\s+k\xE9o\\s+d\xE0i)?\\s+(?:l\u1EA7n\\s*l\u01B0\u1EE3t\\s+)?(?:t\u1EA1i|\u1EDF)\\s+([A-Z])\\s*(?:,|v\xE0)\\s*(?:(?:t\u1EA1i|\u1EDF)\\s+)?([A-Z])(?![A-Z])",
5250
5910
  "gu"
5251
5911
  );
5252
5912
  function circleName3(problem) {
@@ -5308,15 +5968,15 @@ var tangentAtCutsLinesRule = {
5308
5968
  for (const m of c.text.matchAll(SINGLE_RE)) {
5309
5969
  const at = m[1];
5310
5970
  const line = m[2];
5311
- const z46 = m[3];
5312
- if (line.includes(z46) || line.includes(at)) continue;
5971
+ const z47 = m[3];
5972
+ if (line.includes(z47) || line.includes(at)) continue;
5313
5973
  const t = `t${at}`;
5314
5974
  out.push({
5315
5975
  ruleId: "tangentAtCutsLines",
5316
5976
  clauseIds: [c.id],
5317
5977
  intents: [
5318
5978
  drawLine(t, "tangentAt", { through: at, circle }),
5319
- addPoint(z46, { kind: "intersection", of: [t, line] })
5979
+ addPoint(z47, { kind: "intersection", of: [t, line] })
5320
5980
  ]
5321
5981
  });
5322
5982
  }
@@ -5330,12 +5990,16 @@ var RE7 = new RegExp(
5330
5990
  "([A-Z])([A-Z])(?![A-Z])\\s+l\xE0\\s+\u0111\u01B0\u1EDDng\\s*k\xEDnh\\s+(?:c\u1EE7a\\s+)?(?:\u0111\u01B0\u1EDDng\\s*tr\xF2n\\s*)?(?:t\xE2m\\s+)?\\(?\\s*([A-Z])\\s*\\)?",
5331
5991
  "gu"
5332
5992
  );
5993
+ var VERB_GAP = "(?:[^.;,]{0,24}?v\xE0\\s+|(?:(?!\u0111\u01B0\u1EDDng\\s*tr\xF2n|\\()[^.;,]){0,24}?)?";
5333
5994
  var RE_VERB = new RegExp(
5334
- "(?:[Kk]\u1EBB|[Vv]\u1EBD|[Dd]\u1EF1ng)\\s+(?:[^.;,]{0,24}?v\xE0\\s+)?\u0111\u01B0\u1EDDng\\s*k\xEDnh\\s+([A-Z])([A-Z]['\u2032]?)(?![A-Za-z'\u2032])",
5995
+ "(?:[Kk]\u1EBB|[Vv]\u1EBD|[Dd]\u1EF1ng)\\s+" + VERB_GAP + "\u0111\u01B0\u1EDDng\\s*k\xEDnh\\s+([A-Z])([A-Z]['\u2032]?)(?![A-Za-z'\u2032])",
5335
5996
  "gu"
5336
5997
  );
5337
5998
  var RESOLVE_CENTER = /\(\s*([A-Z])(?:\s*[;,]\s*[Rr0-9][^)]*)?\s*\)|tâm\s+([A-Z])(?![A-Za-z])/u;
5338
- var PREFILTER31 = /[A-Z]{2}\s+là\s+đường\s*kính|(?:[Kk]ẻ|[Vv]ẽ|[Dd]ựng)\s+(?:[^.;,]{0,24}?và\s+)?đường\s*kính/u;
5999
+ var PREFILTER31 = new RegExp(
6000
+ "[A-Z]{2}\\s+l\xE0\\s+\u0111\u01B0\u1EDDng\\s*k\xEDnh|(?:[Kk]\u1EBB|[Vv]\u1EBD|[Dd]\u1EF1ng)\\s+" + VERB_GAP + "\u0111\u01B0\u1EDDng\\s*k\xEDnh",
6001
+ "u"
6002
+ );
5339
6003
  var diameterEndpointRule = {
5340
6004
  id: "diameterEndpoint",
5341
6005
  priority: 56,
@@ -5369,7 +6033,7 @@ var diameterEndpointRule = {
5369
6033
  const d = m[2].replace(/′/g, "'");
5370
6034
  if (a === d || center === a || center === d || emitted.has(d)) continue;
5371
6035
  const before = ctx.problem.slice(0, ctx.problem.indexOf(m[0]));
5372
- if (!new RegExp(`(?<![a-z])${a}(?![a-z])`, "u").test(before)) continue;
6036
+ if (!new RegExp(`(?<![a-z])${escapeRe(a)}(?![a-z])`, "u").test(before)) continue;
5373
6037
  emitted.add(d);
5374
6038
  out.push({
5375
6039
  ruleId: "diameterEndpoint",
@@ -5433,8 +6097,69 @@ var circleThroughTwoCutsSidesRule = {
5433
6097
  }
5434
6098
  };
5435
6099
 
6100
+ // src/stamps/geometry-2d/ai/rules/circleCenterRadiusCutsSides.ts
6101
+ var PATTERN4 = new RegExp(
6102
+ CIRCLE_KW + "\\s+t\xE2m\\s+([A-Z])(?![A-Z])\\s*b\xE1n\\s*k\xEDnh\\s+([A-Z])([A-Z])(?![A-Z])[^.;\\n]*?c\u1EAFt\\s+([^.;\\n]*?)\\s+(?:(?:l\u1EA7n\\s*l\u01B0\u1EE3t|theo\\s+th\u1EE9\\s+t\u1EF1)\\s+)?t\u1EA1i\\s+(?:c\xE1c\\s+)?(?:\u0111i\u1EC3m\\s+)?([A-Z](?!\\p{L})(?:\\s*(?:,|v\xE0)\\s*[A-Z](?!\\p{L}))*)",
6103
+ "gu"
6104
+ );
6105
+ var PREFILTER33 = new RegExp(
6106
+ CIRCLE_KW + "\\s+t\xE2m\\s+[A-Z]\\s*b\xE1n\\s*k\xEDnh\\s+[A-Z]{2}[^.;\\n]*?c\u1EAFt",
6107
+ "u"
6108
+ );
6109
+ var circleCenterRadiusCutsSidesRule = {
6110
+ id: "circle-center-radius-cuts-sides",
6111
+ // THẤP HƠN triangle (100) + centers (cần đỉnh + tâm H dựng trước). Cùng họ với
6112
+ // diameterCircleCutsSides (66) / circleThroughTwoCutsSides (58). Đặt 57.
6113
+ priority: 57,
6114
+ languages: ["vi"],
6115
+ patterns: [PREFILTER33],
6116
+ match(ctx) {
6117
+ const out = [];
6118
+ for (const c of ctx.clauses) {
6119
+ PATTERN4.lastIndex = 0;
6120
+ for (const m of c.text.matchAll(PATTERN4)) {
6121
+ const center = m[1];
6122
+ const r0 = m[2];
6123
+ const r1 = m[3];
6124
+ let through;
6125
+ if (r0 === center && r1 !== center) through = r1;
6126
+ else if (r1 === center && r0 !== center) through = r0;
6127
+ if (!through) continue;
6128
+ const lines = m[4].match(/[A-Z][A-Z](?![A-Z])/gu) ?? [];
6129
+ const points = m[5].match(/[A-Z](?![A-Z])/gu) ?? [];
6130
+ if (lines.length === 0 || lines.length !== points.length) continue;
6131
+ const circ = `${center}_c`;
6132
+ const intents = [
6133
+ drawCircle(circ, "centerThrough", { center, through })
6134
+ ];
6135
+ let ok = true;
6136
+ const seen = /* @__PURE__ */ new Set();
6137
+ for (let i = 0; i < lines.length; i++) {
6138
+ const line = lines[i];
6139
+ const pt = points[i];
6140
+ if (!line.includes(through)) {
6141
+ ok = false;
6142
+ break;
6143
+ }
6144
+ if (line.includes(pt) || pt === through || pt === center || seen.has(pt)) {
6145
+ ok = false;
6146
+ break;
6147
+ }
6148
+ seen.add(pt);
6149
+ intents.push(
6150
+ addPoint(pt, { kind: "secondIntersection", line, circle: circ, other: through })
6151
+ );
6152
+ }
6153
+ if (!ok) continue;
6154
+ out.push({ ruleId: "circle-center-radius-cuts-sides", clauseIds: [c.id], intents });
6155
+ }
6156
+ }
6157
+ return out;
6158
+ }
6159
+ };
6160
+
5436
6161
  // src/stamps/geometry-2d/ai/rules/circumcircleNamedDistrib.ts
5437
- var PREFILTER33 = /lần\s*lượt\s+là\s+(?:đường\s*tròn\s+)?ngoại\s*tiếp/u;
6162
+ var PREFILTER34 = /lần\s*lượt\s+là\s+(?:đường\s*tròn\s+)?ngoại\s*tiếp/u;
5438
6163
  var RE9 = new RegExp(
5439
6164
  "([A-Z])\\s*,\\s*([A-Z])\\s+l\u1EA7n\\s*l\u01B0\u1EE3t\\s+l\xE0\\s+(?:\u0111\u01B0\u1EDDng\\s*tr\xF2n\\s+)?ngo\u1EA1i\\s*ti\u1EBFp\\s+(?:c\xE1c\\s+)?tam\\s*gi\xE1c\\s+([A-Z])([A-Z])([A-Z])(?![A-Z])\\s*,\\s*(?:tam\\s*gi\xE1c\\s+)?([A-Z])([A-Z])([A-Z])(?![A-Z])",
5440
6165
  "u"
@@ -5443,7 +6168,7 @@ var circumcircleNamedDistribRule = {
5443
6168
  id: "circumcircleNamedDistrib",
5444
6169
  priority: 67,
5445
6170
  languages: ["vi"],
5446
- patterns: [PREFILTER33],
6171
+ patterns: [PREFILTER34],
5447
6172
  match(ctx) {
5448
6173
  const out = [];
5449
6174
  for (const c of ctx.clauses) {
@@ -5468,7 +6193,7 @@ var circumcircleNamedDistribRule = {
5468
6193
  };
5469
6194
 
5470
6195
  // src/stamps/geometry-2d/ai/rules/circumcirclePairMeet.ts
5471
- var PREFILTER34 = /[Đđ]ường\s*tròn\s+ngoại\s*tiếp\s+(?:các\s+)?tam\s*giác\s+[A-Z]{3}\s*(?:,|\s+(?:và|cắt)\s+(?:lại\s+)?đường\s*tròn\s+ngoại\s*tiếp\s+tam\s*giác)/u;
6196
+ var PREFILTER35 = /[Đđ]ường\s*tròn\s+ngoại\s*tiếp\s+(?:các\s+)?tam\s*giác\s+[A-Z]{3}\s*(?:,|\s+(?:và|cắt)\s+(?:lại\s+)?đường\s*tròn\s+ngoại\s*tiếp\s+tam\s*giác)/u;
5472
6197
  var RE_CAT = new RegExp(
5473
6198
  "[\u0110\u0111]\u01B0\u1EDDng\\s*tr\xF2n\\s+ngo\u1EA1i\\s*ti\u1EBFp\\s+tam\\s*gi\xE1c\\s+([A-Z]{3})(?![A-Z])\\s+c\u1EAFt\\s+(?:l\u1EA1i\\s+)?[\u0110\u0111]\u01B0\u1EDDng\\s*tr\xF2n\\s+ngo\u1EA1i\\s*ti\u1EBFp\\s+tam\\s*gi\xE1c\\s+([A-Z]{3})(?![A-Z])\\s+(?:t\u1EA1i|\u1EDF)\\s+(?:\u0111i\u1EC3m\\s+(?:th\u1EE9\\s+hai\\s+)?)?(?:l\xE0\\s+)?([A-Z])(?![A-Z])(?:\\s+kh\xE1c\\s+([A-Z])(?![A-Z]))?",
5474
6199
  "u"
@@ -5490,22 +6215,27 @@ var RE_PAREN_BEFORE = new RegExp(
5490
6215
  `([A-Z])(?![A-Z])\\s+l\xE0\\s+giao\\s*\u0111i\u1EC3m\\s+(?:th\u1EE9\\s+hai\\s+)?(?:c\u1EE7a\\s+)?${PAR}\\s+v\xE0\\s+${PAR}`,
5491
6216
  "u"
5492
6217
  );
6218
+ var RE_PAREN_CAT = new RegExp(
6219
+ `${PAR}\\s+v\xE0\\s+${PAR}\\s+c\u1EAFt\\s+nhau\\s+(?:t\u1EA1i|\u1EDF)\\s+(?:\u0111i\u1EC3m\\s+(?:th\u1EE9\\s+hai\\s+)?)?([A-Z])(?![A-Z])(?:\\s+kh\xE1c\\s+([A-Z])(?![A-Z]))?`,
6220
+ "u"
6221
+ );
5493
6222
  var circumcirclePairMeetRule = {
5494
6223
  id: "circumcirclePairMeet",
5495
6224
  priority: 59,
5496
6225
  languages: ["vi"],
5497
- patterns: [PREFILTER34, new RegExp(`${PAR}\\s+v\xE0\\s+${PAR}`, "u")],
6226
+ patterns: [PREFILTER35, new RegExp(`${PAR}\\s+v\xE0\\s+${PAR}`, "u")],
5498
6227
  match(ctx) {
5499
6228
  const out = [];
5500
6229
  for (const c of ctx.clauses) {
5501
6230
  const pl = RE_PAREN_LA.exec(c.text);
5502
- const pb = pl ? null : RE_PAREN_BEFORE.exec(c.text);
5503
- if (pl || pb) {
5504
- const t1 = pl ? pl[1] : pb[2];
5505
- const t2 = pl ? pl[2] : pb[3];
5506
- const q2 = pl ? pl[3] : pb[1];
6231
+ const pc = pl ? null : RE_PAREN_CAT.exec(c.text);
6232
+ const pb = pl || pc ? null : RE_PAREN_BEFORE.exec(c.text);
6233
+ if (pl || pc || pb) {
6234
+ const t1 = pl ? pl[1] : pc ? pc[1] : pb[2];
6235
+ const t2 = pl ? pl[2] : pc ? pc[2] : pb[3];
6236
+ const q2 = pl ? pl[3] : pc ? pc[3] : pb[1];
5507
6237
  const tri12 = t1.split(""), tri22 = t2.split("");
5508
- const shared2 = pl && pl[4] || tri12.find((v) => tri22.includes(v));
6238
+ const shared2 = pl && pl[4] || pc && pc[4] || tri12.find((v) => tri22.includes(v));
5509
6239
  if (shared2 && !tri12.includes(q2) && !tri22.includes(q2)) {
5510
6240
  out.push({
5511
6241
  ruleId: "circumcirclePairMeet",
@@ -5543,7 +6273,7 @@ var circumcirclePairMeetRule = {
5543
6273
  };
5544
6274
 
5545
6275
  // src/stamps/geometry-2d/ai/rules/tangentCircleAtPoint.ts
5546
- var PREFILTER35 = /[Đđ]ường\s*tròn\s*\(?[A-Z]\)?[^.]{0,40}?tiếp\s*xúc[^.]{0,20}?tại\s+[A-Z]/u;
6276
+ var PREFILTER36 = /[Đđ]ường\s*tròn\s*\(?[A-Z]\)?[^.]{0,40}?tiếp\s*xúc[^.]{0,20}?tại\s+[A-Z]/u;
5547
6277
  var RE11 = new RegExp(
5548
6278
  "[\u0110\u0111]\u01B0\u1EDDng\\s*tr\xF2n\\s*\\(\\s*([A-Z])\\s*\\)\\s*(?:\u0111i\\s+)?qua\\s+([A-Z])\\s+(?:v\xE0\\s+)?ti\u1EBFp\\s*x\xFAc\\s+(?:v\u1EDBi\\s+)?(?:c\u1EA1nh\\s+|\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+)?([A-Z]{2})\\s+t\u1EA1i\\s+([A-Z])(?!\\p{L})[^.]{0,24}?c\u1EAFt\\s+(?:c\xE1c\\s+(?:c\u1EA1nh|\u0111o\u1EA1n)\\s+|c\u1EA1nh\\s+)?([A-Z]{2})\\s*(?:,|v\xE0)\\s*([A-Z]{2})(?!\\p{L})\\s+(?:l\u1EA7n\\s*l\u01B0\u1EE3t\\s+)?(?:\u1EDF|t\u1EA1i)\\s+([A-Z])\\s*(?:,|v\xE0)\\s*([A-Z])(?![A-Z])",
5549
6279
  "gu"
@@ -5556,7 +6286,7 @@ var tangentCircleAtPointRule = {
5556
6286
  id: "tangentCircleAtPoint",
5557
6287
  priority: 60,
5558
6288
  languages: ["vi"],
5559
- patterns: [PREFILTER35],
6289
+ patterns: [PREFILTER36],
5560
6290
  match(ctx) {
5561
6291
  const out = [];
5562
6292
  for (const c of ctx.clauses) {
@@ -5612,7 +6342,7 @@ var tangentCircleAtPointRule = {
5612
6342
  };
5613
6343
 
5614
6344
  // src/stamps/geometry-2d/ai/rules/externalBisectorCutsLines.ts
5615
- var PREFILTER36 = /phân\s*giác\s+ngoài\s+(?:của\s+)?góc\s+[A-Z]{3}[^.]{0,30}?cắt/u;
6345
+ var PREFILTER37 = /phân\s*giác\s+ngoài\s+(?:của\s+)?góc\s+[A-Z]{3}[^.]{0,30}?cắt/u;
5616
6346
  var RE12 = new RegExp(
5617
6347
  "[\u0110\u0111]\u01B0\u1EDDng\\s*th\u1EB3ng\\s+(?:ch\u1EE9a\\s+)?ph\xE2n\\s*gi\xE1c\\s+ngo\xE0i\\s+(?:c\u1EE7a\\s+)?g\xF3c\\s+([A-Z])([A-Z])([A-Z])(?![A-Z])[^.]{0,20}?c\u1EAFt\\s+(?:c\xE1c\\s+(?:c\u1EA1nh|\u0111\u01B0\u1EDDng\\s*th\u1EB3ng)\\s+)?([A-Z]{2})\\s*(?:,|v\xE0)\\s*([A-Z]{2})(?!\\p{L})\\s+(?:l\u1EA7n\\s*l\u01B0\u1EE3t\\s+)?(?:\u1EDF|t\u1EA1i)\\s+(?:c\xE1c\\s+\u0111i\u1EC3m\\s+)?([A-Z])\\s*(?:,|v\xE0)\\s*([A-Z])(?![A-Z])",
5618
6348
  "gu"
@@ -5621,7 +6351,7 @@ var externalBisectorCutsLinesRule = {
5621
6351
  id: "externalBisectorCutsLines",
5622
6352
  priority: 57,
5623
6353
  languages: ["vi"],
5624
- patterns: [PREFILTER36],
6354
+ patterns: [PREFILTER37],
5625
6355
  match(ctx) {
5626
6356
  const out = [];
5627
6357
  for (const c of ctx.clauses) {
@@ -5652,7 +6382,7 @@ var externalBisectorCutsLinesRule = {
5652
6382
  };
5653
6383
 
5654
6384
  // src/stamps/geometry-2d/ai/rules/tangentTwoSidesCircle.ts
5655
- var PREFILTER37 = /[Đđ]ường\s*tròn[^.]{0,30}?tiếp\s*xúc[^.]{0,40}?tiếp\s*xúc\s+trong/u;
6385
+ var PREFILTER38 = /[Đđ]ường\s*tròn[^.]{0,30}?tiếp\s*xúc[^.]{0,40}?tiếp\s*xúc\s+trong/u;
5656
6386
  var RE13 = new RegExp(
5657
6387
  "[\u0110\u0111]\u01B0\u1EDDng\\s*tr\xF2n\\s*\\(?\\s*([A-Z])\\s*\\)?\\s+ti\u1EBFp\\s*x\xFAc\\s+(?:v\u1EDBi\\s+)?(?:c\xE1c\\s+c\u1EA1nh\\s+|c\u1EA1nh\\s+)?([A-Z]{2})\\s*,\\s*([A-Z]{2})(?!\\p{L})\\s+(?:l\u1EA7n\\s*l\u01B0\u1EE3t\\s+)?(?:\u1EDF|t\u1EA1i)\\s+([A-Z])\\s*,\\s*([A-Z])(?!\\p{L})\\s+v\xE0\\s+ti\u1EBFp\\s*x\xFAc\\s+trong\\s+(?:v\u1EDBi\\s+)?\\(?\\s*[A-Z]\\s*\\)?\\s+(?:\u1EDF|t\u1EA1i)\\s+([A-Z])(?![A-Z])",
5658
6388
  "gu"
@@ -5665,7 +6395,7 @@ var tangentTwoSidesCircleRule = {
5665
6395
  id: "tangentTwoSidesCircle",
5666
6396
  priority: 61,
5667
6397
  languages: ["vi"],
5668
- patterns: [PREFILTER37],
6398
+ patterns: [PREFILTER38],
5669
6399
  match(ctx) {
5670
6400
  const out = [];
5671
6401
  for (const c of ctx.clauses) {
@@ -5701,14 +6431,22 @@ var tangentTwoSidesCircleRule = {
5701
6431
  };
5702
6432
 
5703
6433
  // src/stamps/geometry-2d/ai/rules/interiorPoint.ts
5704
- var PREFILTER38 = /(?:nằm\s+)?(?:bên\s+)?trong\s+(?:tam\s*giác|tứ\s*giác|hình|mặt\s*phẳng)/u;
5705
- var RE14 = /(?:[Gg]ọi\s+|[Ll]ấy\s+)?(?:điểm\s+)?([A-Z])(?!\p{L})\s+(?:là\s+)?(?:một\s+)?điểm\s+(?:nằm\s+)?(?:bên\s+)?trong\s+(?:mặt\s*phẳng\s+(?:chứa\s+)?)?(?:tam\s*giác|tứ\s*giác|hình\s+\S+)\s+[A-Z]{3,4}(?![A-Z])/u;
5706
- var RE22 = /(?:một\s+)?[Đđ]iểm\s+([A-Z])(?!\p{L})\s+nằm\s+(?:bên\s+)?trong\s+(?:tam\s*giác|tứ\s*giác|hình(?:\s+(?:chữ\s*nhật|vuông|thoi|bình\s*hành|thang|\S+))?)(?:\s+[A-Z]{3,4}(?![A-Z]))?/u;
6434
+ var SHAPE = "(?:(?:tam\\s*gi\xE1c|t\u1EE9\\s*gi\xE1c|\u0111\u01B0\u1EDDng\\s*tr\xF2n|h\xECnh(?:\\s+(?:ch\u1EEF\\s*nh\u1EADt|vu\xF4ng|thoi|b\xECnh\\s*h\xE0nh|thang))?)(?:\\s+(?:[A-Z]{3,4}(?![A-Z])|\\([^)]*\\)))?|\\([^)]*\\))";
6435
+ var QUAL = "(?:\\s+(?:b\u1EA5t\\s*k[i\xECy\u1EF3]|t[u\xF9][y\u1EF3]\\s*\xFD))?";
6436
+ var PREFILTER39 = /(?:nằm\s+)?(?:bên\s+)?trong\s+(?:tam\s*giác|tứ\s*giác|đường\s*tròn|hình|mặt\s*phẳng|\()/u;
6437
+ var RE14 = new RegExp(
6438
+ "(?:[Gg]\u1ECDi\\s+|[Ll]\u1EA5y\\s+)?(?:\u0111i\u1EC3m\\s+)?([A-Z])(?!\\p{L})\\s+(?:l\xE0\\s+)?(?:m\u1ED9t\\s+)?\u0111i\u1EC3m" + QUAL + "\\s+(?:n\u1EB1m\\s+)?(?:b\xEAn\\s+)?trong\\s+(?:m\u1EB7t\\s*ph\u1EB3ng\\s+(?:ch\u1EE9a\\s+)?)?" + SHAPE,
6439
+ "u"
6440
+ );
6441
+ var RE22 = new RegExp(
6442
+ "(?:m\u1ED9t\\s+)?[\u0110\u0111]i\u1EC3m\\s+([A-Z])(?!\\p{L})" + QUAL + "\\s+n\u1EB1m\\s+(?:b\xEAn\\s+)?trong\\s+" + SHAPE,
6443
+ "u"
6444
+ );
5707
6445
  var interiorPointRule = {
5708
6446
  id: "interiorPoint",
5709
6447
  priority: 30,
5710
6448
  languages: ["vi"],
5711
- patterns: [PREFILTER38],
6449
+ patterns: [PREFILTER39],
5712
6450
  match(ctx) {
5713
6451
  const out = [];
5714
6452
  for (const c of ctx.clauses) {
@@ -5726,7 +6464,7 @@ var interiorPointRule = {
5726
6464
  };
5727
6465
 
5728
6466
  // src/stamps/geometry-2d/ai/rules/parallelCutsTangentAt.ts
5729
- var PREFILTER39 = /[Đđ]ường\s*thẳng\s+qua\s+[A-Z][^.]{0,40}?tiếp\s*tuyến\s+tại\s+[A-Z]/u;
6467
+ var PREFILTER40 = /[Đđ]ường\s*thẳng\s+qua\s+[A-Z][^.]{0,40}?tiếp\s*tuyến\s+tại\s+[A-Z]/u;
5730
6468
  var CIRCLE_REF5 = /đường\s*tròn\s*\(\s*([A-Z])\s*\)|\(\s*([A-Z])\s*\)/u;
5731
6469
  var RE15 = new RegExp(
5732
6470
  "[\u0110\u0111]\u01B0\u1EDDng\\s*th\u1EB3ng\\s+qua\\s+([A-Z])(?!\\p{L})\\s+(song\\s*song|vu\xF4ng\\s*g\xF3c)\\s+(?:v\u1EDBi\\s+)?([A-Z]{2})(?!\\p{L})\\s+c\u1EAFt\\s+ti\u1EBFp\\s*tuy\u1EBFn\\s+t\u1EA1i\\s+([A-Z])(?!\\p{L})(?:\\s+c\u1EE7a\\s+[^.]{0,12}?)?\\s+(?:\u1EDF|t\u1EA1i)\\s+([A-Z])(?![A-Z])",
@@ -5740,7 +6478,7 @@ var parallelCutsTangentAtRule = {
5740
6478
  id: "parallelCutsTangentAt",
5741
6479
  priority: 52,
5742
6480
  languages: ["vi"],
5743
- patterns: [PREFILTER39],
6481
+ patterns: [PREFILTER40],
5744
6482
  match(ctx) {
5745
6483
  const circle = circleName4(ctx.problem);
5746
6484
  if (!circle) return [];
@@ -5771,7 +6509,7 @@ var parallelCutsTangentAtRule = {
5771
6509
  };
5772
6510
 
5773
6511
  // src/stamps/geometry-2d/ai/rules/inExCircleTangentBC.ts
5774
- var PREFILTER40 = /[Đđ]ường\s*tròn\s+(?:nội|bàng)\s*tiếp[^.]{0,60}?tâm\s+[A-Z][^.]{0,30}?tiếp\s*xúc/u;
6512
+ var PREFILTER41 = /[Đđ]ường\s*tròn\s+(?:nội|bàng)\s*tiếp[^.]{0,60}?tâm\s+[A-Z][^.]{0,30}?tiếp\s*xúc/u;
5775
6513
  var TANGENT_TAIL = "\\s*,?\\s*ti\u1EBFp\\s*x\xFAc\\s+(?:v\u1EDBi\\s+)?(?:c\u1EA1nh\\s+|\u0111o\u1EA1n(?:\\s+th\u1EB3ng)?\\s+)?([A-Z]{2})(?!\\p{L})\\s+(?:\u1EDF|t\u1EA1i)\\s+([A-Z])(?![A-Z])";
5776
6514
  var INCIRCLE = new RegExp(
5777
6515
  "[\u0110\u0111]\u01B0\u1EDDng\\s*tr\xF2n\\s+n\u1ED9i\\s*ti\u1EBFp\\s+tam\\s*gi\xE1c\\s+([A-Z]{3})(?![A-Z])[^.]{0,20}?t\xE2m\\s+([A-Z])(?!\\p{L})" + TANGENT_TAIL,
@@ -5785,7 +6523,7 @@ var inExCircleTangentBCRule = {
5785
6523
  id: "inExCircleTangentBC",
5786
6524
  priority: 69,
5787
6525
  languages: ["vi"],
5788
- patterns: [PREFILTER40],
6526
+ patterns: [PREFILTER41],
5789
6527
  match(ctx) {
5790
6528
  const out = [];
5791
6529
  for (const c of ctx.clauses) {
@@ -5827,7 +6565,7 @@ var inExCircleTangentBCRule = {
5827
6565
  };
5828
6566
 
5829
6567
  // src/stamps/geometry-2d/ai/rules/tangentPointsFromExt.ts
5830
- var PREFILTER41 = /[Tt]ừ\s+(?:điểm\s+)?[A-Z][^.]{0,60}?tiếp\s*tuyến\s+[A-Z]{2}\s*,\s*[A-Z]{2}/u;
6568
+ var PREFILTER42 = /[Tt]ừ\s+(?:điểm\s+)?[A-Z][^.]{0,60}?tiếp\s*tuyến\s+[A-Z]{2}\s*,\s*[A-Z]{2}/u;
5831
6569
  var RE16 = new RegExp(
5832
6570
  "[Tt]\u1EEB\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?!\\p{L})[^.]{0,60}?ti\u1EBFp\\s*tuy\u1EBFn\\s+([A-Z])([A-Z])\\s*,\\s*([A-Z])([A-Z])(?!\\p{L})",
5833
6571
  "gu"
@@ -5837,7 +6575,7 @@ var tangentPointsFromExtRule = {
5837
6575
  id: "tangentPointsFromExt",
5838
6576
  priority: 50,
5839
6577
  languages: ["vi"],
5840
- patterns: [PREFILTER41],
6578
+ patterns: [PREFILTER42],
5841
6579
  match(ctx) {
5842
6580
  const out = [];
5843
6581
  for (const c of ctx.clauses) {
@@ -5872,7 +6610,11 @@ var VN_FORM2 = new RegExp(
5872
6610
  "u"
5873
6611
  );
5874
6612
  var VN_REV = new RegExp(
5875
- String.raw`điểm\s+([A-Z])(?:['′]?)\s+(?:nằm\s+|ở\s+)?ngoài\s+đường\s*tròn\s*(?:tâm\s+)?\(?\s*([A-Z])?`,
6613
+ String.raw`điểm\s+([A-Z])(?:['′]?)[^.A-Z]{0,18}?ngoài\s+đường\s*tròn\s*(?:tâm\s+)?\(?\s*([A-Z])?`,
6614
+ "u"
6615
+ );
6616
+ var VN_REV_BARE = new RegExp(
6617
+ String.raw`(?<![\p{L}])([A-Z])(?:['′]?)[^.A-Z]{0,18}?ngoài\s+(?:đường\s*tròn\s*)?\(\s*([A-Z])\s*\)`,
5876
6618
  "u"
5877
6619
  );
5878
6620
  var RESOLVE_CENTER2 = /đường\s*tròn\s*(?:\(\s*)?(?:tâm\s+)?([A-Z])(?![A-Za-z])|\(\s*([A-Z])(?:\s*[;,]\s*[Rr])?\s*\)/u;
@@ -5901,17 +6643,23 @@ var circleExternalPointRule = {
5901
6643
  } else if (VN_METRIC.test(c.text)) {
5902
6644
  const mm = VN_METRIC.exec(c.text);
5903
6645
  const [ctr, r, pt, pair, d] = [mm[1], mm[2], mm[3], mm[4], mm[5]];
5904
- const norm2 = (s) => parseFloat(s.replace(",", "."));
5905
- if (pair.includes(ctr) && pair.includes(pt) && norm2(d) > norm2(r)) {
6646
+ const norm5 = (s) => parseFloat(s.replace(",", "."));
6647
+ if (pair.includes(ctr) && pair.includes(pt) && norm5(d) > norm5(r)) {
5906
6648
  center = ctr;
5907
6649
  ext = pt;
5908
6650
  }
5909
6651
  } else {
5910
6652
  const rv = VN_REV.exec(c.text);
5911
- if (!rv) continue;
5912
- ext = rv[1];
5913
- const rc = rv[2] ? void 0 : RESOLVE_CENTER2.exec(ctx.problem);
5914
- center = rv[2] ?? rc?.[1] ?? rc?.[2];
6653
+ if (rv) {
6654
+ ext = rv[1];
6655
+ const rc = rv[2] ? void 0 : RESOLVE_CENTER2.exec(ctx.problem);
6656
+ center = rv[2] ?? rc?.[1] ?? rc?.[2] ?? "O";
6657
+ } else {
6658
+ const rb = VN_REV_BARE.exec(c.text);
6659
+ if (!rb) continue;
6660
+ ext = rb[1];
6661
+ center = rb[2];
6662
+ }
5915
6663
  }
5916
6664
  if (!center || !ext || center === ext) continue;
5917
6665
  out.push({
@@ -5928,7 +6676,7 @@ var circleExternalPointRule = {
5928
6676
  };
5929
6677
 
5930
6678
  // src/stamps/geometry-2d/ai/rules/tangentNamedFromExt.ts
5931
- var PREFILTER42 = /(?:[Kk]ẻ|[Vv]ẽ)\s+(?:các\s+|hai\s+|2\s+)?tiếp\s*tuyến\s+(?:thứ\s+\S+\s+)?[A-Z]{2}|[Cc]ác\s+tiếp\s*tuyến\s+với/u;
6679
+ var PREFILTER43 = /(?:[Kk]ẻ|[Vv]ẽ)\s+(?:các\s+|hai\s+|2\s+)?tiếp\s*tuyến\s+(?:thứ\s+\S+\s+)?[A-Z]{2}|[Cc]ác\s+tiếp\s*tuyến\s+với|[Tt]iếp\s*tuyến\s+[A-Z]{2}\s*(?:,|và)\s*[A-Z]{2}|[Tt]ừ\s+[A-Z][^.;]{0,20}?(?:kẻ|vẽ)\s+(?:hai|2|các)\s+tiếp\s*tuyến|[A-Z]{2}\s*(?:,|và)\s*[A-Z]{2}\s+là\s+(?:hai\s+|2\s+|các\s+)?tiếp\s*tuyến/u;
5932
6680
  var TWO = new RegExp(
5933
6681
  "(?:[Kk]\u1EBB|[Vv]\u1EBD)\\s+(?:c\xE1c\\s+|hai\\s+|2\\s+)?ti\u1EBFp\\s*tuy\u1EBFn\\s+([A-Z])([A-Z])\\s*(?:,\\s*|v\xE0\\s+)([A-Z])([A-Z])(?!\\p{L})",
5934
6682
  "gu"
@@ -5941,6 +6689,18 @@ var FROM_TOUCH = new RegExp(
5941
6689
  "[Cc]\xE1c\\s+ti\u1EBFp\\s*tuy\u1EBFn\\s+v\u1EDBi\\s+\u0111\u01B0\u1EDDng\\s*tr\xF2n\\s+k\u1EBB\\s+t\u1EEB\\s+([A-Z])(?!\\p{L})[^.;]{0,40}?t\u1EA1i\\s+([A-Z])\\s*(?:,\\s*|v\xE0\\s+)([A-Z])(?!\\p{L})",
5942
6690
  "gu"
5943
6691
  );
6692
+ var FROM_DRAW_TOUCH = new RegExp(
6693
+ "[Tt]\u1EEB\\s+([A-Z])(?!\\p{L})[^.;]{0,20}?(?:k\u1EBB|v\u1EBD)\\s+(?:hai|2|c\xE1c)\\s+ti\u1EBFp\\s*tuy\u1EBFn[^.;]{0,40}?t\u1EA1i\\s+([A-Z])\\s*(?:,\\s*|v\xE0\\s+)([A-Z])(?!\\p{L})",
6694
+ "gu"
6695
+ );
6696
+ var TWO_BARE = new RegExp(
6697
+ "[Tt]i\u1EBFp\\s*tuy\u1EBFn\\s+([A-Z])([A-Z])\\s*(?:,\\s*|v\xE0\\s+)([A-Z])([A-Z])(?!\\p{L})",
6698
+ "gu"
6699
+ );
6700
+ var NAME_BEFORE_TWO = new RegExp(
6701
+ "([A-Z])([A-Z])\\s*(?:,\\s*|v\xE0\\s+)([A-Z])([A-Z])(?!\\p{L})\\s+l\xE0\\s+(?:hai\\s+|2\\s+|c\xE1c\\s+)?ti\u1EBFp\\s*tuy\u1EBFn",
6702
+ "gu"
6703
+ );
5944
6704
  var TOUCH_APPOS = new RegExp(
5945
6705
  "^(?:v\u1EDBi\\s+)?([A-Z])\\s*(?:,\\s*|v\xE0\\s+)([A-Z])\\s+l\xE0\\s+(?:c\xE1c\\s+|hai\\s+|2\\s+)?ti\u1EBFp\\s*\u0111i\u1EC3m",
5946
6706
  "u"
@@ -5950,7 +6710,7 @@ var tangentNamedFromExtRule = {
5950
6710
  id: "tangentNamedFromExt",
5951
6711
  priority: 50,
5952
6712
  languages: ["vi"],
5953
- patterns: [PREFILTER42],
6713
+ patterns: [PREFILTER43],
5954
6714
  match(ctx) {
5955
6715
  const par = PAREN_CIRCLE2.exec(ctx.problem);
5956
6716
  const circle = par ? par[1] : "O";
@@ -5979,14 +6739,43 @@ var tangentNamedFromExtRule = {
5979
6739
  });
5980
6740
  }
5981
6741
  }
6742
+ for (const c of ctx.clauses) {
6743
+ if (twoClauses.has(c.id)) continue;
6744
+ TWO_BARE.lastIndex = 0;
6745
+ NAME_BEFORE_TWO.lastIndex = 0;
6746
+ const hits = [...c.text.matchAll(TWO_BARE), ...c.text.matchAll(NAME_BEFORE_TWO)];
6747
+ for (const m of hits) {
6748
+ if (twoClauses.has(c.id)) continue;
6749
+ const ext = m[1];
6750
+ if (m[3] !== ext) continue;
6751
+ const p = m[2];
6752
+ const q = m[4];
6753
+ if (p === q || p === ext || q === ext) continue;
6754
+ twoClauses.add(c.id);
6755
+ whichOf.set(ext, 1);
6756
+ out.push({
6757
+ ruleId: "tangentNamedFromExt",
6758
+ clauseIds: [c.id],
6759
+ intents: [
6760
+ addPoint(p, { kind: "tangentPoint", from: ext, circle, which: 0 }),
6761
+ addPoint(q, { kind: "tangentPoint", from: ext, circle, which: 1 }),
6762
+ connect(ext, p, "segment"),
6763
+ connect(ext, q, "segment")
6764
+ ]
6765
+ });
6766
+ }
6767
+ }
5982
6768
  for (const c of ctx.clauses) {
5983
6769
  if (twoClauses.has(c.id)) continue;
5984
6770
  FROM_TOUCH.lastIndex = 0;
5985
- for (const m of c.text.matchAll(FROM_TOUCH)) {
6771
+ FROM_DRAW_TOUCH.lastIndex = 0;
6772
+ const hits = [...c.text.matchAll(FROM_TOUCH), ...c.text.matchAll(FROM_DRAW_TOUCH)];
6773
+ for (const m of hits) {
5986
6774
  const ext = m[1];
5987
6775
  const p = m[2];
5988
6776
  const q = m[3];
5989
6777
  if (p === q || p === ext || q === ext) continue;
6778
+ if (twoClauses.has(c.id)) continue;
5990
6779
  twoClauses.add(c.id);
5991
6780
  whichOf.set(ext, 1);
5992
6781
  out.push({
@@ -6040,11 +6829,16 @@ var tangentNamedFromExtRule = {
6040
6829
  // src/stamps/geometry-2d/ai/rules/secant.ts
6041
6830
  var CAT_TUYEN = /cát\s*tuyến\s+([A-Z])([A-Z])([A-Z])(?![A-Z])/gu;
6042
6831
  var LINE_THROUGH = new RegExp(
6043
- String.raw`[Qq]ua\s+(?:điểm\s+)?([A-Z])(?!\p{L})[^.]{0,30}?cắt\s+(?:đường\s*tròn\s*)?(?:\(\s*([A-Z])\s*\)\s*)?(?:tại|ở)\s+(?:hai\s+|2\s+)?(?:điểm\s+)?([A-Z])\s*(?:và|,)\s*([A-Z])(?![A-Z])`,
6832
+ String.raw`[Qq]ua\s+(?:điểm\s+)?([A-Z])(?!\p{L})[^.]{0,30}?cắt\s+(?:lại\s+)?(?:nửa\s+)?(?:đường\s*tròn\s*)?(?:\(\s*([A-Z])\s*\)\s*|[A-Z]\s+)?(?:tại|ở)\s+(?:hai\s+|2\s+)?(?:điểm\s+)?([A-Z])\s*(?:và|,)\s*([A-Z])(?![A-Z])`,
6833
+ "gu"
6834
+ );
6835
+ var CAT_CUTS = new RegExp(
6836
+ String.raw`cát\s*tuyến\s+(?:[a-z][a-z0-9]?\s+)?cắt\s+(?:lại\s+)?(?:nửa\s+)?(?:đường\s*tròn\s*)?(?:\(\s*([A-Z])\s*\)\s*|[A-Z]\s+)?(?:tại|ở)\s+(?:hai\s+|2\s+)?(?:điểm\s+)?([A-Z])\s*(?:và|,)\s*([A-Z])(?![A-Z])`,
6044
6837
  "gu"
6045
6838
  );
6046
6839
  var PAREN_CIRCLE3 = /\(\s*([A-Z])\s*\)/u;
6047
- var PREFILTER43 = /cát\s*tuyến|[Qq]ua\s+(?:điểm\s+)?[A-Z][^.]{0,30}?cắt/u;
6840
+ var EXT_RESOLVE = /([A-Z])(?!\p{L})[^.A-Z]{0,25}?(?:ở\s+|nằm\s+)?ngoài\s+(?:\(|nửa|đường\s*tròn)/u;
6841
+ var PREFILTER44 = /cát\s*tuyến|[Qq]ua\s+(?:điểm\s+)?[A-Z][^.]{0,30}?cắt/u;
6048
6842
  function secantIntents(ext, near, far, circle, theta) {
6049
6843
  if ((/* @__PURE__ */ new Set([ext, near, far])).size !== 3) return null;
6050
6844
  const line = ext + near;
@@ -6058,13 +6852,26 @@ var secantRule = {
6058
6852
  id: "secant",
6059
6853
  priority: 58,
6060
6854
  languages: ["vi"],
6061
- patterns: [PREFILTER43],
6855
+ patterns: [PREFILTER44],
6062
6856
  match(ctx) {
6063
6857
  const par = PAREN_CIRCLE3.exec(ctx.problem);
6064
6858
  const fallbackCircle = par ? par[1] : "O";
6859
+ const extM = EXT_RESOLVE.exec(ctx.problem);
6860
+ const resolvedExt = extM ? extM[1] : void 0;
6065
6861
  const out = [];
6066
6862
  let theta = 2.4;
6067
6863
  for (const c of ctx.clauses) {
6864
+ if (resolvedExt) {
6865
+ CAT_CUTS.lastIndex = 0;
6866
+ for (const m of c.text.matchAll(CAT_CUTS)) {
6867
+ const circle = m[1] ?? fallbackCircle;
6868
+ const intents = secantIntents(resolvedExt, m[2], m[3], circle, theta);
6869
+ if (intents) {
6870
+ out.push({ ruleId: "secant", clauseIds: [c.id], intents });
6871
+ theta += 0.7;
6872
+ }
6873
+ }
6874
+ }
6068
6875
  CAT_TUYEN.lastIndex = 0;
6069
6876
  for (const m of c.text.matchAll(CAT_TUYEN)) {
6070
6877
  const intents = secantIntents(m[1], m[2], m[3], fallbackCircle, theta);
@@ -6089,14 +6896,21 @@ var secantRule = {
6089
6896
 
6090
6897
  // src/stamps/geometry-2d/ai/rules/oppositeRayPoint.ts
6091
6898
  var RE17 = new RegExp(
6092
- String.raw`[Tt]rên\s+tia\s+đối\s+(?:của\s+)?tia\s+([A-Z])([A-Z])(?![A-Z])[^.]{0,20}?lấy\s+(?:một\s+)?(?:điểm\s+)?([A-Z])(?![A-Z])`,
6899
+ String.raw`[Tt]rên\s+tia\s+đối\s+(?:của\s+)?(?:tia\s+)?([A-Z])([A-Z])(?![A-Z])[^.]{0,20}?lấy\s+(?:một\s+)?(?:điểm\s+)?([A-Z])(?![A-Z])`,
6093
6900
  "gu"
6094
6901
  );
6095
6902
  var RE_NAME_FIRST = new RegExp(
6096
- String.raw`[Ll]ấy\s+(?:điểm\s+)?([A-Z])(?![A-Z])\s+trên\s+tia\s+đối\s+(?:của\s+)?tia\s+([A-Z])([A-Z])(?![A-Z])`,
6903
+ String.raw`[Ll]ấy\s+(?:điểm\s+)?([A-Z])(?![A-Z])\s+trên\s+tia\s+đối\s+(?:của\s+)?(?:tia\s+)?([A-Z])([A-Z])(?![A-Z])`,
6097
6904
  "gu"
6098
6905
  );
6906
+ var RE_DISTRIB = new RegExp(
6907
+ String.raw`[Ll]ấy\s+(?:các\s+)?(?:điểm\s+)?((?:[A-Z]\s*(?:,|và)\s*)+[A-Z])(?![A-Z])\s+(?:lần\s*lượt\s+|(?:theo\s+)?thứ\s+tự\s+)?(?:thuộc|trên|nằm\s+trên)\s+(?:các\s+)?tia\s+đối\s+(?:của\s+)?(?:các\s+)?(?:tia\s+)?((?:[A-Z]{2}\s*(?:,|và)\s*)+[A-Z]{2})(?![A-Z])`,
6908
+ "u"
6909
+ );
6099
6910
  var CANON = 2.5;
6911
+ function splitNames2(blob) {
6912
+ return blob.split(/\s*,\s*|\s+và\s+/u).map((s) => s.trim()).filter(Boolean);
6913
+ }
6100
6914
  var oppositeRayPointRule = {
6101
6915
  id: "oppositeRayPoint",
6102
6916
  priority: 56,
@@ -6105,6 +6919,35 @@ var oppositeRayPointRule = {
6105
6919
  match(ctx) {
6106
6920
  const out = [];
6107
6921
  for (const c of ctx.clauses) {
6922
+ const dm = RE_DISTRIB.exec(c.text);
6923
+ if (dm) {
6924
+ const names = splitNames2(dm[1]);
6925
+ const rays = splitNames2(dm[2]);
6926
+ if (names.length >= 2 && names.length === rays.length) {
6927
+ const intents = [];
6928
+ let valid3 = true;
6929
+ for (let i = 0; i < names.length; i++) {
6930
+ const a = rays[i][0];
6931
+ const b = rays[i][1];
6932
+ if ((/* @__PURE__ */ new Set([a, b, names[i]])).size !== 3) {
6933
+ valid3 = false;
6934
+ break;
6935
+ }
6936
+ intents.push(
6937
+ addPoint(names[i], {
6938
+ kind: "pointAtDistance",
6939
+ from: b,
6940
+ through: a,
6941
+ distance: { kind: "literal", value: CANON }
6942
+ })
6943
+ );
6944
+ }
6945
+ if (valid3) {
6946
+ out.push({ ruleId: "oppositeRayPoint", clauseIds: [c.id], intents });
6947
+ continue;
6948
+ }
6949
+ }
6950
+ }
6108
6951
  RE17.lastIndex = 0;
6109
6952
  for (const m of c.text.matchAll(RE17)) {
6110
6953
  const a = m[1];
@@ -6160,7 +7003,7 @@ var CENTER3 = /\(\s*([A-Z])\s*[;,)]|tâm\s+([A-Z])(?![A-Za-z])/u;
6160
7003
  function appearsBefore(problem, chordText, d) {
6161
7004
  const pos = problem.indexOf(chordText);
6162
7005
  if (pos < 0) return true;
6163
- return new RegExp(`(?<![A-Za-z])${d}(?![a-z])`, "u").test(problem.slice(0, pos));
7006
+ return new RegExp(`(?<![A-Za-z])${escapeRe(d)}(?![a-z])`, "u").test(problem.slice(0, pos));
6164
7007
  }
6165
7008
  var perpChordAtFootRule = {
6166
7009
  id: "perpChordAtFoot",
@@ -6242,12 +7085,12 @@ var diameterCircleSecantRule = {
6242
7085
  CUTS.lastIndex = 0;
6243
7086
  for (const m of c.text.matchAll(CUTS)) {
6244
7087
  const line = m[1];
6245
- const z46 = m[2];
6246
- if (line.includes(z46) || onCircle.has(z46)) continue;
7088
+ const z47 = m[2];
7089
+ if (line.includes(z47) || onCircle.has(z47)) continue;
6247
7090
  const other = onCircle.has(line[0]) ? line[0] : onCircle.has(line[1]) ? line[1] : void 0;
6248
7091
  if (!other) continue;
6249
- onCircle.add(z46);
6250
- const intent = addPoint(z46, { kind: "secondIntersection", line, circle, other });
7092
+ onCircle.add(z47);
7093
+ const intent = addPoint(z47, { kind: "secondIntersection", line, circle, other });
6251
7094
  out.push({ ruleId: "diameterCircleSecant", clauseIds: [c.id], intents: [intent] });
6252
7095
  }
6253
7096
  }
@@ -6256,7 +7099,7 @@ var diameterCircleSecantRule = {
6256
7099
  };
6257
7100
 
6258
7101
  // src/stamps/geometry-2d/ai/rules/tangentsAtMeet.ts
6259
- var PREFILTER44 = /[Tt]iếp\s*tuyến[^.]{0,40}?tại\s+[A-Z][^.]{0,30}?(?:và|,)\s*[A-Z][^.]{0,30}?cắt\s+nhau/u;
7102
+ var PREFILTER45 = /[Tt]iếp\s*tuyến[^.]{0,40}?tại\s+[A-Z][^.]{0,30}?(?:và|,)\s*[A-Z][^.]{0,30}?cắt\s+nhau/u;
6260
7103
  var CIRCLE_REF6 = /đường\s*tròn\s*(?:tâm\s+)?\(?\s*([A-ZωΩ])(?:\s*[;,]\s*[Rr])?\s*\)?|\(\s*([A-ZωΩ])\s*\)/u;
6261
7104
  var RE19 = new RegExp(
6262
7105
  "[Tt]i\u1EBFp\\s*tuy\u1EBFn\\s+(?:c\u1EE7a\\s+[^.]{0,14}?\\s+)?t\u1EA1i\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])\\s*(?:v\xE0|,)\\s*([A-Z])(?!\\p{L})(?:\\s+c\u1EE7a\\s+[^.]{0,14}?)?\\s+c\u1EAFt\\s+nhau\\s+(?:t\u1EA1i|\u1EDF)\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?![A-Z])",
@@ -6273,7 +7116,7 @@ var tangentsAtMeetRule = {
6273
7116
  priority: 55,
6274
7117
  // trên intersection(45) — 2 tiếp tuyến + giao trong CÙNG match nên tự đủ thứ tự
6275
7118
  languages: ["vi"],
6276
- patterns: [PREFILTER44],
7119
+ patterns: [PREFILTER45],
6277
7120
  match(ctx) {
6278
7121
  const circle = circleName5(ctx.problem);
6279
7122
  if (!circle) return [];
@@ -6301,7 +7144,7 @@ var tangentsAtMeetRule = {
6301
7144
  };
6302
7145
 
6303
7146
  // src/stamps/geometry-2d/ai/rules/angleBisectorCutsSideCircle.ts
6304
- var PREFILTER45 = /phân\s*giác[^.]{0,40}?cắt/u;
7147
+ var PREFILTER46 = /phân\s*giác[^.]{0,40}?cắt/u;
6305
7148
  var FOOT = new RegExp(
6306
7149
  "ph\xE2n\\s*gi\xE1c\\s+(?:trong\\s+)?(?:c\u1EE7a\\s+)?g\xF3c\\s+([A-Z])([A-Z])?([A-Z])?(?![A-Z])[^.]{0,20}?c\u1EAFt\\s+(?:c\u1EA1nh\\s+)?([A-Z])([A-Z])(?![A-Z])\\s+(?:t\u1EA1i|\u1EDF)\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?![A-Z])",
6307
7150
  "u"
@@ -6316,7 +7159,7 @@ var angleBisectorCutsSideCircleRule = {
6316
7159
  priority: 48,
6317
7160
  // sau triangle/circle/cevian; trước intersection(45)
6318
7161
  languages: ["vi"],
6319
- patterns: [PREFILTER45],
7162
+ patterns: [PREFILTER46],
6320
7163
  match(ctx) {
6321
7164
  const out = [];
6322
7165
  for (const c of ctx.clauses) {
@@ -6362,7 +7205,7 @@ var angleBisectorCutsSideCircleRule = {
6362
7205
  };
6363
7206
 
6364
7207
  // src/stamps/geometry-2d/ai/rules/circleCutsArcSecond.ts
6365
- var PREFILTER46 = /đường\s*tròn\s+đường\s*kính\s+[A-Z]{2}[^.]{0,40}?cắt\s+(?:cung|(?:nửa\s+)?đường\s*tròn)/u;
7208
+ var PREFILTER47 = /đường\s*tròn\s+đường\s*kính\s+[A-Z]{2}[^.]{0,40}?cắt\s+(?:cung|(?:nửa\s+)?đường\s*tròn)/u;
6366
7209
  var CUTS_ARC = new RegExp(
6367
7210
  "\u0111\u01B0\u1EDDng\\s*tr\xF2n\\s+\u0111\u01B0\u1EDDng\\s*k\xEDnh\\s+([A-Z])([A-Z])(?![A-Z])[^.]{0,20}?c\u1EAFt\\s+cung\\s+(?:nh\u1ECF\\s+|l\u1EDBn\\s+)?([A-Z])([A-Z])(?![A-Z])[^.]{0,24}?(?:t\u1EA1i|\u1EDF)\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?![A-Z])(?:\\s+kh\xE1c\\s+([A-Z])(?![A-Z]))?",
6368
7211
  "u"
@@ -6377,7 +7220,7 @@ var circleCutsArcSecondRule = {
6377
7220
  priority: 45,
6378
7221
  // sau mọi rule dựng đường tròn + điểm chung
6379
7222
  languages: ["vi"],
6380
- patterns: [PREFILTER46],
7223
+ patterns: [PREFILTER47],
6381
7224
  match(ctx) {
6382
7225
  const out = [];
6383
7226
  for (const c of ctx.clauses) {
@@ -6416,7 +7259,7 @@ var circleCutsArcSecondRule = {
6416
7259
  };
6417
7260
 
6418
7261
  // src/stamps/geometry-2d/ai/rules/angleBisectorCutsCircleLine.ts
6419
- var PREFILTER47 = /[Pp]hân\s*giác[^.]{0,40}?cắt\s+(?:đường\s*tròn|\()/u;
7262
+ var PREFILTER48 = /[Pp]hân\s*giác[^.]{0,40}?cắt\s+(?:đường\s*tròn|\()/u;
6420
7263
  var RE20 = new RegExp(
6421
7264
  "[Pp]h\xE2n\\s*gi\xE1c\\s+(?:trong\\s+)?(?:c\u1EE7a\\s+)?g\xF3c\\s+([A-Z])([A-Z])([A-Z])(?![A-Z])\\s+c\u1EAFt\\s+(?:\u0111\u01B0\u1EDDng\\s*tr\xF2n\\s*)?(?:\\(\\s*([A-Z])(?:['\u2032]?)\\s*\\))?[^.]{0,16}?(?:t\u1EA1i|\u1EDF)\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?![A-Z])(?:\\s+kh\xE1c\\s+[A-Z](?![A-Z]))?(?:\\s*,?\\s*v\xE0\\s+(?:c\u1EAFt\\s+)?(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|c\u1EA1nh\\s+|\u0111o\u1EA1n\\s+)?([A-Z]{2})(?![A-Z])\\s+(?:t\u1EA1i|\u1EDF)\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?![A-Z]))?",
6422
7265
  "u"
@@ -6429,7 +7272,7 @@ var angleBisectorCutsCircleLineRule = {
6429
7272
  // perpFoot 65 dựng tia/C/H trước).
6430
7273
  priority: 49,
6431
7274
  languages: ["vi"],
6432
- patterns: [PREFILTER47],
7275
+ patterns: [PREFILTER48],
6433
7276
  match(ctx) {
6434
7277
  const out = [];
6435
7278
  for (const c of ctx.clauses) {
@@ -6461,17 +7304,34 @@ var angleBisectorCutsCircleLineRule = {
6461
7304
  };
6462
7305
 
6463
7306
  // src/stamps/geometry-2d/ai/rules/tangentLineNamedAtPoint.ts
6464
- var PREFILTER48 = /(?:trên|thuộc)\s+\(\s*[A-Z][^)]*\)[^.]{0,24}?kẻ\s+tiếp\s*tuyến\s+[a-z]/u;
7307
+ var PREFILTER49 = /(?:trên|thuộc)\s+\(\s*[A-Z][^)]*\)[^.]{0,24}?kẻ\s+tiếp\s*tuyến\s+[a-z]|tiếp\s*tuyến\s+[a-z][0-9]?\s+(?:của|với)\s+|(?:[Qq]ua|[Tt]ừ)\s+[A-Z]\s+(?:vẽ|kẻ)\s+tiếp\s*tuyến\s+[a-z]/u;
6465
7308
  var RE21 = new RegExp(
6466
7309
  "(?:t\u1EEB\\s+|qua\\s+)?(?:m\u1ED9t\\s+)?\u0111i\u1EC3m\\s+([A-Z])(?![A-Z])\\s+(?:tr\xEAn|thu\u1ED9c)\\s+\\(\\s*([A-Z])(?:['\u2032]?)\\s*\\)[^.]{0,24}?k\u1EBB\\s+ti\u1EBFp\\s*tuy\u1EBFn\\s+([a-z][0-9]?)(?!\\p{L})",
6467
7310
  "gu"
6468
7311
  );
7312
+ var RE_NAMED_OF_AT = new RegExp(
7313
+ "(?:[VvKk]\u1EBD|[Kk]\u1EBB|[Dd]\u1EF1ng)?\\s*ti\u1EBFp\\s*tuy\u1EBFn\\s+([a-z][0-9]?)(?!\\p{L})\\s+(?:c\u1EE7a|v\u1EDBi)\\s+(?:\u0111\u01B0\u1EDDng\\s*tr\xF2n\\s*)?\\(\\s*([A-Z])(?:['\u2032]?)\\s*\\)\\s+t\u1EA1i\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?![A-Z])",
7314
+ "gu"
7315
+ );
7316
+ var RE_THROUGH_NAMED = new RegExp(
7317
+ "(?:[Qq]ua|[Tt]\u1EEB)\\s+([A-Z])(?![A-Z])\\s+(?:v\u1EBD|k\u1EBB)\\s+ti\u1EBFp\\s*tuy\u1EBFn\\s+([a-z]{1,2}[0-9]?)(?!\\p{L})",
7318
+ "gu"
7319
+ );
7320
+ var PAREN_CIRCLE4 = /\(\s*([A-Z])(?:\s*[;,]\s*[Rr])?\s*\)|đường\s*tròn\s+tâm\s+([A-Z])(?![A-Za-z])/u;
7321
+ function circleDslName(problem, center) {
7322
+ return /đường\s*kính/u.test(problem) ? `${center}_c` : center;
7323
+ }
7324
+ function resolveProblemCircle(problem) {
7325
+ const m = PAREN_CIRCLE4.exec(problem);
7326
+ const center = m?.[1] ?? m?.[2];
7327
+ return center ? circleDslName(problem, center) : void 0;
7328
+ }
6469
7329
  var tangentLineNamedAtPointRule = {
6470
7330
  id: "tangentLineNamedAtPoint",
6471
7331
  priority: 63,
6472
7332
  // cao như tangentAt — đường d phải dựng trước điểm-trên-d
6473
7333
  languages: ["vi"],
6474
- patterns: [PREFILTER48],
7334
+ patterns: [PREFILTER49],
6475
7335
  match(ctx) {
6476
7336
  const out = [];
6477
7337
  for (const c of ctx.clauses) {
@@ -6486,6 +7346,30 @@ var tangentLineNamedAtPointRule = {
6486
7346
  intents: [drawLine(line, "tangentAt", { through, circle })]
6487
7347
  });
6488
7348
  }
7349
+ RE_NAMED_OF_AT.lastIndex = 0;
7350
+ for (const m of c.text.matchAll(RE_NAMED_OF_AT)) {
7351
+ const line = m[1];
7352
+ const circle = circleDslName(ctx.problem, m[2]);
7353
+ const through = m[3];
7354
+ out.push({
7355
+ ruleId: "tangentLineNamedAtPoint",
7356
+ clauseIds: [c.id],
7357
+ intents: [drawLine(line, "tangentAt", { through, circle })]
7358
+ });
7359
+ }
7360
+ RE_THROUGH_NAMED.lastIndex = 0;
7361
+ const probCircle = resolveProblemCircle(ctx.problem);
7362
+ if (probCircle) {
7363
+ for (const m of c.text.matchAll(RE_THROUGH_NAMED)) {
7364
+ const through = m[1];
7365
+ const line = m[2];
7366
+ out.push({
7367
+ ruleId: "tangentLineNamedAtPoint",
7368
+ clauseIds: [c.id],
7369
+ intents: [drawLine(line, "tangentAt", { through, circle: probCircle })]
7370
+ });
7371
+ }
7372
+ }
6489
7373
  }
6490
7374
  return out;
6491
7375
  }
@@ -6531,13 +7415,13 @@ var givenDiameterCircleRule = {
6531
7415
 
6532
7416
  // src/stamps/geometry-2d/ai/rules/multiDiameterCircles.ts
6533
7417
  var RE23 = /đường\s*kính\s+(?:theo\s+thứ\s+tự\s+)?(?:là\s+)?((?:[A-Z]{2}\s*,\s*)+[A-Z]{2})(?![A-Z])[^.]{0,30}?tâm\s+(?:theo\s+thứ\s+tự\s+)?(?:là\s+)?((?:[A-Z](?:['′])?\s*,\s*)+[A-Z](?:['′])?)(?![A-Z])/u;
6534
- var PREFILTER49 = /đường\s*kính[^.]{0,40}?tâm/u;
7418
+ var PREFILTER50 = /đường\s*kính[^.]{0,40}?tâm/u;
6535
7419
  var multiDiameterCirclesRule = {
6536
7420
  id: "multiDiameterCircles",
6537
7421
  priority: 67,
6538
7422
  // như circleDiameter — dựng đầu mút + tâm trước điểm phái sinh
6539
7423
  languages: ["vi"],
6540
- patterns: [PREFILTER49],
7424
+ patterns: [PREFILTER50],
6541
7425
  match(ctx) {
6542
7426
  for (const c of ctx.clauses) {
6543
7427
  const m = RE23.exec(c.text);
@@ -6589,12 +7473,12 @@ function diameterMap(problem) {
6589
7473
  }
6590
7474
  return map;
6591
7475
  }
6592
- var PREFILTER50 = /giao\s*điểm\s+(?:của\s+)?[A-Z]{2}\s*,\s*[A-Z]{2}[^.]{0,30}?(?:với|cắt)\s+(?:các\s+)?(?:(?:nửa\s+)?đường\s*tròn\s*)?\(/u;
7476
+ var PREFILTER51 = /giao\s*điểm\s+(?:của\s+)?[A-Z]{2}\s*,\s*[A-Z]{2}[^.]{0,30}?(?:với|cắt)\s+(?:các\s+)?(?:(?:nửa\s+)?đường\s*tròn\s*)?\(/u;
6593
7477
  var cutCirclesDistribRule = {
6594
7478
  id: "cutCirclesDistrib",
6595
7479
  priority: 45,
6596
7480
  languages: ["vi"],
6597
- patterns: [PREFILTER50],
7481
+ patterns: [PREFILTER51],
6598
7482
  match(ctx) {
6599
7483
  const map = diameterMap(ctx.problem);
6600
7484
  const out = [];
@@ -6622,6 +7506,7 @@ var cutCirclesDistribRule = {
6622
7506
  // src/stamps/geometry-2d/ai/rules/givenNamedCircle.ts
6623
7507
  var BARE_PAREN2 = /\(\s*([A-Z])\s*\)/gu;
6624
7508
  var HAS_POINTS_ON = /(?:trên|thuộc)\s+(?:nửa\s+)?(?:đường\s*tròn|cung|\(\s*[A-Z]\s*\))/u;
7509
+ var HAS_TANGENT_OR_SECANT = /tiếp\s*tuyến|cát\s*tuyến/u;
6625
7510
  var givenNamedCircleRule = {
6626
7511
  id: "givenNamedCircle",
6627
7512
  priority: 74,
@@ -6629,7 +7514,7 @@ var givenNamedCircleRule = {
6629
7514
  languages: ["vi"],
6630
7515
  patterns: [/\(\s*[A-Z]\s*\)/u],
6631
7516
  match(ctx) {
6632
- if (!HAS_POINTS_ON.test(ctx.problem)) return [];
7517
+ if (!HAS_POINTS_ON.test(ctx.problem) && !HAS_TANGENT_OR_SECANT.test(ctx.problem)) return [];
6633
7518
  const out = [];
6634
7519
  const seen = /* @__PURE__ */ new Set();
6635
7520
  BARE_PAREN2.lastIndex = 0;
@@ -6637,12 +7522,14 @@ var givenNamedCircleRule = {
6637
7522
  const center = m[1];
6638
7523
  if (seen.has(center)) continue;
6639
7524
  const c = center;
6640
- if (new RegExp(`\\(\\s*${c}\\s*[;,]`, "u").test(ctx.problem)) continue;
6641
- if (new RegExp(`\\(\\s*${c}\\s*\\)[^.]{0,30}?\u0111\u01B0\u1EDDng\\s*k\xEDnh|\u0111\u01B0\u1EDDng\\s*k\xEDnh[^.]{0,30}?\\(\\s*${c}\\s*\\)`, "u").test(ctx.problem)) continue;
6642
- if (new RegExp(`(?:n\u1ED9i|ngo\u1EA1i)\\s*ti\u1EBFp[^.]{0,30}?\\(\\s*${c}\\s*\\)|\\(\\s*${c}\\s*\\)[^.]{0,30}?(?:n\u1ED9i|ngo\u1EA1i)\\s*ti\u1EBFp`, "u").test(ctx.problem)) continue;
6643
- if (!new RegExp(`${CIRCLE_KW}\\s*\\(\\s*${c}\\s*\\)`, "u").test(ctx.problem)) continue;
7525
+ if (new RegExp(`\\(\\s*${escapeRe(c)}\\s*[;,]`, "u").test(ctx.problem)) continue;
7526
+ if (new RegExp(`\\(\\s*${escapeRe(c)}\\s*\\)[^.]{0,30}?\u0111\u01B0\u1EDDng\\s*k\xEDnh|\u0111\u01B0\u1EDDng\\s*k\xEDnh[^.]{0,30}?\\(\\s*${escapeRe(c)}\\s*\\)`, "u").test(ctx.problem)) continue;
7527
+ if (new RegExp(`(?:n\u1ED9i|ngo\u1EA1i)\\s*ti\u1EBFp[^.]{0,30}?\\(\\s*${escapeRe(c)}\\s*\\)|\\(\\s*${escapeRe(c)}\\s*\\)[^.]{0,30}?(?:n\u1ED9i|ngo\u1EA1i)\\s*ti\u1EBFp`, "u").test(ctx.problem)) continue;
7528
+ const isNamedCircle = new RegExp(`${CIRCLE_KW}\\s*\\(\\s*${escapeRe(c)}\\s*\\)`, "u").test(ctx.problem);
7529
+ const isOpenedBare = new RegExp(`^[Cc]ho\\s+\\(\\s*${escapeRe(c)}\\s*\\)`, "u").test(ctx.problem.trimStart());
7530
+ if (!isNamedCircle && !isOpenedBare) continue;
6644
7531
  seen.add(center);
6645
- const owner = ctx.clauses.find((cl) => new RegExp(`\\(\\s*${c}\\s*\\)`, "u").test(cl.text));
7532
+ const owner = ctx.clauses.find((cl) => new RegExp(`\\(\\s*${escapeRe(c)}\\s*\\)`, "u").test(cl.text));
6646
7533
  out.push({
6647
7534
  ruleId: "givenNamedCircle",
6648
7535
  clauseIds: owner ? [owner.id] : [],
@@ -6654,12 +7541,16 @@ var givenNamedCircleRule = {
6654
7541
  };
6655
7542
 
6656
7543
  // src/stamps/geometry-2d/ai/rules/twoPerpLinesMeet.ts
6657
- var PREFILTER51 = /[Đđ]ường\s*thẳng\s+qua\s+[A-Z][^.]{0,30}?(?:vuông\s*góc|⊥)[^.]{0,40}?cắt\s+(?:[Đđ]ường\s*thẳng\s+qua|nhau)/u;
7544
+ var PREFILTER52 = /(?:[Đđ]ường\s*thẳng\s+qua|(?:Qua|qua|Từ|từ)\s+[A-Z][^.]{0,20}?(?:vẽ|kẻ|dựng)\s+đường\s*thẳng)\s*[^.]{0,30}?(?:vuông\s*góc|⊥)[^.]{0,40}?cắt\s+(?:[Đđ]ường\s*thẳng\s+qua|nhau)/u;
6658
7545
  var RE25 = new RegExp(
6659
7546
  "[\u0110\u0111]\u01B0\u1EDDng\\s*th\u1EB3ng\\s+qua\\s+([A-Z])(?!\\p{L})\\s+(?:vu\xF4ng\\s*g\xF3c|\u22A5)\\s+(?:v\u1EDBi\\s+)?([A-Z])([A-Z])(?![A-Z])[^.]{0,20}?c\u1EAFt\\s+[\u0110\u0111]\u01B0\u1EDDng\\s*th\u1EB3ng\\s+qua\\s+([A-Z])(?!\\p{L})\\s+(?:vu\xF4ng\\s*g\xF3c|\u22A5)\\s+(?:v\u1EDBi\\s+)?([A-Z])([A-Z])(?![A-Z])\\s+(?:t\u1EA1i|\u1EDF)\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?![A-Z])",
6660
7547
  "gu"
6661
7548
  );
6662
- var RE_DISTRIB = new RegExp(
7549
+ var RE_DRAW_FIRST = new RegExp(
7550
+ "(?:Qua|qua|T\u1EEB|t\u1EEB)\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?!\\p{L})\\s*[^.]{0,16}?(?:v\u1EBD|k\u1EBB|d\u1EF1ng)\\s+\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+(?:vu\xF4ng\\s*g\xF3c|\u22A5)\\s+(?:v\u1EDBi\\s+)?([A-Z])([A-Z])(?![A-Z])\\s*[^.]{0,16}?c\u1EAFt\\s+[\u0110\u0111]\u01B0\u1EDDng\\s*th\u1EB3ng\\s+qua\\s+([A-Z])(?!\\p{L})\\s+(?:vu\xF4ng\\s*g\xF3c|\u22A5)\\s+(?:v\u1EDBi\\s+)?([A-Z])([A-Z])(?![A-Z])\\s+(?:t\u1EA1i|\u1EDF)\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?![A-Z])",
7551
+ "gu"
7552
+ );
7553
+ var RE_DISTRIB2 = new RegExp(
6663
7554
  "[\u0110\u0111]\u01B0\u1EDDng\\s*th\u1EB3ng\\s+qua\\s+([A-Z])(?!\\p{L})\\s*,\\s*([A-Z])(?!\\p{L})\\s+(?:l\u1EA7n\\s*l\u01B0\u1EE3t\\s+|theo\\s*th\u1EE9\\s*t\u1EF1\\s+)?(?:vu\xF4ng\\s*g\xF3c|\u22A5)\\s+(?:v\u1EDBi\\s+)?([A-Z])([A-Z])(?![A-Z])\\s*,\\s*([A-Z])([A-Z])(?![A-Z])[^.]{0,20}?c\u1EAFt\\s+nhau\\s+(?:t\u1EA1i|\u1EDF)\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?![A-Z])",
6664
7555
  "gu"
6665
7556
  );
@@ -6668,7 +7559,7 @@ var twoPerpLinesMeetRule = {
6668
7559
  priority: 49,
6669
7560
  // sau parallelPerp (dựng đường), trước/đủ để S tham chiếu 2 đường
6670
7561
  languages: ["vi"],
6671
- patterns: [PREFILTER51],
7562
+ patterns: [PREFILTER52],
6672
7563
  match(ctx) {
6673
7564
  const out = [];
6674
7565
  const emit = (c, p1, l1, p2, l2, s) => {
@@ -6690,8 +7581,12 @@ var twoPerpLinesMeetRule = {
6690
7581
  for (const m of c.text.matchAll(RE25)) {
6691
7582
  emit(c, m[1], m[2] + m[3], m[4], m[5] + m[6], m[7]);
6692
7583
  }
6693
- RE_DISTRIB.lastIndex = 0;
6694
- for (const m of c.text.matchAll(RE_DISTRIB)) {
7584
+ RE_DRAW_FIRST.lastIndex = 0;
7585
+ for (const m of c.text.matchAll(RE_DRAW_FIRST)) {
7586
+ emit(c, m[1], m[2] + m[3], m[4], m[5] + m[6], m[7]);
7587
+ }
7588
+ RE_DISTRIB2.lastIndex = 0;
7589
+ for (const m of c.text.matchAll(RE_DISTRIB2)) {
6695
7590
  emit(c, m[1], m[3] + m[4], m[2], m[5] + m[6], m[7]);
6696
7591
  }
6697
7592
  }
@@ -6700,27 +7595,57 @@ var twoPerpLinesMeetRule = {
6700
7595
  };
6701
7596
 
6702
7597
  // src/stamps/geometry-2d/ai/rules/circumcircleCutsLine.ts
6703
- var PREFILTER52 = /[Đđ]ường\s*tròn\s+ngoại\s*tiếp\s+(?:tam\s*giác\s+)?[A-Z]{3}[^.]{0,20}?cắt\s+[A-Z]{2}/u;
7598
+ var PREFILTER53 = /[Đđ]ường\s*tròn\s+ngoại\s*tiếp\s+(?:tam\s*giác\s+)?[A-Z]{3}[^.]{0,20}?cắt\s+[A-Z]{2}|[Đđ]ường\s*tròn\s+\([A-Z]{3}\)[^.]{0,20}?cắt\s+[A-Z]{2}|\([A-Z]{3}\)\s*∩\s*[A-Z]{2}\s*=\s*\{/u;
6704
7599
  var RE26 = new RegExp(
6705
7600
  "[\u0110\u0111]\u01B0\u1EDDng\\s*tr\xF2n\\s+ngo\u1EA1i\\s*ti\u1EBFp\\s+(?:tam\\s*gi\xE1c\\s+)?([A-Z]{3})(?![A-Z])\\s+c\u1EAFt\\s+(?:l\u1EA1i\\s+)?(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|\u0111o\u1EA1n\\s+|tia\\s+|c\u1EA1nh\\s+)?([A-Z]{2})(?![A-Z])\\s+(?:t\u1EA1i|\u1EDF)\\s+(?:\u0111i\u1EC3m\\s+(?:th\u1EE9\\s+hai\\s+)?)?([A-Z])(?![A-Z])(?:\\s+kh\xE1c\\s+([A-Z])(?![A-Z]))?",
6706
7601
  "gu"
6707
7602
  );
7603
+ var RE_PAREN = new RegExp(
7604
+ "[\u0110\u0111]\u01B0\u1EDDng\\s*tr\xF2n\\s+\\(([A-Z]{3})\\)\\s+c\u1EAFt\\s+(?:l\u1EA1i\\s+)?(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|\u0111o\u1EA1n\\s+|tia\\s+|c\u1EA1nh\\s+)?([A-Z]{2})(?![A-Z])\\s+(?:t\u1EA1i|\u1EDF)\\s+(?:\u0111i\u1EC3m\\s+(?:th\u1EE9\\s+(?:hai|2)\\s+)?)?(?:l\xE0\\s+)?([A-Z])(?![A-Z])(?:\\s+kh\xE1c\\s+([A-Z])(?![A-Z]))?",
7605
+ "gu"
7606
+ );
7607
+ var RE_SET = new RegExp(
7608
+ "\\(([A-Z]{3})\\)\\s*\u2229\\s*(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|\u0111o\u1EA1n\\s+|tia\\s+|c\u1EA1nh\\s+)?([A-Z]{2})(?![A-Z])\\s*=\\s*\\{\\s*([A-Z])(?![A-Z])\\s*,\\s*([A-Z])(?![A-Z])\\s*\\}",
7609
+ "gu"
7610
+ );
6708
7611
  var circumcircleCutsLineRule = {
6709
7612
  id: "circumcircleCutsLine",
6710
7613
  priority: 58,
6711
7614
  // sau circleTriangle/cevian; trước intersection(45)
6712
7615
  languages: ["vi"],
6713
- patterns: [PREFILTER52],
7616
+ patterns: [PREFILTER53],
6714
7617
  match(ctx) {
6715
7618
  const out = [];
6716
7619
  for (const c of ctx.clauses) {
6717
- RE26.lastIndex = 0;
6718
- for (const m of c.text.matchAll(RE26)) {
7620
+ for (const re of [RE26, RE_PAREN]) {
7621
+ re.lastIndex = 0;
7622
+ for (const m of c.text.matchAll(re)) {
7623
+ const tri = m[1].split("");
7624
+ const line = m[2];
7625
+ const k = m[3];
7626
+ const shared = m[4] ?? line.split("").find((v) => tri.includes(v));
7627
+ if (!shared || tri.includes(k) || line.includes(k)) continue;
7628
+ const w = `w${m[1]}`;
7629
+ out.push({
7630
+ ruleId: "circumcircleCutsLine",
7631
+ clauseIds: [c.id],
7632
+ intents: [
7633
+ drawCircle(w, "through3", { points: tri }),
7634
+ addPoint(k, { kind: "secondIntersection", line, circle: w, other: shared })
7635
+ ]
7636
+ });
7637
+ }
7638
+ }
7639
+ RE_SET.lastIndex = 0;
7640
+ for (const m of c.text.matchAll(RE_SET)) {
6719
7641
  const tri = m[1].split("");
6720
7642
  const line = m[2];
6721
- const k = m[3];
6722
- const shared = m[4] ?? line.split("").find((v) => tri.includes(v));
6723
- if (!shared || tri.includes(k) || line.includes(k)) continue;
7643
+ const set = [m[3], m[4]];
7644
+ const onLine = set.filter((v) => line.includes(v));
7645
+ if (onLine.length !== 1) continue;
7646
+ const shared = onLine[0];
7647
+ const k = set.find((v) => v !== shared);
7648
+ if (tri.includes(k) || line.includes(k)) continue;
6724
7649
  const w = `w${m[1]}`;
6725
7650
  out.push({
6726
7651
  ruleId: "circumcircleCutsLine",
@@ -6750,7 +7675,7 @@ var LINE_TYPES = [
6750
7675
  var CONCURRENCY = /(?:cắt\s+nhau|đồng\s*quy|gặp\s+nhau|cùng\s+đi\s+qua)\s+(?:tại\s+|qua\s+)?(?:điểm\s+)?([A-Z])(?!\p{L})/u;
6751
7676
  var VERTEX_PAIR = /(?<!\p{L})[A-Z][A-Z](?:['′])?(?!\p{L})/gu;
6752
7677
  var TRI_G9 = /tam\s*giác\s+(?:(?:nhọn|cân|đều|vuông|tù)\s+)*([A-Z])([A-Z])([A-Z])(?![A-Z])/gu;
6753
- var PREFILTER53 = LINE_TYPES.map((t) => t.kw);
7678
+ var PREFILTER54 = LINE_TYPES.map((t) => t.kw);
6754
7679
  function trianglesIn4(text) {
6755
7680
  const out = [];
6756
7681
  TRI_G9.lastIndex = 0;
@@ -6783,7 +7708,7 @@ var lineConcurrencyRule = {
6783
7708
  id: "lineConcurrency",
6784
7709
  priority: 69,
6785
7710
  languages: ["vi"],
6786
- patterns: PREFILTER53,
7711
+ patterns: PREFILTER54,
6787
7712
  match(ctx) {
6788
7713
  const fallbackTri = uniqueTriangle5(ctx.problem);
6789
7714
  const out = [];
@@ -6815,7 +7740,7 @@ var lineConcurrencyRule = {
6815
7740
  };
6816
7741
 
6817
7742
  // src/stamps/geometry-2d/ai/rules/twoCirclesMeet.ts
6818
- var PREFILTER54 = /hai\s+đường\s*tròn[^.]{0,70}?cắt\s+nhau/u;
7743
+ var PREFILTER55 = /hai\s+đường\s*tròn[^.]{0,70}?cắt\s+nhau/u;
6819
7744
  var CIRCLE2 = "\\(\\s*([A-Z]['\u2032]?)(?:\\s*[;,]\\s*(?:[Rr]['\u2032]?|\\d+(?:[.,]\\d+)?\\s*[a-z]*))?\\s*\\)";
6820
7745
  var TWO_MEET2 = new RegExp(
6821
7746
  "hai\\s+\u0111\u01B0\u1EDDng\\s*tr\xF2n\\s*" + CIRCLE2 + "\\s*(?:[;,]\\s*)?v\xE0\\s*" + CIRCLE2 + "[^.]{0,50}?c\u1EAFt\\s+nhau\\s+(?:\u1EDF|t\u1EA1i)\\s+(?:hai\\s+\u0111i\u1EC3m\\s+)?([A-Z])(?![A-Za-z])\\s*(?:,|v\xE0)\\s*([A-Z])(?![A-Za-z])",
@@ -6828,7 +7753,7 @@ var twoCirclesMeetRule = {
6828
7753
  // phái sinh downstream → dựng sớm. DƯỚI circleRadius (75) cho nhất quán.
6829
7754
  priority: 74,
6830
7755
  languages: ["vi"],
6831
- patterns: [PREFILTER54],
7756
+ patterns: [PREFILTER55],
6832
7757
  match(ctx) {
6833
7758
  const m = TWO_MEET2.exec(ctx.problem);
6834
7759
  if (!m) return [];
@@ -6855,7 +7780,7 @@ var twoCirclesMeetRule = {
6855
7780
  };
6856
7781
 
6857
7782
  // src/stamps/geometry-2d/ai/rules/radius.ts
6858
- var PREFILTER55 = /bán\s*kính\s+[A-Z]{2}/u;
7783
+ var PREFILTER56 = /bán\s*kính\s+[A-Z]{2}/u;
6859
7784
  var DRAW_RADII = new RegExp(
6860
7785
  "(?:[Vv]\u1EBD|[Kk]\u1EBB|[Dd]\u1EF1ng)\\s+(?:hai\\s+|ba\\s+|2\\s+|3\\s+|c\xE1c\\s+)?b\xE1n\\s*k\xEDnh\\s+([A-Z]{2}(?:\\s*(?:,|v\xE0)\\s*[A-Z]{2})*)(?![A-Z])",
6861
7786
  "u"
@@ -6866,7 +7791,7 @@ var radiusRule = {
6866
7791
  id: "radius",
6867
7792
  priority: 60,
6868
7793
  languages: ["vi"],
6869
- patterns: [PREFILTER55],
7794
+ patterns: [PREFILTER56],
6870
7795
  match(ctx) {
6871
7796
  const out = [];
6872
7797
  for (const c of ctx.clauses) {
@@ -6890,17 +7815,17 @@ var radiusRule = {
6890
7815
  };
6891
7816
 
6892
7817
  // src/stamps/geometry-2d/ai/rules/perpNamedLine.ts
6893
- var PREFILTER56 = /(?:[Kk]ẻ|[Vv]ẽ|[Dd]ựng)\s+(?:đường\s*thẳng\s+)?[A-Z][a-z]\s*(?:⊥|vuông\s*góc)/u;
7818
+ var PREFILTER57 = /(?:[Kk]ẻ|[Vv]ẽ|[Dd]ựng)\s+(?:đường\s*thẳng\s+|tia\s+)?[A-Z][a-z]\s*(?:⊥|vuông\s*góc)/u;
6894
7819
  var RE27 = new RegExp(
6895
7820
  // `⊥\\s*` (KHÔNG `\\s+`): OCR hay dính "Ax⊥MN" (vao10:11). Dạng chữ giữ `\\s+`.
6896
- "(?:[Kk]\u1EBB|[Vv]\u1EBD|[Dd]\u1EF1ng)\\s+(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+)?([A-Z][a-z])(?![A-Za-z])\\s*(?:\u22A5\\s*|vu\xF4ng\\s*g\xF3c(?:\\s+v\u1EDBi)?\\s+)(?:v\u1EDBi\\s+)?(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|c\u1EA1nh\\s+|\u0111o\u1EA1n\\s+)?([A-Z]{1,2})(?![A-Z])(?:\\s+(?:t\u1EA1i|\u1EDF)\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?![A-Za-z]))?",
7821
+ "(?:[Kk]\u1EBB|[Vv]\u1EBD|[Dd]\u1EF1ng)\\s+(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|tia\\s+)?([A-Z][a-z])(?![A-Za-z])\\s*(?:\u22A5\\s*|vu\xF4ng\\s*g\xF3c(?:\\s+v\u1EDBi)?\\s+)(?:v\u1EDBi\\s+)?(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|c\u1EA1nh\\s+|\u0111o\u1EA1n\\s+)?([A-Z]{1,2})(?![A-Z])(?:\\s*,?\\s*c\u1EAFt\\s+[A-Z]{1,2})?(?:\\s+(?:t\u1EA1i|\u1EDF)\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?![A-Za-z]))?",
6897
7822
  "gu"
6898
7823
  );
6899
7824
  var perpNamedLineRule = {
6900
7825
  id: "perp-named-line",
6901
7826
  priority: 60,
6902
7827
  languages: ["vi"],
6903
- patterns: [PREFILTER56],
7828
+ patterns: [PREFILTER57],
6904
7829
  match(ctx) {
6905
7830
  const out = [];
6906
7831
  for (const c of ctx.clauses) {
@@ -6926,7 +7851,7 @@ var perpNamedLineRule = {
6926
7851
  };
6927
7852
 
6928
7853
  // src/stamps/geometry-2d/ai/rules/parallelSidePoints.ts
6929
- var PREFILTER57 = /thuộc\s+[A-Z]{2}\s+và\s+[A-Z]{2}\s+sao\s+cho[^.]*?(?:∥|\/\/|song\s*song)/u;
7854
+ var PREFILTER58 = /thuộc\s+[A-Z]{2}\s+và\s+[A-Z]{2}\s+sao\s+cho[^.]*?(?:∥|\/\/|song\s*song)/u;
6930
7855
  var PAR2 = "(?:\u2225|\\/\\/|song\\s*song(?:\\s+v\u1EDBi)?)";
6931
7856
  var RE28 = new RegExp(
6932
7857
  "([A-Z])\\s*,\\s*([A-Z])(?![A-Z])\\s+(?:l\u1EA7n\\s*l\u01B0\u1EE3t\\s+|theo\\s+th\u1EE9\\s+t\u1EF1\\s+)?thu\u1ED9c\\s+([A-Z]{2})\\s+v\xE0\\s+([A-Z]{2})(?![A-Z])\\s+sao\\s+cho\\s+([A-Z])([A-Z])(?![A-Z])\\s*" + PAR2 + "\\s*([A-Z]{2})(?![A-Z])\\s*(?:,|v\xE0)\\s*([A-Z])([A-Z])(?![A-Z])\\s*" + PAR2 + "\\s*([A-Z]{2})(?![A-Z])",
@@ -6936,7 +7861,7 @@ var parallelSidePointsRule = {
6936
7861
  id: "parallel-side-points",
6937
7862
  priority: 55,
6938
7863
  languages: ["vi"],
6939
- patterns: [PREFILTER57],
7864
+ patterns: [PREFILTER58],
6940
7865
  match(ctx) {
6941
7866
  const out = [];
6942
7867
  for (const c of ctx.clauses) {
@@ -6959,36 +7884,370 @@ var parallelSidePointsRule = {
6959
7884
  }
6960
7885
  };
6961
7886
 
6962
- // src/stamps/geometry-2d/ai/rules/parallelThroughCutsCircle.ts
6963
- var DISTRIB4 = new RegExp(
6964
- String.raw`(?:[CcNn]ác\s+)?đường\s*thẳng\s+qua\s+(?:hai\s+điểm\s+)?([A-Z])(?!\p{L})\s+và\s+([A-Z])(?!\p{L})` + String.raw`\s+song\s*song\s+(?:với\s+)?(?:đường\s*thẳng\s+)?([A-Z])([A-Z])(?!\p{L})` + String.raw`[^.]{0,30}?cắt\s+(?:đường\s*tròn\s*)?\(\s*([A-Z])\s*\)` + String.raw`[^.]{0,30}?lần\s*lượt\s+(?:ở|tại)\s+([A-Z])\s+và\s+([A-Z])(?!\p{L})`,
6965
- "gu"
6966
- );
6967
- var DECL_NAMED = new RegExp(
6968
- String.raw`(?:[Qq]ua|[Tt]ừ)\s+(?:điểm\s+)?([A-Z])(?!\p{L})` + String.raw`\s+(?:kẻ|vẽ|dựng)\s+đường\s*thẳng\s+([a-z])(?!\p{L})` + String.raw`\s+song\s*song\s+(?:với\s+)?(?:đường\s*thẳng\s+)?([A-Z])([A-Z])(?!\p{L})`,
6969
- "u"
6970
- );
6971
- var NAMED_CUT_BOTH = new RegExp(
6972
- String.raw`[Đđ]ường\s*thẳng\s+([a-z])(?!\p{L})` + String.raw`\s+cắt\s+(?:đường\s*thẳng\s+|cạnh\s+|đoạn\s+)?([A-Z])([A-Z])(?!\p{L})\s+(?:ở|tại)\s+(?:điểm\s+)?([A-Z])(?!\p{L})` + String.raw`[^.]{0,8}?cắt\s+(?:đường\s*tròn\s*)?\(\s*([A-Z])\s*\)\s+(?:ở|tại)\s+(?:điểm\s+(?:thứ\s+hai\s+)?)?(?:là\s+)?([A-Z])(?!\p{L})`,
6973
- "u"
6974
- );
6975
- var PREFILTER58 = /(?:[Qq]ua|đường\s*thẳng\s+qua)\s+[^.]{0,60}?song\s*song[^.]{0,80}?cắt|(?:[Qq]ua|[Tt]ừ)\s+(?:điểm\s+)?[A-Z][^.]{0,20}?(?:kẻ|vẽ|dựng)\s+đường\s*thẳng\s+[a-z]\s+song\s*song/u;
6976
- function lineNameOf2(through) {
6977
- return "par" + through;
7887
+ // src/stamps/geometry-2d/ai/intent-builders/shared.ts
7888
+ var SQRT3 = 1.7320508075688772;
7889
+ function triangleCanonical(variant) {
7890
+ switch (variant) {
7891
+ case "equilateral":
7892
+ return [[0, 0], [4, 0], [2, 2 * SQRT3]];
7893
+ case "isoceles-AB":
7894
+ return [[0, 0], [4, 0], [2, 3]];
7895
+ case "isoceles-BC":
7896
+ return [[0, 3], [-2, 0], [2, 0]];
7897
+ case "isoceles-CA":
7898
+ return [[0, 0], [2, 3], [4, 0]];
7899
+ case "right-at-A":
7900
+ return [[0, 0], [4, 0], [0, 3]];
7901
+ case "right-at-B":
7902
+ return [[0, 0], [4, 0], [4, 3]];
7903
+ case "right-at-C":
7904
+ return [[0, 0], [6, 0], [3, 3]];
7905
+ case "any":
7906
+ default:
7907
+ return [[0, 0], [5, 0], [2, 3]];
7908
+ }
6978
7909
  }
6979
- function resolveCircle4(problem) {
6980
- const m = /đường\s*tròn\s*\(\s*([A-Z])\s*\)|\(\s*([A-Z])\s*\)/u.exec(problem);
6981
- if (!m) return void 0;
6982
- return m[1] ?? m[2];
7910
+ function squareCanonical() {
7911
+ return [[0, 0], [4, 0], [4, 4], [0, 4]];
6983
7912
  }
6984
- function secondIntersection2(name, line, circle, other) {
6985
- return addPoint(name, { kind: "secondIntersection", line, circle, other });
7913
+ function rectangleCanonical(variant) {
7914
+ if (variant === "tall") return [[0, 0], [2.5, 0], [2.5, 4], [0, 4]];
7915
+ return [[0, 0], [4, 0], [4, 2.5], [0, 2.5]];
6986
7916
  }
6987
- var parallelThroughCutsCircleRule = {
6988
- id: "parallelThroughCutsCircle",
6989
- priority: 46,
6990
- languages: ["vi"],
6991
- patterns: [PREFILTER58],
7917
+ function rhombusCanonical() {
7918
+ return [[-2, 0], [0, -1.5], [2, 0], [0, 1.5]];
7919
+ }
7920
+ function trapezoidCanonical(variant) {
7921
+ switch (variant) {
7922
+ case "right":
7923
+ return [[0, 0], [5, 0], [3, 3], [0, 3]];
7924
+ case "isoceles":
7925
+ return [[0, 0], [5, 0], [4, 3], [1, 3]];
7926
+ case "general":
7927
+ default:
7928
+ return [[0, 0], [5, 0], [3.5, 3], [1, 3]];
7929
+ }
7930
+ }
7931
+ function parallelogramCanonical() {
7932
+ return [[0, 0], [4, 0], [5, 3], [1, 3]];
7933
+ }
7934
+ function quadrilateralCanonical() {
7935
+ return [[0, 0], [5, 0], [4.5, 3.5], [0.5, 3]];
7936
+ }
7937
+ function addPoint2(s, p) {
7938
+ if (s.pointNames.has(p.name)) {
7939
+ if (p.kind !== "free") {
7940
+ const idx = s.points.findIndex((q) => q.name === p.name);
7941
+ if (idx >= 0 && s.points[idx].kind === "free") s.points[idx] = p;
7942
+ }
7943
+ return;
7944
+ }
7945
+ s.points.push(p);
7946
+ s.pointNames.add(p.name);
7947
+ }
7948
+ var FREE_DEFAULT_SPREAD = [
7949
+ [3, 3],
7950
+ [-3, 3],
7951
+ [3, -3],
7952
+ [-3, -3],
7953
+ [4, 0],
7954
+ [-4, 0],
7955
+ [0, 4],
7956
+ [0, -4]
7957
+ ];
7958
+ function defaultFreeCoord(s) {
7959
+ let count = 0;
7960
+ for (const p of s.points) if (p.kind === "free") count++;
7961
+ return [...FREE_DEFAULT_SPREAD[count % FREE_DEFAULT_SPREAD.length]];
7962
+ }
7963
+ function addShape(s, sh) {
7964
+ if (s.shapeNames.has(sh.name)) return;
7965
+ s.shapes.push(sh);
7966
+ s.shapeNames.add(sh.name);
7967
+ }
7968
+ function uniqueShapeName(s, base) {
7969
+ if (!s.shapeNames.has(base)) return base;
7970
+ let i = 2;
7971
+ while (s.shapeNames.has(`${base}${i}`)) i++;
7972
+ return `${base}${i}`;
7973
+ }
7974
+ function uniquePointName(s, base) {
7975
+ if (!s.pointNames.has(base)) return base;
7976
+ let i = 2;
7977
+ while (s.pointNames.has(`${base}${i}`)) i++;
7978
+ return `${base}${i}`;
7979
+ }
7980
+ function ensureSegment(s, a, b) {
7981
+ const k1 = `${a}-${b}`;
7982
+ const k2 = `${b}-${a}`;
7983
+ const existing = s.segmentByEnds.get(k1) ?? s.segmentByEnds.get(k2);
7984
+ if (existing) return existing;
7985
+ const name = uniqueShapeName(s, `${a}${b}`);
7986
+ addShape(s, { name, kind: "segment", p1: a, p2: b });
7987
+ s.segmentByEnds.set(k1, name);
7988
+ return name;
7989
+ }
7990
+ function resolveLineRefWithFallback(s, ref, anchor) {
7991
+ if (/^[A-Za-z]$/.test(ref) && /^[A-Za-z]$/.test(anchor) && ref !== anchor && s.pointNames.has(ref) && s.pointNames.has(anchor)) {
7992
+ const [a, b] = ref < anchor ? [ref, anchor] : [anchor, ref];
7993
+ return ensureSegment(s, a, b);
7994
+ }
7995
+ return resolveSegmentRef(s, ref);
7996
+ }
7997
+ function resolveSegmentRef(s, ref) {
7998
+ if (s.shapeNames.has(ref)) return ref;
7999
+ if (/^[A-Z][A-Z]$/u.test(ref)) {
8000
+ return ensureSegment(s, ref[0], ref[1]);
8001
+ }
8002
+ if (ref.length === 2 && s.pointNames.has(ref[0]) && s.pointNames.has(ref[1])) {
8003
+ return ensureSegment(s, ref[0], ref[1]);
8004
+ }
8005
+ const dashMatch = ref.match(/^([A-Za-z][A-Za-z0-9'_]*)[-_]?([A-Za-z][A-Za-z0-9'_]*)$/);
8006
+ if (dashMatch && s.pointNames.has(dashMatch[1]) && s.pointNames.has(dashMatch[2])) {
8007
+ return ensureSegment(s, dashMatch[1], dashMatch[2]);
8008
+ }
8009
+ const sub2 = splitKnownPair(s, ref);
8010
+ if (sub2) return ensureSegment(s, sub2[0], sub2[1]);
8011
+ return ref;
8012
+ }
8013
+ function splitKnownPair(s, ref) {
8014
+ for (let i = ref.length - 1; i >= 1; i--) {
8015
+ const a = ref.slice(0, i);
8016
+ const b = ref.slice(i);
8017
+ if (s.pointNames.has(a) && s.pointNames.has(b)) return [a, b];
8018
+ }
8019
+ return null;
8020
+ }
8021
+ function parseEnds(ref) {
8022
+ if (ref.length === 2) return [ref[0], ref[1]];
8023
+ const m = ref.match(/^([A-Za-z][A-Za-z0-9'_]*)[-_]?([A-Za-z][A-Za-z0-9'_]*)$/);
8024
+ if (m) return [m[1], m[2]];
8025
+ return null;
8026
+ }
8027
+ var SHAPE_VARIANTS = {
8028
+ triangle: ["any", "equilateral", "isoceles-AB", "isoceles-BC", "isoceles-CA", "right-at-A", "right-at-B", "right-at-C"],
8029
+ square: ["standard"],
8030
+ rectangle: ["standard", "wide", "tall"],
8031
+ rhombus: ["standard"],
8032
+ trapezoid: ["right", "isoceles", "general"],
8033
+ parallelogram: ["standard"],
8034
+ quadrilateral: ["any"]
8035
+ };
8036
+
8037
+ // src/stamps/geometry-2d/ai/rules/inscribedSquare.ts
8038
+ function sub(p, q) {
8039
+ return [p[0] - q[0], p[1] - q[1]];
8040
+ }
8041
+ function add(p, q) {
8042
+ return [p[0] + q[0], p[1] + q[1]];
8043
+ }
8044
+ function mul(p, k) {
8045
+ return [p[0] * k, p[1] * k];
8046
+ }
8047
+ function dot(p, q) {
8048
+ return p[0] * q[0] + p[1] * q[1];
8049
+ }
8050
+ function cross(p, q) {
8051
+ return p[0] * q[1] - p[1] * q[0];
8052
+ }
8053
+ function len(p) {
8054
+ return Math.hypot(p[0], p[1]);
8055
+ }
8056
+ function norm2(p) {
8057
+ const l = len(p) || 1;
8058
+ return [p[0] / l, p[1] / l];
8059
+ }
8060
+ function inscribedSquareCoords(A, B, C) {
8061
+ const u = norm2(sub(C, B));
8062
+ const a = len(sub(C, B));
8063
+ const tA = dot(sub(A, B), u);
8064
+ const foot = add(B, mul(u, tA));
8065
+ const h = len(sub(A, foot));
8066
+ const s = a * h / (a + h);
8067
+ const n = norm2(sub(A, foot));
8068
+ const BA = sub(B, A);
8069
+ const q0 = -(s * cross(n, BA)) / cross(u, BA);
8070
+ const Q = add(B, mul(u, q0));
8071
+ const P = add(B, mul(u, q0 + s));
8072
+ const M = add(Q, mul(n, s));
8073
+ const N = add(P, mul(n, s));
8074
+ return { M, N, P, Q };
8075
+ }
8076
+ function paramOn(pt, X, Y) {
8077
+ const d = sub(Y, X);
8078
+ return dot(sub(pt, X), d) / (dot(d, d) || 1);
8079
+ }
8080
+ function inscribedSquareParams(A, B, C) {
8081
+ const { M, N, P, Q } = inscribedSquareCoords(A, B, C);
8082
+ return {
8083
+ tM: paramOn(M, A, B),
8084
+ tN: paramOn(N, A, C),
8085
+ tP: paramOn(P, B, C),
8086
+ tQ: paramOn(Q, B, C)
8087
+ };
8088
+ }
8089
+ var PREFILTER59 = /hình\s*vuông[^.]*?(?:trên|∈|thuộc|\bNe\b|\bEe\b)/iu;
8090
+ var SQUARE_LABELS = /hình\s*vuông\s+([A-Z])\s*([A-Z])\s*([A-Z])\s*([A-Z])(?![A-Z])/u;
8091
+ var RIGHT_AT2 = /vuông\s+tại\s+([A-Z])(?![A-Za-z])/u;
8092
+ var RIGHT_BY_IDX2 = ["right-at-A", "right-at-B", "right-at-C"];
8093
+ var MEMBERSHIP_G = /(?:([A-Z])\s*(?:∈|trên|thuộc)|\b([A-Z])e\b)\s*([A-Z])([A-Z])(?![A-Z])/gu;
8094
+ var LIST_MEMBERSHIP_G = /([A-Z])\s*,\s*([A-Z])\s*(?:∈|trên|thuộc)\s*([A-Z])([A-Z])(?![A-Z])/gu;
8095
+ function parseMemberships(text) {
8096
+ const out = [];
8097
+ let m;
8098
+ LIST_MEMBERSHIP_G.lastIndex = 0;
8099
+ const listSpans = [];
8100
+ while ((m = LIST_MEMBERSHIP_G.exec(text)) !== null) {
8101
+ const side = [m[3], m[4]];
8102
+ out.push({ pt: m[1], side });
8103
+ out.push({ pt: m[2], side });
8104
+ listSpans.push([m.index, m.index + m[0].length]);
8105
+ }
8106
+ MEMBERSHIP_G.lastIndex = 0;
8107
+ while ((m = MEMBERSHIP_G.exec(text)) !== null) {
8108
+ if (listSpans.some(([a, b]) => m.index >= a && m.index < b)) continue;
8109
+ const pt = m[1] ?? m[2];
8110
+ out.push({ pt, side: [m[3], m[4]] });
8111
+ }
8112
+ return out;
8113
+ }
8114
+ var inscribedSquareRule = {
8115
+ id: "inscribed-square",
8116
+ priority: 56,
8117
+ languages: ["vi"],
8118
+ patterns: [PREFILTER59],
8119
+ match(ctx) {
8120
+ const out = [];
8121
+ for (const c of ctx.clauses) {
8122
+ const labelM = SQUARE_LABELS.exec(c.text);
8123
+ if (!labelM) continue;
8124
+ const labels = [labelM[1], labelM[2], labelM[3], labelM[4]];
8125
+ if (new Set(labels).size !== 4) continue;
8126
+ const memberships = parseMemberships(c.text);
8127
+ if (memberships.length < 4) continue;
8128
+ const bySide = /* @__PURE__ */ new Map();
8129
+ const ptSide = /* @__PURE__ */ new Map();
8130
+ for (const mem of memberships) {
8131
+ if (!labels.includes(mem.pt)) continue;
8132
+ ptSide.set(mem.pt, mem.side);
8133
+ const key = [...mem.side].sort().join("");
8134
+ const arr = bySide.get(key) ?? [];
8135
+ if (!arr.includes(mem.pt)) arr.push(mem.pt);
8136
+ bySide.set(key, arr);
8137
+ }
8138
+ if (labels.some((l) => !ptSide.has(l))) continue;
8139
+ const sideKeys = [...bySide.keys()];
8140
+ const hypKey = sideKeys.find((k) => (bySide.get(k) ?? []).length === 2);
8141
+ if (!hypKey) continue;
8142
+ const legKeys = sideKeys.filter((k) => k !== hypKey);
8143
+ if (legKeys.length !== 2) continue;
8144
+ if ((bySide.get(legKeys[0]) ?? []).length !== 1) continue;
8145
+ if ((bySide.get(legKeys[1]) ?? []).length !== 1) continue;
8146
+ const triVerts = /* @__PURE__ */ new Set();
8147
+ for (const [, side] of ptSide) side.forEach((v) => triVerts.add(v));
8148
+ if (triVerts.size !== 3) continue;
8149
+ const legVerts = legKeys.map((k) => k.split(""));
8150
+ const apex = legVerts[0].find((v) => legVerts[1].includes(v));
8151
+ if (!apex) continue;
8152
+ const triLabels = [apex, ...[...triVerts].filter((v) => v !== apex).sort()];
8153
+ const explicitRight = collectRightAt(ctx.clauses);
8154
+ if (explicitRight && explicitRight !== apex) continue;
8155
+ const tri = triangleCanonical("right-at-A");
8156
+ const params = inscribedSquareParams(tri[0], tri[1], tri[2]);
8157
+ const placement = assignSquarePlacement(
8158
+ labels,
8159
+ ptSide,
8160
+ bySide.get(hypKey),
8161
+ triLabels[0],
8162
+ // apex (đỉnh góc vuông)
8163
+ triLabels[1],
8164
+ // đỉnh B của tam giác (đầu mút cạnh huyền gần Q)
8165
+ triLabels[2],
8166
+ // đỉnh C của tam giác (đầu mút cạnh huyền gần P)
8167
+ params
8168
+ );
8169
+ if (!placement) continue;
8170
+ const rightIdx = triLabels.indexOf(apex);
8171
+ const variant = RIGHT_BY_IDX2[rightIdx] ?? "right-at-A";
8172
+ const intents = [drawShape("triangle", triLabels, variant)];
8173
+ for (const lbl of labels) {
8174
+ const { of, t } = placement.get(lbl);
8175
+ intents.push(addPoint(lbl, { kind: "onSegment", of, t }));
8176
+ }
8177
+ intents.push(markShape("quadrilateral", labels));
8178
+ const claimed = /* @__PURE__ */ new Set([c.id]);
8179
+ for (const cc of ctx.clauses) {
8180
+ const rm = RIGHT_AT2.exec(cc.text);
8181
+ if (rm && rm[1] === apex) claimed.add(cc.id);
8182
+ }
8183
+ out.push({ ruleId: "inscribed-square", clauseIds: [...claimed], intents });
8184
+ }
8185
+ return out;
8186
+ }
8187
+ };
8188
+ function collectRightAt(clauses) {
8189
+ for (const c of clauses) {
8190
+ const m = RIGHT_AT2.exec(c.text);
8191
+ if (m) return m[1];
8192
+ }
8193
+ return void 0;
8194
+ }
8195
+ function assignSquarePlacement(labels, ptSide, hypPts, triA, triB, triC, params) {
8196
+ const place = /* @__PURE__ */ new Map();
8197
+ const onAB = labels.find((l) => !hypPts.includes(l) && ptSide.get(l).includes(triB));
8198
+ const onAC = labels.find((l) => !hypPts.includes(l) && ptSide.get(l).includes(triC));
8199
+ if (!onAB || !onAC) return null;
8200
+ place.set(onAB, { of: `${triA}${triB}`, t: params.tM });
8201
+ place.set(onAC, { of: `${triA}${triC}`, t: params.tN });
8202
+ const neighborInHyp = (anchor) => {
8203
+ const i = labels.indexOf(anchor);
8204
+ const next = labels[(i + 1) % labels.length];
8205
+ const prev = labels[(i - 1 + labels.length) % labels.length];
8206
+ if (hypPts.includes(next)) return next;
8207
+ if (hypPts.includes(prev)) return prev;
8208
+ return void 0;
8209
+ };
8210
+ const qVtx = neighborInHyp(onAB);
8211
+ const pVtx = neighborInHyp(onAC);
8212
+ const hyp = `${triB}${triC}`;
8213
+ if (qVtx) place.set(qVtx, { of: hyp, t: params.tQ });
8214
+ if (pVtx) place.set(pVtx, { of: hyp, t: params.tP });
8215
+ for (const hp of hypPts) {
8216
+ if (!place.has(hp)) place.set(hp, { of: hyp, t: hp === qVtx ? params.tQ : params.tP });
8217
+ }
8218
+ return labels.every((l) => place.has(l)) ? place : null;
8219
+ }
8220
+
8221
+ // src/stamps/geometry-2d/ai/rules/parallelThroughCutsCircle.ts
8222
+ var DISTRIB4 = new RegExp(
8223
+ String.raw`(?:[CcNn]ác\s+)?đường\s*thẳng\s+qua\s+(?:hai\s+điểm\s+)?([A-Z])(?!\p{L})\s+và\s+([A-Z])(?!\p{L})` + String.raw`\s+song\s*song\s+(?:với\s+)?(?:đường\s*thẳng\s+)?([A-Z])([A-Z])(?!\p{L})` + String.raw`[^.]{0,30}?cắt\s+(?:đường\s*tròn\s*)?\(\s*([A-Z])\s*\)` + String.raw`[^.]{0,30}?lần\s*lượt\s+(?:ở|tại)\s+([A-Z])\s+và\s+([A-Z])(?!\p{L})`,
8224
+ "gu"
8225
+ );
8226
+ var DECL_NAMED = new RegExp(
8227
+ String.raw`(?:[Qq]ua|[Tt]ừ)\s+(?:điểm\s+)?([A-Z])(?!\p{L})` + String.raw`\s+(?:kẻ|vẽ|dựng)\s+đường\s*thẳng\s+([a-z])(?!\p{L})` + String.raw`\s+song\s*song\s+(?:với\s+)?(?:đường\s*thẳng\s+)?([A-Z])([A-Z])(?!\p{L})`,
8228
+ "u"
8229
+ );
8230
+ var NAMED_CUT_BOTH = new RegExp(
8231
+ String.raw`[Đđ]ường\s*thẳng\s+([a-z])(?!\p{L})` + String.raw`\s+cắt\s+(?:đường\s*thẳng\s+|cạnh\s+|đoạn\s+)?([A-Z])([A-Z])(?!\p{L})\s+(?:ở|tại)\s+(?:điểm\s+)?([A-Z])(?!\p{L})` + String.raw`[^.]{0,8}?cắt\s+(?:đường\s*tròn\s*)?\(\s*([A-Z])\s*\)\s+(?:ở|tại)\s+(?:điểm\s+(?:thứ\s+hai\s+)?)?(?:là\s+)?([A-Z])(?!\p{L})`,
8232
+ "u"
8233
+ );
8234
+ var PREFILTER60 = /(?:[Qq]ua|đường\s*thẳng\s+qua)\s+[^.]{0,60}?song\s*song[^.]{0,80}?cắt|(?:[Qq]ua|[Tt]ừ)\s+(?:điểm\s+)?[A-Z][^.]{0,20}?(?:kẻ|vẽ|dựng)\s+đường\s*thẳng\s+[a-z]\s+song\s*song/u;
8235
+ function lineNameOf2(through) {
8236
+ return "par" + through;
8237
+ }
8238
+ function resolveCircle4(problem) {
8239
+ const m = /đường\s*tròn\s*\(\s*([A-Z])\s*\)|\(\s*([A-Z])\s*\)/u.exec(problem);
8240
+ if (!m) return void 0;
8241
+ return m[1] ?? m[2];
8242
+ }
8243
+ function secondIntersection2(name, line, circle, other) {
8244
+ return addPoint(name, { kind: "secondIntersection", line, circle, other });
8245
+ }
8246
+ var parallelThroughCutsCircleRule = {
8247
+ id: "parallelThroughCutsCircle",
8248
+ priority: 46,
8249
+ languages: ["vi"],
8250
+ patterns: [PREFILTER60],
6992
8251
  match(ctx) {
6993
8252
  const circle = resolveCircle4(ctx.problem);
6994
8253
  if (!circle) return [];
@@ -7064,12 +8323,12 @@ var RE29 = new RegExp(
7064
8323
  "(?:[Mm]\u1ED9t\\s+)?\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+(?:b\u1EA5t\\s*k[i\xECy\u1EF3]|t\xF9y\\s*\xFD)\\s+(?:n\xE0o\\s+)?(?:\u0111i\\s+)?qua\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?!\\p{L})[^.]{0,20}?c\u1EAFt\\s+(?:c\xE1c\\s+)?(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|c\u1EA1nh\\s+|\u0111o\u1EA1n(?:\\s+th\u1EB3ng)?\\s+)?([A-Z]\\s*[A-Z])(?![A-Z])\\s*(?:,|v\xE0)\\s*([A-Z]\\s*[A-Z])(?![A-Z])[^.]{0,20}?(?:l\u1EA7n\\s*l\u01B0\u1EE3t\\s+|theo\\s+th\u1EE9\\s+t\u1EF1\\s+)?(?:t\u1EA1i|\u1EDF)\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])\\s*(?:,|v\xE0)\\s*([A-Z])(?![A-Z])",
7065
8324
  "gu"
7066
8325
  );
7067
- var PREFILTER59 = [/đường\s*thẳng\s+(?:bất\s*k[iìyỳ]|tùy\s*ý)\s+(?:nào\s+)?(?:đi\s+)?qua\s+[A-Z]/u];
8326
+ var PREFILTER61 = [/đường\s*thẳng\s+(?:bất\s*k[iìyỳ]|tùy\s*ý)\s+(?:nào\s+)?(?:đi\s+)?qua\s+[A-Z]/u];
7068
8327
  var lineThroughCutsTwoLinesRule = {
7069
8328
  id: "lineThroughCutsTwoLines",
7070
8329
  priority: 44,
7071
8330
  languages: ["vi"],
7072
- patterns: PREFILTER59,
8331
+ patterns: PREFILTER61,
7073
8332
  match(ctx) {
7074
8333
  const out = [];
7075
8334
  for (const c of ctx.clauses) {
@@ -7154,47 +8413,644 @@ function detectSpec(problem) {
7154
8413
  }
7155
8414
  return null;
7156
8415
  }
7157
- var rightAngleViewingRule = {
7158
- id: "rightAngleViewing",
7159
- priority: 63,
8416
+ var rightAngleViewingRule = {
8417
+ id: "rightAngleViewing",
8418
+ priority: 63,
8419
+ languages: ["vi"],
8420
+ patterns: [
8421
+ /(?:[Gg]óc|∠)[^.;\n]{0,18}90/u,
8422
+ /(?:[Gg]óc|∠)\s*[A-Z]{3}\s*vuông/u,
8423
+ /[A-Z]{2}\s*(?:⊥|vuông\s*góc)\s*[A-Z]{2}/u
8424
+ ],
8425
+ match(ctx) {
8426
+ const spec = detectSpec(ctx.problem);
8427
+ if (!spec) return [];
8428
+ const clauseIds = ctx.clauses.filter(
8429
+ (c) => ANGLE_EQ_RE.test(c.text) || ANGLE_VUONG_RE.test(c.text) || PERP_RE.test(c.text) || findOnLine(c.text, spec.vertex) !== null
8430
+ ).map((c) => c.id);
8431
+ const out = [
8432
+ {
8433
+ ruleId: "rightAngleViewing",
8434
+ clauseIds,
8435
+ intents: [
8436
+ addPoint(spec.vertex, {
8437
+ kind: "rightAngleViewing",
8438
+ a: spec.a,
8439
+ b: spec.b,
8440
+ onLine: spec.onLine
8441
+ })
8442
+ ]
8443
+ }
8444
+ ];
8445
+ return out;
8446
+ }
8447
+ };
8448
+
8449
+ // src/stamps/geometry-2d/ai/rules/collinearPoints.ts
8450
+ var PREFILTER62 = /thẳng\s+hàng|(?:cùng\s+)?(?:thuộc|nằm\s+trên|ở\s+trên)\s+(?:một\s+)?(?:cùng\s+)?(?:một\s+)?[Đđ]ư[ờơ]ng\s*thẳng/u;
8451
+ var NAMES = "([A-Z](?:['\u2032])?(?:\\s*(?:,|v\xE0)\\s*[A-Z](?:['\u2032])?){2,})";
8452
+ var ON_LINE = new RegExp(
8453
+ "(?:[Cc]\xE1c\\s+|[Cc]ho\\s+)?(?:\\d+\\s+|(?:hai|ba|b\u1ED1n|n\u0103m|s\xE1u)\\s+)?\u0111i\u1EC3m\\s+" + NAMES + "\\s+(?:c\xF9ng\\s+)?(?:thu\u1ED9c|n\u1EB1m\\s+tr\xEAn|\u1EDF\\s+tr\xEAn)\\s+(?:m\u1ED9t\\s+)?[\u0110\u0111]\u01B0[\u1EDD\u01A1]ng\\s*th\u1EB3ng",
8454
+ "u"
8455
+ );
8456
+ var COLLINEAR = new RegExp(
8457
+ "(?:[Cc]\xE1c\\s+|[Cc]ho\\s+)?(?:\\d+\\s+|(?:hai|ba|b\u1ED1n|n\u0103m|s\xE1u)\\s+)?\u0111i\u1EC3m\\s+" + NAMES + "\\s+th\u1EB3ng\\s+h\xE0ng",
8458
+ "u"
8459
+ );
8460
+ function parseNames(blob) {
8461
+ return blob.split(/\s*(?:,|và)\s*/u).map((s) => s.trim().replace("\u2032", "'")).filter((s) => /^[A-Z]'?$/u.test(s));
8462
+ }
8463
+ var collinearPointsRule = {
8464
+ id: "collinear-points",
8465
+ priority: 95,
8466
+ // cao: dựng "bộ khung" điểm thẳng hàng sớm (như triangle/quad).
8467
+ languages: ["vi"],
8468
+ patterns: [PREFILTER62],
8469
+ match(ctx) {
8470
+ const out = [];
8471
+ for (const c of ctx.clauses) {
8472
+ const m = ON_LINE.exec(c.text) ?? COLLINEAR.exec(c.text);
8473
+ if (!m) continue;
8474
+ const names = parseNames(m[1]);
8475
+ if (names.length < 3) continue;
8476
+ if (new Set(names).size !== names.length) continue;
8477
+ const first = names[0];
8478
+ const last = names[names.length - 1];
8479
+ const span = 12;
8480
+ const intents = [
8481
+ addPoint(first, { kind: "free", at: [-span / 2, 0] }),
8482
+ addPoint(last, { kind: "free", at: [span / 2, 0] }),
8483
+ // Điểm giữa: onSegment trên đoạn đầu-cuối, t cách đều theo thứ tự liệt kê.
8484
+ ...names.slice(1, -1).map(
8485
+ (nm, i) => addPoint(nm, { kind: "onSegment", of: `${first}${last}`, t: (i + 1) / (names.length - 1) })
8486
+ ),
8487
+ connect(first, last, "segment")
8488
+ ];
8489
+ out.push({ ruleId: "collinear-points", clauseIds: [c.id], intents });
8490
+ }
8491
+ return out;
8492
+ }
8493
+ };
8494
+
8495
+ // src/stamps/geometry-2d/ai/rules/quadDiagonals.ts
8496
+ var PREFILTER63 = /[A-Z]{4}\s+là\s+(?:một\s+)?tứ\s*giác|tứ\s*giác\s+[A-Z]{4}[^.]{0,60}?đường\s*chéo/u;
8497
+ var QUAD_DECL = /([A-Z])([A-Z])([A-Z])([A-Z])(?![A-Z])\s+là\s+(?:một\s+)?tứ\s*giác/u;
8498
+ var DIAGONALS = new RegExp(
8499
+ "(?:hai\\s+|c\xE1c\\s+)?\u0111\u01B0\u1EDDng\\s*ch\xE9o\\s+([A-Z]{2})\\s+(?:v\xE0|,)\\s+([A-Z]{2})[^.]{0,40}?c\u1EAFt\\s+nhau\\s+(?:t\u1EA1i|\u1EDF)\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?![A-Z])",
8500
+ "u"
8501
+ );
8502
+ var quadDiagonalsRule = {
8503
+ id: "quad-diagonals",
8504
+ priority: 99,
8505
+ // ~quad (100): vẽ tứ giác sớm; intersection (45) dựng E sau.
8506
+ languages: ["vi"],
8507
+ patterns: [PREFILTER63],
8508
+ match(ctx) {
8509
+ const out = [];
8510
+ for (const c of ctx.clauses) {
8511
+ const q = QUAD_DECL.exec(c.text);
8512
+ if (!q) continue;
8513
+ const labels = [q[1], q[2], q[3], q[4]];
8514
+ if (new Set(labels).size !== 4) continue;
8515
+ const intents = [drawShape("quadrilateral", labels, "any")];
8516
+ const d = DIAGONALS.exec(c.text);
8517
+ if (d) {
8518
+ const diag1 = d[1];
8519
+ const diag2 = d[2];
8520
+ const e = d[3];
8521
+ const ends = [diag1[0], diag1[1], diag2[0], diag2[1]];
8522
+ const okDiag = new Set(ends).size === 4 && ends.every((x) => labels.includes(x)) && !labels.includes(e);
8523
+ if (okDiag) {
8524
+ intents.push(connect(diag1[0], diag1[1], "segment"));
8525
+ intents.push(connect(diag2[0], diag2[1], "segment"));
8526
+ intents.push(addPoint(e, { kind: "intersection", of: [diag1, diag2] }));
8527
+ }
8528
+ }
8529
+ out.push({ ruleId: "quad-diagonals", clauseIds: [c.id], intents });
8530
+ }
8531
+ return out;
8532
+ }
8533
+ };
8534
+
8535
+ // src/stamps/geometry-2d/ai/rules/twoCirclesTangent.ts
8536
+ var PREFILTER64 = /hai\s+[Đđ]ư[ờơ]ng\s*tròn[^.]{0,70}?tiếp\s*xúc/u;
8537
+ var CIRCLE3 = "\\(\\s*([A-Z]['\u2032]?)(?:\\s*[;,]\\s*(?:[Rr]['\u2032]?|\\d+(?:[.,]\\d+)?\\s*[a-z]*))?\\s*\\)";
8538
+ var TWO_TANGENT = new RegExp(
8539
+ "hai\\s+[\u0110\u0111]\u01B0[\u1EDD\u01A1]ng\\s*tr\xF2n\\s*" + CIRCLE3 + "\\s*(?:[;,]\\s*)?v\xE0\\s*" + CIRCLE3 + "\\s*ti\u1EBFp\\s*x\xFAc\\s+(ngo\xE0i|trong|nhau)?\\s*(?:nhau\\s+)?(?:t\u1EA1i|\u1EDF)\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?![A-Za-z])",
8540
+ "u"
8541
+ );
8542
+ var norm3 = (s) => s.replace(/′/g, "'");
8543
+ var twoCirclesTangentRule = {
8544
+ id: "two-circles-tangent",
8545
+ // TRÊN chord/externalPoint, cạnh twoCirclesMeet (74): tiếp điểm A là gốc nhiều
8546
+ // phái sinh → dựng sớm.
8547
+ priority: 74,
8548
+ languages: ["vi"],
8549
+ patterns: [PREFILTER64],
8550
+ match(ctx) {
8551
+ const m = TWO_TANGENT.exec(ctx.problem);
8552
+ if (!m) return [];
8553
+ const c1 = norm3(m[1]);
8554
+ const c2 = norm3(m[2]);
8555
+ const mode = m[3];
8556
+ const a = m[4];
8557
+ if (mode === "trong") return [];
8558
+ if (c1 === c2) return [];
8559
+ if (a === c1 || a === c2) return [];
8560
+ const r2 = 2;
8561
+ const declId = ctx.clauses.find((c) => /tiếp\s*xúc/u.test(c.text))?.id;
8562
+ return [
8563
+ {
8564
+ ruleId: "two-circles-tangent",
8565
+ clauseIds: declId === void 0 ? [] : [declId],
8566
+ intents: [
8567
+ // A = gốc; O bên trái cách r1, O' bên phải cách r2 → tiếp xúc ngoài tại A.
8568
+ addPoint(a, { kind: "free", at: [0, 0] }),
8569
+ addPoint(c1, { kind: "free", at: [-3, 0] }),
8570
+ addPoint(c2, { kind: "free", at: [r2, 0] }),
8571
+ drawCircle(`${c1}_t`, "centerThrough", { center: c1, through: a }),
8572
+ drawCircle(`${c2}_t`, "centerThrough", { center: c2, through: a })
8573
+ ]
8574
+ }
8575
+ ];
8576
+ }
8577
+ };
8578
+
8579
+ // src/stamps/geometry-2d/ai/rules/commonTangent.ts
8580
+ var PREFILTER65 = /[Tt]iếp\s*tuyến\s+chung/u;
8581
+ var TANGENT_GLUE = /[Tt]iếp\s*tuyến\s+chung\s+(?:(?:ngoài|trong)\s+)?([A-Z])([A-Z])(?![A-Za-z])/u;
8582
+ var TANGENT_NAMED_BEFORE2 = /([A-Z])([A-Z])(?![A-Za-z])\s+là\s+tiếp\s*tuyến\s+chung/u;
8583
+ var INTERNAL = /tiếp\s*tuyến\s+chung\s+trong/u;
8584
+ var CIRCLE_G = /\(\s*([A-Z]['′]?)(?:\s*[;,]\s*(?:[Rr]['′]?|\d+(?:[.,]\d+)?\s*[a-z]*))?\s*\)/gu;
8585
+ var CIRC2 = "\\(\\s*([A-Z]['\u2032]?)(?:\\s*[;,]\\s*(?:[Rr]['\u2032]?|\\d+(?:[.,]\\d+)?\\s*[a-z]*))?\\s*\\)";
8586
+ var TANGENT_TOUCH = new RegExp(
8587
+ "ti\u1EBFp\\s*x\xFAc\\s+(?:v\u1EDBi\\s+)?" + CIRC2 + "\\s*(?:,|v\xE0)\\s*" + CIRC2 + "\\s+(?:l\u1EA7n\\s*l\u01B0\u1EE3t\\s+|t\u01B0\u01A1ng\\s+\u1EE9ng\\s+|theo\\s+th\u1EE9\\s+t\u1EF1\\s+)?(?:t\u1EA1i|\u1EDF)\\s+(?:c\xE1c\\s+)?(?:\u0111i\u1EC3m\\s+)?([A-Z]['\u2032]?)(?![A-Za-z])\\s*(?:,|v\xE0)\\s*([A-Z]['\u2032]?)(?![A-Za-z])",
8588
+ "u"
8589
+ );
8590
+ var norm4 = (s) => s.replace(/′/g, "'");
8591
+ function membership(problem, pt) {
8592
+ const re = new RegExp(
8593
+ pt + "\\s*(?:\u2208|thu\u1ED9c|n\u1EB1m\\s+tr\xEAn)\\s*(?:[\u0110\u0111]\u01B0[\u1EDD\u01A1]ng\\s*tr\xF2n\\s*)?(?:\\(\\s*)?(?:t\xE2m\\s+)?([A-Z]['\u2032]?)",
8594
+ "u"
8595
+ );
8596
+ const m = re.exec(problem);
8597
+ return m ? norm4(m[1]) : void 0;
8598
+ }
8599
+ var commonTangentRule = {
8600
+ id: "common-tangent",
8601
+ // Cạnh twoCirclesTangent (74): 2 đtròn + tiếp điểm là gốc nhiều phái sinh.
8602
+ priority: 73,
8603
+ languages: ["vi"],
8604
+ patterns: [PREFILTER65],
8605
+ match(ctx) {
8606
+ const p = ctx.problem;
8607
+ let t1;
8608
+ let t2;
8609
+ let c1;
8610
+ let c2;
8611
+ const mt = TANGENT_TOUCH.exec(p);
8612
+ if (mt) {
8613
+ c1 = norm4(mt[1]);
8614
+ c2 = norm4(mt[2]);
8615
+ t1 = norm4(mt[3]);
8616
+ t2 = norm4(mt[4]);
8617
+ } else {
8618
+ const mg = TANGENT_GLUE.exec(p);
8619
+ if (mg) {
8620
+ t1 = mg[1];
8621
+ t2 = mg[2];
8622
+ } else {
8623
+ const mb = TANGENT_NAMED_BEFORE2.exec(p);
8624
+ if (mb) {
8625
+ t1 = mb[1];
8626
+ t2 = mb[2];
8627
+ }
8628
+ }
8629
+ }
8630
+ if (!t1 || !t2 || t1 === t2) return [];
8631
+ const m1 = c1 ? void 0 : membership(p, t1);
8632
+ const m2 = c2 ? void 0 : membership(p, t2);
8633
+ if (c1 && c2) ; else if (m1 && m2 && m1 !== m2) {
8634
+ c1 = m1;
8635
+ c2 = m2;
8636
+ } else {
8637
+ const seen = [];
8638
+ CIRCLE_G.lastIndex = 0;
8639
+ let cm;
8640
+ while ((cm = CIRCLE_G.exec(p)) !== null) {
8641
+ const nm = norm4(cm[1]);
8642
+ if (!seen.includes(nm)) seen.push(nm);
8643
+ if (seen.length === 2) break;
8644
+ }
8645
+ if (seen.length === 2) {
8646
+ [c1, c2] = seen;
8647
+ }
8648
+ }
8649
+ if (!c1 || !c2 || c1 === c2) return [];
8650
+ if (t1 === c1 || t1 === c2 || t2 === c1 || t2 === c2) return [];
8651
+ const variant = INTERNAL.test(p) ? "internal" : "external";
8652
+ const r1 = 3;
8653
+ const r2 = 2;
8654
+ const declId = ctx.clauses.find((c) => /tiếp\s*tuyến\s+chung/u.test(c.text))?.id;
8655
+ const intents = [
8656
+ addPoint(c1, { kind: "free", at: [0, 0] }),
8657
+ addPoint(c2, { kind: "free", at: [12, 0] }),
8658
+ drawCircle(c1, "centerRadius", { center: c1, radius: r1 }),
8659
+ drawCircle(c2, "centerRadius", { center: c2, radius: r2 }),
8660
+ addPoint(t1, { kind: "commonTangentPoint", circles: [c1, c2], on: 0, variant, side: 0 }),
8661
+ addPoint(t2, { kind: "commonTangentPoint", circles: [c1, c2], on: 1, variant, side: 0 }),
8662
+ connect(t1, t2, "segment")
8663
+ ];
8664
+ return [
8665
+ {
8666
+ ruleId: "common-tangent",
8667
+ clauseIds: declId === void 0 ? [] : [declId],
8668
+ intents
8669
+ }
8670
+ ];
8671
+ }
8672
+ };
8673
+
8674
+ // src/stamps/geometry-2d/ai/rules/twoCirclesCenterRadiusMeet.ts
8675
+ var CENTER_RADIUS = new RegExp(
8676
+ "([A-Z])(?![A-Za-z])\\s+l\xE0m\\s+t\xE2m\\s*,?\\s*(?:v\u1EBD|d\u1EF1ng|k\u1EBB)\\s+" + CIRCLE_KW + "\\s+b\xE1n\\s*k\xEDnh\\s+([A-Z])([A-Z])(?![A-Za-z])",
8677
+ "gu"
8678
+ );
8679
+ var MEET = new RegExp(
8680
+ "[Hh]ai\\s+" + CIRCLE_KW + "\\s*(?:n\xE0y\\s+)?c\u1EAFt\\s+nhau\\s+(?:(?:\u1EDF|t\u1EA1i)\\s+)?(?:\u0111i\u1EC3m\\s+th\u1EE9\\s+hai\\s+)?(?:l\xE0|t\u1EA1i)\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?![A-Za-z])",
8681
+ "u"
8682
+ );
8683
+ var PREFILTER66 = /làm\s+tâm[^.]{0,40}?bán\s*kính/u;
8684
+ var twoCirclesCenterRadiusMeetRule = {
8685
+ id: "two-circles-center-radius-meet",
8686
+ // Cạnh twoCirclesMeet (74): 2 đtròn + giao điểm là gốc phái sinh.
8687
+ priority: 73,
8688
+ languages: ["vi"],
8689
+ patterns: [PREFILTER66],
8690
+ match(ctx) {
8691
+ const p = ctx.problem;
8692
+ CENTER_RADIUS.lastIndex = 0;
8693
+ const circles = [];
8694
+ let cm;
8695
+ while ((cm = CENTER_RADIUS.exec(p)) !== null) {
8696
+ const center = cm[1];
8697
+ const a = cm[2];
8698
+ const b = cm[3];
8699
+ if (a !== center || b === center) continue;
8700
+ circles.push({ center, through: b });
8701
+ }
8702
+ if (circles.length !== 2) return [];
8703
+ const [k1, k2] = circles;
8704
+ if (k1.center === k2.center) return [];
8705
+ if (k1.through !== k2.through) return [];
8706
+ const mm = MEET.exec(p);
8707
+ if (!mm) return [];
8708
+ const d = mm[1];
8709
+ if (d === k1.center || d === k2.center || d === k1.through) return [];
8710
+ const clauseIds = ctx.clauses.filter((c) => /làm\s+tâm/u.test(c.text) || /cắt\s+nhau/u.test(c.text)).map((c) => c.id);
8711
+ const intents = [
8712
+ drawCircle(k1.center, "centerThrough", { center: k1.center, through: k1.through }),
8713
+ drawCircle(k2.center, "centerThrough", { center: k2.center, through: k2.through }),
8714
+ addPoint(d, { kind: "circleIntersection", c1: k1.center, c2: k2.center, which: 1 })
8715
+ ];
8716
+ return [
8717
+ {
8718
+ ruleId: "two-circles-center-radius-meet",
8719
+ clauseIds,
8720
+ intents
8721
+ }
8722
+ ];
8723
+ }
8724
+ };
8725
+
8726
+ // src/stamps/geometry-2d/ai/rules/rightAngleVertexCircles.ts
8727
+ var PREFILTER67 = /góc\s+vuông\s+[a-z][A-Z][a-z]/u;
8728
+ var RIGHT_ANGLE = /góc\s+vuông\s+([a-z])([A-Z])([a-z])(?![\p{L}])/u;
8729
+ var TWO_ON_RAYS = new RegExp(
8730
+ "L\u1EA5y\\s+(?:c\xE1c\\s+|hai\\s+)?\u0111i\u1EC3m\\s+([A-Z])\\s+v\xE0\\s+([A-Z])(?![A-Z])\\s+(?:l\u1EA7n\\s*l\u01B0\u1EE3t\\s+)?tr\xEAn\\s+(?:c\xE1c\\s+|hai\\s+)?tia\\s+[A-Z][a-z]\\s+v\xE0\\s+[A-Z][a-z]",
8731
+ "u"
8732
+ );
8733
+ var CIRCLE_CUTS_RAY = new RegExp(
8734
+ "[Vv]\u1EBD\\s+[\u0110\u0111]\u01B0[\u1EDD\u01A1]ng\\s*tr\xF2n\\s*\\(\\s*([A-Z])\\s*[;,]\\s*([A-Z])([A-Z])\\s*\\)\\s*c\u1EAFt\\s+tia\\s+([A-Z])([a-z])\\s+(?:t\u1EA1i|\u1EDF)\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?![A-Z])",
8735
+ "gu"
8736
+ );
8737
+ var rightAngleVertexCirclesRule = {
8738
+ id: "right-angle-vertex-circles",
8739
+ priority: 76,
8740
+ // dựng bộ khung góc + điểm sớm (trên twoCirclesMeet/tangent 74).
8741
+ languages: ["vi"],
8742
+ patterns: [PREFILTER67],
8743
+ match(ctx) {
8744
+ const ra = RIGHT_ANGLE.exec(ctx.problem);
8745
+ if (!ra) return [];
8746
+ const o = ra[2];
8747
+ const xRay = ra[1];
8748
+ const yRay = ra[3];
8749
+ let pI = "";
8750
+ let pK = "";
8751
+ for (const c of ctx.clauses) {
8752
+ const t = TWO_ON_RAYS.exec(c.text);
8753
+ if (t) {
8754
+ pI = t[1];
8755
+ pK = t[2];
8756
+ break;
8757
+ }
8758
+ }
8759
+ if (!pI || !pK || pI === pK || pI === o || pK === o) return [];
8760
+ const xEnd = xRay.toUpperCase() === o ? `${o}x` : xRay.toUpperCase();
8761
+ const yEnd = yRay.toUpperCase() === o ? `${o}y` : yRay.toUpperCase();
8762
+ const used = /* @__PURE__ */ new Set([o, pI, pK]);
8763
+ const ax = used.has(xEnd) ? "X9" : xEnd;
8764
+ const ay = used.has(yEnd) ? "Y9" : yEnd;
8765
+ const intents = [
8766
+ addPoint(o, { kind: "free", at: [0, 0] }),
8767
+ addPoint(ax, { kind: "free", at: [8, 0] }),
8768
+ addPoint(ay, { kind: "free", at: [0, 8] }),
8769
+ addPoint(pI, { kind: "onSegment", of: `${o}${ax}`, t: 0.35 }),
8770
+ addPoint(pK, { kind: "onSegment", of: `${o}${ay}`, t: 0.35 }),
8771
+ connect(o, ax, "segment"),
8772
+ connect(o, ay, "segment")
8773
+ ];
8774
+ for (const c of ctx.clauses) {
8775
+ CIRCLE_CUTS_RAY.lastIndex = 0;
8776
+ for (const m of c.text.matchAll(CIRCLE_CUTS_RAY)) {
8777
+ const center = m[1];
8778
+ const rayVertex = m[4];
8779
+ const rayDir = m[5];
8780
+ const hit = m[6];
8781
+ if (![pI, pK, o].includes(center)) continue;
8782
+ if (hit === center || hit === o) continue;
8783
+ const rayAxisEnd = rayDir.toUpperCase() === xRay.toUpperCase() ? ax : rayDir.toUpperCase() === yRay.toUpperCase() ? ay : "";
8784
+ if (!rayAxisEnd || rayVertex !== o) continue;
8785
+ const otherCenter = center === pI ? pK : pI;
8786
+ const circ = `${center}_c`;
8787
+ intents.push(drawCircle(circ, "centerThrough", { center, through: otherCenter }));
8788
+ intents.push(
8789
+ addPoint(hit, { kind: "secondIntersection", line: `${o}${rayAxisEnd}`, circle: circ, other: o })
8790
+ );
8791
+ }
8792
+ }
8793
+ return [{ ruleId: "right-angle-vertex-circles", clauseIds: ctx.clauses.map((c) => c.id), intents }];
8794
+ }
8795
+ };
8796
+
8797
+ // src/stamps/geometry-2d/ai/rules/namedLine.ts
8798
+ var LC = String.raw`([a-z]{1,2}[0-9]?)(?!\p{L})`;
8799
+ var REF2 = String.raw`(?:${DUONG_KW}\s*thẳng\s+|đoạn(?:\s+thẳng)?\s+|cạnh\s+|tia\s+)?([A-Z]\s*[A-Z])(?![A-Z])`;
8800
+ var PT3 = String.raw`([A-Z])(?![A-Za-z])`;
8801
+ var VERB = String.raw`(?:[Cc]ho|[Vv]ẽ|[Kk]ẻ|[Dd]ựng)\s+(?:một\s+)?${DUONG_KW}\s*thẳng\s+`;
8802
+ var DECL = String.raw`(?:(?:và|[Cc]ho|[Vv]ẽ|[Kk]ẻ|[Dd]ựng)\s+)?(?:một\s+)?${DUONG_KW}\s*thẳng\s+`;
8803
+ var PERP = new RegExp(
8804
+ DECL + LC + String.raw`\s*(?:⊥\s*|vuông\s*góc(?:\s+với)?\s+)(?:với\s+)?` + REF2 + String.raw`(?:\s*,?\s*cắt\s+[A-Z]{2})?\s+(?:tại|ở)\s+(?:điểm\s+)?` + PT3,
8805
+ "gu"
8806
+ );
8807
+ var PARA_AFTER = new RegExp(
8808
+ VERB + LC + String.raw`\s*(?://\s*|song\s*song(?:\s+với)?\s+)(?:với\s+)?` + REF2 + String.raw`\s+(?:đi\s+)?qua\s+(?:điểm\s+)?` + PT3,
8809
+ "gu"
8810
+ );
8811
+ var PARA_BEFORE = new RegExp(
8812
+ String.raw`(?:[Qq]ua|[Tt]ừ)\s+(?:điểm\s+)?` + PT3 + String.raw`[^.]{0,12}?` + VERB + LC + String.raw`\s*(?://\s*|song\s*song(?:\s+với)?\s+)(?:với\s+)?` + REF2,
8813
+ "gu"
8814
+ );
8815
+ var THRU_TWO = new RegExp(
8816
+ VERB + LC + String.raw`\s+(?:đi\s+)?qua\s+(?:hai\s+điểm\s+)?` + PT3 + String.raw`\s+và\s+(?:điểm\s+)?` + PT3,
8817
+ "gu"
8818
+ );
8819
+ var SECANT_LINE = new RegExp(
8820
+ DECL + LC + String.raw`[^.]{0,40}?cắt[^.]{0,30}?(?:tại|ở)\s+(?:2\s+|hai\s+)?(?:điểm\s+)?` + PT3 + String.raw`\s+và\s+(?:điểm\s+)?` + PT3,
8821
+ "gu"
8822
+ );
8823
+ var FREE_DECL = new RegExp(
8824
+ String.raw`(?:[Cc]ho|[Vv]ẽ|[Kk]ẻ|[Dd]ựng)\s+(?:một\s+)?${DUONG_KW}\s*thẳng\s+` + LC + // KHÔNG theo sau bởi ràng buộc: ⊥/vuông góc, //|song song, (đi)?qua, cắt,
8825
+ // tiếp xúc. Cho phép ranh giới câu / "và" / "," / "(".
8826
+ String.raw`(?!\s*(?:⊥|//|vuông\s*góc|song\s*song|(?:đi\s+)?qua|cắt|tiếp\s*xúc|tại|ở)\b)`,
8827
+ "gu"
8828
+ );
8829
+ function lineReferenced(problem, name) {
8830
+ const esc = escapeRe(name);
8831
+ const N = String.raw`(?<![\p{L}\d])${esc}(?![\p{L}\d])`;
8832
+ const REFS = [
8833
+ // trước "d": "⊥ d", "song song d", "thuộc/trên/nằm trên d", "cắt … d", "của d"
8834
+ new RegExp(String.raw`(?:⊥|vuông\s*góc(?:\s+với)?|//|song\s*song(?:\s+với)?|thuộc|trên|cắt|tiếp\s*tuyến\s+(?:của|với)?)\s+(?:${N})`, "u"),
8835
+ // sau "d": "d cắt", "d tại", "d lấy", "d, "
8836
+ new RegExp(N + String.raw`\s+(?:cắt|tại|ở|lấy)`, "u")
8837
+ ];
8838
+ return REFS.some((re) => re.test(problem));
8839
+ }
8840
+ var PREFILTER68 = new RegExp(
8841
+ String.raw`${DUONG_KW}\s*thẳng\s+[a-z]{1,2}[0-9]?(?![\p{L}])`,
8842
+ "u"
8843
+ );
8844
+ var namedLineRule = {
8845
+ id: "named-line",
8846
+ // Cao (63) như tangentLineNamedAtPoint — đường <d> phải dựng TRƯỚC điểm-trên-d
8847
+ // / intersection tham chiếu d.
8848
+ priority: 63,
8849
+ languages: ["vi"],
8850
+ patterns: [PREFILTER68],
8851
+ match(ctx) {
8852
+ const out = [];
8853
+ const push = (clauseId, intents) => {
8854
+ out.push({ ruleId: "named-line", clauseIds: [clauseId], intents });
8855
+ };
8856
+ for (const c of ctx.clauses) {
8857
+ const seen = /* @__PURE__ */ new Set();
8858
+ PERP.lastIndex = 0;
8859
+ for (const m of c.text.matchAll(PERP)) {
8860
+ const line = m[1];
8861
+ const to = m[2].replace(/\s+/g, "");
8862
+ const anchor = m[3];
8863
+ if (seen.has(line)) continue;
8864
+ seen.add(line);
8865
+ push(c.id, [drawLine(line, "perpThrough", { through: anchor, to })]);
8866
+ }
8867
+ PARA_AFTER.lastIndex = 0;
8868
+ for (const m of c.text.matchAll(PARA_AFTER)) {
8869
+ const line = m[1];
8870
+ const to = m[2].replace(/\s+/g, "");
8871
+ const anchor = m[3];
8872
+ if (seen.has(line) || to.includes(anchor)) continue;
8873
+ seen.add(line);
8874
+ push(c.id, [drawLine(line, "parallelThrough", { through: anchor, to })]);
8875
+ }
8876
+ PARA_BEFORE.lastIndex = 0;
8877
+ for (const m of c.text.matchAll(PARA_BEFORE)) {
8878
+ const anchor = m[1];
8879
+ const line = m[2];
8880
+ const to = m[3].replace(/\s+/g, "");
8881
+ if (seen.has(line) || to.includes(anchor)) continue;
8882
+ seen.add(line);
8883
+ push(c.id, [drawLine(line, "parallelThrough", { through: anchor, to })]);
8884
+ }
8885
+ THRU_TWO.lastIndex = 0;
8886
+ for (const m of c.text.matchAll(THRU_TWO)) {
8887
+ const line = m[1];
8888
+ const a = m[2];
8889
+ const b = m[3];
8890
+ if (seen.has(line) || a === b) continue;
8891
+ seen.add(line);
8892
+ push(c.id, [drawLine(line, "lineThrough", { points: [a, b] })]);
8893
+ }
8894
+ SECANT_LINE.lastIndex = 0;
8895
+ for (const m of c.text.matchAll(SECANT_LINE)) {
8896
+ const line = m[1];
8897
+ const a = m[2];
8898
+ const b = m[3];
8899
+ if (seen.has(line) || a === b) continue;
8900
+ seen.add(line);
8901
+ push(c.id, [drawLine(line, "lineThrough", { points: [a, b] })]);
8902
+ }
8903
+ FREE_DECL.lastIndex = 0;
8904
+ for (const m of c.text.matchAll(FREE_DECL)) {
8905
+ const line = m[1];
8906
+ if (seen.has(line) || !lineReferenced(ctx.problem, line)) continue;
8907
+ seen.add(line);
8908
+ const p1 = `${line}_p1`;
8909
+ const p2 = `${line}_p2`;
8910
+ push(c.id, [
8911
+ addPoint(p1, { kind: "free" }),
8912
+ addPoint(p2, { kind: "free" }),
8913
+ drawLine(line, "lineThrough", { points: [p1, p2] })
8914
+ ]);
8915
+ }
8916
+ }
8917
+ return out;
8918
+ }
8919
+ };
8920
+
8921
+ // src/stamps/geometry-2d/ai/rules/bisectorsMeet.ts
8922
+ var ANG_HEAD = "(?:\u2220\\s*|g\xF3c\\s+)([A-Z])([A-Z])([A-Z])(?![A-Z])";
8923
+ var ANG_TAIL = "(?:\u2220\\s*|g\xF3c\\s+)?([A-Z])([A-Z])([A-Z])(?![A-Z])";
8924
+ var RE30 = new RegExp(
8925
+ "[Pp]h\xE2n\\s*gi\xE1c\\s+" + ANG_HEAD + "\\s+v\xE0\\s+" + ANG_TAIL + "\\s+(?:c\u1EAFt|giao)\\s+nhau\\s+(?:t\u1EA1i|\u1EDF)\\s+(?:\u0111i\u1EC3m\\s+)?([A-Z])(?![A-Z])",
8926
+ "gu"
8927
+ );
8928
+ var PREFILTER69 = /[Pp]hân\s*giác[^.]{0,80}?(?:cắt|giao)\s+nhau\s+(?:tại|ở)/u;
8929
+ var bisectorsMeetRule = {
8930
+ id: "bisectorsMeet",
8931
+ // Trên intersection (45) để các tia phân giác (draw-line) + điểm giao gom cùng
8932
+ // 1 match; intent-layer order-retry topo lo thứ tự build. 49 = ngang
8933
+ // twoPerpLinesMeet (cùng họ "2 đường đặt-tên giao nhau").
8934
+ priority: 49,
8935
+ languages: ["vi"],
8936
+ patterns: [PREFILTER69],
8937
+ match(ctx) {
8938
+ const out = [];
8939
+ for (const c of ctx.clauses) {
8940
+ RE30.lastIndex = 0;
8941
+ for (const m of c.text.matchAll(RE30)) {
8942
+ const a1 = [m[1], m[2], m[3]];
8943
+ const a2 = [m[4], m[5], m[6]];
8944
+ const p = m[7];
8945
+ const n1 = `bis${a1[0]}${a1[1]}${a1[2]}`;
8946
+ const n2 = `bis${a2[0]}${a2[1]}${a2[2]}`;
8947
+ const intents = [
8948
+ drawLine(n1, "angleBisector", { p1: a1[0], vertex: a1[1], p2: a1[2] }),
8949
+ drawLine(n2, "angleBisector", { p1: a2[0], vertex: a2[1], p2: a2[2] })
8950
+ ];
8951
+ const angleLetters = /* @__PURE__ */ new Set([...a1, ...a2]);
8952
+ if (n1 !== n2 && !angleLetters.has(p)) {
8953
+ intents.push(addPoint(p, { kind: "intersection", of: [n1, n2] }));
8954
+ }
8955
+ out.push({ ruleId: "bisectorsMeet", clauseIds: [c.id], intents });
8956
+ }
8957
+ }
8958
+ return out;
8959
+ }
8960
+ };
8961
+
8962
+ // src/stamps/geometry-2d/ai/rules/hexagon.ts
8963
+ var HEX = "([A-Z])([A-Z])([A-Z])([A-Z])([A-Z])([A-Z])(?![A-Z])";
8964
+ var HEXAGON_DECL = new RegExp("[Ll]\u1EE5c\\s+gi\xE1c\\s+" + HEX, "gu");
8965
+ var PREFILTER70 = /[Ll]ục\s+giác/u;
8966
+ var HEX_RADIUS = 5;
8967
+ var HEX_APOTHEM = HEX_RADIUS * Math.cos(Math.PI / 6);
8968
+ var HEX_THETAS = Array.from(
8969
+ { length: 6 },
8970
+ (_, i) => Math.PI / 2 - i * Math.PI / 3
8971
+ );
8972
+ function hexVertexCoord(i) {
8973
+ const t = HEX_THETAS[i];
8974
+ return [
8975
+ +(HEX_RADIUS * Math.cos(t)).toFixed(4),
8976
+ +(HEX_RADIUS * Math.sin(t)).toFixed(4)
8977
+ ];
8978
+ }
8979
+ var CENTER4 = "(?:\\(\\s*([A-Z])\\s*\\)|t\xE2m\\s+([A-Z]))";
8980
+ var HEX_CIRCUMSCRIBES = new RegExp(
8981
+ "^[\\s,]*ngo\u1EA1i\\s*ti\u1EBFp\\s+(?:(?:" + CIRCLE_KW + ")\\s*)?" + CENTER4,
8982
+ "u"
8983
+ );
8984
+ var HEX_INSCRIBED_BY = new RegExp(
8985
+ "(?:" + CIRCLE_KW + ")\\s*" + CENTER4 + "\\s*n\u1ED9i\\s*ti\u1EBFp\\s+$",
8986
+ "u"
8987
+ );
8988
+ function detectInscribedCircle(text, start, afterEnd) {
8989
+ const a = HEX_CIRCUMSCRIBES.exec(text.slice(afterEnd));
8990
+ if (a) return a[1] ?? a[2] ?? "O";
8991
+ const b = HEX_INSCRIBED_BY.exec(text.slice(0, start));
8992
+ if (b) return b[1] ?? b[2] ?? "O";
8993
+ return void 0;
8994
+ }
8995
+ var hexagonRule = {
8996
+ id: "hexagon",
8997
+ priority: 100,
8998
+ // ngang quad (đa giác cơ sở) — gate riêng prefilter "lục giác".
7160
8999
  languages: ["vi"],
7161
- patterns: [
7162
- /(?:[Gg]óc|∠)[^.;\n]{0,18}90/u,
7163
- /(?:[Gg]óc|∠)\s*[A-Z]{3}\s*vuông/u,
7164
- /[A-Z]{2}\s*(?:⊥|vuông\s*góc)\s*[A-Z]{2}/u
7165
- ],
9000
+ patterns: [PREFILTER70],
7166
9001
  match(ctx) {
7167
- const spec = detectSpec(ctx.problem);
7168
- if (!spec) return [];
7169
- const clauseIds = ctx.clauses.filter(
7170
- (c) => ANGLE_EQ_RE.test(c.text) || ANGLE_VUONG_RE.test(c.text) || PERP_RE.test(c.text) || findOnLine(c.text, spec.vertex) !== null
7171
- ).map((c) => c.id);
7172
- const out = [
7173
- {
7174
- ruleId: "rightAngleViewing",
7175
- clauseIds,
7176
- intents: [
7177
- addPoint(spec.vertex, {
7178
- kind: "rightAngleViewing",
7179
- a: spec.a,
7180
- b: spec.b,
7181
- onLine: spec.onLine
7182
- })
7183
- ]
9002
+ const out = [];
9003
+ for (const c of ctx.clauses) {
9004
+ HEXAGON_DECL.lastIndex = 0;
9005
+ for (const m of c.text.matchAll(HEXAGON_DECL)) {
9006
+ const labels = [m[1], m[2], m[3], m[4], m[5], m[6]];
9007
+ const start = m.index ?? 0;
9008
+ const afterEnd = start + m[0].length;
9009
+ const vertices = labels.map(
9010
+ (lbl, i) => addPoint(lbl, { kind: "free", at: hexVertexCoord(i) })
9011
+ );
9012
+ const center = detectInscribedCircle(c.text, start, afterEnd);
9013
+ const intents = [...vertices];
9014
+ if (center !== void 0) {
9015
+ const centerName = escapeRe(center) === center ? center : "O";
9016
+ intents.unshift(
9017
+ addPoint(centerName, {
9018
+ kind: "circumcenter",
9019
+ of: [labels[0], labels[2], labels[4]]
9020
+ })
9021
+ );
9022
+ intents.push(
9023
+ drawCircle(centerName, "centerRadius", {
9024
+ center: centerName,
9025
+ radius: HEX_APOTHEM
9026
+ })
9027
+ );
9028
+ }
9029
+ intents.push(markShape("polygon", labels));
9030
+ out.push({ ruleId: "hexagon", clauseIds: [c.id], intents });
7184
9031
  }
7185
- ];
9032
+ }
7186
9033
  return out;
7187
9034
  }
7188
9035
  };
7189
9036
 
7190
9037
  // src/stamps/geometry-2d/ai/rules/registry.ts
7191
9038
  var RULES = [
9039
+ namedLineRule,
9040
+ collinearPointsRule,
9041
+ quadDiagonalsRule,
9042
+ twoCirclesTangentRule,
9043
+ commonTangentRule,
9044
+ twoCirclesCenterRadiusMeetRule,
9045
+ rightAngleVertexCirclesRule,
7192
9046
  circumcircleCutsLineRule,
7193
9047
  lineConcurrencyRule,
7194
9048
  parallelThroughCutsCircleRule,
7195
9049
  lineThroughCutsTwoLinesRule,
7196
9050
  rightAngleViewingRule,
7197
9051
  triangleRule,
9052
+ inscribedSquareRule,
9053
+ hexagonRule,
7198
9054
  quadRule,
7199
9055
  connectRule,
7200
9056
  midpointRule,
@@ -7207,6 +9063,7 @@ var RULES = [
7207
9063
  circleTriangleRule,
7208
9064
  tangentFromExtRule,
7209
9065
  externalPointRule,
9066
+ externalPointAtRadiusRule,
7210
9067
  arcMidpointRule,
7211
9068
  excenterRule,
7212
9069
  parallelPerpRule,
@@ -7246,6 +9103,7 @@ var RULES = [
7246
9103
  tangentAtCutsLinesRule,
7247
9104
  diameterEndpointRule,
7248
9105
  circleThroughTwoCutsSidesRule,
9106
+ circleCenterRadiusCutsSidesRule,
7249
9107
  circumcircleNamedDistribRule,
7250
9108
  circumcirclePairMeetRule,
7251
9109
  tangentCircleAtPointRule,
@@ -7274,7 +9132,8 @@ var RULES = [
7274
9132
  twoCirclesMeetRule,
7275
9133
  radiusRule,
7276
9134
  perpNamedLineRule,
7277
- parallelSidePointsRule
9135
+ parallelSidePointsRule,
9136
+ bisectorsMeetRule
7278
9137
  ];
7279
9138
  var ALL_RULES = [...RULES].sort(
7280
9139
  (a, b) => b.priority - a.priority
@@ -7289,20 +9148,329 @@ function runRules(ctx) {
7289
9148
  }
7290
9149
 
7291
9150
  // src/stamps/geometry-2d/ai/deterministic/normalizeText.ts
7292
- var TRIANGLE_SYMBOL = /[Δ∆]\s*(?=[A-Z])/gu;
9151
+ var TRIANGLE_SYMBOL = /[Δ∆]\s*(?=[A-Z])/gu;
7293
9152
  var CIRCLE_SYNONYM = /vòng\s+tròn/giu;
9153
+ var CIRCLE_PAREN_RESTORE = /(đường\s*tròn\s+)([A-Z])(['′]?)\s*;\s*([Rr])(['′]?)(?![A-Za-z'′])/gu;
7294
9154
  var QUAD_SYMBOL = /[◊▱□]\s*(?=[A-Z])/gu;
9155
+ var MIDPOINT_TYPO = /trung\s*điển(?!\p{L})/gu;
7295
9156
  var SQRT_NOISE = /√/gu;
7296
9157
  var PRIME_VARIANT = /([A-Za-z0-9])[’′´]/gu;
9158
+ var DIAERESIS_O = /(?<!\p{Ll})Ö(?!\p{Ll})/gu;
9159
+ var INTERSECT_PAREN = /(\([A-Z]{3}\))\s+N\s+([A-Z]{2})(\s*=\s*\{)/gu;
7297
9160
  var GLUE_WORD_LABEL = /(cắt|tâm|điểm|cạnh|tia|dây|cung|qua|của|và|tại|trên|đến)([A-Z])/gu;
7298
9161
  var GLUE_LABEL_WORD = /([A-Z])(sao|nằm|lần|thuộc|cắt|của|đến|trên|tại|và)/gu;
9162
+ var GLUE_RAYTOKEN_WORD = /([A-Z][a-z])(tại|cắt|và|của|đến|trên|thuộc)(?![\p{L}])/gu;
9163
+ var GLUE_VOCAB = [
9164
+ "\u0111\u01B0\u1EDDng",
9165
+ "ngo\u1EA1i",
9166
+ "trung",
9167
+ "ph\xE2n",
9168
+ "\u0111i\u1EC3m",
9169
+ "gi\xE1c",
9170
+ "tr\xF2n",
9171
+ "c\u1EA1nh",
9172
+ "k\xEDnh",
9173
+ "n\u1ED9i",
9174
+ "ti\u1EBFp",
9175
+ "cung",
9176
+ "d\xE2y",
9177
+ "tam",
9178
+ "t\u1EE9",
9179
+ "g\xF3c",
9180
+ "vu\xF4ng",
9181
+ "c\xE2n",
9182
+ "nh\u1ECDn",
9183
+ "n\u1EEDa",
9184
+ "\u0111o\u1EA1n",
9185
+ "h\xECnh",
9186
+ "qua",
9187
+ "c\u1EAFt",
9188
+ "l\u1EA1i",
9189
+ "c\xE1c",
9190
+ "v\xE0",
9191
+ "t\u1EA1i",
9192
+ "m\u1ED9t",
9193
+ "ba",
9194
+ "b\u1EA5t",
9195
+ "k\xEC",
9196
+ "k\u1EF3",
9197
+ "c\xF3",
9198
+ "\u0111i",
9199
+ "v\u1EDBi",
9200
+ "l\u1EA5y",
9201
+ "c\u1EE7a",
9202
+ "l\u1EA7n",
9203
+ "l\u01B0\u1EE3t",
9204
+ "th\u1EE9",
9205
+ "hai",
9206
+ "g\u1ECDi",
9207
+ "k\u1EBB",
9208
+ "v\u1EBD",
9209
+ "cho",
9210
+ "t\xE2m",
9211
+ "b\xE1n",
9212
+ // 'là' (hệ từ) + từ-vựng hình-chiếu/⊥ — tách glue "làtrung"/"làhình"/
9213
+ // "chiếucủa"/"xuốngđường" (OCR HHP dày). Whitelist↔whitelist nên 'là' chỉ tách
9214
+ // khi theo NGAY bởi một từ-khoá hình-học khác (an toàn, regression-gated).
9215
+ "l\xE0",
9216
+ "chi\u1EBFu",
9217
+ "xu\u1ED1ng",
9218
+ "l\xEAn",
9219
+ "\u0111\u1ED1i",
9220
+ "x\u1EE9ng",
9221
+ "song",
9222
+ "th\u1EB3ng"
9223
+ ];
9224
+ var GLUE_VOCAB_SORTED = [...GLUE_VOCAB].sort((a, b) => b.length - a.length);
9225
+ var GLUE_ALT = GLUE_VOCAB_SORTED.join("|");
9226
+ var GLUE_VOCAB_RE = new RegExp(`(${GLUE_ALT})(?=${GLUE_ALT})`, "gu");
9227
+ var GLUE_VOCAB_LABEL = new RegExp(
9228
+ `(gi\xE1c|tr\xF2n|ti\u1EBFp|x\xFAc|k\xEDnh|\u0111i\u1EC3m|\u0111o\u1EA1n|c\u1EA1nh|d\xE2y|cung|t\u1EA1i|qua|\u0111i|v\u1EDBi|c\xF3|t\xE2m|th\u1EB3ng|l\xEAn|xu\u1ED1ng)([A-Z])`,
9229
+ "gu"
9230
+ );
9231
+ var CAP_GLUE = /(Cho|Đường|Gọi|Trên|Vẽ|Kẻ|Lấy|Các|Một|Trung|Tâm|Nửa|Hình)(đường|tam|tứ|nửa|hình|điểm|các|cạnh|đoạn|trung|tròn|giác|một|ba|hai|tia|dây|cung)/gu;
9232
+ var GLUE_LABEL_VOCAB = new RegExp(`([A-Z])(${GLUE_ALT})(?![\\p{L}])`, "gu");
9233
+ var GLUE_PAREN_OPEN = /(\p{Ll})\(/gu;
9234
+ var GLUE_PAREN_CLOSE = /\)(\p{L})/gu;
9235
+ function deglueMultiWord(s) {
9236
+ let out = s.replace(CAP_GLUE, "$1 $2");
9237
+ for (let k = 0; k < 8; k++) {
9238
+ const next = out.replace(GLUE_VOCAB_RE, "$1 ");
9239
+ if (next === out) break;
9240
+ out = next;
9241
+ }
9242
+ out = out.replace(GLUE_VOCAB_LABEL, "$1 $2").replace(GLUE_LABEL_VOCAB, "$1 $2").replace(GLUE_PAREN_OPEN, "$1 (").replace(GLUE_PAREN_CLOSE, ") $1");
9243
+ return out;
9244
+ }
7299
9245
  function normalizeProblemText(problem) {
7300
- return problem.replace(TRIANGLE_SYMBOL, "tam gi\xE1c ").replace(QUAD_SYMBOL, "t\u1EE9 gi\xE1c ").replace(CIRCLE_SYNONYM, "\u0111\u01B0\u1EDDng tr\xF2n").replace(SQRT_NOISE, " ").replace(PRIME_VARIANT, "$1'").replace(GLUE_WORD_LABEL, "$1 $2").replace(GLUE_LABEL_WORD, "$1 $2");
9246
+ let s = problem.replace(TRIANGLE_SYMBOL, "tam gi\xE1c ").replace(QUAD_SYMBOL, "t\u1EE9 gi\xE1c ").replace(MIDPOINT_TYPO, "trung \u0111i\u1EC3m").replace(DIAERESIS_O, "O").replace(INTERSECT_PAREN, "$1 \u2229 $2$3").replace(CIRCLE_SYNONYM, "\u0111\u01B0\u1EDDng tr\xF2n");
9247
+ if (!/\(\s*[A-Z]/u.test(s)) s = s.replace(CIRCLE_PAREN_RESTORE, "$1($2$3;$4$5)");
9248
+ return deglueMultiWord(
9249
+ s.replace(SQRT_NOISE, " ").replace(PRIME_VARIANT, "$1'").replace(GLUE_WORD_LABEL, "$1 $2").replace(GLUE_LABEL_WORD, "$1 $2").replace(GLUE_RAYTOKEN_WORD, "$1 $2")
9250
+ );
9251
+ }
9252
+
9253
+ // src/stamps/geometry-2d/ai/deterministic/correctUserInput.ts
9254
+ function foldVietnamese(s) {
9255
+ return s.toLowerCase().replace(/đ/g, "d").normalize("NFD").replace(/[̀-ͯ]/g, "");
9256
+ }
9257
+ function levenshtein(a, b) {
9258
+ const m = a.length;
9259
+ const n = b.length;
9260
+ if (m === 0) return n;
9261
+ if (n === 0) return m;
9262
+ let prev = new Array(n + 1);
9263
+ for (let j = 0; j <= n; j++) prev[j] = j;
9264
+ for (let i = 1; i <= m; i++) {
9265
+ const cur = new Array(n + 1);
9266
+ cur[0] = i;
9267
+ for (let j = 1; j <= n; j++) {
9268
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
9269
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);
9270
+ }
9271
+ prev = cur;
9272
+ }
9273
+ return prev[n];
9274
+ }
9275
+ var CORRECTION_VOCAB = [
9276
+ // circle / line core
9277
+ "\u0111\u01B0\u1EDDng",
9278
+ "tr\xF2n",
9279
+ "b\xE1n",
9280
+ "k\xEDnh",
9281
+ "v\xF2ng",
9282
+ // polygon / triangle (TAM before TÂM, THANG before THẲNG — see collision-winner warning above)
9283
+ "tam",
9284
+ "t\xE2m",
9285
+ "thang",
9286
+ "th\u1EB3ng",
9287
+ "gi\xE1c",
9288
+ "t\u1EE9",
9289
+ "g\xF3c",
9290
+ "c\u1EA1nh",
9291
+ "\u0111o\u1EA1n",
9292
+ "h\xECnh",
9293
+ "ch\u1EEF",
9294
+ "nh\u1EADt",
9295
+ "vu\xF4ng",
9296
+ "c\xE2n",
9297
+ "nh\u1ECDn",
9298
+ "\u0111\u1EC1u",
9299
+ "thoi",
9300
+ "b\xECnh",
9301
+ "h\xE0nh",
9302
+ // chord / arc / point
9303
+ "d\xE2y",
9304
+ "cung",
9305
+ "\u0111i\u1EC3m",
9306
+ "tia",
9307
+ "n\u1EEDa",
9308
+ // tangent / inscribed
9309
+ "ti\u1EBFp",
9310
+ "tuy\u1EBFn",
9311
+ "n\u1ED9i",
9312
+ "ngo\u1EA1i",
9313
+ "x\xFAc",
9314
+ // cevians / projection
9315
+ "trung",
9316
+ "tr\u1EF1c",
9317
+ "cao",
9318
+ "ph\xE2n",
9319
+ "chi\u1EBFu",
9320
+ "\u0111\u1ED1i",
9321
+ "x\u1EE9ng",
9322
+ // verbs / connectors phổ biến trong đề
9323
+ "cho",
9324
+ "g\u1ECDi",
9325
+ "v\u1EBD",
9326
+ "k\u1EBB",
9327
+ "l\u1EA5y",
9328
+ "qua",
9329
+ "c\u1EAFt",
9330
+ "tr\xEAn",
9331
+ "d\u01B0\u1EDBi",
9332
+ "\u0111\u1EBFn",
9333
+ // 'đi' ĐÃ GỠ: "di"→"đi" net-harmful trên corpus (regress 7 bài locus "di động"
9334
+ // → "đi động"). Từ TRẦN 2 ký tự đa-nghĩa: rủi ro cao, lợi ích thấp.
9335
+ "song",
9336
+ "v\u1EDBi",
9337
+ "c\u1EE7a",
9338
+ "v\xE0",
9339
+ "l\xE0",
9340
+ "c\xF3",
9341
+ "n\u1EB1m",
9342
+ "thu\u1ED9c",
9343
+ "t\u1EA1i",
9344
+ "m\u1ED9t",
9345
+ "hai",
9346
+ "ba",
9347
+ "c\xE1c",
9348
+ "l\u1EA7n",
9349
+ "l\u01B0\u1EE3t",
9350
+ "th\u1EE9",
9351
+ "b\u1EA5t",
9352
+ "k\xEC",
9353
+ "k\u1EF3",
9354
+ "trong",
9355
+ "ngo\xE0i",
9356
+ "l\xEAn",
9357
+ "xu\u1ED1ng"
9358
+ ];
9359
+ var FOLDED_VOCAB = (() => {
9360
+ const m = /* @__PURE__ */ new Map();
9361
+ for (const w of CORRECTION_VOCAB) {
9362
+ const f = foldVietnamese(w);
9363
+ if (!m.has(f)) m.set(f, w);
9364
+ }
9365
+ return m;
9366
+ })();
9367
+ var LETTERS_ONLY = /^\p{L}+$/u;
9368
+ var UPPER_LABEL = /^\p{Lu}{1,4}$/u;
9369
+ var HAS_UPPER = /\p{Lu}/u;
9370
+ function classifyToken(token) {
9371
+ if (!LETTERS_ONLY.test(token)) return "protected";
9372
+ if (UPPER_LABEL.test(token)) return "protected";
9373
+ return HAS_UPPER.test(token) ? "upper" : "lower";
9374
+ }
9375
+ var DEFAULT_CORRECT_CONFIG = {
9376
+ structure: true,
9377
+ accents: true,
9378
+ // Tầng 3 (fuzzy typo) MẶC ĐỊNH TẮT: trên corpus thật nó băm từ TRẦN hợp lệ
9379
+ // ("nhau"→"nhật", "giao"→"giác", "di"→"đi") vì vocab hình-học trùng không-gian
9380
+ // âm với văn xuôi Việt → net-harmful (đo qua diag-all). Vẫn cài đặt đầy đủ + bật
9381
+ // được qua flag (mutation test / opt-in tương lai). accents+structure đủ an toàn.
9382
+ typo: false,
9383
+ maxTypoDistance: 1,
9384
+ minTypoLen: 4
9385
+ };
9386
+ var SYMBOL_MAP = [
9387
+ // "độ" có dấu = độ-góc rõ ràng → luôn đổi. "do" trần mơ hồ (do đó/do dai/lý do)
9388
+ // → CHỈ đổi khi KHÔNG theo sau bởi một từ-chữ (cuối cụm / trước dấu câu), tránh
9389
+ // băm "2 do dai"→"2° dai". "90 do"/"90 do." vẫn đổi.
9390
+ [/(\d+)\s*độ(?!\p{L})/giu, "$1\xB0"],
9391
+ [/(\d+)\s*do(?!\s*\p{L})/giu, "$1\xB0"]
9392
+ ];
9393
+ function applyStructure(s) {
9394
+ let out = s;
9395
+ for (const [re, to] of SYMBOL_MAP) out = out.replace(re, to);
9396
+ return out.replace(/\s+/g, " ").trim();
9397
+ }
9398
+ function applyLeadingCase(canonical, original) {
9399
+ if (/^\p{Lu}/u.test(original)) {
9400
+ return canonical.charAt(0).toUpperCase() + canonical.slice(1);
9401
+ }
9402
+ return canonical;
9403
+ }
9404
+ function fuzzyLookup(folded, cfg) {
9405
+ if (folded.length < cfg.minTypoLen) return null;
9406
+ let best = null;
9407
+ let bestDist = cfg.maxTypoDistance + 1;
9408
+ let tie = false;
9409
+ for (const [key, canonical] of FOLDED_VOCAB) {
9410
+ if (canonical === key) continue;
9411
+ if (Math.abs(key.length - folded.length) > cfg.maxTypoDistance) continue;
9412
+ const d = levenshtein(folded, key);
9413
+ if (d < bestDist) {
9414
+ bestDist = d;
9415
+ best = canonical;
9416
+ tie = false;
9417
+ } else if (d === bestDist) {
9418
+ tie = true;
9419
+ }
9420
+ }
9421
+ if (best === null || bestDist > cfg.maxTypoDistance || tie) return null;
9422
+ return best;
9423
+ }
9424
+ function letterFold(token) {
9425
+ return foldVietnamese(token).replace(/[^a-z]+/g, "");
9426
+ }
9427
+ var DISAMBIG = {
9428
+ // "tam giác" giữ "tam"; còn lại (tâm O, tâm đường tròn…) → "tâm".
9429
+ tam: (_p, n) => n === "giac" ? "tam" : "t\xE2m",
9430
+ // "hình thang" giữ "thang"; còn lại (đường thẳng, thẳng hàng…) → "thẳng".
9431
+ thang: (p, _n) => p === "hinh" ? "thang" : "th\u1EB3ng",
9432
+ // "ngoại tiếp" → "ngoại"; còn lại (nằm ngoài, điểm ngoài…) → "ngoài".
9433
+ ngoai: (_p, n) => n === "tiep" ? "ngo\u1EA1i" : "ngo\xE0i"
9434
+ };
9435
+ function correctToken(token, cfg, prevFold = "", nextFold = "") {
9436
+ const klass = classifyToken(token);
9437
+ if (klass === "protected") return token;
9438
+ const folded = foldVietnamese(token);
9439
+ if (folded !== token.toLowerCase()) return token;
9440
+ if (cfg.accents) {
9441
+ const dis = DISAMBIG[folded];
9442
+ if (dis) return applyLeadingCase(dis(prevFold, nextFold), token);
9443
+ const exact = FOLDED_VOCAB.get(folded);
9444
+ if (exact) return applyLeadingCase(exact, token);
9445
+ }
9446
+ if (cfg.typo && klass === "lower") {
9447
+ const near = fuzzyLookup(folded, cfg);
9448
+ if (near) return near;
9449
+ }
9450
+ return token;
9451
+ }
9452
+ function correctUserInput(input, cfg = DEFAULT_CORRECT_CONFIG) {
9453
+ let s = input;
9454
+ if (cfg.structure) s = applyStructure(s);
9455
+ if (!cfg.accents && !cfg.typo) return s;
9456
+ const parts = s.split(/(\s+)/);
9457
+ const wordIdx = [];
9458
+ for (let i = 0; i < parts.length; i++) {
9459
+ if (parts[i] !== "" && !/\s/.test(parts[i])) wordIdx.push(i);
9460
+ }
9461
+ const out = parts.slice();
9462
+ for (let w = 0; w < wordIdx.length; w++) {
9463
+ const i = wordIdx[w];
9464
+ const prevFold = w > 0 ? letterFold(parts[wordIdx[w - 1]]) : "";
9465
+ const nextFold = w < wordIdx.length - 1 ? letterFold(parts[wordIdx[w + 1]]) : "";
9466
+ out[i] = correctToken(parts[i], cfg, prevFold, nextFold);
9467
+ }
9468
+ return out.join("");
7301
9469
  }
7302
9470
 
7303
9471
  // src/stamps/geometry-2d/ai/deterministic/runDeterministicIntents.ts
7304
9472
  function collectDeterministic(rawProblem) {
7305
- const problem = normalizeProblemText(rawProblem);
9473
+ const problem = normalizeProblemText(correctUserInput(rawProblem));
7306
9474
  const clauses = segmentClauses(problem);
7307
9475
  const drawableClauses = clauses.filter((c) => c.hasGeometry);
7308
9476
  let drawableProblem = problem;
@@ -7351,7 +9519,7 @@ var SHAPE_TRI = /tam giác\s+([A-Z]{3})(?![A-Z])/gu;
7351
9519
  var SHAPE_QUAD = /(?:tứ giác|hình\s+(?:vuông|chữ nhật|bình hành|thoi|thang))\s+([A-Z]{4})(?![A-Z])/gu;
7352
9520
  function collectExpectedNames(problem) {
7353
9521
  const names = /* @__PURE__ */ new Set();
7354
- const add = (re, groups) => {
9522
+ const add2 = (re, groups) => {
7355
9523
  re.lastIndex = 0;
7356
9524
  let m;
7357
9525
  while ((m = re.exec(problem)) !== null) {
@@ -7361,9 +9529,9 @@ function collectExpectedNames(problem) {
7361
9529
  }
7362
9530
  }
7363
9531
  };
7364
- add(NAMED_INTRO, [[1, 2]]);
7365
- add(NAMED_LA, [[1, 2]]);
7366
- add(NAMED_LANLUOT, [[1, 2], [3, 4]]);
9532
+ add2(NAMED_INTRO, [[1, 2]]);
9533
+ add2(NAMED_LA, [[1, 2]]);
9534
+ add2(NAMED_LANLUOT, [[1, 2], [3, 4]]);
7367
9535
  for (const re of [SHAPE_TRI, SHAPE_QUAD]) {
7368
9536
  re.lastIndex = 0;
7369
9537
  let m;
@@ -7614,6 +9782,12 @@ function rewriteCircleRefs(intent, rename) {
7614
9782
  if (newC1 !== c.c1 || newC2 !== c.c2) {
7615
9783
  return { ...intent, constraint: { ...c, c1: newC1, c2: newC2 } };
7616
9784
  }
9785
+ } else if (c.kind === "commonTangentPoint") {
9786
+ const n0 = rename.get(c.circles[0]) ?? c.circles[0];
9787
+ const n1 = rename.get(c.circles[1]) ?? c.circles[1];
9788
+ if (n0 !== c.circles[0] || n1 !== c.circles[1]) {
9789
+ return { ...intent, constraint: { ...c, circles: [n0, n1] } };
9790
+ }
7617
9791
  } else if (c.kind === "intersection") {
7618
9792
  const newOf = c.of.map((r) => rename.get(r) ?? r);
7619
9793
  if (newOf[0] !== c.of[0] || newOf[1] !== c.of[1]) {
@@ -7681,14 +9855,14 @@ function repairCircleIntersections(points, shapes) {
7681
9855
  const targetD = (lower + upper) / 2;
7682
9856
  let dx = mover.x - anchor[0];
7683
9857
  let dy = mover.y - anchor[1];
7684
- const len = Math.hypot(dx, dy);
7685
- if (len < 1e-9) {
9858
+ const len2 = Math.hypot(dx, dy);
9859
+ if (len2 < 1e-9) {
7686
9860
  dx = 1;
7687
9861
  dy = 0;
7688
9862
  }
7689
- const norm2 = Math.hypot(dx, dy) || 1;
7690
- mover.x = anchor[0] + targetD * dx / norm2;
7691
- mover.y = anchor[1] + targetD * dy / norm2;
9863
+ const norm5 = Math.hypot(dx, dy) || 1;
9864
+ mover.x = anchor[0] + targetD * dx / norm5;
9865
+ mover.y = anchor[1] + targetD * dy / norm5;
7692
9866
  }
7693
9867
  }
7694
9868
  var NameZ = zod.z.string().regex(/^[A-Za-z][A-Za-z0-9_'₀-₉]{0,11}$/);
@@ -7785,6 +9959,7 @@ var onPerpBisectorModule = defineModule({
7785
9959
  t: zod.z.number().optional()
7786
9960
  }),
7787
9961
  collectRefs: (e) => [e.p1, e.p2],
9962
+ refSpecs: [{ field: "p1", role: "point" }, { field: "p2", role: "point" }],
7788
9963
  emit: (e, ctx) => [{
7789
9964
  role: "primary",
7790
9965
  object: emitPointObject(ctx.resolveId(e.name), e.name, {
@@ -8428,6 +10603,7 @@ var circleDiameterModule = defineModule({
8428
10603
  visible: zod.z.boolean().optional()
8429
10604
  }),
8430
10605
  collectRefs: (e) => [e.p1, e.p2],
10606
+ refSpecs: [{ field: "p1", role: "point" }, { field: "p2", role: "point" }],
8431
10607
  emit: (e, ctx) => [{
8432
10608
  role: "primary",
8433
10609
  object: {
@@ -8774,6 +10950,7 @@ var mixtilinearPointModule = defineModule({
8774
10950
  which: zod.z.union([zod.z.literal("center"), zod.z.literal("touch")])
8775
10951
  }),
8776
10952
  collectRefs: (e) => [...e.vertices],
10953
+ refSpecs: [{ field: "vertices", role: "point", many: true }],
8777
10954
  emit: (e, ctx) => [{
8778
10955
  role: "primary",
8779
10956
  object: emitPointObject(ctx.resolveId(e.name), e.name, {
@@ -8888,6 +11065,32 @@ var pointAtDistanceModule = defineModule({
8888
11065
  }];
8889
11066
  }
8890
11067
  });
11068
+ var commonTangentPointModule = defineModule({
11069
+ kind: "commonTangentPoint",
11070
+ role: "point",
11071
+ category: "points",
11072
+ prefix: "p",
11073
+ schema: zod.z.object({
11074
+ name: NameZ,
11075
+ kind: zod.z.literal("commonTangentPoint"),
11076
+ circles: zod.z.tuple([NameZ, NameZ]),
11077
+ on: zod.z.union([zod.z.literal(0), zod.z.literal(1)]),
11078
+ variant: zod.z.enum(["external", "internal"]),
11079
+ side: zod.z.union([zod.z.literal(0), zod.z.literal(1)])
11080
+ }),
11081
+ collectRefs: (e) => [e.circles[0], e.circles[1]],
11082
+ refSpecs: [{ field: "circles", role: "circle", many: true }],
11083
+ emit: (e, ctx) => [{
11084
+ role: "primary",
11085
+ object: emitPointObject(ctx.resolveId(e.name), e.name, {
11086
+ kind: "commonTangentPoint",
11087
+ circles: [ctx.resolveId(e.circles[0]), ctx.resolveId(e.circles[1])],
11088
+ on: e.on,
11089
+ variant: e.variant,
11090
+ side: e.side
11091
+ })
11092
+ }]
11093
+ });
8891
11094
 
8892
11095
  // src/stamps/geometry-2d/dsl/registry.ts
8893
11096
  var ALL_MODULES = [
@@ -8934,7 +11137,9 @@ var ALL_MODULES = [
8934
11137
  reflectPointModule,
8935
11138
  reflectLineModule,
8936
11139
  // Cụm B points
8937
- pointAtDistanceModule
11140
+ pointAtDistanceModule,
11141
+ // 2-circle relations (spec mục A)
11142
+ commonTangentPointModule
8938
11143
  ];
8939
11144
  var KIND_REGISTRY = new Map(ALL_MODULES.map((m) => [m.kind, m]));
8940
11145
  var POINT_KINDS = new Set(
@@ -9137,156 +11342,6 @@ var IntentBuilderError = class extends Error {
9137
11342
  }
9138
11343
  };
9139
11344
 
9140
- // src/stamps/geometry-2d/ai/intent-builders/shared.ts
9141
- var SQRT3 = 1.7320508075688772;
9142
- function triangleCanonical(variant) {
9143
- switch (variant) {
9144
- case "equilateral":
9145
- return [[0, 0], [4, 0], [2, 2 * SQRT3]];
9146
- case "isoceles-AB":
9147
- return [[0, 0], [4, 0], [2, 3]];
9148
- case "isoceles-BC":
9149
- return [[0, 3], [-2, 0], [2, 0]];
9150
- case "isoceles-CA":
9151
- return [[0, 0], [2, 3], [4, 0]];
9152
- case "right-at-A":
9153
- return [[0, 0], [4, 0], [0, 3]];
9154
- case "right-at-B":
9155
- return [[0, 0], [4, 0], [4, 3]];
9156
- case "right-at-C":
9157
- return [[0, 0], [6, 0], [3, 3]];
9158
- case "any":
9159
- default:
9160
- return [[0, 0], [5, 0], [2, 3]];
9161
- }
9162
- }
9163
- function squareCanonical() {
9164
- return [[0, 0], [4, 0], [4, 4], [0, 4]];
9165
- }
9166
- function rectangleCanonical(variant) {
9167
- if (variant === "tall") return [[0, 0], [2.5, 0], [2.5, 4], [0, 4]];
9168
- return [[0, 0], [4, 0], [4, 2.5], [0, 2.5]];
9169
- }
9170
- function rhombusCanonical() {
9171
- return [[-2, 0], [0, -1.5], [2, 0], [0, 1.5]];
9172
- }
9173
- function trapezoidCanonical(variant) {
9174
- switch (variant) {
9175
- case "right":
9176
- return [[0, 0], [5, 0], [3, 3], [0, 3]];
9177
- case "isoceles":
9178
- return [[0, 0], [5, 0], [4, 3], [1, 3]];
9179
- case "general":
9180
- default:
9181
- return [[0, 0], [5, 0], [3.5, 3], [1, 3]];
9182
- }
9183
- }
9184
- function parallelogramCanonical() {
9185
- return [[0, 0], [4, 0], [5, 3], [1, 3]];
9186
- }
9187
- function quadrilateralCanonical() {
9188
- return [[0, 0], [5, 0], [4.5, 3.5], [0.5, 3]];
9189
- }
9190
- function addPoint2(s, p) {
9191
- if (s.pointNames.has(p.name)) {
9192
- if (p.kind !== "free") {
9193
- const idx = s.points.findIndex((q) => q.name === p.name);
9194
- if (idx >= 0 && s.points[idx].kind === "free") s.points[idx] = p;
9195
- }
9196
- return;
9197
- }
9198
- s.points.push(p);
9199
- s.pointNames.add(p.name);
9200
- }
9201
- var FREE_DEFAULT_SPREAD = [
9202
- [3, 3],
9203
- [-3, 3],
9204
- [3, -3],
9205
- [-3, -3],
9206
- [4, 0],
9207
- [-4, 0],
9208
- [0, 4],
9209
- [0, -4]
9210
- ];
9211
- function defaultFreeCoord(s) {
9212
- let count = 0;
9213
- for (const p of s.points) if (p.kind === "free") count++;
9214
- return [...FREE_DEFAULT_SPREAD[count % FREE_DEFAULT_SPREAD.length]];
9215
- }
9216
- function addShape(s, sh) {
9217
- if (s.shapeNames.has(sh.name)) return;
9218
- s.shapes.push(sh);
9219
- s.shapeNames.add(sh.name);
9220
- }
9221
- function uniqueShapeName(s, base) {
9222
- if (!s.shapeNames.has(base)) return base;
9223
- let i = 2;
9224
- while (s.shapeNames.has(`${base}${i}`)) i++;
9225
- return `${base}${i}`;
9226
- }
9227
- function uniquePointName(s, base) {
9228
- if (!s.pointNames.has(base)) return base;
9229
- let i = 2;
9230
- while (s.pointNames.has(`${base}${i}`)) i++;
9231
- return `${base}${i}`;
9232
- }
9233
- function ensureSegment(s, a, b) {
9234
- const k1 = `${a}-${b}`;
9235
- const k2 = `${b}-${a}`;
9236
- const existing = s.segmentByEnds.get(k1) ?? s.segmentByEnds.get(k2);
9237
- if (existing) return existing;
9238
- const name = uniqueShapeName(s, `${a}${b}`);
9239
- addShape(s, { name, kind: "segment", p1: a, p2: b });
9240
- s.segmentByEnds.set(k1, name);
9241
- return name;
9242
- }
9243
- function resolveLineRefWithFallback(s, ref, anchor) {
9244
- if (/^[A-Za-z]$/.test(ref) && /^[A-Za-z]$/.test(anchor) && ref !== anchor && s.pointNames.has(ref) && s.pointNames.has(anchor)) {
9245
- const [a, b] = ref < anchor ? [ref, anchor] : [anchor, ref];
9246
- return ensureSegment(s, a, b);
9247
- }
9248
- return resolveSegmentRef(s, ref);
9249
- }
9250
- function resolveSegmentRef(s, ref) {
9251
- if (s.shapeNames.has(ref)) return ref;
9252
- if (/^[A-Z][A-Z]$/u.test(ref)) {
9253
- return ensureSegment(s, ref[0], ref[1]);
9254
- }
9255
- if (ref.length === 2 && s.pointNames.has(ref[0]) && s.pointNames.has(ref[1])) {
9256
- return ensureSegment(s, ref[0], ref[1]);
9257
- }
9258
- const dashMatch = ref.match(/^([A-Za-z][A-Za-z0-9'_]*)[-_]?([A-Za-z][A-Za-z0-9'_]*)$/);
9259
- if (dashMatch && s.pointNames.has(dashMatch[1]) && s.pointNames.has(dashMatch[2])) {
9260
- return ensureSegment(s, dashMatch[1], dashMatch[2]);
9261
- }
9262
- const sub = splitKnownPair(s, ref);
9263
- if (sub) return ensureSegment(s, sub[0], sub[1]);
9264
- return ref;
9265
- }
9266
- function splitKnownPair(s, ref) {
9267
- for (let i = ref.length - 1; i >= 1; i--) {
9268
- const a = ref.slice(0, i);
9269
- const b = ref.slice(i);
9270
- if (s.pointNames.has(a) && s.pointNames.has(b)) return [a, b];
9271
- }
9272
- return null;
9273
- }
9274
- function parseEnds(ref) {
9275
- if (ref.length === 2) return [ref[0], ref[1]];
9276
- const m = ref.match(/^([A-Za-z][A-Za-z0-9'_]*)[-_]?([A-Za-z][A-Za-z0-9'_]*)$/);
9277
- if (m) return [m[1], m[2]];
9278
- return null;
9279
- }
9280
- var SHAPE_VARIANTS = {
9281
- triangle: ["any", "equilateral", "isoceles-AB", "isoceles-BC", "isoceles-CA", "right-at-A", "right-at-B", "right-at-C"],
9282
- square: ["standard"],
9283
- rectangle: ["standard", "wide", "tall"],
9284
- rhombus: ["standard"],
9285
- trapezoid: ["right", "isoceles", "general"],
9286
- parallelogram: ["standard"],
9287
- quadrilateral: ["any"]
9288
- };
9289
-
9290
11345
  // src/stamps/geometry-2d/ai/intent-builders/draw-shape.ts
9291
11346
  var buildDrawShape = (s, intent) => {
9292
11347
  const labels = intent.labels;
@@ -9559,6 +11614,25 @@ var buildPointAtDistance = (s, intent) => {
9559
11614
  addPoint2(s, { name: intent.name, kind: "pointAtDistance", from: c.from, through: c.through, distance: c.distance });
9560
11615
  };
9561
11616
 
11617
+ // src/stamps/geometry-2d/ai/intent-builders/add-point/commonTangentPoint.ts
11618
+ var buildCommonTangentPoint = (s, intent) => {
11619
+ const c = intent.constraint;
11620
+ if (c.kind !== "commonTangentPoint") return;
11621
+ const isCircle = (name) => {
11622
+ const sh = s.shapes.find((x) => x.name === name);
11623
+ return !!sh && CIRCLE_KINDS.has(sh.kind);
11624
+ };
11625
+ if (!isCircle(c.circles[0]) || !isCircle(c.circles[1])) return;
11626
+ addPoint2(s, {
11627
+ name: intent.name,
11628
+ kind: "commonTangentPoint",
11629
+ circles: [c.circles[0], c.circles[1]],
11630
+ on: c.on,
11631
+ variant: c.variant,
11632
+ side: c.side
11633
+ });
11634
+ };
11635
+
9562
11636
  // src/stamps/geometry-2d/ai/intent-builders/add-point/externalToCircle.ts
9563
11637
  var buildExternalToCircle = (s, intent) => {
9564
11638
  const c = intent.constraint;
@@ -9628,6 +11702,7 @@ var ADD_POINT_BUILDERS = {
9628
11702
  reflectPoint: buildReflectPoint,
9629
11703
  reflectLine: buildReflectLine,
9630
11704
  pointAtDistance: buildPointAtDistance,
11705
+ commonTangentPoint: buildCommonTangentPoint,
9631
11706
  externalToCircle: buildExternalToCircle,
9632
11707
  onCircle: buildOnCircle,
9633
11708
  mixtilinearPoint: buildMixtilinearPoint
@@ -10283,6 +12358,8 @@ function constraintKey(c) {
10283
12358
  return `externalToCircle:${c.circle}`;
10284
12359
  case "onCircle":
10285
12360
  return `onCircle:${c.circle}:${c.theta ?? ""}`;
12361
+ case "commonTangentPoint":
12362
+ return `commonTangentPoint:${c.circles.join(",")}:${c.on}:${c.variant}:${c.side}`;
10286
12363
  }
10287
12364
  }
10288
12365
  function samePrefix(a, b, depth) {
@@ -10310,11 +12387,11 @@ function verifyGeometry(intents, dsl) {
10310
12387
  const other2 = apex === "A" ? pC : apex === "B" ? pC : pB;
10311
12388
  const v1 = [other1[0] - apexP[0], other1[1] - apexP[1]];
10312
12389
  const v2 = [other2[0] - apexP[0], other2[1] - apexP[1]];
10313
- const dot = v1[0] * v2[0] + v1[1] * v2[1];
10314
- if (Math.abs(dot) > 1e-6) {
12390
+ const dot2 = v1[0] * v2[0] + v1[1] * v2[1];
12391
+ if (Math.abs(dot2) > 1e-6) {
10315
12392
  issues.push({
10316
12393
  axis: "wrong",
10317
- detail: `triangle ${A}${B}${C} variant=right-at-${apex} kh\xF4ng vu\xF4ng (dot=${dot.toFixed(3)})`
12394
+ detail: `triangle ${A}${B}${C} variant=right-at-${apex} kh\xF4ng vu\xF4ng (dot=${dot2.toFixed(3)})`
10318
12395
  });
10319
12396
  }
10320
12397
  }
@@ -10362,7 +12439,7 @@ function buildAndTranspile(intents) {
10362
12439
  return second.ok ? second : first;
10363
12440
  }
10364
12441
  function tryDeterministicFigure(rawProblem) {
10365
- const problem = normalizeProblemText(rawProblem);
12442
+ const problem = normalizeProblemText(correctUserInput(rawProblem));
10366
12443
  const det = runDeterministicIntents(problem);
10367
12444
  if (!det.ok) return { ok: false, reason: det.reason, coverage: det.coverage };
10368
12445
  const built = buildAndTranspile(
@@ -10437,7 +12514,7 @@ function describeDeterministicMiss(miss) {
10437
12514
  // src/stamps/geometry-2d/ai/deterministic/partialFigure.ts
10438
12515
  var MAX_SALVAGE_ITERS = 6;
10439
12516
  function tryPartialFigure(rawProblem) {
10440
- const problem = normalizeProblemText(rawProblem);
12517
+ const problem = normalizeProblemText(correctUserInput(rawProblem));
10441
12518
  const part = tryPartialDeterministic(problem);
10442
12519
  if (part.detIntents.length === 0) return null;
10443
12520
  const intents = resolveCircleNameCollisions(normalizeIntents(part.detIntents, problem));
@@ -10821,6 +12898,16 @@ var AddPointIntentZ = zod.z.object({
10821
12898
  zod.z.object({ kind: zod.z.literal("segmentLength"), p1: LabelZ, p2: LabelZ, scale: zod.z.number().positive().optional(), offset: zod.z.number().optional() }),
10822
12899
  zod.z.object({ kind: zod.z.literal("literal"), value: zod.z.number().positive(), scale: zod.z.number().positive().optional(), offset: zod.z.number().optional() })
10823
12900
  ])
12901
+ }),
12902
+ // Tiếp điểm tiếp tuyến CHUNG 2 đường tròn (spec mục A). circles = 2 tên đtròn;
12903
+ // on = tiếp điểm trên đtròn 0 hay 1; variant = ngoài/trong; side = chọn 1 trong
12904
+ // 2 tiếp tuyến cùng loại.
12905
+ zod.z.object({
12906
+ kind: zod.z.literal("commonTangentPoint"),
12907
+ circles: zod.z.tuple([LabelZ, LabelZ]),
12908
+ on: zod.z.union([zod.z.literal(0), zod.z.literal(1)]),
12909
+ variant: zod.z.enum(["external", "internal"]),
12910
+ side: zod.z.union([zod.z.literal(0), zod.z.literal(1)])
10824
12911
  })
10825
12912
  ])
10826
12913
  });
@@ -10871,8 +12958,8 @@ var DrawLineIntentZ = zod.z.object({
10871
12958
  });
10872
12959
  var MarkShapeIntentZ = zod.z.object({
10873
12960
  op: zod.z.literal("mark-shape"),
10874
- shape: zod.z.enum(["triangle", "quadrilateral"]),
10875
- labels: zod.z.array(LabelZ).min(3).max(4)
12961
+ shape: zod.z.enum(["triangle", "quadrilateral", "polygon"]),
12962
+ labels: zod.z.array(LabelZ).min(3).max(8)
10876
12963
  });
10877
12964
  var IntentZ = zod.z.discriminatedUnion("op", [
10878
12965
  DrawShapeIntentZ,
@@ -11184,7 +13271,7 @@ ho\u1EB7c
11184
13271
  - quadrilateral: any
11185
13272
 
11186
13273
  ## Constraint kinds (cho add-point)
11187
- midpoint, perpFoot, centroid, circumcenter, incenter, orthocenter, intersection, onSegment, free, secondIntersection, circleIntersection, tangencyPoint, tangentPoint, angleBisectorFoot, externalAngleBisectorFoot, arcMidpoint, reflectPoint, reflectLine, excenter, rightAngleViewing, pointAtDistance
13274
+ midpoint, perpFoot, centroid, circumcenter, incenter, orthocenter, intersection, onSegment, free, secondIntersection, circleIntersection, tangencyPoint, tangentPoint, angleBisectorFoot, externalAngleBisectorFoot, arcMidpoint, reflectPoint, reflectLine, excenter, rightAngleViewing, pointAtDistance, commonTangentPoint
11188
13275
 
11189
13276
  ## Style enum (cho connect)
11190
13277
  segment, line, ray, perpBisector
@@ -11328,6 +13415,131 @@ async function runTesseractOcr(image, opts = {}) {
11328
13415
  }
11329
13416
  }
11330
13417
 
13418
+ // src/stamps/geometry-2d/ai/vision/repairOcrSymbols.ts
13419
+ var PT4 = `[A-Z]{2,3}['\u2032]?`;
13420
+ var PERP_RE2 = new RegExp(
13421
+ `(?<![\\p{L}\\d])(${PT4})\\s+([1|L])\\s+(${PT4})(?![\\p{L}\\d])`,
13422
+ "gu"
13423
+ );
13424
+ var TRI_STRICT_RE = /(?<!giác )(?<!thang )(?<![\p{L}\d])A([A-Z]{3})\s+(đều|cân|nhọn|vuông)(?![\p{L}])/gu;
13425
+ var TRI_DOUBLE_RE = /(?<![\p{L}\d])AA([A-Z]{2})(?![A-Z])/gu;
13426
+ var CIRCLE_O_RE = /\(0\)/gu;
13427
+ var ELEM_RE = /(?<![\p{L}\d])([A-Z](?:\s*,\s*[A-Z])+)\s*e\s*\((?:O|0)\)/gu;
13428
+ var INTERSECT_RE = /([A-Z]{2,3})N\s+([A-Z]\S{0,2})\s*=\s*\{/gu;
13429
+ var SQUARE_RE = /([A-Z])\?(?=\s*[-=+)])/gu;
13430
+ var HORN_O_C_RE = /(?<!\p{Ll})Ơ(?!\p{Ll})/gu;
13431
+ var 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;
13432
+ var DUONG_TRON_RE = /([Đđ]ường\s+)tron(?![\p{L}])/gu;
13433
+ var FLORIN_F_RE = /Ƒ/gu;
13434
+ var TAI_TAI_RE = /(?<![\p{L}])tai(\s+)(?=[A-Z(]|điểm)/gu;
13435
+ var TAM_TAM_RE = /(?<![\p{L}])tam(\s+)(?=đường|\(|đối xứng)/gu;
13436
+ var EURO_ELEM_RE = /€/gu;
13437
+ var PARALLEL_RE = /\|\|/gu;
13438
+ var CENT_C_RE = /¢/gu;
13439
+ var ETH_D_RE = /(?<!\p{Ll})Ð(?!\p{Ll})/gu;
13440
+ var SLASH_O_RE = /(?<!\p{Ll})Ø(?!\p{Ll})/gu;
13441
+ var GOI_RE = /(?<!\p{L})Goi(?!\p{L})/gu;
13442
+ var LAY_RE = /(?<!\p{L})Lay(?!\p{L})/gu;
13443
+ var DIQUA_RE = /(?<!\p{L})di qua(?!\p{L})/gu;
13444
+ var VA_VAND_RE = /(?<!\p{L})va (?=[A-Z(]|dây|đường)/gu;
13445
+ var TREN_RE = /([Tt])rén(?![\p{L}])/gu;
13446
+ var 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;
13447
+ var DAY_RE = /(?<![\p{L}])day(?=\s+(?:cung(?![\p{L}])|[A-Z]{2}))/gu;
13448
+ var CHUNG_MINH_RE = /([Cc]hứng )mình(?![\p{L}])/gu;
13449
+ var THANG_HANG_RE = /thẳng hang(?![\p{L}])/gu;
13450
+ var TINH_RE = /(?<![\p{L}])Tinh(?= (?:[A-Z]|do(?![\p{L}])|độ|diện|bán|chu|số|giá))/gu;
13451
+ var DO_DAI_RE = /(?<![\p{L}])do dai(?![\p{L}])/gu;
13452
+ var NIM_RE = /(?<![\p{L}])nim(?= (?:giữa|trên|trong)(?![\p{L}]))/gu;
13453
+ var NAM_GIUA_RE = /(?<![\p{L}])năm(?= (?:giữa|trên) [A-Z])/gu;
13454
+ var ANGLE_Z_RE = /(?<![\p{L}\d])4?Z([A-Z]{2,3})(?![A-Z])/gu;
13455
+ var DE_PAREN_RE = /\(Dé /gu;
13456
+ var DE_VERB_RE = /(\(Đề )\S+( bởi)/gu;
13457
+ var DE_THI_LOWER_RE = /(?<![\p{L}])dé(?= thi)/gu;
13458
+ var DI_DONG_RE = /(?<![\p{L}])di dong(?![\p{L}])/gu;
13459
+ var POUND_E_RE = /£/gu;
13460
+ var TRI_SPACE_2_1 = /(tam\s*giác\s+)([A-Z])([A-Z])\s+([A-Z])(?![A-Z\p{Ll}])/gu;
13461
+ var TRI_SPACE_1_2 = /(tam\s*giác\s+)([A-Z])\s+([A-Z])([A-Z])(?![A-Z\p{Ll}])/gu;
13462
+ var DIAERESIS_O_RE = /(?<!\p{Ll})Ö(?!\p{Ll})/gu;
13463
+ var INTERSECT_PAREN_RE = /(\([A-Z]{3}\))\s+N\s+([A-Z]{2})(\s*=\s*\{)/gu;
13464
+ function repairOcrSymbols(text) {
13465
+ let t = text;
13466
+ t = t.replace(PERP_RE2, "$1 \u22A5 $3");
13467
+ t = t.replace(TRI_STRICT_RE, "tam gi\xE1c $1 $2");
13468
+ t = t.replace(TRI_DOUBLE_RE, "tam gi\xE1c A$1");
13469
+ t = t.replace(CIRCLE_O_RE, "(O)");
13470
+ t = t.replace(ELEM_RE, "$1 \u2208 (O)");
13471
+ t = t.replace(INTERSECT_RE, "$1 \u2229 $2 = {");
13472
+ t = t.replace(SQUARE_RE, "$1\xB2");
13473
+ t = t.replace(HORN_O_C_RE, "C");
13474
+ t = t.replace(DUONG_DBAR_RE, "\u0110\u01B0\u1EDDng");
13475
+ t = t.replace(DUONG_LOWER_RE, "\u0111\u01B0\u1EDDng");
13476
+ t = t.replace(DUONG_TRON_RE, "$1tr\xF2n");
13477
+ t = t.replace(FLORIN_F_RE, "F");
13478
+ t = t.replace(TAI_TAI_RE, "t\u1EA1i$1");
13479
+ t = t.replace(TAM_TAM_RE, "t\xE2m$1");
13480
+ t = t.replace(EURO_ELEM_RE, "\u2208");
13481
+ t = t.replace(PARALLEL_RE, "\u2225");
13482
+ t = t.replace(CENT_C_RE, "c");
13483
+ t = t.replace(ETH_D_RE, "D");
13484
+ t = t.replace(SLASH_O_RE, "O");
13485
+ t = t.replace(GOI_RE, "G\u1ECDi");
13486
+ t = t.replace(LAY_RE, "L\u1EA5y");
13487
+ t = t.replace(DIQUA_RE, "\u0111i qua");
13488
+ t = t.replace(VA_VAND_RE, "v\xE0 ");
13489
+ t = t.replace(TREN_RE, "$1r\xEAn");
13490
+ t = t.replace(DAY_RE, "d\xE2y");
13491
+ t = t.replace(CHUNG_MINH_RE, "$1minh");
13492
+ t = t.replace(THANG_HANG_RE, "th\u1EB3ng h\xE0ng");
13493
+ t = t.replace(TINH_RE, "T\xEDnh");
13494
+ t = t.replace(DO_DAI_RE, "\u0111\u1ED9 d\xE0i");
13495
+ t = t.replace(NIM_RE, "n\u1EB1m");
13496
+ t = t.replace(NAM_GIUA_RE, "n\u1EB1m");
13497
+ t = t.replace(ANGLE_Z_RE, "\u2220$1");
13498
+ t = t.replace(DE_PAREN_RE, "(\u0110\u1EC1 ");
13499
+ t = t.replace(DE_VERB_RE, "$1xu\u1EA5t$2");
13500
+ t = t.replace(DE_THI_LOWER_RE, "\u0111\u1EC1");
13501
+ t = t.replace(DI_DONG_RE, "di \u0111\u1ED9ng");
13502
+ t = t.replace(POUND_E_RE, "E");
13503
+ t = t.replace(TRI_SPACE_2_1, "$1$2$3$4");
13504
+ t = t.replace(TRI_SPACE_1_2, "$1$2$3$4");
13505
+ t = t.replace(DIAERESIS_O_RE, "O");
13506
+ t = t.replace(INTERSECT_PAREN_RE, "$1 \u2229 $2$3");
13507
+ return t;
13508
+ }
13509
+
13510
+ // src/stamps/geometry-2d/ai/vision/detectFormulaRisk.ts
13511
+ var SUP_RESIDUE = /[A-Z0-9]\?(?=\s*[-=+)(])/u;
13512
+ var ANGLE_DEG = /([\p{L}]{1,6})\s*=\s*\d+\s*°/gu;
13513
+ function hasManglingLtGt(text) {
13514
+ const re = /[<>]/gu;
13515
+ let m;
13516
+ while ((m = re.exec(text)) !== null) {
13517
+ const prev = text.slice(0, m.index).replace(/\s+$/u, "").slice(-1);
13518
+ const next = text.slice(m.index + 1).replace(/^\s+/u, "").slice(0, 1);
13519
+ const prevOperand = /[A-Za-z0-9]/.test(prev);
13520
+ const nextOperand = /[A-Za-z0-9]/.test(next);
13521
+ if (!(prevOperand && nextOperand)) return true;
13522
+ }
13523
+ return false;
13524
+ }
13525
+ function detectFormulaRisk(text) {
13526
+ const reasons = [];
13527
+ if (hasManglingLtGt(text)) {
13528
+ reasons.push('ph\xE2n s\u1ED1/bi\u1EC3u th\u1EE9c nghi b\u1ECB nu\u1ED1t (k\xFD hi\u1EC7u "<"/">")');
13529
+ }
13530
+ if (SUP_RESIDUE.test(text)) {
13531
+ reasons.push('s\u1ED1 m\u0169 ho\u1EB7c \u0111\u1ED9 nghi b\u1ECB m\u1EA5t (d\u1EA5u "?")');
13532
+ }
13533
+ for (const m of text.matchAll(ANGLE_DEG)) {
13534
+ const tok = m[1];
13535
+ if (/[A-Z]/.test(tok) && /[^\x00-\x7F]/u.test(tok)) {
13536
+ reasons.push("t\xEAn g\xF3c nghi b\u1ECB nh\u1EADn d\u1EA1ng sai (k\xFD t\u1EF1 c\xF3 d\u1EA5u)");
13537
+ break;
13538
+ }
13539
+ }
13540
+ return reasons;
13541
+ }
13542
+
11331
13543
  // src/stamps/geometry-2d/ai/vision/extractProblem.ts
11332
13544
  var MIN_HIGH_CONFIDENCE_CHARS = 10;
11333
13545
  var MAX_TEXT_CHARS = 2e3;
@@ -11358,6 +13570,7 @@ async function extractProblemFromImage(image, opts = {}) {
11358
13570
  ok: true,
11359
13571
  text,
11360
13572
  confidence,
13573
+ warnings: detectFormulaRisk(text),
11361
13574
  usage: { inputTokens: 0, outputTokens: 0 }
11362
13575
  };
11363
13576
  }
@@ -11369,6 +13582,7 @@ function postProcess(raw) {
11369
13582
  t = t.replace(/```[\s\S]*?```/g, "").replace(/`([^`]+)`/g, "$1");
11370
13583
  t = t.replace(/\s+/g, " ").trim();
11371
13584
  t = t.normalize("NFC");
13585
+ t = repairOcrSymbols(t);
11372
13586
  if (t.length > MAX_TEXT_CHARS) t = t.slice(0, MAX_TEXT_CHARS);
11373
13587
  return t;
11374
13588
  }
@@ -11378,6 +13592,14 @@ async function handleExtractProblem(image, opts = {}) {
11378
13592
  try {
11379
13593
  const r = await extractProblemFromImage(image, opts);
11380
13594
  if (r.ok) {
13595
+ if (r.warnings.length > 0) {
13596
+ return {
13597
+ kind: "low-confidence",
13598
+ text: r.text,
13599
+ 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.`,
13600
+ usage: r.usage
13601
+ };
13602
+ }
11381
13603
  if (r.confidence === "low") {
11382
13604
  return {
11383
13605
  kind: "low-confidence",