@xom11/whiteboard 0.31.0 → 0.33.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 (93) hide show
  1. package/dist/ai.d.mts +149 -28
  2. package/dist/ai.d.ts +149 -28
  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-TMRFSOM7.mjs → chunk-4NJUTCFF.mjs} +4 -4
  11. package/dist/{chunk-TMRFSOM7.mjs.map → chunk-4NJUTCFF.mjs.map} +1 -1
  12. package/dist/{chunk-LTLLQUMN.mjs → chunk-BZZ5BROM.mjs} +6 -7
  13. package/dist/chunk-BZZ5BROM.mjs.map +1 -0
  14. package/dist/{host-KMWP7KBT.mjs → chunk-C3RDFBQ5.mjs} +20 -35
  15. package/dist/chunk-C3RDFBQ5.mjs.map +1 -0
  16. package/dist/chunk-C7C66BAV.mjs +11 -0
  17. package/dist/chunk-C7C66BAV.mjs.map +1 -0
  18. package/dist/{chunk-T3N4BSJV.mjs → chunk-DCFZTKFE.mjs} +13 -5
  19. package/dist/chunk-DCFZTKFE.mjs.map +1 -0
  20. package/dist/{chunk-44JY2AKC.mjs → chunk-F42MZ745.mjs} +3 -3
  21. package/dist/{chunk-44JY2AKC.mjs.map → chunk-F42MZ745.mjs.map} +1 -1
  22. package/dist/{chunk-BMYC2ILT.mjs → chunk-FOPZOVI3.mjs} +5 -4
  23. package/dist/chunk-FOPZOVI3.mjs.map +1 -0
  24. package/dist/{chunk-K5BS2H56.mjs → chunk-GKDQ5IV7.mjs} +4 -4
  25. package/dist/{chunk-K5BS2H56.mjs.map → chunk-GKDQ5IV7.mjs.map} +1 -1
  26. package/dist/{chunk-73Q7ADVL.mjs → chunk-MNHOFOM5.mjs} +2 -2
  27. package/dist/{chunk-73Q7ADVL.mjs.map → chunk-MNHOFOM5.mjs.map} +1 -1
  28. package/dist/{chunk-KOXOC2FI.mjs → chunk-NBMYET5H.mjs} +11 -15
  29. package/dist/chunk-NBMYET5H.mjs.map +1 -0
  30. package/dist/{chunk-DWIEVCGK.mjs → chunk-NTFG7ZRN.mjs} +53 -3
  31. package/dist/chunk-NTFG7ZRN.mjs.map +1 -0
  32. package/dist/{chunk-C76SOFXF.mjs → chunk-QCK3Z5WN.mjs} +3 -3
  33. package/dist/{chunk-C76SOFXF.mjs.map → chunk-QCK3Z5WN.mjs.map} +1 -1
  34. package/dist/chunk-SUXIMLYN.mjs +238 -0
  35. package/dist/chunk-SUXIMLYN.mjs.map +1 -0
  36. package/dist/geometry-2d.d.mts +1 -1
  37. package/dist/geometry-2d.d.ts +1 -1
  38. package/dist/geometry-2d.js +1026 -231
  39. package/dist/geometry-2d.js.map +1 -1
  40. package/dist/geometry-2d.mjs +6 -5
  41. package/dist/geometry-3d.d.mts +1 -1
  42. package/dist/geometry-3d.d.ts +1 -1
  43. package/dist/geometry-3d.js +4001 -475
  44. package/dist/geometry-3d.js.map +1 -1
  45. package/dist/geometry-3d.mjs +5 -5
  46. package/dist/graph-2d.d.mts +1 -1
  47. package/dist/graph-2d.d.ts +1 -1
  48. package/dist/graph-2d.js +712 -115
  49. package/dist/graph-2d.js.map +1 -1
  50. package/dist/graph-2d.mjs +5 -5
  51. package/dist/{handleExtractProblem-BrDY9ifM.d.ts → handleExtractProblem-mtJyhEw3.d.mts} +2 -0
  52. package/dist/{handleExtractProblem-BrDY9ifM.d.mts → handleExtractProblem-mtJyhEw3.d.ts} +2 -0
  53. package/dist/host-6WTLCXUE.mjs +62 -0
  54. package/dist/host-6WTLCXUE.mjs.map +1 -0
  55. package/dist/{host-H2IGOKJU.mjs → host-BWIXT5TR.mjs} +3 -3
  56. package/dist/{host-H2IGOKJU.mjs.map → host-BWIXT5TR.mjs.map} +1 -1
  57. package/dist/{host-4FIUNIDQ.mjs → host-QDVRULJV.mjs} +14 -11
  58. package/dist/host-QDVRULJV.mjs.map +1 -0
  59. package/dist/host-USOIVTQB.mjs +4976 -0
  60. package/dist/host-USOIVTQB.mjs.map +1 -0
  61. package/dist/index.d.mts +14 -4
  62. package/dist/index.d.ts +14 -4
  63. package/dist/index.js +4273 -521
  64. package/dist/index.js.map +1 -1
  65. package/dist/index.mjs +48 -27
  66. package/dist/index.mjs.map +1 -1
  67. package/dist/latex.d.mts +1 -1
  68. package/dist/latex.d.ts +1 -1
  69. package/dist/latex.mjs +1 -1
  70. package/dist/serialize-7UZWQQ4I.mjs +9 -0
  71. package/dist/{serialize-PGHQZEPV.mjs.map → serialize-7UZWQQ4I.mjs.map} +1 -1
  72. package/dist/studio.d.mts +35 -0
  73. package/dist/studio.d.ts +35 -0
  74. package/dist/studio.js +10883 -0
  75. package/dist/studio.js.map +1 -0
  76. package/dist/studio.mjs +28 -0
  77. package/dist/studio.mjs.map +1 -0
  78. package/dist/{types-tePd94vW.d.ts → types-eEEEVsLF.d.mts} +1 -1
  79. package/dist/{types-tePd94vW.d.mts → types-eEEEVsLF.d.ts} +1 -1
  80. package/package.json +12 -2
  81. package/dist/chunk-BMYC2ILT.mjs.map +0 -1
  82. package/dist/chunk-DWIEVCGK.mjs.map +0 -1
  83. package/dist/chunk-JJ4FPCBE.mjs.map +0 -1
  84. package/dist/chunk-KOXOC2FI.mjs.map +0 -1
  85. package/dist/chunk-LTLLQUMN.mjs.map +0 -1
  86. package/dist/chunk-QK6OVDLC.mjs +0 -103
  87. package/dist/chunk-QK6OVDLC.mjs.map +0 -1
  88. package/dist/chunk-T3N4BSJV.mjs.map +0 -1
  89. package/dist/host-4FIUNIDQ.mjs.map +0 -1
  90. package/dist/host-4ZB4XD4S.mjs +0 -2378
  91. package/dist/host-4ZB4XD4S.mjs.map +0 -1
  92. package/dist/host-KMWP7KBT.mjs.map +0 -1
  93. package/dist/serialize-PGHQZEPV.mjs +0 -9
package/dist/ai.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { KIND_REGISTRY, POINT_KINDS, LINE_LIKE_SHAPE_KINDS, CIRCLE_KINDS } from './chunk-DWIEVCGK.mjs';
2
- export { fileToImagePart, inferMediaType, validateFile } from './chunk-DWIEVCGK.mjs';
3
- export { extractProblemFromImage, handleExtractProblem, runTesseractOcr } from './chunk-QK6OVDLC.mjs';
4
- import { createEmptyState } from './chunk-73Q7ADVL.mjs';
1
+ import { KIND_REGISTRY, POINT_KINDS, LINE_LIKE_SHAPE_KINDS, CIRCLE_KINDS } from './chunk-NTFG7ZRN.mjs';
2
+ export { fileToImagePart, inferMediaType, validateFile } from './chunk-NTFG7ZRN.mjs';
3
+ export { extractProblemFromImage, handleExtractProblem, runTesseractOcr } from './chunk-SUXIMLYN.mjs';
4
+ import { createEmptyState } from './chunk-MNHOFOM5.mjs';
5
5
  import './chunk-B4NJJZFR.mjs';
6
6
  import './chunk-J5LGTIGS.mjs';
7
7
  import { z } from 'zod';
@@ -19,6 +19,11 @@ var GEOMETRY_KEYWORDS = [
19
19
  "h\xECnh thoi",
20
20
  "h\xECnh thang",
21
21
  "t\u1EE9 gi\xE1c",
22
+ // Đa giác ≥5 đỉnh: "Cho lục giác ABCDEF …" — thiếu các từ này thì clause chỉ
23
+ // có tên đa giác (không "ngoại/nội tiếp") bị coi văn xuôi → hexagonRule miss.
24
+ "ng\u0169 gi\xE1c",
25
+ "l\u1EE5c gi\xE1c",
26
+ "\u0111a gi\xE1c",
22
27
  // Triangle variants
23
28
  "vu\xF4ng t\u1EA1i",
24
29
  "c\xE2n t\u1EA1i",
@@ -100,6 +105,13 @@ var GEOMETRY_KEYWORDS = [
100
105
  "c\u1EAFt",
101
106
  "\u0111\u01B0\u1EDDng th\u1EB3ng",
102
107
  "\u0111\u01B0\u01A1ng th\u1EB3ng",
108
+ // "thẳng hàng" — "Cho 3 điểm A, B, C thẳng hàng theo thứ tự" (C41): clause GIỚI
109
+ // THIỆU bộ điểm thẳng hàng (collinearPointsRule dựng A,B,C) NHƯNG không có keyword
110
+ // nào khác → hasGeometry=false → clause bị lọc khỏi rule engine → A,B,C KHÔNG dựng
111
+ // → cascade làm sụp tiếp tuyến/trung điểm phụ thuộc chúng. "thẳng hàng" (≥8 ký tự,
112
+ // phân biệt) đánh dấu geo-clause. Clause CHỨNG MINH ("Chứng minh A,B,C thẳng hàng")
113
+ // vẫn non-geo nhờ guard proofOnly (startsProofSection) — không bị ảnh hưởng.
114
+ "th\u1EB3ng h\xE0ng",
103
115
  // Ký hiệu (symbol-only phrasing tương đương từ chữ): "⊥" ≡ "vuông góc".
104
116
  // perpFoot rule đã nhận "⊥" ở tầng construct (PERP_DRAW), nhưng gate
105
117
  // hasGeometry trước đây CHỈ nhận chữ "vuông góc" → clause mà "⊥" là tín hiệu
@@ -215,17 +227,18 @@ function countGeometryKeywords(text) {
215
227
  var MASK_SEMI = "";
216
228
  var MASK_DOT = "";
217
229
  var MASK_COMMA = "";
230
+ var MASK_SEMI_LIST = "";
218
231
  function maskNonBoundaryPunct(s) {
219
232
  return s.replace(
220
233
  /\(([^()\n]{1,40})\)/g,
221
234
  (m) => m.replace(/;/g, MASK_SEMI).replace(/\./g, MASK_DOT).replace(/,/g, MASK_COMMA)
222
235
  ).replace(
223
- /;(?=\s*[A-Z][A-Z]?['′]?\d?\s*(?:[;,]|cắt nhau|đồng quy))/gu,
224
- MASK_SEMI
225
- );
236
+ /;(?=\s*[A-Z][A-Z]?['′]?\d?\s*(?:[;,]|lần lượt|cắt nhau|đồng quy|đôi một))/gu,
237
+ MASK_SEMI_LIST
238
+ ).replace(/;(?=\s*[A-Z][A-Z]?['′]?\d?\s*(?:[;,.]|$))/gu, MASK_SEMI_LIST);
226
239
  }
227
240
  function unmask(s) {
228
- return s.replace(/\u0001/g, ";").replace(/\u0002/g, ".").replace(/\u0003/g, ",");
241
+ return s.replace(/\u0001/g, ";").replace(/\u0002/g, ".").replace(/\u0003/g, ",").replace(/\u0004/g, ",");
229
242
  }
230
243
  function segmentClauses(problem) {
231
244
  let proofMode = false;
@@ -234,7 +247,7 @@ function segmentClauses(problem) {
234
247
  // giờ khớp trước space → split chết im lặng (bug class \b+tiếng Việt).
235
248
  /[.;\n]+|,\s*(?=(?:Gọi|Vẽ|Kẻ|Cho|Lấy|Dựng|trên|với|Let|Draw|Mark|Take|Construct|Join)(?!\p{L}))/u
236
249
  ).map((s) => unmask(s).trim()).filter((s) => s.length > 0).map((text, id) => {
237
- const hasGeometryKeyword = countGeometryKeywords(text) > 0;
250
+ const hasGeometryKeyword = countGeometryKeywords(text) > 0 || NAMED_LINE_PICK.test(text) || BARE_SEG_PICK.test(text) && !/tia\s+đối/u.test(text);
238
251
  const proofOnly = isProofOnlyClause(text, proofMode);
239
252
  if (startsProofSection(text)) proofMode = true;
240
253
  const locusOnly = LOCUS_CLAUSE.test(text) && !CONSTRUCTION_LEAD.test(text);
@@ -243,6 +256,8 @@ function segmentClauses(problem) {
243
256
  }
244
257
  var ENUM_PREFIX = "(?:[0-9]+\\s*[.)]?\\s*|[a-zA-Z]\\s*[.)]\\s*)*";
245
258
  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;
259
+ 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;
260
+ 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;
246
261
  var PROOF_SECTION_START = new RegExp(
247
262
  `^${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})`,
248
263
  "u"
@@ -311,6 +326,9 @@ function pairFromToken(token) {
311
326
  return m ? [m[1], m[2]] : [];
312
327
  }
313
328
  var SIDE_PREFIX = "(?:c\u1EA1nh(?:\\s+huy\u1EC1n)?\\s+|\u0111o\u1EA1n(?:\\s+th\u1EB3ng)?\\s+|d\xE2y(?:\\s+cung)?\\s+)?";
329
+ function escapeRe(s) {
330
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
331
+ }
314
332
  var DUONG_KW = "[\u0110\u0111]\u01B0[\u1EDD\u01A1]ng";
315
333
  var CIRCLE_KW = DUONG_KW + "\\s*tr\xF2n";
316
334
 
@@ -479,44 +497,44 @@ var QUAD = "([A-Z])([A-Z])([A-Z])([A-Z])(?![A-Z])";
479
497
  var KINDS = [
480
498
  // hình thang cân / vuông (modifier TRƯỚC đỉnh) — phải match trước thang chung
481
499
  {
482
- name: new RegExp("h\xECnh\\s+thang\\s+c\xE2n\\s+" + QUAD, "gu"),
500
+ name: new RegExp("[Hh]\xECnh\\s+thang\\s+c\xE2n\\s+" + QUAD, "gu"),
483
501
  shape: "trapezoid",
484
502
  variant: "isoceles"
485
503
  },
486
504
  {
487
- name: new RegExp("h\xECnh\\s+thang\\s+vu\xF4ng\\s+" + QUAD, "gu"),
505
+ name: new RegExp("[Hh]\xECnh\\s+thang\\s+vu\xF4ng\\s+" + QUAD, "gu"),
488
506
  shape: "trapezoid",
489
507
  variant: "right"
490
508
  },
491
509
  // hình thang chung — sau đó dò modifier 'vuông'/'cân' đứng SAU đỉnh
492
510
  // ("hình thang ABCD vuông tại A" → right; "...cân" → isoceles).
493
511
  {
494
- name: new RegExp("h\xECnh\\s+thang\\s+" + QUAD, "gu"),
512
+ name: new RegExp("[Hh]\xECnh\\s+thang\\s+" + QUAD, "gu"),
495
513
  shape: "trapezoid",
496
514
  variant: "general"
497
515
  },
498
516
  {
499
- name: new RegExp("h\xECnh\\s+vu\xF4ng\\s+" + QUAD, "gu"),
517
+ name: new RegExp("[Hh]\xECnh\\s+vu\xF4ng\\s+" + QUAD, "gu"),
500
518
  shape: "square",
501
519
  variant: "standard"
502
520
  },
503
521
  {
504
- name: new RegExp("h\xECnh\\s+ch\u1EEF\\s+nh\u1EADt\\s+" + QUAD, "gu"),
522
+ name: new RegExp("[Hh]\xECnh\\s+ch\u1EEF\\s+nh\u1EADt\\s+" + QUAD, "gu"),
505
523
  shape: "rectangle",
506
524
  variant: "wide"
507
525
  },
508
526
  {
509
- name: new RegExp("h\xECnh\\s+b\xECnh\\s+h\xE0nh\\s+" + QUAD, "gu"),
527
+ name: new RegExp("[Hh]\xECnh\\s+b\xECnh\\s+h\xE0nh\\s+" + QUAD, "gu"),
510
528
  shape: "parallelogram",
511
529
  variant: "standard"
512
530
  },
513
531
  {
514
- name: new RegExp("h\xECnh\\s+thoi\\s+" + QUAD, "gu"),
532
+ name: new RegExp("[Hh]\xECnh\\s+thoi\\s+" + QUAD, "gu"),
515
533
  shape: "rhombus",
516
534
  variant: "standard"
517
535
  },
518
536
  {
519
- name: new RegExp("t\u1EE9\\s+gi\xE1c(?:\\s+l\u1ED3i)?\\s+" + QUAD, "gu"),
537
+ name: new RegExp("[Tt]\u1EE9\\s+gi\xE1c(?:\\s+l\u1ED3i)?\\s+" + QUAD, "gu"),
520
538
  shape: "quadrilateral",
521
539
  variant: "any"
522
540
  },
@@ -566,10 +584,10 @@ var KINDS = [
566
584
  variant: "any"
567
585
  }
568
586
  ];
569
- 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;
587
+ 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;
570
588
  var RIGHT_AFTER = /vuông(?!\p{L})/u;
571
589
  var ISO_AFTER = /(?<!\p{L})cân(?!\p{L})/u;
572
- var NEXT_SHAPE = /hình\s|tứ\s+giác/u;
590
+ var NEXT_SHAPE = /[Hh]ình\s|[Tt]ứ\s+giác/u;
573
591
  function scanClause(text) {
574
592
  const claimed = [];
575
593
  const taken = [];
@@ -613,13 +631,17 @@ var CYCLIC_QUAD_THETAS = CYCLIC_QUAD_COORDS.map(
613
631
  );
614
632
  var CENTER = "(?:\\(\\s*([A-Z])\\s*\\)|t\xE2m\\s+([A-Z]))?";
615
633
  var QUAD_INSCRIBED_IN_CIRCLE = new RegExp(
616
- "^[\\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,
634
+ "^[\\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 + ")",
617
635
  "iu"
618
636
  );
619
637
  var CIRCLE_CIRCUMSCRIBES_QUAD = new RegExp(
620
638
  "(?:\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+$",
621
639
  "iu"
622
640
  );
641
+ var TUGIAC_NOITIEP = new RegExp(
642
+ "[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,
643
+ "gu"
644
+ );
623
645
  var TRIANGLE_DECL = /(?:tam\s*giác|[Tt]riangle)\s+([A-Z])([A-Z])([A-Z])(?![A-Z])/gu;
624
646
  function ownedByOthers(ctx, selfLabels) {
625
647
  const owned = /* @__PURE__ */ new Set();
@@ -638,9 +660,26 @@ function ownedByOthers(ctx, selfLabels) {
638
660
  }
639
661
  return owned;
640
662
  }
663
+ function conflictingOwners(ctx, quadLabels) {
664
+ const quadSet = new Set(quadLabels);
665
+ const owned = /* @__PURE__ */ new Set();
666
+ const addIfOutside = (verts) => {
667
+ if (verts.some((v) => !quadSet.has(v))) for (const v of verts) owned.add(v);
668
+ };
669
+ TRIANGLE_DECL.lastIndex = 0;
670
+ for (const m of ctx.problem.matchAll(TRIANGLE_DECL)) addIfOutside([m[1], m[2], m[3]]);
671
+ const selfKey = quadLabels.join("");
672
+ for (const c of ctx.clauses) {
673
+ for (const h of scanClause(c.text)) {
674
+ if (h.labels.join("") === selfKey) continue;
675
+ addIfOutside(h.labels);
676
+ }
677
+ }
678
+ return owned;
679
+ }
641
680
  function detectCyclic(text, hit) {
642
681
  const a = QUAD_INSCRIBED_IN_CIRCLE.exec(text.slice(hit.afterEnd));
643
- if (a) return a[1] ?? a[2] ?? "";
682
+ if (a) return a[1] ?? a[2] ?? a[3] ?? a[4] ?? a[5] ?? "";
644
683
  const b = CIRCLE_CIRCUMSCRIBES_QUAD.exec(text.slice(0, hit.index));
645
684
  if (b) return b[1] ?? b[2] ?? "";
646
685
  return void 0;
@@ -653,12 +692,30 @@ var quadRule = {
653
692
  match(ctx) {
654
693
  const out = [];
655
694
  for (const c of ctx.clauses) {
695
+ TUGIAC_NOITIEP.lastIndex = 0;
696
+ for (const m of c.text.matchAll(TUGIAC_NOITIEP)) {
697
+ const labels = [m[2], m[3], m[4], m[5]];
698
+ const owned = ownedByOthers(ctx, labels);
699
+ if (labels.some((lbl) => owned.has(lbl))) continue;
700
+ const centerName = m[1] || "O";
701
+ out.push({
702
+ ruleId: "quad",
703
+ clauseIds: [c.id],
704
+ intents: [
705
+ drawCircle(centerName, "centerRadius", { center: centerName, radius: CYCLIC_RADIUS }),
706
+ ...labels.map(
707
+ (lbl, i) => addPoint(lbl, { kind: "onCircle", circle: centerName, theta: CYCLIC_QUAD_THETAS[i] })
708
+ ),
709
+ markShape("quadrilateral", labels)
710
+ ]
711
+ });
712
+ }
656
713
  for (const hit of scanClause(c.text)) {
657
714
  let intents = [drawShape(hit.shape, hit.labels, hit.variant)];
658
715
  if (hit.shape === "quadrilateral") {
659
716
  const center = detectCyclic(c.text, hit);
660
717
  if (center !== void 0) {
661
- const owned = ownedByOthers(ctx, hit.labels);
718
+ const owned = conflictingOwners(ctx, hit.labels);
662
719
  const shared = hit.labels.some((lbl) => owned.has(lbl));
663
720
  if (!shared) {
664
721
  const centerName = center || "O";
@@ -768,11 +825,11 @@ var NAME_AFTER_G = new RegExp(
768
825
  "gu"
769
826
  );
770
827
  var DISTRIB = new RegExp(
771
- `((?:[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])?)`,
828
+ `((?:[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])?)`,
772
829
  "u"
773
830
  );
774
831
  var DISTRIB_AFTER = new RegExp(
775
- `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})`,
832
+ `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})`,
776
833
  "u"
777
834
  );
778
835
  function nameToken(raw) {
@@ -1138,8 +1195,8 @@ var PREFILTER4 = [
1138
1195
  /[Bb]isector\s+of\s+(?:the\s+)?(?:[Aa]ngle|∠)/u
1139
1196
  // Form A EN
1140
1197
  ];
1141
- 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;
1142
- var ANGLE1 = /phân\s*giác\s+(?:trong\s+)?(?:(?:của|cho)\s+)?góc\s+([A-Z])(?![A-Z])/giu;
1198
+ 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;
1199
+ var ANGLE1 = /phân\s*giác\s+(?:trong\s+)?(?:(?:của|cho)\s+)?(?:góc\s+|∠\s*)([A-Z])(?![A-Z])/giu;
1143
1200
  var CHAN_BEFORE = /chân\s+(?:của\s+)?(?:đường\s*|tia\s+)?$/u;
1144
1201
  var ANGLE3_EN_A = /[Bb]isector\s+of\s+(?:the\s+)?(?:[Aa]ngle|∠)\s*([A-Z])([A-Z])([A-Z])(?![A-Z])/gu;
1145
1202
  var ANGLE3_EN_B = /[Aa]ngle\s+bisector\s+of\s+([A-Z])([A-Z])([A-Z])(?![A-Z])/gu;
@@ -1317,6 +1374,13 @@ function resolveTriangle(clauseText, kwIndex, uniqueProblemTri) {
1317
1374
  }
1318
1375
  return uniqueProblemTri;
1319
1376
  }
1377
+ function dropDegenerate(intents) {
1378
+ return intents.filter((i) => {
1379
+ if (i.op !== "add-point") return true;
1380
+ const of = i.constraint.of;
1381
+ return !(Array.isArray(of) && of.includes(i.name));
1382
+ });
1383
+ }
1320
1384
  var centersRule = {
1321
1385
  id: "centers",
1322
1386
  priority: 70,
@@ -1349,7 +1413,8 @@ var centersRule = {
1349
1413
  if (of) {
1350
1414
  intents.push(addPoint(dc[1], { kind: centerTypeToKind(dc[3]), of }));
1351
1415
  intents.push(addPoint(dc[2], { kind: centerTypeToKind(dc[4]), of }));
1352
- out.push({ ruleId: "centers", clauseIds: [c.id], intents });
1416
+ const kept2 = dropDegenerate(intents);
1417
+ if (kept2.length) out.push({ ruleId: "centers", clauseIds: [c.id], intents: kept2 });
1353
1418
  continue;
1354
1419
  }
1355
1420
  }
@@ -1398,8 +1463,9 @@ var centersRule = {
1398
1463
  const of = ofFor(c.text, INCENTER_KW_EN);
1399
1464
  if (name && of) intents.push(addPoint(name, { kind: "incenter", of }));
1400
1465
  }
1401
- if (intents.length > 0) {
1402
- out.push({ ruleId: "centers", clauseIds: [c.id], intents });
1466
+ const kept = dropDegenerate(intents);
1467
+ if (kept.length > 0) {
1468
+ out.push({ ruleId: "centers", clauseIds: [c.id], intents: kept });
1403
1469
  }
1404
1470
  }
1405
1471
  return out;
@@ -1412,9 +1478,11 @@ var PREFILTER5 = new RegExp(
1412
1478
  "u"
1413
1479
  );
1414
1480
  var PREFILTER_EN = /projection|perpendicular|foot\s+of/i;
1415
- var LINE = "(?:" + DUONG_KW + "\\s*th\u1EB3ng\\s+|c\u1EA1nh\\s+|\u0111o\u1EA1n(?:\\s+th\u1EB3ng)?\\s+)?([A-Z]{1,2})(?![A-Z])";
1481
+ var LINE_PREFIX = "(?:" + DUONG_KW + "\\s*th\u1EB3ng\\s+|ti\u1EBFp\\s*tuy\u1EBFn\\s+|c\u1EA1nh\\s+|\u0111o\u1EA1n(?:\\s+th\u1EB3ng)?\\s+)?";
1482
+ var LINE_TOK = "([A-Z]{1,2}(?![A-Za-z])|[A-Z]?[a-z][a-z]?[0-9]?(?![A-Za-z]))";
1483
+ var LINE = LINE_PREFIX + LINE_TOK;
1416
1484
  var PREP = "(?:tr\xEAn|l\xEAn|xu\u1ED1ng|\u0111\u1EBFn|t\u1EDBi)";
1417
- var LANLUOT_OPT = "(?:(?:l\u1EA7n\\s*l\u01B0\u1EE3t|theo\\s+th\u1EE9\\s+t\u1EF1|th\u1EE9\\s+t\u1EF1)\\s+)?";
1485
+ 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+)?";
1418
1486
  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}`;
1419
1487
  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}`;
1420
1488
  var LANLUOT = new RegExp(
@@ -1425,14 +1493,27 @@ var SINGLE = new RegExp(`(?:${PROJ_CORE})|(?:${FOOT_CORE})`, "gu");
1425
1493
  var NAMES_BLOB = "((?:[A-Z](?:['\u2032]?)\\s*(?:,|v\xE0)\\s*)+[A-Z](?:['\u2032]?))";
1426
1494
  var LINES_BLOB = "((?:[A-Z]{1,2}\\s*(?:,|v\xE0)\\s*)+[A-Z]{1,2})(?!\\p{L})";
1427
1495
  var LINES_PREFIX = "(?:c\xE1c\\s+)?(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|c\u1EA1nh\\s+|\u0111o\u1EA1n\\s+)?";
1496
+ var NO_HOA_BEFORE = "(?<![A-Z])";
1428
1497
  var DISTRIB_PROJ = new RegExp(
1429
- 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,
1498
+ 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,
1430
1499
  "gu"
1431
1500
  );
1432
1501
  var DISTRIB_FOOT = new RegExp(
1433
- 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,
1502
+ 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,
1503
+ "gu"
1504
+ );
1505
+ var NAMES_BLOB_IDX = "((?:[A-Z][0-9]?(?:['\u2032]?)\\s*(?:,|v\xE0)\\s*)+[A-Z][0-9]?(?:['\u2032]?))";
1506
+ var FROMS_BLOB_IDX = "((?:[A-Z][0-9]?(?:['\u2032]?)\\s*(?:,|v\xE0)\\s*)+[A-Z][0-9]?(?:['\u2032]?))(?!\\p{L})";
1507
+ var ZIP_FROM_PROJ = new RegExp(
1508
+ 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,
1434
1509
  "gu"
1435
1510
  );
1511
+ function splitNamesIdx(blob) {
1512
+ return blob.split(/,|và/).map((s) => {
1513
+ const mm = /^([A-Z][0-9]?)(['′]?)/u.exec(s.trim());
1514
+ return mm ? mm[2] ? `${mm[1]}'` : mm[1] : "";
1515
+ }).filter(Boolean);
1516
+ }
1436
1517
  var TWO_LINES_PREP = "(?:tr\xEAn|l\xEAn|xu\u1ED1ng|\u0111\u1EBFn|t\u1EDBi)";
1437
1518
  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])";
1438
1519
  var SHARED_FROM_TWO = new RegExp(
@@ -1440,21 +1521,44 @@ var SHARED_FROM_TWO = new RegExp(
1440
1521
  "gu"
1441
1522
  );
1442
1523
  var FEET_FROM_SHARED_LINE = new RegExp(
1443
- "(?:[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])",
1524
+ "(?:[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).
1525
+ 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])",
1444
1526
  "gu"
1445
1527
  );
1528
+ var PERP_LINE_PREFIX = "(?:\u0111\u01B0\u1EDDng\\s*th\u1EB3ng\\s+|ti\u1EBFp\\s*tuy\u1EBFn\\s+|c\u1EA1nh\\s+|\u0111o\u1EA1n\\s+)?";
1529
+ var PERP_LINE_TOK = "([A-Z]{1,2}(?![A-Za-z])|[A-Z]?[a-z][a-z]?[0-9]?(?![A-Za-z]))";
1446
1530
  var PERP_DRAW_DISTRIB = new RegExp(
1447
1531
  "(?:[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])",
1448
1532
  "gu"
1449
1533
  );
1534
+ var PAIRS_BLOB = "((?:[A-Z]{2}\\s*,\\s*)+[A-Z]{2})(?![A-Z])";
1535
+ var PERP_DRAW_ARE = new RegExp(
1536
+ "(?:[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])",
1537
+ "gu"
1538
+ );
1450
1539
  var PERP_SHARED_CUNG = new RegExp(
1451
- "(?:[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])",
1540
+ "(?:[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,
1452
1541
  "gu"
1453
1542
  );
1454
1543
  var FROM_DRAW_DISTRIB = new RegExp(
1455
1544
  "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])",
1456
1545
  "gu"
1457
1546
  );
1547
+ var DECLARE_LINE = new RegExp(
1548
+ "(?:\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])",
1549
+ "gu"
1550
+ );
1551
+ function declaredNamedLines(problem) {
1552
+ const out = /* @__PURE__ */ new Set();
1553
+ DECLARE_LINE.lastIndex = 0;
1554
+ let m;
1555
+ while ((m = DECLARE_LINE.exec(problem)) !== null) out.add(m[1]);
1556
+ return out;
1557
+ }
1558
+ function isAllowedLine(line, declared) {
1559
+ if (/^[A-Z]{1,2}$/u.test(line)) return true;
1560
+ return declared.has(line);
1561
+ }
1458
1562
  function splitNames(blob) {
1459
1563
  return blob.split(/,|và/).map((s) => {
1460
1564
  const mm = /^([A-Z])(['′]?)/u.exec(s.trim());
@@ -1462,11 +1566,11 @@ function splitNames(blob) {
1462
1566
  }).filter(Boolean);
1463
1567
  }
1464
1568
  var PERP_DRAW = new RegExp(
1465
- `(?:[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]))?`,
1569
+ `(?:[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]))?`,
1466
1570
  "gu"
1467
1571
  );
1468
1572
  var PERP_BARE = new RegExp(
1469
- `([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]))?`,
1573
+ `([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]))?`,
1470
1574
  "gu"
1471
1575
  );
1472
1576
  var DRAW_VERB = /[Kk]ẻ|[Vv]ẽ|[Dd]ựng/u;
@@ -1535,7 +1639,7 @@ function parseAltitudeBundle(text, fallbackTri) {
1535
1639
  }
1536
1640
  return out;
1537
1641
  }
1538
- function parseFeet(text, fallbackTri) {
1642
+ function parseFeet(text, fallbackTri, declared = /* @__PURE__ */ new Set()) {
1539
1643
  const out = [];
1540
1644
  const consumed = [];
1541
1645
  const bundled = parseAltitudeBundle(text, fallbackTri);
@@ -1577,6 +1681,24 @@ function parseFeet(text, fallbackTri) {
1577
1681
  }
1578
1682
  if (ok) return local;
1579
1683
  }
1684
+ ZIP_FROM_PROJ.lastIndex = 0;
1685
+ for (const zm of text.matchAll(ZIP_FROM_PROJ)) {
1686
+ const names = splitNamesIdx(zm[1]);
1687
+ const froms = splitNamesIdx(zm[2]);
1688
+ const lines = zm[3].split(/,|và/).map((s) => s.trim()).filter(Boolean);
1689
+ if (names.length < 2) continue;
1690
+ if (names.length !== froms.length || names.length !== lines.length) continue;
1691
+ const local = [];
1692
+ let ok = true;
1693
+ for (let i = 0; i < names.length; i++) {
1694
+ if (lines[i].includes(names[i]) || names[i] === froms[i]) {
1695
+ ok = false;
1696
+ break;
1697
+ }
1698
+ local.push({ name: names[i], from: froms[i], onLine: lines[i], withSegment: true });
1699
+ }
1700
+ if (ok && local.length > 0) return local;
1701
+ }
1580
1702
  for (const re of [DISTRIB_PROJ, DISTRIB_FOOT]) {
1581
1703
  re.lastIndex = 0;
1582
1704
  for (const dm2 of text.matchAll(re)) {
@@ -1617,8 +1739,8 @@ function parseFeet(text, fallbackTri) {
1617
1739
  for (const fm of text.matchAll(FEET_FROM_SHARED_LINE)) {
1618
1740
  const start = fm.index ?? 0;
1619
1741
  if (consumed.some(([a, b]) => start >= a && start < b)) continue;
1620
- const norm2 = (s) => s.replace(/′/g, "'");
1621
- const [n1, n2, f1, f2, line] = [fm[1], fm[2], norm2(fm[3]), norm2(fm[4]), norm2(fm[5])];
1742
+ const norm5 = (s) => s.replace(/′/g, "'");
1743
+ const [n1, n2, f1, f2, line] = [fm[1], fm[2], norm5(fm[3]), norm5(fm[4]), norm5(fm[5])];
1622
1744
  if (n1 === n2 || f1 === f2 || line.includes(n1) || line.includes(n2)) continue;
1623
1745
  out.push({ name: n1, from: f1, onLine: line, withSegment: true });
1624
1746
  out.push({ name: n2, from: f2, onLine: line, withSegment: true });
@@ -1643,7 +1765,9 @@ function parseFeet(text, fallbackTri) {
1643
1765
  if (MID_BEFORE.test(before)) continue;
1644
1766
  const nm = NAME_BEFORE.exec(before);
1645
1767
  if (!nm) continue;
1646
- out.push({ name: nm[1], from: m[1] ?? m[3], onLine: m[2] ?? m[4] });
1768
+ const onLine = m[2] ?? m[4];
1769
+ if (!isAllowedLine(onLine, declared)) continue;
1770
+ out.push({ name: nm[1], from: m[1] ?? m[3], onLine });
1647
1771
  }
1648
1772
  PERP_DRAW_DISTRIB.lastIndex = 0;
1649
1773
  for (const ddm of text.matchAll(PERP_DRAW_DISTRIB)) {
@@ -1654,10 +1778,36 @@ function parseFeet(text, fallbackTri) {
1654
1778
  out.push({ name: foot2, from: from2, onLine: line2, withSegment: true });
1655
1779
  consumed.push([ddm.index ?? 0, (ddm.index ?? 0) + ddm[0].length]);
1656
1780
  }
1781
+ PERP_DRAW_ARE.lastIndex = 0;
1782
+ for (const am of text.matchAll(PERP_DRAW_ARE)) {
1783
+ const start = am.index ?? 0;
1784
+ if (consumed.some(([a, b]) => start >= a && start < b)) continue;
1785
+ const pairs = am[1].split(",").map((s) => s.trim()).filter((s) => /^[A-Z]{2}$/.test(s));
1786
+ const lines = am[2].split(/,|và/).map((s) => s.trim()).filter((s) => /^[A-Z]{2}$/.test(s));
1787
+ if (pairs.length < 2 || pairs.length !== lines.length) continue;
1788
+ const from = pairs[0][0];
1789
+ if (!pairs.every((p) => p[0] === from)) continue;
1790
+ const local = [];
1791
+ let ok = true;
1792
+ for (let i = 0; i < pairs.length; i++) {
1793
+ const foot = pairs[i][1];
1794
+ const onLine = lines[i];
1795
+ if (onLine.includes(foot)) {
1796
+ ok = false;
1797
+ break;
1798
+ }
1799
+ local.push({ name: foot, from, onLine, withSegment: true });
1800
+ }
1801
+ if (ok && local.length > 0) {
1802
+ out.push(...local);
1803
+ consumed.push([start, start + am[0].length]);
1804
+ }
1805
+ }
1657
1806
  PERP_SHARED_CUNG.lastIndex = 0;
1658
1807
  for (const sm of text.matchAll(PERP_SHARED_CUNG)) {
1659
1808
  const [f1, t1, f2, t2, line] = [sm[1], sm[2], sm[3], sm[4], sm[5]];
1660
1809
  if (t1 === t2 || line.includes(t1) || line.includes(t2)) continue;
1810
+ if (!isAllowedLine(line, declared)) continue;
1661
1811
  out.push({ name: t1, from: f1, onLine: line, withSegment: true });
1662
1812
  out.push({ name: t2, from: f2, onLine: line, withSegment: true });
1663
1813
  consumed.push([sm.index ?? 0, (sm.index ?? 0) + sm[0].length]);
@@ -1671,6 +1821,7 @@ function parseFeet(text, fallbackTri) {
1671
1821
  const at = pm[4];
1672
1822
  if (at && at !== foot) continue;
1673
1823
  if (onLine.includes(foot)) continue;
1824
+ if (!isAllowedLine(onLine, declared)) continue;
1674
1825
  out.push({ name: foot, from, onLine });
1675
1826
  }
1676
1827
  if (DRAW_VERB.test(text)) {
@@ -1683,6 +1834,7 @@ function parseFeet(text, fallbackTri) {
1683
1834
  const at = bm[4];
1684
1835
  if (at && at !== foot) continue;
1685
1836
  if (onLine.includes(foot)) continue;
1837
+ if (!isAllowedLine(onLine, declared)) continue;
1686
1838
  if (out.some((o) => o.name === foot)) continue;
1687
1839
  out.push({ name: foot, from, onLine });
1688
1840
  }
@@ -1718,13 +1870,14 @@ var perpFootRule = {
1718
1870
  match(ctx) {
1719
1871
  const out = [];
1720
1872
  const fallbackTri = uniqueTriangle(ctx.problem);
1873
+ const declared = declaredNamedLines(ctx.problem);
1721
1874
  for (const c of ctx.clauses) {
1722
- const feet = parseFeet(c.text, fallbackTri);
1875
+ const feet = parseFeet(c.text, fallbackTri, declared);
1723
1876
  if (feet.length === 0) continue;
1724
1877
  const intents = feet.flatMap((f) => {
1725
1878
  if (f.from === "") return [addPoint(f.name, { kind: "orthocenter", of: f.onLine.split("") })];
1726
- const add = addPoint(f.name, { kind: "perpFoot", from: f.from, onLine: f.onLine });
1727
- return f.withSegment ? [add, connect(f.from, f.name, "segment")] : [add];
1879
+ const add2 = addPoint(f.name, { kind: "perpFoot", from: f.from, onLine: f.onLine });
1880
+ return f.withSegment ? [add2, connect(f.from, f.name, "segment")] : [add2];
1728
1881
  });
1729
1882
  out.push({ ruleId: "perpFoot", clauseIds: [c.id], intents });
1730
1883
  }
@@ -1771,28 +1924,28 @@ function parseNum(raw) {
1771
1924
  function isInscribedCircumscribed(problem, center) {
1772
1925
  const REL = "(?:(?:n\u1ED9i|ngo\u1EA1i)\\s*ti\u1EBFp|[Ii]nscribed|[Cc]ircumscrib(?:es|ed))";
1773
1926
  const after = new RegExp(
1774
- `${REL}[^.]{0,30}?(?:\u0111\u01B0\u1EDDng\\s*tr\xF2n\\s*|[Cc]ircle\\s*)?\\(?\\s*${center}(?![A-Z])`,
1927
+ `${REL}[^.]{0,30}?(?:\u0111\u01B0\u1EDDng\\s*tr\xF2n\\s*|[Cc]ircle\\s*)?\\(?\\s*${escapeRe(center)}(?![A-Z])`,
1775
1928
  "u"
1776
1929
  );
1777
1930
  const before = new RegExp(
1778
- `\\(\\s*${center}\\s*[;,][^()]*\\)[^.]{0,30}?${REL}`,
1931
+ `\\(\\s*${escapeRe(center)}\\s*[;,][^()]*\\)[^.]{0,30}?${REL}`,
1779
1932
  "u"
1780
1933
  );
1781
1934
  return after.test(problem) || before.test(problem);
1782
1935
  }
1783
1936
  function isDiameterCircle(problem, center) {
1784
1937
  const parenThenDiameter = new RegExp(
1785
- `\\(\\s*${center}\\s*[;,][^()]*\\)[^.]{0,40}?${DUONG_KW}\\s*k\xEDnh\\s+[A-Z]{2}(?![A-Z])`,
1938
+ `\\(\\s*${escapeRe(center)}\\s*[;,][^()]*\\)[^.]{0,40}?${DUONG_KW}\\s*k\xEDnh\\s+[A-Z]{2}(?![A-Z])`,
1786
1939
  "u"
1787
1940
  );
1788
1941
  const namedThenDiameter = new RegExp(
1789
- `${CIRCLE_KW}\\s*\\(\\s*${center}\\s*\\)[^.]{0,40}?${DUONG_KW}\\s*k\xEDnh\\s+[A-Z]{2}(?![A-Z])`,
1942
+ `${CIRCLE_KW}\\s*\\(\\s*${escapeRe(center)}\\s*\\)[^.]{0,40}?${DUONG_KW}\\s*k\xEDnh\\s+[A-Z]{2}(?![A-Z])`,
1790
1943
  "u"
1791
1944
  );
1792
1945
  return parenThenDiameter.test(problem) || namedThenDiameter.test(problem);
1793
1946
  }
1794
1947
  function findParenClauseId(clauses, center) {
1795
- const frag = new RegExp(`\\(\\s*${center}(?![A-Z])`, "u");
1948
+ const frag = new RegExp(`\\(\\s*${escapeRe(center)}(?![A-Z])`, "u");
1796
1949
  for (const c of clauses) if (frag.test(c.text)) return c.id;
1797
1950
  return void 0;
1798
1951
  }
@@ -2330,8 +2483,52 @@ var externalPointRule = {
2330
2483
  }
2331
2484
  };
2332
2485
 
2486
+ // src/stamps/geometry-2d/ai/rules/externalPointAtRadius.ts
2487
+ var METRIC = /([A-Z])(?:['′]?)([A-Z])(?:['′]?)\s*=\s*([2-9])\s*[Rr](?![A-Za-z])/gu;
2488
+ function isCenter(problem, c) {
2489
+ const e = escapeRe(c);
2490
+ return new RegExp(
2491
+ String.raw`\(\s*${e}[\s);,]|tâm\s*${e}(?![A-Za-z])|đường\s*tròn\s*\(?\s*${e}(?![A-Za-z])`,
2492
+ "u"
2493
+ ).test(problem);
2494
+ }
2495
+ var externalPointAtRadiusRule = {
2496
+ id: "externalPointAtRadius",
2497
+ priority: 67,
2498
+ languages: ["vi"],
2499
+ patterns: [/[A-Z](?:['′]?)[A-Z](?:['′]?)\s*=\s*[2-9]\s*[Rr](?![A-Za-z])/u],
2500
+ match(ctx) {
2501
+ const out = [];
2502
+ for (const c of ctx.clauses) {
2503
+ METRIC.lastIndex = 0;
2504
+ for (const m of c.text.matchAll(METRIC)) {
2505
+ const a = m[1];
2506
+ const b = m[2];
2507
+ let center;
2508
+ let point;
2509
+ const aC = isCenter(ctx.problem, a);
2510
+ const bC = isCenter(ctx.problem, b);
2511
+ if (aC && !bC) {
2512
+ center = a;
2513
+ point = b;
2514
+ } else if (bC && !aC) {
2515
+ center = b;
2516
+ point = a;
2517
+ }
2518
+ if (!center || !point || center === point) continue;
2519
+ out.push({
2520
+ ruleId: "externalPointAtRadius",
2521
+ clauseIds: [c.id],
2522
+ intents: [addPoint(point, { kind: "externalToCircle", circle: center })]
2523
+ });
2524
+ }
2525
+ }
2526
+ return out;
2527
+ }
2528
+ };
2529
+
2333
2530
  // src/stamps/geometry-2d/ai/rules/arcMidpoint.ts
2334
- var ARC_MID = /(?:chính\s+giữa|trung\s*điểm)\s+(?:của\s+)?cung/u;
2531
+ var ARC_MID = /(?:chính\s+giữa|trung\s*điểm)\s+(?:của\s+|các\s+)*cung/u;
2335
2532
  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;
2336
2533
  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;
2337
2534
  var TRI3 = /tam giác\s+([A-Z])([A-Z])([A-Z])/u;
@@ -2367,6 +2564,10 @@ var DISTRIB_TWO_ARCS = new RegExp(
2367
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+)?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])",
2368
2565
  "u"
2369
2566
  );
2567
+ var DISTRIB_TWO_ARCS3 = new RegExp(
2568
+ "([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])",
2569
+ "u"
2570
+ );
2370
2571
  function resolveCircle(problem) {
2371
2572
  const w = CIRCLE_WORDS.exec(problem);
2372
2573
  if (w) return w[1];
@@ -2414,6 +2615,27 @@ var arcMidpointRule = {
2414
2615
  };
2415
2616
  for (const c of ctx.clauses) {
2416
2617
  if (!ARC_MID.test(c.text)) continue;
2618
+ const twoArcs3 = DISTRIB_TWO_ARCS3.exec(c.text);
2619
+ if (twoArcs3) {
2620
+ const [, n1, n2, x1, y1, z1, x2, y2, z22] = twoArcs3;
2621
+ const triples = [
2622
+ [n1, x1, z1, y1],
2623
+ [n2, x2, z22, y2]
2624
+ ];
2625
+ const ok = triples.every(
2626
+ ([n, a2, b2, ct]) => arcOnCircum(a2, b2) && (/* @__PURE__ */ new Set([a2, b2, ct])).size === 3 && n !== a2 && n !== b2 && n !== ct
2627
+ );
2628
+ if (ok) {
2629
+ const mark = out.length;
2630
+ let good = true;
2631
+ for (const [n, a2, b2, ct] of triples) {
2632
+ good = pushArc(c, n, a2, b2, { rel: "in", point: ct });
2633
+ if (!good) break;
2634
+ }
2635
+ if (!good) out.length = mark;
2636
+ }
2637
+ continue;
2638
+ }
2417
2639
  const twoArcs = DISTRIB_TWO_ARCS.exec(c.text);
2418
2640
  if (twoArcs) {
2419
2641
  const [, n1, n2, a1, b1, a2, b2] = twoArcs;
@@ -2437,7 +2659,7 @@ var arcMidpointRule = {
2437
2659
  }
2438
2660
  const pairM = ARC_PAIR.exec(c.text);
2439
2661
  if (!pairM) continue;
2440
- if (pairM[1] && /lớn/u.test(pairM[1])) continue;
2662
+ const isMajor = !!pairM[1] && /lớn/u.test(pairM[1]);
2441
2663
  const pair = pairFromToken(pairM[2] + pairM[3]);
2442
2664
  if (pair.length !== 2) continue;
2443
2665
  const [a, b] = pair;
@@ -2462,7 +2684,9 @@ var arcMidpointRule = {
2462
2684
  pushArc(c, name, a, b, conts[0]);
2463
2685
  } else if (tri) {
2464
2686
  const third = [tri[1], tri[2], tri[3]].find((v) => v !== a && v !== b);
2465
- if (third) pushArc(c, name, a, b, { rel: "not", point: third });
2687
+ if (third) pushArc(c, name, a, b, isMajor ? { rel: "in", point: third } : { rel: "not", point: third });
2688
+ } else if (isMajor && explicitCircle) {
2689
+ pushArc(c, name, a, b, { rel: "in", point: explicitCircle });
2466
2690
  } else {
2467
2691
  out.push({
2468
2692
  ruleId: "arcMidpoint",
@@ -2558,6 +2782,14 @@ var RE_CUT = new RegExp(
2558
2782
  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])",
2559
2783
  "gu"
2560
2784
  );
2785
+ var FOOT2 = "([A-Z])([A-Z])\\s*,\\s*([A-Z])([A-Z])";
2786
+ var FOOT3 = "([A-Z])([A-Z])\\s*,\\s*([A-Z])([A-Z])\\s*,\\s*([A-Z])([A-Z])";
2787
+ var TARGET2 = "([A-Z]{2})\\s*,\\s*([A-Z]{2})(?![A-Z])";
2788
+ var TARGET3 = "([A-Z]{2})\\s*,\\s*([A-Z]{2})\\s*,\\s*([A-Z]{2})(?![A-Z])";
2789
+ 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+)?";
2790
+ 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+";
2791
+ var PERP_NAMED_DISTRIB3 = new RegExp(FROM + FOOT3 + "\\s+" + PERP_KW + TARGET3, "gu");
2792
+ var PERP_NAMED_DISTRIB2 = new RegExp(FROM + FOOT2 + "\\s+" + PERP_KW + TARGET2, "gu");
2561
2793
  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;
2562
2794
  function lineNameOf(isParallel, through) {
2563
2795
  return (isParallel ? "par" : "prp") + through;
@@ -2587,6 +2819,43 @@ var parallelPerpRule = {
2587
2819
  });
2588
2820
  cutSpans.push([start, end]);
2589
2821
  };
2822
+ const emitNamedPerp = (from, foot, target, start, end) => {
2823
+ if (foot === from || target.includes(foot)) return;
2824
+ const lineName = lineNameOf(false, from) + foot;
2825
+ out.push({
2826
+ ruleId: "parallelPerp",
2827
+ clauseIds: [c.id],
2828
+ intents: [
2829
+ drawLine(lineName, "perpThrough", { through: from, to: target }),
2830
+ addPoint(foot, { kind: "intersection", of: [lineName, target] })
2831
+ ]
2832
+ });
2833
+ cutSpans.push([start, end]);
2834
+ };
2835
+ PERP_NAMED_DISTRIB3.lastIndex = 0;
2836
+ for (const m of c.text.matchAll(PERP_NAMED_DISTRIB3)) {
2837
+ const from = m[1];
2838
+ const triples = [
2839
+ [m[3], m[8]],
2840
+ [m[5], m[9]],
2841
+ [m[7], m[10]]
2842
+ ];
2843
+ if ([m[2], m[4], m[6]].every((f) => f === from)) {
2844
+ const s = m.index ?? 0;
2845
+ const e = s + m[0].length;
2846
+ for (const [foot, target] of triples) emitNamedPerp(from, foot, target, s, e);
2847
+ }
2848
+ }
2849
+ PERP_NAMED_DISTRIB2.lastIndex = 0;
2850
+ for (const m of c.text.matchAll(PERP_NAMED_DISTRIB2)) {
2851
+ const s = m.index ?? 0;
2852
+ if (cutSpans.some(([a, b]) => s >= a && s < b)) continue;
2853
+ const from = m[1];
2854
+ if (m[2] !== from || m[4] !== from) continue;
2855
+ const pairs = [[m[3], m[6]], [m[5], m[7]]];
2856
+ const e = s + m[0].length;
2857
+ for (const [foot, target] of pairs) emitNamedPerp(from, foot, target, s, e);
2858
+ }
2590
2859
  RE_GIAO.lastIndex = 0;
2591
2860
  for (const m of c.text.matchAll(RE_GIAO)) {
2592
2861
  emitCut(m[2], m[3], m[4], m[5], m[6] + m[7], m[1], m.index ?? 0, (m.index ?? 0) + m[0].length);
@@ -2620,12 +2889,23 @@ var REFLECT = /đối\s*xứng/u;
2620
2889
  var PT = `[A-Z](?:['\u2032])?`;
2621
2890
  var NAME_BEFORE4 = new RegExp(
2622
2891
  `(${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']?)`,
2623
- "u"
2892
+ "gu"
2624
2893
  );
2625
2894
  var DISTRIB2 = new RegExp(
2626
2895
  `(${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]?)`,
2627
2896
  "u"
2628
2897
  );
2898
+ var LL = `(?:l\u1EA7n\\s*l\u01B0\u1EE3t|t\u01B0\u01A1ng\\s*\u1EE9ng|(?:theo\\s+)?th\u1EE9\\s+t\u1EF1)`;
2899
+ var NAMES_BLOB2 = `(${PT}(?:\\s*(?:,|v\xE0)\\s*${PT})+)`;
2900
+ var SRCS_BLOB = `(${PT}(?:\\s*(?:,|v\xE0)\\s*${PT})+)`;
2901
+ var MIRRORS_BLOB = `([A-Za-z]{1,2}(?:['\u2032]?)(?:\\s*(?:,|v\xE0)\\s*[A-Za-z]{1,2}(?:['\u2032]?))+)`;
2902
+ var DISTRIB_MULTI = new RegExp(
2903
+ `${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}`,
2904
+ "u"
2905
+ );
2906
+ function splitList(blob) {
2907
+ return blob.split(/\s*,\s*|\s+và\s+/u).map((s) => s.trim()).filter(Boolean);
2908
+ }
2629
2909
  var NAME_AFTER = new RegExp(
2630
2910
  `(?:\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']?)`,
2631
2911
  "u"
@@ -2651,6 +2931,28 @@ var reflectionRule = {
2651
2931
  const out = [];
2652
2932
  for (const c of ctx.clauses) {
2653
2933
  if (!REFLECT.test(c.text) && !REFLECT_EN_PRE.test(c.text)) continue;
2934
+ const dmm = DISTRIB_MULTI.exec(c.text);
2935
+ if (dmm) {
2936
+ const names = splitList(dmm[1]).map(normalizePointName);
2937
+ const srcs = splitList(dmm[2]).map(normalizePointName);
2938
+ const mirrors = splitList(dmm[3]);
2939
+ if (names.length >= 2 && names.length === srcs.length && names.length === mirrors.length) {
2940
+ const through2 = mirrors.map(classifyThrough);
2941
+ if (through2.every((t) => !!t)) {
2942
+ out.push({
2943
+ ruleId: "reflection",
2944
+ clauseIds: [c.id],
2945
+ intents: names.map(
2946
+ (nm, i) => addPoint(
2947
+ nm,
2948
+ through2[i].kind === "point" ? { kind: "reflectPoint", of: srcs[i], through: through2[i].value } : { kind: "reflectLine", of: srcs[i], through: through2[i].value }
2949
+ )
2950
+ )
2951
+ });
2952
+ continue;
2953
+ }
2954
+ }
2955
+ }
2654
2956
  const dm = DISTRIB2.exec(c.text);
2655
2957
  if (dm) {
2656
2958
  const [n1, n2, ofPt, raw1, raw2] = [
@@ -2672,15 +2974,31 @@ var reflectionRule = {
2672
2974
  continue;
2673
2975
  }
2674
2976
  }
2977
+ NAME_BEFORE4.lastIndex = 0;
2978
+ const befores = [...c.text.matchAll(NAME_BEFORE4)];
2979
+ if (befores.length) {
2980
+ for (const b of befores) {
2981
+ const nm = normalizePointName(b[1]);
2982
+ const ofP = normalizePointName(b[2]);
2983
+ const thr = classifyThrough(b[3]);
2984
+ if (!thr) continue;
2985
+ out.push({
2986
+ ruleId: "reflection",
2987
+ clauseIds: [c.id],
2988
+ intents: [
2989
+ addPoint(
2990
+ nm,
2991
+ thr.kind === "point" ? { kind: "reflectPoint", of: ofP, through: thr.value } : { kind: "reflectLine", of: ofP, through: thr.value }
2992
+ )
2993
+ ]
2994
+ });
2995
+ }
2996
+ continue;
2997
+ }
2675
2998
  let name;
2676
2999
  let of;
2677
3000
  let throughRaw;
2678
- const before = NAME_BEFORE4.exec(c.text);
2679
- if (before) {
2680
- name = normalizePointName(before[1]);
2681
- of = normalizePointName(before[2]);
2682
- throughRaw = before[3];
2683
- } else {
3001
+ {
2684
3002
  const after = NAME_AFTER.exec(c.text);
2685
3003
  if (after) {
2686
3004
  name = extractPointName(c.text);
@@ -3183,7 +3501,7 @@ var DISTRIB_TWO = new RegExp(
3183
3501
  "gu"
3184
3502
  );
3185
3503
  var CAT_ONE_TWO = new RegExp(
3186
- `${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])`,
3504
+ `${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])`,
3187
3505
  "gu"
3188
3506
  );
3189
3507
  var CAT_TWO_ONE = new RegExp(
@@ -3198,12 +3516,22 @@ var CAT_ONE_RAY = new RegExp(
3198
3516
  `${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])`,
3199
3517
  "gu"
3200
3518
  );
3519
+ var NAMED_CAT_TAI = new RegExp(
3520
+ `(?:\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])`,
3521
+ "gu"
3522
+ );
3201
3523
  var GIAO_TWO_ONE_LA = new RegExp(
3202
3524
  `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])`,
3203
3525
  "gu"
3204
3526
  );
3527
+ var ZIP_MK = `(?:l\u1EA7n\\s*l\u01B0\u1EE3t|theo\\s+th\u1EE9\\s+t\u1EF1|t\u01B0\u01A1ng\\s+\u1EE9ng)`;
3528
+ var ZIP_SIDE = `(?:\u0111o\u1EA1n(?:\\s+th\u1EB3ng)?\\s+|c\u1EA1nh\\s+|tia\\s+|${DUONG_KW}\\s*th\u1EB3ng\\s+)?`;
3205
3529
  var CAT_ZIP = new RegExp(
3206
- `${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])`,
3530
+ `${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])`,
3531
+ "gu"
3532
+ );
3533
+ var CAT_ZIP_POST = new RegExp(
3534
+ `${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])`,
3207
3535
  "gu"
3208
3536
  );
3209
3537
  var NAME_BEFORE6 = /([A-Z])(?:['′]?)\s+là\s+$/u;
@@ -3218,6 +3546,10 @@ var CAP_SYMBOL = new RegExp(
3218
3546
  `(${PT2})\\s*=\\s*(${PT2})\\s*(${PT2})\\s*\u2229\\s*(${PT2})\\s*(${PT2})(?!${PT2})`,
3219
3547
  "gu"
3220
3548
  );
3549
+ var CAP_SYMBOL_POST = new RegExp(
3550
+ `(${PT2})\\s*(${PT2})\\s*\u2229\\s*(${PT2})\\s*(${PT2})\\s*=\\s*\\{?\\s*(${PT2})\\s*\\}?`,
3551
+ "gu"
3552
+ );
3221
3553
  var PTP = "[A-Z](?:['\u2032])?";
3222
3554
  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])`;
3223
3555
  var GIAO_PRIMED = new RegExp(
@@ -3242,6 +3574,12 @@ function makeIntent(name, ref1, ref2) {
3242
3574
  if (ends.includes(name)) return null;
3243
3575
  return addPoint(name, { kind: "intersection", of: [ref1, ref2] });
3244
3576
  }
3577
+ function declaredNamedLines2(problem) {
3578
+ const out = /* @__PURE__ */ new Set();
3579
+ 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;
3580
+ for (const m of problem.matchAll(RE31)) out.add(m[1]);
3581
+ return out;
3582
+ }
3245
3583
  function makeIntentEnds(name, e1, e2, e3, e4) {
3246
3584
  const ends = [e1, e2, e3, e4];
3247
3585
  if (new Set(ends).size !== 4) return null;
@@ -3259,6 +3597,7 @@ var intersectionRule = {
3259
3597
  patterns: [PREFILTER8],
3260
3598
  match(ctx) {
3261
3599
  const out = [];
3600
+ const namedLines = declaredNamedLines2(ctx.problem);
3262
3601
  for (const c of ctx.clauses) {
3263
3602
  const seen = /* @__PURE__ */ new Set();
3264
3603
  const emit = (name, ref1, ref2) => {
@@ -3282,6 +3621,15 @@ var intersectionRule = {
3282
3621
  seen.add(name);
3283
3622
  out.push({ ruleId: "intersection", clauseIds: [c.id], intents: [intent] });
3284
3623
  }
3624
+ CAP_SYMBOL_POST.lastIndex = 0;
3625
+ for (const m of c.text.matchAll(CAP_SYMBOL_POST)) {
3626
+ const name = m[5];
3627
+ if (seen.has(name)) continue;
3628
+ const intent = makeIntentEnds(name, m[1], m[2], m[3], m[4]);
3629
+ if (!intent) continue;
3630
+ seen.add(name);
3631
+ out.push({ ruleId: "intersection", clauseIds: [c.id], intents: [intent] });
3632
+ }
3285
3633
  CAT_ONE_TWO.lastIndex = 0;
3286
3634
  for (const m of c.text.matchAll(CAT_ONE_TWO)) {
3287
3635
  emit(m[4], m[1], m[2]);
@@ -3313,6 +3661,12 @@ var intersectionRule = {
3313
3661
  for (const m of c.text.matchAll(CAT_ONE_RAY)) {
3314
3662
  emitShape(m[3], m[1], m[2]);
3315
3663
  }
3664
+ if (namedLines.size > 0) {
3665
+ NAMED_CAT_TAI.lastIndex = 0;
3666
+ for (const m of c.text.matchAll(NAMED_CAT_TAI)) {
3667
+ if (namedLines.has(m[1])) emitShape(m[3], m[2], m[1]);
3668
+ }
3669
+ }
3316
3670
  GIAO_TWO_ONE_LA.lastIndex = 0;
3317
3671
  for (const m of c.text.matchAll(GIAO_TWO_ONE_LA)) {
3318
3672
  emit(m[4], m[1], m[3]);
@@ -3323,6 +3677,11 @@ var intersectionRule = {
3323
3677
  emit(m[5], m[1], m[3]);
3324
3678
  emit(m[6], m[2], m[4]);
3325
3679
  }
3680
+ CAT_ZIP_POST.lastIndex = 0;
3681
+ for (const m of c.text.matchAll(CAT_ZIP_POST)) {
3682
+ emit(m[5], m[1], m[3]);
3683
+ emit(m[6], m[2], m[4]);
3684
+ }
3326
3685
  GIAO_LA.lastIndex = 0;
3327
3686
  for (const m of c.text.matchAll(GIAO_LA)) emit(m[3], m[1], m[2]);
3328
3687
  GIAO_DIEM.lastIndex = 0;
@@ -3406,9 +3765,6 @@ var THETA_A = 2.3;
3406
3765
  var THETA_B = 0.7;
3407
3766
  var THETA_STEP = 0.9;
3408
3767
  var PREFILTER9 = /[Dd]ây/u;
3409
- function escapeRe(s) {
3410
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3411
- }
3412
3768
  function findCircleClauseId(clauses, circle) {
3413
3769
  const frag = new RegExp(CIRCLE_KW + `|\\(\\s*${escapeRe(circle)}\\s*\\)`, "u");
3414
3770
  for (const c of clauses) if (frag.test(c.text)) return c.id;
@@ -3421,6 +3777,29 @@ function resolveCircle2(problem) {
3421
3777
  if (p) return p[1];
3422
3778
  return void 0;
3423
3779
  }
3780
+ function inscribedTriangleVertices(problem, circle) {
3781
+ const out = /* @__PURE__ */ new Set();
3782
+ const NAMED_O = "(?:\\(\\s*([^\\s;,).:]+)\\s*\\)|t\xE2m\\s+([A-Z]))?";
3783
+ 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])";
3784
+ const RE_A = new RegExp(TRI7 + "[^.]{0,40}?n\u1ED9i\\s*ti\u1EBFp\\s+(?:trong\\s+)?(?:" + CIRCLE_KW + ")?\\s*" + NAMED_O, "gu");
3785
+ for (const m of problem.matchAll(RE_A)) {
3786
+ const named = m[4] ?? m[5];
3787
+ if (named && named !== circle) continue;
3788
+ out.add(m[1]);
3789
+ out.add(m[2]);
3790
+ out.add(m[3]);
3791
+ }
3792
+ const NAMED_O_B = "(?:\\(\\s*([^\\s;,).:]+)\\s*\\)|t\xE2m\\s+([A-Z])|([A-Z]))?";
3793
+ const RE_B = new RegExp(CIRCLE_KW + "\\s*" + NAMED_O_B + "\\s*ngo\u1EA1i\\s*ti\u1EBFp\\s+" + TRI7, "gu");
3794
+ for (const m of problem.matchAll(RE_B)) {
3795
+ const named = m[1] ?? m[2] ?? m[3];
3796
+ if (named && named !== circle) continue;
3797
+ out.add(m[4]);
3798
+ out.add(m[5]);
3799
+ out.add(m[6]);
3800
+ }
3801
+ return out;
3802
+ }
3424
3803
  var chordRule = {
3425
3804
  id: "chord",
3426
3805
  // Dưới circleRadius (75) — nếu circle O có bán kính cụ thể, định nghĩa đó thắng.
@@ -3434,6 +3813,7 @@ var chordRule = {
3434
3813
  match(ctx) {
3435
3814
  const circle = resolveCircle2(ctx.problem);
3436
3815
  if (!circle) return [];
3816
+ const inscribedVerts = inscribedTriangleVertices(ctx.problem, circle);
3437
3817
  const chords = [];
3438
3818
  const seen = /* @__PURE__ */ new Set();
3439
3819
  for (const c of ctx.clauses) {
@@ -3468,15 +3848,11 @@ var chordRule = {
3468
3848
  });
3469
3849
  chords.forEach((ch, k) => {
3470
3850
  const offset = k * THETA_STEP;
3471
- out.push({
3472
- ruleId: "chord",
3473
- clauseIds: [ch.clauseId],
3474
- intents: [
3475
- addPoint(ch.a, { kind: "onCircle", circle, theta: THETA_A + offset }),
3476
- addPoint(ch.b, { kind: "onCircle", circle, theta: THETA_B + offset }),
3477
- connect(ch.a, ch.b, "segment")
3478
- ]
3479
- });
3851
+ const intents = [];
3852
+ if (!inscribedVerts.has(ch.a)) intents.push(addPoint(ch.a, { kind: "onCircle", circle, theta: THETA_A + offset }));
3853
+ if (!inscribedVerts.has(ch.b)) intents.push(addPoint(ch.b, { kind: "onCircle", circle, theta: THETA_B + offset }));
3854
+ intents.push(connect(ch.a, ch.b, "segment"));
3855
+ out.push({ ruleId: "chord", clauseIds: [ch.clauseId], intents });
3480
3856
  });
3481
3857
  return out;
3482
3858
  }
@@ -3486,9 +3862,13 @@ var chordRule = {
3486
3862
  var PREFILTER10 = /tiếp\s*xúc|tiếp\s*điểm/u;
3487
3863
  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;
3488
3864
  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;
3865
+ 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;
3489
3866
  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;
3490
3867
  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;
3491
3868
  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;
3869
+ 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;
3870
+ var PAIR_RE = /([A-Z]{2})\s+tại\s+([A-Z]'?)/giu;
3871
+ 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;
3492
3872
  function splitCsv(blob) {
3493
3873
  return blob.split(/,|và/iu).map((s) => s.trim()).filter(Boolean);
3494
3874
  }
@@ -3526,6 +3906,22 @@ var incircleTangencyRule = {
3526
3906
  match(ctx) {
3527
3907
  const out = [];
3528
3908
  for (const chunk of logicalChunks(ctx.problem, ctx.clauses)) {
3909
+ const tb = TANGENT_NAMED_BEFORE.exec(chunk.text);
3910
+ if (tb) {
3911
+ const names = splitCsv(tb[1]);
3912
+ const sides2 = splitCsv(tb[2]);
3913
+ const circle2 = tb[3];
3914
+ if (sides2.length >= 1 && sides2.length === names.length) {
3915
+ const verts2 = [...new Set(sides2.join("").split(""))];
3916
+ const intents2 = [];
3917
+ if (verts2.length === 3) intents2.push(drawCircle(circle2, "inscribedIn", { triangle: verts2 }));
3918
+ for (let i = 0; i < names.length; i++) {
3919
+ intents2.push(addPoint(names[i], { kind: "tangencyPoint", circle: circle2, onLine: sides2[i] }));
3920
+ }
3921
+ out.push({ ruleId: "incircleTangency", clauseIds: chunk.clauseIds, intents: intents2 });
3922
+ continue;
3923
+ }
3924
+ }
3529
3925
  const tn = TANGENT_NAMED_FWD.exec(chunk.text) ?? TANGENT_NAMED_REV.exec(chunk.text);
3530
3926
  if (tn) {
3531
3927
  const fwd = TANGENT_NAMED_FWD.test(chunk.text);
@@ -3543,6 +3939,20 @@ var incircleTangencyRule = {
3543
3939
  continue;
3544
3940
  }
3545
3941
  }
3942
+ const il = INTERLEAVED_CIRCLE.exec(chunk.text);
3943
+ if (il) {
3944
+ const circle2 = il[1];
3945
+ const pairs = [];
3946
+ PAIR_RE.lastIndex = 0;
3947
+ for (const pm of il[2].matchAll(PAIR_RE)) pairs.push([pm[1], pm[2]]);
3948
+ if (pairs.length >= 1) {
3949
+ const intents2 = pairs.map(
3950
+ ([side, name]) => addPoint(name, { kind: "tangencyPoint", circle: circle2, onLine: side })
3951
+ );
3952
+ out.push({ ruleId: "incircleTangency", clauseIds: chunk.clauseIds, intents: intents2 });
3953
+ continue;
3954
+ }
3955
+ }
3546
3956
  const rev = REVERSED_SIDE_POINT.exec(chunk.text);
3547
3957
  if (rev) {
3548
3958
  const sides2 = splitCsv(rev[1]);
@@ -3561,6 +3971,19 @@ var incircleTangencyRule = {
3561
3971
  continue;
3562
3972
  }
3563
3973
  }
3974
+ const rb = REVERSED_BARE.exec(chunk.text);
3975
+ if (rb) {
3976
+ const sides2 = splitCsv(rb[1]);
3977
+ const circle2 = rb[2];
3978
+ const names = splitCsv(rb[3]);
3979
+ if (sides2.length >= 1 && sides2.length === names.length) {
3980
+ const intents2 = sides2.map(
3981
+ (side, i) => addPoint(names[i], { kind: "tangencyPoint", circle: circle2, onLine: side })
3982
+ );
3983
+ out.push({ ruleId: "incircleTangency", clauseIds: chunk.clauseIds, intents: intents2 });
3984
+ continue;
3985
+ }
3986
+ }
3564
3987
  let circle = parseIncircleName(chunk.text);
3565
3988
  if (!circle) {
3566
3989
  const fm = FORWARD_CIRCLE.exec(chunk.text);
@@ -3675,16 +4098,17 @@ var altitudeDiameterCirclesRule = {
3675
4098
 
3676
4099
  // src/stamps/geometry-2d/ai/rules/diameterCircleCutsSides.ts
3677
4100
  var PATTERN = new RegExp(
3678
- // Chỉ-báo đường tròn: "đường tròn (tâm X|(X))?" HOẶC "(X)" trần (httcd:2 "(O)
3679
- // đường kính BC" KHÔNG chữ "đường tròn"). Bắt buộc có 1 trong 2.
3680
- "(?:" + 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…").
3681
- // "theo thứ tự" = "lần lượt". Anchor điểm = "tại" (dạng "" tách sang
3682
- // PATTERN_O lazy-region dừng "ở" đầu tiên thể nuốt thiếu vao10:155).
3683
- "\\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}))*)",
4101
+ // Chỉ-báo đường tròn: "đường tròn ((X)|tâm X|X-trần)?" HOẶC "(X)" trần (httcd:2
4102
+ // "(O) đường kính BC"). "X-trần" = tâm 1-HOA không ngoặc ("đường tròn O đường
4103
+ // kính BC" vxhung:14). Bắt buộc 1 trong 2 nhánh.
4104
+ "(?:" + 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…").
4105
+ // "theo thứ tự" = "lần lượt". Anchor điểm = "tại"; "(các)? (điểm)?" trước danh
4106
+ // sách tên ("tại các điểm D và E" — vxhung:6).
4107
+ "\\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}))*)",
3684
4108
  "gu"
3685
4109
  );
3686
4110
  var PATTERN_O = new RegExp(
3687
- "(?:" + 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}))+)",
4111
+ "(?:" + 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}))+)",
3688
4112
  "gu"
3689
4113
  );
3690
4114
  var PATTERN_INTERLEAVED = new RegExp(
@@ -3783,6 +4207,10 @@ var GIVEN_BARE = new RegExp(
3783
4207
  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])`,
3784
4208
  "gu"
3785
4209
  );
4210
+ var CHO_BARE = new RegExp(
4211
+ 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])`,
4212
+ "gu"
4213
+ );
3786
4214
  var BARE_PAREN = new RegExp(
3787
4215
  String.raw`\(\s*(${CTR})\s*\)\s*(?:có\s+)?` + DUONG_KW + String.raw`\s*kính\s+([A-Z])([A-Z])(?![A-Z])`,
3788
4216
  "gu"
@@ -3873,6 +4301,22 @@ var circleDiameterRule = {
3873
4301
  intents: intentsFor(p)
3874
4302
  });
3875
4303
  }
4304
+ CHO_BARE.lastIndex = 0;
4305
+ let cm;
4306
+ while ((cm = CHO_BARE.exec(ctx.problem)) !== null) {
4307
+ const p = { center: cm[1], a: cm[2], b: cm[3] };
4308
+ if (!p.center || p.center === p.a || p.center === p.b || p.a === p.b) continue;
4309
+ const key = `${p.center}|${p.a}${p.b}`;
4310
+ if (emitted.has(key)) continue;
4311
+ emitted.add(key);
4312
+ const matched = cm[0];
4313
+ const claim = ctx.clauses.filter((c) => c.text.includes(matched.replace(/^[Cc]ho\s+/u, ""))).map((c) => c.id);
4314
+ out.push({
4315
+ ruleId: "circle-diameter",
4316
+ clauseIds: claim.length > 0 ? claim : [ctx.clauses[0]?.id ?? 0],
4317
+ intents: intentsFor(p)
4318
+ });
4319
+ }
3876
4320
  BARE_PAREN.lastIndex = 0;
3877
4321
  let bm;
3878
4322
  while ((bm = BARE_PAREN.exec(ctx.problem)) !== null) {
@@ -3883,7 +4327,7 @@ var circleDiameterRule = {
3883
4327
  const key = `${center}|${a}${b}`;
3884
4328
  if (emitted.has(key)) continue;
3885
4329
  const before = ctx.problem.slice(0, bm.index);
3886
- const known = (p) => new RegExp(`(?<![a-z])${p}(?![a-z])`, "u").test(before);
4330
+ const known = (p) => new RegExp(`(?<![a-z])${escapeRe(p)}(?![a-z])`, "u").test(before);
3887
4331
  if (!known(a) || !known(b)) continue;
3888
4332
  emitted.add(key);
3889
4333
  const matched = bm[0];
@@ -3905,7 +4349,7 @@ var circleDiameterRule = {
3905
4349
  var PREFILTER12 = /(?:nằm|thuộc|lấy\s+điểm|trên\s+(?:nửa\s+)?(?:đường\s*tròn|cung))/iu;
3906
4350
  var NAMED_CIRCLE = /(?:nửa\s+)?đường\s*tròn\s*(?:tâm\s+)?\(?\s*([A-Z])(?:\s*[;,]\s*[Rr])?\s*\)?/u;
3907
4351
  var COMPACT_CIRCLE = /\(\s*([A-Z])\s*[;,]\s*[Rr]\s*\)/u;
3908
- 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*\\))";
4352
+ 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*\\))";
3909
4353
  var POINT_ON = new RegExp(
3910
4354
  // "nằm" optional → bắt "M (bất kì)? trên (nửa)? đường tròn" / "điểm A trên (O)"
3911
4355
  // (chỉ "trên" trần, không "nằm trên"). ON_SUFFIX neo circle/cung nên "trên"
@@ -3926,18 +4370,21 @@ var TAKE_ON_CUNG = /[Tt]rên\s+cung\s+(?:nhỏ\s+|lớn\s+)?[A-Z]{2}(?:\s*(?:nh
3926
4370
  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;
3927
4371
  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;
3928
4372
  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;
3929
- 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;
4373
+ 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;
3930
4374
  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;
3931
4375
  var BARE_CIRCLE = /\(\s*([A-Z])\s*\)/u;
3932
4376
  var UNNAMED_DIAMETER = /(?:nửa\s+)?đường\s*tròn\s+đường\s*kính\s+([A-Z])([A-Z])(?![A-Z])/u;
4377
+ var BARE_CENTER_DIAMETER = /(?<![\p{L}\d(])([A-Z])\s+đường\s*kính/u;
3933
4378
  function resolveCircle3(problem) {
3934
4379
  const m = NAMED_CIRCLE.exec(problem) ?? COMPACT_CIRCLE.exec(problem) ?? BARE_CIRCLE.exec(problem);
3935
4380
  if (!m) {
3936
4381
  const dm = UNNAMED_DIAMETER.exec(problem);
3937
- return dm ? `k${dm[1]}${dm[2]}` : void 0;
4382
+ if (dm) return `k${dm[1]}${dm[2]}`;
4383
+ const bd = BARE_CENTER_DIAMETER.exec(problem);
4384
+ return bd ? `${bd[1]}_c` : void 0;
3938
4385
  }
3939
4386
  const center = m[1];
3940
- const c = center.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4387
+ const c = escapeRe(center);
3941
4388
  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);
3942
4389
  return diameterOfThisCenter ? `${center}_c` : center;
3943
4390
  }
@@ -3982,10 +4429,12 @@ var onCirclePointRule = {
3982
4429
  if (!m) continue;
3983
4430
  const name = m[1];
3984
4431
  if (name.length !== 1) continue;
4432
+ const major = /cung\s+lớn|lớn\s+[A-Z]{2}/u.test(c.text);
4433
+ const ptTheta = major ? -1.6 : theta;
3985
4434
  out.push({
3986
4435
  ruleId: "on-circle-point",
3987
4436
  clauseIds: [c.id],
3988
- intents: [addPoint(name, { kind: "onCircle", circle, theta })]
4437
+ intents: [addPoint(name, { kind: "onCircle", circle, theta: ptTheta })]
3989
4438
  });
3990
4439
  theta += 0.8;
3991
4440
  }
@@ -3994,10 +4443,10 @@ var onCirclePointRule = {
3994
4443
  };
3995
4444
 
3996
4445
  // src/stamps/geometry-2d/ai/rules/tangentAt.ts
3997
- var PREFILTER13 = /[Tt]iếp\s*tuyến\s+(?:với\s+|của\s+)?(?:đường\s*tròn\s*)?(?:\([^)]*\)\s*)?tại/u;
4446
+ 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;
3998
4447
  var CIRCLE_REF = /đường\s*tròn\s*\(\s*([A-Z])(?:\s*[;,]\s*[Rr])?\s*\)|\(\s*([A-Z])(?:\s*[;,]\s*[Rr])?\s*\)/u;
3999
4448
  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;
4000
- 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;
4449
+ 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;
4001
4450
  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;
4002
4451
  function circleName(problem) {
4003
4452
  const m = CIRCLE_REF.exec(problem);
@@ -4068,17 +4517,37 @@ var tangentAtRule = {
4068
4517
  };
4069
4518
 
4070
4519
  // src/stamps/geometry-2d/ai/rules/onSegmentPoint.ts
4071
- 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;
4520
+ 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;
4072
4521
  var ON_NAMED_LINE = new RegExp(
4073
4522
  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])`,
4074
4523
  "gu"
4075
4524
  );
4525
+ var ON_NAMED_LINE_BARE = new RegExp(
4526
+ 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])`,
4527
+ "gu"
4528
+ );
4529
+ var POINT_THUOC_NAMED_BARE = new RegExp(
4530
+ 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])`,
4531
+ "gu"
4532
+ );
4076
4533
  var SEG = "([A-Z]{2})(?![A-Z])";
4077
4534
  var POINT = "([A-Z](?:['\u2032])?)(?![A-Z])";
4535
+ var TAKE_POINT_ON_SEG = new RegExp(
4536
+ 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}`,
4537
+ "gu"
4538
+ );
4539
+ var TAKE_TWO_ON_TWO_SEG = new RegExp(
4540
+ 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])`,
4541
+ "gu"
4542
+ );
4078
4543
  var ON_SEG_THEN_POINT = new RegExp(
4079
4544
  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}`,
4080
4545
  "gu"
4081
4546
  );
4547
+ var ON_BARE_SEG_THEN_POINT = new RegExp(
4548
+ String.raw`[Tt]rên\s+${SEG}[^.]{0,14}?\blấy\s+(?:một\s+)?(?:điểm\s+)?${POINT}`,
4549
+ "gu"
4550
+ );
4082
4551
  var ON_SEG_MULTI_POINT = new RegExp(
4083
4552
  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])`,
4084
4553
  "gu"
@@ -4112,13 +4581,17 @@ var ZIP_SEG = new RegExp(
4112
4581
  "gu"
4113
4582
  );
4114
4583
  var SEGS_THEN_POINTS = new RegExp(
4115
- 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])`,
4584
+ 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])`,
4116
4585
  "gu"
4117
4586
  );
4118
4587
  var TWO_SEG_LAY = new RegExp(
4119
4588
  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}`,
4120
4589
  "gu"
4121
4590
  );
4591
+ var TWO_ON_NAMED_RAY = new RegExp(
4592
+ 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])`,
4593
+ "gu"
4594
+ );
4122
4595
  function normalizePoint(name) {
4123
4596
  return name.replace("\u2032", "'");
4124
4597
  }
@@ -4128,6 +4601,18 @@ function validOnSegment(name, segment) {
4128
4601
  function hasMetricConstraint(text) {
4129
4602
  return /sao\s+cho[^.]{0,40}(?:=|>|<)/u.test(text);
4130
4603
  }
4604
+ function declaredNamedLines3(problem) {
4605
+ const out = /* @__PURE__ */ new Set();
4606
+ 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;
4607
+ for (const m of problem.matchAll(RE31)) out.add(m[1]);
4608
+ return out;
4609
+ }
4610
+ function declaredNamedRays(problem) {
4611
+ const out = /* @__PURE__ */ new Set();
4612
+ const RE31 = /tiếp\s*tuyến\s+([A-Z][xyzt])(?![A-Za-z])/gu;
4613
+ for (const m of problem.matchAll(RE31)) out.add(m[1]);
4614
+ return out;
4615
+ }
4131
4616
  var onSegmentPointRule = {
4132
4617
  id: "on-segment-point",
4133
4618
  priority: 62,
@@ -4135,8 +4620,73 @@ var onSegmentPointRule = {
4135
4620
  patterns: [PREFILTER14],
4136
4621
  match(ctx) {
4137
4622
  const out = [];
4623
+ const namedLines = declaredNamedLines3(ctx.problem);
4624
+ const namedRays = declaredNamedRays(ctx.problem);
4138
4625
  for (const c of ctx.clauses) {
4139
4626
  const intents = [];
4627
+ ON_NAMED_LINE.lastIndex = 0;
4628
+ for (const m of c.text.matchAll(ON_NAMED_LINE)) {
4629
+ const line = m[1];
4630
+ const name = normalizePoint(m[2]);
4631
+ if (/^[A-Z]['′]?$/u.test(name)) intents.push(addPoint(name, { kind: "onSegment", of: line }));
4632
+ }
4633
+ if (namedLines.size > 0) {
4634
+ ON_NAMED_LINE_BARE.lastIndex = 0;
4635
+ for (const m of c.text.matchAll(ON_NAMED_LINE_BARE)) {
4636
+ const line = m[1];
4637
+ const name = normalizePoint(m[2]);
4638
+ if (namedLines.has(line) && /^[A-Z]['′]?$/u.test(name)) {
4639
+ intents.push(addPoint(name, { kind: "onSegment", of: line }));
4640
+ }
4641
+ }
4642
+ POINT_THUOC_NAMED_BARE.lastIndex = 0;
4643
+ for (const m of c.text.matchAll(POINT_THUOC_NAMED_BARE)) {
4644
+ const name = normalizePoint(m[1]);
4645
+ const line = m[2];
4646
+ if (namedLines.has(line) && /^[A-Z]['′]?$/u.test(name)) {
4647
+ intents.push(addPoint(name, { kind: "onSegment", of: line }));
4648
+ }
4649
+ }
4650
+ }
4651
+ if (namedRays.size > 0) {
4652
+ TWO_ON_NAMED_RAY.lastIndex = 0;
4653
+ for (const m of c.text.matchAll(TWO_ON_NAMED_RAY)) {
4654
+ const ray = m[1];
4655
+ if (!namedRays.has(ray)) continue;
4656
+ for (const raw of [m[2], m[3]]) {
4657
+ const name = normalizePoint(raw);
4658
+ if (/^[A-Z]['′]?$/u.test(name) && name[0] !== ray[0]) {
4659
+ intents.push(addPoint(name, { kind: "onSegment", of: ray }));
4660
+ }
4661
+ }
4662
+ }
4663
+ }
4664
+ if (!/tia\s+đối/u.test(c.text)) {
4665
+ TAKE_TWO_ON_TWO_SEG.lastIndex = 0;
4666
+ for (const m of c.text.matchAll(TAKE_TWO_ON_TWO_SEG)) {
4667
+ const [n1, n2, s1, s2] = [normalizePoint(m[1]), normalizePoint(m[2]), m[3], m[4]];
4668
+ if (validOnSegment(n1, s1) && validOnSegment(n2, s2)) {
4669
+ intents.push(addPoint(n1, { kind: "onSegment", of: s1 }));
4670
+ intents.push(addPoint(n2, { kind: "onSegment", of: s2 }));
4671
+ }
4672
+ }
4673
+ TAKE_POINT_ON_SEG.lastIndex = 0;
4674
+ for (const m of c.text.matchAll(TAKE_POINT_ON_SEG)) {
4675
+ const name = normalizePoint(m[1]);
4676
+ const segment = m[2];
4677
+ if (validOnSegment(name, segment)) {
4678
+ intents.push(addPoint(name, { kind: "onSegment", of: segment }));
4679
+ }
4680
+ }
4681
+ ON_BARE_SEG_THEN_POINT.lastIndex = 0;
4682
+ for (const m of c.text.matchAll(ON_BARE_SEG_THEN_POINT)) {
4683
+ const segment = m[1];
4684
+ const name = normalizePoint(m[2]);
4685
+ if (validOnSegment(name, segment)) {
4686
+ intents.push(addPoint(name, { kind: "onSegment", of: segment }));
4687
+ }
4688
+ }
4689
+ }
4140
4690
  SEGS_THEN_POINTS.lastIndex = 0;
4141
4691
  for (const m of c.text.matchAll(SEGS_THEN_POINTS)) {
4142
4692
  const segs = m[1].split(",").map((s) => s.trim());
@@ -4196,12 +4746,6 @@ var onSegmentPointRule = {
4196
4746
  if (intents.length > 0) out.push({ ruleId: "on-segment-point", clauseIds: [c.id], intents });
4197
4747
  continue;
4198
4748
  }
4199
- ON_NAMED_LINE.lastIndex = 0;
4200
- for (const m of c.text.matchAll(ON_NAMED_LINE)) {
4201
- const line = m[1];
4202
- const name = normalizePoint(m[2]);
4203
- if (/^[A-Z]['′]?$/u.test(name)) intents.push(addPoint(name, { kind: "onSegment", of: line }));
4204
- }
4205
4749
  ON_SEG_THEN_POINT.lastIndex = 0;
4206
4750
  for (const m of c.text.matchAll(ON_SEG_THEN_POINT)) {
4207
4751
  const segment = m[1];
@@ -4253,7 +4797,7 @@ var onSegmentPointRule = {
4253
4797
  };
4254
4798
 
4255
4799
  // src/stamps/geometry-2d/ai/rules/lineCircleIntersection.ts
4256
- 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;
4800
+ 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;
4257
4801
  var CIRCLE = String.raw`(?:(?:nửa\s+)?đường\s*tròn\s*)?\(\s*([A-Z])(?:['′]?)\s*(?:[;,]\s*[Rr]\s*)?\)`;
4258
4802
  var CIRCLE_P = String.raw`(?:(?:nửa\s+)?đường\s*tròn\s*)?\(\s*([A-Z](?:['′])?)\s*(?:[;,]\s*[Rr]\s*)?\)`;
4259
4803
  var PERP_CUTS_CIRCLE = new RegExp(
@@ -4269,13 +4813,21 @@ var DOUBLE_DISTRIB = new RegExp(
4269
4813
  "gu"
4270
4814
  );
4271
4815
  var SINGLE3 = new RegExp(
4272
- 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]))?`,
4816
+ 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]))?`,
4817
+ "gu"
4818
+ );
4819
+ var SINGLE_NAMED_CENTER = new RegExp(
4820
+ 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])`,
4273
4821
  "gu"
4274
4822
  );
4275
4823
  var BOTH = new RegExp(
4276
4824
  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])`,
4277
4825
  "gu"
4278
4826
  );
4827
+ var TWO_NAMED = new RegExp(
4828
+ 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])`,
4829
+ "gu"
4830
+ );
4279
4831
  var LINE_AND_CIRCLE = new RegExp(
4280
4832
  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])`,
4281
4833
  "gu"
@@ -4292,12 +4844,16 @@ var CIRCLE_SUBJECT = new RegExp(
4292
4844
  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])`,
4293
4845
  "gu"
4294
4846
  );
4847
+ var CIRCLE_SUBJECT_TWO = new RegExp(
4848
+ 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])`,
4849
+ "gu"
4850
+ );
4295
4851
  var GIAO_CIRCLE = new RegExp(
4296
4852
  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]))?`,
4297
4853
  "gu"
4298
4854
  );
4299
4855
  var NAME_2ND_CUA = new RegExp(
4300
- 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,
4856
+ 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,
4301
4857
  "gu"
4302
4858
  );
4303
4859
  var NAME_2ND_BARE = new RegExp(
@@ -4313,13 +4869,28 @@ var NAME_KHAC_CUA = new RegExp(
4313
4869
  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,
4314
4870
  "gu"
4315
4871
  );
4872
+ var NAME_CUA = new RegExp(
4873
+ 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,
4874
+ "gu"
4875
+ );
4316
4876
  var NAME_2ND_CIRCUM = new RegExp(
4317
4877
  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]))?`,
4318
4878
  "gu"
4319
4879
  );
4880
+ var LINE_CUTS_PAREN_PAIR = new RegExp(
4881
+ 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])`,
4882
+ "gu"
4883
+ );
4884
+ var PAREN_PAIR_CUTS = new RegExp(
4885
+ 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])`,
4886
+ "gu"
4887
+ );
4320
4888
  function secondIntersection(name, line, circle, other) {
4321
4889
  return addPoint(name, { kind: "secondIntersection", line, circle, other: other ?? line[0] });
4322
4890
  }
4891
+ function vertexOnLine(verts, line) {
4892
+ return verts.split("").find((v) => line.includes(v));
4893
+ }
4323
4894
  function valid(name, line) {
4324
4895
  return /^[A-Z]$/u.test(name) && /^[A-Z]{2}$/u.test(line) && !line.includes(name);
4325
4896
  }
@@ -4356,6 +4927,13 @@ var lineCircleIntersectionRule = {
4356
4927
  const other = m[4];
4357
4928
  if (valid(name, line)) intents.push(secondIntersection(name, line, circle, other));
4358
4929
  }
4930
+ SINGLE_NAMED_CENTER.lastIndex = 0;
4931
+ for (const m of c.text.matchAll(SINGLE_NAMED_CENTER)) {
4932
+ const line = m[1];
4933
+ const circle = m[2].replace(/′/g, "'");
4934
+ const name = m[3];
4935
+ if (valid(name, line)) intents.push(secondIntersection(name, line, circle, line[0]));
4936
+ }
4359
4937
  LINE_AND_CIRCLE.lastIndex = 0;
4360
4938
  for (const m of c.text.matchAll(LINE_AND_CIRCLE)) {
4361
4939
  const [line, line2, circle, n1, n2] = [m[1], m[2], m[3], m[4], m[5]];
@@ -4376,7 +4954,14 @@ var lineCircleIntersectionRule = {
4376
4954
  addPoint(y, { kind: "intersection", of: [line, circle], branch: 1 })
4377
4955
  );
4378
4956
  }
4379
- NAME_2ND_BARE.lastIndex = 0;
4957
+ TWO_NAMED.lastIndex = 0;
4958
+ for (const m of c.text.matchAll(TWO_NAMED)) {
4959
+ const [line, circle, p, q] = [m[1], m[2], m[3], m[4]];
4960
+ if (p === q) continue;
4961
+ if (line.includes(p) && !line.includes(q)) intents.push(secondIntersection(q, line, circle, p));
4962
+ else if (line.includes(q) && !line.includes(p)) intents.push(secondIntersection(p, line, circle, q));
4963
+ }
4964
+ NAME_2ND_BARE.lastIndex = 0;
4380
4965
  for (const m of c.text.matchAll(NAME_2ND_BARE)) {
4381
4966
  const rc = RESOLVE_CIRCLE_BARE.exec(ctx.problem);
4382
4967
  const circle = rc?.[1] ?? rc?.[2];
@@ -4413,6 +4998,13 @@ var lineCircleIntersectionRule = {
4413
4998
  const [line, name] = [m[1], m[2]];
4414
4999
  if (circle && valid(name, line)) intents.push(secondIntersection(name, line, circle, line[0]));
4415
5000
  }
5001
+ CIRCLE_SUBJECT_TWO.lastIndex = 0;
5002
+ for (const m of c.text.matchAll(CIRCLE_SUBJECT_TWO)) {
5003
+ const [circle, l1, l2, n1, n2] = [m[1], m[2], m[3], m[4], m[5]];
5004
+ if (n1 === n2) continue;
5005
+ if (valid(n1, l1)) intents.push(secondIntersection(n1, l1, circle));
5006
+ if (valid(n2, l2)) intents.push(secondIntersection(n2, l2, circle));
5007
+ }
4416
5008
  CIRCLE_SUBJECT.lastIndex = 0;
4417
5009
  for (const m of c.text.matchAll(CIRCLE_SUBJECT)) {
4418
5010
  const [circle, line, name] = [m[1], m[2], m[3]];
@@ -4434,6 +5026,11 @@ var lineCircleIntersectionRule = {
4434
5026
  const circle = m[4];
4435
5027
  if (valid(name, line)) intents.push(secondIntersection(name, line, circle, other));
4436
5028
  }
5029
+ NAME_CUA.lastIndex = 0;
5030
+ for (const m of c.text.matchAll(NAME_CUA)) {
5031
+ const [name, line, circle] = [m[1], m[2], m[3]];
5032
+ if (valid(name, line)) intents.push(secondIntersection(name, line, circle));
5033
+ }
4437
5034
  NAME_2ND_CIRCUM.lastIndex = 0;
4438
5035
  for (const m of c.text.matchAll(NAME_2ND_CIRCUM)) {
4439
5036
  const line = m[1];
@@ -4449,6 +5046,34 @@ var lineCircleIntersectionRule = {
4449
5046
  if (line.includes(name)) continue;
4450
5047
  intents.push(addPoint(name, { kind: "intersection", of: [line, circle], branch: 0 }));
4451
5048
  }
5049
+ LINE_CUTS_PAREN_PAIR.lastIndex = 0;
5050
+ for (const m of c.text.matchAll(LINE_CUTS_PAREN_PAIR)) {
5051
+ const [line, t1, t2, n1, n2] = [m[1], m[2], m[3], m[4], m[5]];
5052
+ const o1 = vertexOnLine(t1, line);
5053
+ const o2 = vertexOnLine(t2, line);
5054
+ if (!o1 || !o2 || n1 === n2) continue;
5055
+ if (line.includes(n1) || line.includes(n2)) continue;
5056
+ intents.push(
5057
+ drawCircle(`w${t1}`, "through3", { points: t1.split("") }),
5058
+ drawCircle(`w${t2}`, "through3", { points: t2.split("") }),
5059
+ secondIntersection(n1, line, `w${t1}`, o1),
5060
+ secondIntersection(n2, line, `w${t2}`, o2)
5061
+ );
5062
+ }
5063
+ PAREN_PAIR_CUTS.lastIndex = 0;
5064
+ for (const m of c.text.matchAll(PAREN_PAIR_CUTS)) {
5065
+ const [t1, t2, l1, l2, n1, n2] = [m[1], m[2], m[3], m[4], m[5], m[6]];
5066
+ const o1 = vertexOnLine(t1, l1);
5067
+ const o2 = vertexOnLine(t2, l2);
5068
+ if (!o1 || !o2 || n1 === n2) continue;
5069
+ if (l1.includes(n1) || l2.includes(n2)) continue;
5070
+ intents.push(
5071
+ drawCircle(`w${t1}`, "through3", { points: t1.split("") }),
5072
+ drawCircle(`w${t2}`, "through3", { points: t2.split("") }),
5073
+ secondIntersection(n1, l1, `w${t1}`, o1),
5074
+ secondIntersection(n2, l2, `w${t2}`, o2)
5075
+ );
5076
+ }
4452
5077
  if (intents.length > 0) out.push({ ruleId: "line-circle-intersection", clauseIds: [c.id], intents });
4453
5078
  }
4454
5079
  return out;
@@ -4509,8 +5134,9 @@ var incenterNamedTriangleRule = {
4509
5134
  };
4510
5135
 
4511
5136
  // src/stamps/geometry-2d/ai/rules/perpThroughCutsLines.ts
5137
+ var LTOK = "(?:[A-Z]{2}|[A-Z][a-z][0-9]?)";
4512
5138
  var RE2 = new RegExp(
4513
- "(?: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])",
5139
+ `(?: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])`,
4514
5140
  "gu"
4515
5141
  );
4516
5142
  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;
@@ -4533,10 +5159,10 @@ var perpThroughCutsLinesRule = {
4533
5159
  const through = m[1];
4534
5160
  const isParallel = /song/.test(m[2]);
4535
5161
  const to = m[3] + m[4];
4536
- const line1 = m[5] + m[6];
4537
- const line2 = m[7] + m[8];
4538
- const h = m[9];
4539
- const k = m[10];
5162
+ const line1 = m[5];
5163
+ const line2 = m[6];
5164
+ const h = m[7];
5165
+ const k = m[8];
4540
5166
  if (isParallel && to.includes(through)) continue;
4541
5167
  if (h === k) continue;
4542
5168
  const kind = isParallel ? "parallelThrough" : "perpThrough";
@@ -4582,6 +5208,40 @@ var CIRCLE_CENTER3 = /\(\s*([A-Z])(?:\s*[;,]\s*[Rr])?\s*\)|tâm\s+([A-Z])(?![A-Z
4582
5208
  var DIAMETER_ENDS = /đường\s*kính\s+([A-Z])([A-Z])(?![A-Z])/u;
4583
5209
  var AFTER_TANGENT = /tiếp\s*tuyến\s+((?:[A-Z][xyzt](?![A-Za-z])\s*[,và\s]*)+)/gu;
4584
5210
  var RAY_TOKEN = /([A-Z])([xyzt])(?![A-Za-z])/gu;
5211
+ 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;
5212
+ function matchFreeTangentPoint(ctx, cm) {
5213
+ const center = cm ? cm[1] ?? cm[2] : void 0;
5214
+ if (!center) return [];
5215
+ const known = /* @__PURE__ */ new Set();
5216
+ SHAPE_VERTICES.lastIndex = 0;
5217
+ for (const m of ctx.problem.matchAll(SHAPE_VERTICES)) {
5218
+ for (const ch of m[1]) known.add(ch);
5219
+ }
5220
+ const out = [];
5221
+ for (const c of ctx.clauses) {
5222
+ const intents = [];
5223
+ const seen = /* @__PURE__ */ new Set();
5224
+ AFTER_TANGENT.lastIndex = 0;
5225
+ for (const grp of c.text.matchAll(AFTER_TANGENT)) {
5226
+ RAY_TOKEN.lastIndex = 0;
5227
+ for (const m of grp[1].matchAll(RAY_TOKEN)) {
5228
+ const endpoint = m[1];
5229
+ const token = `${m[1]}${m[2]}`;
5230
+ if (known.has(endpoint)) continue;
5231
+ if (seen.has(token)) continue;
5232
+ seen.add(token);
5233
+ intents.push(
5234
+ addPoint(endpoint, { kind: "onCircle", circle: center, theta: 0.7 }),
5235
+ drawLine(token, "tangentAt", { through: endpoint, circle: center })
5236
+ );
5237
+ }
5238
+ }
5239
+ if (intents.length > 0) {
5240
+ out.push({ ruleId: "tangent-ray", clauseIds: [c.id], intents });
5241
+ }
5242
+ }
5243
+ return out;
5244
+ }
4585
5245
  var tangentRayRule = {
4586
5246
  id: "tangent-ray",
4587
5247
  // Cao hơn point-on-ray (55) + intersect-ray (48): token tia phải được dựng
@@ -4593,7 +5253,7 @@ var tangentRayRule = {
4593
5253
  match(ctx) {
4594
5254
  const cm = CIRCLE_CENTER3.exec(ctx.problem);
4595
5255
  const dm = DIAMETER_ENDS.exec(ctx.problem);
4596
- if (!dm) return [];
5256
+ if (!dm) return matchFreeTangentPoint(ctx, cm);
4597
5257
  const center = cm ? cm[1] ?? cm[2] : `k${dm[1]}${dm[2]}`;
4598
5258
  const ends = /* @__PURE__ */ new Set([dm[1], dm[2]]);
4599
5259
  const out = [];
@@ -5242,7 +5902,7 @@ var PREFILTER30 = /[Tt]iếp\s*tuyến[^.]{0,30}?tại\s+[A-Z][^.]{0,40}?cắt|Q
5242
5902
  var CIRCLE_REF4 = /đường\s*tròn\s*(?:tâm\s+)?\(?\s*([A-Z])(?:\s*[;,]\s*[Rr])?\s*\)?/u;
5243
5903
  var UNNAMED_DIAMETER2 = /(?:nửa\s+)?đường\s*tròn\s+đường\s*kính\s+([A-Z])([A-Z])(?![A-Z])/u;
5244
5904
  var QUA_RE = new RegExp(
5245
- "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])",
5905
+ "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])",
5246
5906
  "gu"
5247
5907
  );
5248
5908
  var SINGLE_RE = new RegExp(
@@ -5250,7 +5910,7 @@ var SINGLE_RE = new RegExp(
5250
5910
  "gu"
5251
5911
  );
5252
5912
  var RE6 = new RegExp(
5253
- "[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])",
5913
+ "[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])",
5254
5914
  "gu"
5255
5915
  );
5256
5916
  function circleName3(problem) {
@@ -5334,12 +5994,16 @@ var RE7 = new RegExp(
5334
5994
  "([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*\\)?",
5335
5995
  "gu"
5336
5996
  );
5997
+ var VERB_GAP = "(?:[^.;,]{0,24}?v\xE0\\s+|(?:(?!\u0111\u01B0\u1EDDng\\s*tr\xF2n|\\()[^.;,]){0,24}?)?";
5337
5998
  var RE_VERB = new RegExp(
5338
- "(?:[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])",
5999
+ "(?:[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])",
5339
6000
  "gu"
5340
6001
  );
5341
6002
  var RESOLVE_CENTER = /\(\s*([A-Z])(?:\s*[;,]\s*[Rr0-9][^)]*)?\s*\)|tâm\s+([A-Z])(?![A-Za-z])/u;
5342
- 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;
6003
+ var PREFILTER31 = new RegExp(
6004
+ "[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",
6005
+ "u"
6006
+ );
5343
6007
  var diameterEndpointRule = {
5344
6008
  id: "diameterEndpoint",
5345
6009
  priority: 56,
@@ -5373,7 +6037,7 @@ var diameterEndpointRule = {
5373
6037
  const d = m[2].replace(/′/g, "'");
5374
6038
  if (a === d || center === a || center === d || emitted.has(d)) continue;
5375
6039
  const before = ctx.problem.slice(0, ctx.problem.indexOf(m[0]));
5376
- if (!new RegExp(`(?<![a-z])${a}(?![a-z])`, "u").test(before)) continue;
6040
+ if (!new RegExp(`(?<![a-z])${escapeRe(a)}(?![a-z])`, "u").test(before)) continue;
5377
6041
  emitted.add(d);
5378
6042
  out.push({
5379
6043
  ruleId: "diameterEndpoint",
@@ -5437,8 +6101,69 @@ var circleThroughTwoCutsSidesRule = {
5437
6101
  }
5438
6102
  };
5439
6103
 
6104
+ // src/stamps/geometry-2d/ai/rules/circleCenterRadiusCutsSides.ts
6105
+ var PATTERN4 = new RegExp(
6106
+ 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}))*)",
6107
+ "gu"
6108
+ );
6109
+ var PREFILTER33 = new RegExp(
6110
+ CIRCLE_KW + "\\s+t\xE2m\\s+[A-Z]\\s*b\xE1n\\s*k\xEDnh\\s+[A-Z]{2}[^.;\\n]*?c\u1EAFt",
6111
+ "u"
6112
+ );
6113
+ var circleCenterRadiusCutsSidesRule = {
6114
+ id: "circle-center-radius-cuts-sides",
6115
+ // THẤP HƠN triangle (100) + centers (cần đỉnh + tâm H dựng trước). Cùng họ với
6116
+ // diameterCircleCutsSides (66) / circleThroughTwoCutsSides (58). Đặt 57.
6117
+ priority: 57,
6118
+ languages: ["vi"],
6119
+ patterns: [PREFILTER33],
6120
+ match(ctx) {
6121
+ const out = [];
6122
+ for (const c of ctx.clauses) {
6123
+ PATTERN4.lastIndex = 0;
6124
+ for (const m of c.text.matchAll(PATTERN4)) {
6125
+ const center = m[1];
6126
+ const r0 = m[2];
6127
+ const r1 = m[3];
6128
+ let through;
6129
+ if (r0 === center && r1 !== center) through = r1;
6130
+ else if (r1 === center && r0 !== center) through = r0;
6131
+ if (!through) continue;
6132
+ const lines = m[4].match(/[A-Z][A-Z](?![A-Z])/gu) ?? [];
6133
+ const points = m[5].match(/[A-Z](?![A-Z])/gu) ?? [];
6134
+ if (lines.length === 0 || lines.length !== points.length) continue;
6135
+ const circ = `${center}_c`;
6136
+ const intents = [
6137
+ drawCircle(circ, "centerThrough", { center, through })
6138
+ ];
6139
+ let ok = true;
6140
+ const seen = /* @__PURE__ */ new Set();
6141
+ for (let i = 0; i < lines.length; i++) {
6142
+ const line = lines[i];
6143
+ const pt = points[i];
6144
+ if (!line.includes(through)) {
6145
+ ok = false;
6146
+ break;
6147
+ }
6148
+ if (line.includes(pt) || pt === through || pt === center || seen.has(pt)) {
6149
+ ok = false;
6150
+ break;
6151
+ }
6152
+ seen.add(pt);
6153
+ intents.push(
6154
+ addPoint(pt, { kind: "secondIntersection", line, circle: circ, other: through })
6155
+ );
6156
+ }
6157
+ if (!ok) continue;
6158
+ out.push({ ruleId: "circle-center-radius-cuts-sides", clauseIds: [c.id], intents });
6159
+ }
6160
+ }
6161
+ return out;
6162
+ }
6163
+ };
6164
+
5440
6165
  // src/stamps/geometry-2d/ai/rules/circumcircleNamedDistrib.ts
5441
- var PREFILTER33 = /lần\s*lượt\s+là\s+(?:đường\s*tròn\s+)?ngoại\s*tiếp/u;
6166
+ var PREFILTER34 = /lần\s*lượt\s+là\s+(?:đường\s*tròn\s+)?ngoại\s*tiếp/u;
5442
6167
  var RE9 = new RegExp(
5443
6168
  "([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])",
5444
6169
  "u"
@@ -5447,7 +6172,7 @@ var circumcircleNamedDistribRule = {
5447
6172
  id: "circumcircleNamedDistrib",
5448
6173
  priority: 67,
5449
6174
  languages: ["vi"],
5450
- patterns: [PREFILTER33],
6175
+ patterns: [PREFILTER34],
5451
6176
  match(ctx) {
5452
6177
  const out = [];
5453
6178
  for (const c of ctx.clauses) {
@@ -5472,7 +6197,7 @@ var circumcircleNamedDistribRule = {
5472
6197
  };
5473
6198
 
5474
6199
  // src/stamps/geometry-2d/ai/rules/circumcirclePairMeet.ts
5475
- 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;
6200
+ 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;
5476
6201
  var RE_CAT = new RegExp(
5477
6202
  "[\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]))?",
5478
6203
  "u"
@@ -5494,22 +6219,27 @@ var RE_PAREN_BEFORE = new RegExp(
5494
6219
  `([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}`,
5495
6220
  "u"
5496
6221
  );
6222
+ var RE_PAREN_CAT = new RegExp(
6223
+ `${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]))?`,
6224
+ "u"
6225
+ );
5497
6226
  var circumcirclePairMeetRule = {
5498
6227
  id: "circumcirclePairMeet",
5499
6228
  priority: 59,
5500
6229
  languages: ["vi"],
5501
- patterns: [PREFILTER34, new RegExp(`${PAR}\\s+v\xE0\\s+${PAR}`, "u")],
6230
+ patterns: [PREFILTER35, new RegExp(`${PAR}\\s+v\xE0\\s+${PAR}`, "u")],
5502
6231
  match(ctx) {
5503
6232
  const out = [];
5504
6233
  for (const c of ctx.clauses) {
5505
6234
  const pl = RE_PAREN_LA.exec(c.text);
5506
- const pb = pl ? null : RE_PAREN_BEFORE.exec(c.text);
5507
- if (pl || pb) {
5508
- const t1 = pl ? pl[1] : pb[2];
5509
- const t2 = pl ? pl[2] : pb[3];
5510
- const q2 = pl ? pl[3] : pb[1];
6235
+ const pc = pl ? null : RE_PAREN_CAT.exec(c.text);
6236
+ const pb = pl || pc ? null : RE_PAREN_BEFORE.exec(c.text);
6237
+ if (pl || pc || pb) {
6238
+ const t1 = pl ? pl[1] : pc ? pc[1] : pb[2];
6239
+ const t2 = pl ? pl[2] : pc ? pc[2] : pb[3];
6240
+ const q2 = pl ? pl[3] : pc ? pc[3] : pb[1];
5511
6241
  const tri12 = t1.split(""), tri22 = t2.split("");
5512
- const shared2 = pl && pl[4] || tri12.find((v) => tri22.includes(v));
6242
+ const shared2 = pl && pl[4] || pc && pc[4] || tri12.find((v) => tri22.includes(v));
5513
6243
  if (shared2 && !tri12.includes(q2) && !tri22.includes(q2)) {
5514
6244
  out.push({
5515
6245
  ruleId: "circumcirclePairMeet",
@@ -5547,7 +6277,7 @@ var circumcirclePairMeetRule = {
5547
6277
  };
5548
6278
 
5549
6279
  // src/stamps/geometry-2d/ai/rules/tangentCircleAtPoint.ts
5550
- var PREFILTER35 = /[Đđ]ường\s*tròn\s*\(?[A-Z]\)?[^.]{0,40}?tiếp\s*xúc[^.]{0,20}?tại\s+[A-Z]/u;
6280
+ var PREFILTER36 = /[Đđ]ường\s*tròn\s*\(?[A-Z]\)?[^.]{0,40}?tiếp\s*xúc[^.]{0,20}?tại\s+[A-Z]/u;
5551
6281
  var RE11 = new RegExp(
5552
6282
  "[\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])",
5553
6283
  "gu"
@@ -5560,7 +6290,7 @@ var tangentCircleAtPointRule = {
5560
6290
  id: "tangentCircleAtPoint",
5561
6291
  priority: 60,
5562
6292
  languages: ["vi"],
5563
- patterns: [PREFILTER35],
6293
+ patterns: [PREFILTER36],
5564
6294
  match(ctx) {
5565
6295
  const out = [];
5566
6296
  for (const c of ctx.clauses) {
@@ -5616,7 +6346,7 @@ var tangentCircleAtPointRule = {
5616
6346
  };
5617
6347
 
5618
6348
  // src/stamps/geometry-2d/ai/rules/externalBisectorCutsLines.ts
5619
- 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;
6349
+ 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;
5620
6350
  var RE12 = new RegExp(
5621
6351
  "[\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])",
5622
6352
  "gu"
@@ -5625,7 +6355,7 @@ var externalBisectorCutsLinesRule = {
5625
6355
  id: "externalBisectorCutsLines",
5626
6356
  priority: 57,
5627
6357
  languages: ["vi"],
5628
- patterns: [PREFILTER36],
6358
+ patterns: [PREFILTER37],
5629
6359
  match(ctx) {
5630
6360
  const out = [];
5631
6361
  for (const c of ctx.clauses) {
@@ -5656,7 +6386,7 @@ var externalBisectorCutsLinesRule = {
5656
6386
  };
5657
6387
 
5658
6388
  // src/stamps/geometry-2d/ai/rules/tangentTwoSidesCircle.ts
5659
- var PREFILTER37 = /[Đđ]ường\s*tròn[^.]{0,30}?tiếp\s*xúc[^.]{0,40}?tiếp\s*xúc\s+trong/u;
6389
+ var PREFILTER38 = /[Đđ]ường\s*tròn[^.]{0,30}?tiếp\s*xúc[^.]{0,40}?tiếp\s*xúc\s+trong/u;
5660
6390
  var RE13 = new RegExp(
5661
6391
  "[\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])",
5662
6392
  "gu"
@@ -5669,7 +6399,7 @@ var tangentTwoSidesCircleRule = {
5669
6399
  id: "tangentTwoSidesCircle",
5670
6400
  priority: 61,
5671
6401
  languages: ["vi"],
5672
- patterns: [PREFILTER37],
6402
+ patterns: [PREFILTER38],
5673
6403
  match(ctx) {
5674
6404
  const out = [];
5675
6405
  for (const c of ctx.clauses) {
@@ -5705,14 +6435,22 @@ var tangentTwoSidesCircleRule = {
5705
6435
  };
5706
6436
 
5707
6437
  // src/stamps/geometry-2d/ai/rules/interiorPoint.ts
5708
- 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;
5709
- 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;
5710
- 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;
6438
+ 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])|\\([^)]*\\)))?|\\([^)]*\\))";
6439
+ var QUAL = "(?:\\s+(?:b\u1EA5t\\s*k[i\xECy\u1EF3]|t[u\xF9][y\u1EF3]\\s*\xFD))?";
6440
+ 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;
6441
+ var RE14 = new RegExp(
6442
+ "(?:[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,
6443
+ "u"
6444
+ );
6445
+ var RE22 = new RegExp(
6446
+ "(?:m\u1ED9t\\s+)?[\u0110\u0111]i\u1EC3m\\s+([A-Z])(?!\\p{L})" + QUAL + "\\s+n\u1EB1m\\s+(?:b\xEAn\\s+)?trong\\s+" + SHAPE,
6447
+ "u"
6448
+ );
5711
6449
  var interiorPointRule = {
5712
6450
  id: "interiorPoint",
5713
6451
  priority: 30,
5714
6452
  languages: ["vi"],
5715
- patterns: [PREFILTER38],
6453
+ patterns: [PREFILTER39],
5716
6454
  match(ctx) {
5717
6455
  const out = [];
5718
6456
  for (const c of ctx.clauses) {
@@ -5730,7 +6468,7 @@ var interiorPointRule = {
5730
6468
  };
5731
6469
 
5732
6470
  // src/stamps/geometry-2d/ai/rules/parallelCutsTangentAt.ts
5733
- 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;
6471
+ 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;
5734
6472
  var CIRCLE_REF5 = /đường\s*tròn\s*\(\s*([A-Z])\s*\)|\(\s*([A-Z])\s*\)/u;
5735
6473
  var RE15 = new RegExp(
5736
6474
  "[\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])",
@@ -5744,7 +6482,7 @@ var parallelCutsTangentAtRule = {
5744
6482
  id: "parallelCutsTangentAt",
5745
6483
  priority: 52,
5746
6484
  languages: ["vi"],
5747
- patterns: [PREFILTER39],
6485
+ patterns: [PREFILTER40],
5748
6486
  match(ctx) {
5749
6487
  const circle = circleName4(ctx.problem);
5750
6488
  if (!circle) return [];
@@ -5775,7 +6513,7 @@ var parallelCutsTangentAtRule = {
5775
6513
  };
5776
6514
 
5777
6515
  // src/stamps/geometry-2d/ai/rules/inExCircleTangentBC.ts
5778
- 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;
6516
+ 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;
5779
6517
  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])";
5780
6518
  var INCIRCLE = new RegExp(
5781
6519
  "[\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,
@@ -5789,7 +6527,7 @@ var inExCircleTangentBCRule = {
5789
6527
  id: "inExCircleTangentBC",
5790
6528
  priority: 69,
5791
6529
  languages: ["vi"],
5792
- patterns: [PREFILTER40],
6530
+ patterns: [PREFILTER41],
5793
6531
  match(ctx) {
5794
6532
  const out = [];
5795
6533
  for (const c of ctx.clauses) {
@@ -5831,7 +6569,7 @@ var inExCircleTangentBCRule = {
5831
6569
  };
5832
6570
 
5833
6571
  // src/stamps/geometry-2d/ai/rules/tangentPointsFromExt.ts
5834
- 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;
6572
+ 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;
5835
6573
  var RE16 = new RegExp(
5836
6574
  "[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})",
5837
6575
  "gu"
@@ -5841,7 +6579,7 @@ var tangentPointsFromExtRule = {
5841
6579
  id: "tangentPointsFromExt",
5842
6580
  priority: 50,
5843
6581
  languages: ["vi"],
5844
- patterns: [PREFILTER41],
6582
+ patterns: [PREFILTER42],
5845
6583
  match(ctx) {
5846
6584
  const out = [];
5847
6585
  for (const c of ctx.clauses) {
@@ -5876,7 +6614,11 @@ var VN_FORM2 = new RegExp(
5876
6614
  "u"
5877
6615
  );
5878
6616
  var VN_REV = new RegExp(
5879
- 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])?`,
6617
+ 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])?`,
6618
+ "u"
6619
+ );
6620
+ var VN_REV_BARE = new RegExp(
6621
+ String.raw`(?<![\p{L}])([A-Z])(?:['′]?)[^.A-Z]{0,18}?ngoài\s+(?:đường\s*tròn\s*)?\(\s*([A-Z])\s*\)`,
5880
6622
  "u"
5881
6623
  );
5882
6624
  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;
@@ -5905,17 +6647,23 @@ var circleExternalPointRule = {
5905
6647
  } else if (VN_METRIC.test(c.text)) {
5906
6648
  const mm = VN_METRIC.exec(c.text);
5907
6649
  const [ctr, r, pt, pair, d] = [mm[1], mm[2], mm[3], mm[4], mm[5]];
5908
- const norm2 = (s) => parseFloat(s.replace(",", "."));
5909
- if (pair.includes(ctr) && pair.includes(pt) && norm2(d) > norm2(r)) {
6650
+ const norm5 = (s) => parseFloat(s.replace(",", "."));
6651
+ if (pair.includes(ctr) && pair.includes(pt) && norm5(d) > norm5(r)) {
5910
6652
  center = ctr;
5911
6653
  ext = pt;
5912
6654
  }
5913
6655
  } else {
5914
6656
  const rv = VN_REV.exec(c.text);
5915
- if (!rv) continue;
5916
- ext = rv[1];
5917
- const rc = rv[2] ? void 0 : RESOLVE_CENTER2.exec(ctx.problem);
5918
- center = rv[2] ?? rc?.[1] ?? rc?.[2];
6657
+ if (rv) {
6658
+ ext = rv[1];
6659
+ const rc = rv[2] ? void 0 : RESOLVE_CENTER2.exec(ctx.problem);
6660
+ center = rv[2] ?? rc?.[1] ?? rc?.[2] ?? "O";
6661
+ } else {
6662
+ const rb = VN_REV_BARE.exec(c.text);
6663
+ if (!rb) continue;
6664
+ ext = rb[1];
6665
+ center = rb[2];
6666
+ }
5919
6667
  }
5920
6668
  if (!center || !ext || center === ext) continue;
5921
6669
  out.push({
@@ -5932,7 +6680,7 @@ var circleExternalPointRule = {
5932
6680
  };
5933
6681
 
5934
6682
  // src/stamps/geometry-2d/ai/rules/tangentNamedFromExt.ts
5935
- 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;
6683
+ 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;
5936
6684
  var TWO = new RegExp(
5937
6685
  "(?:[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})",
5938
6686
  "gu"
@@ -5945,6 +6693,18 @@ var FROM_TOUCH = new RegExp(
5945
6693
  "[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})",
5946
6694
  "gu"
5947
6695
  );
6696
+ var FROM_DRAW_TOUCH = new RegExp(
6697
+ "[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})",
6698
+ "gu"
6699
+ );
6700
+ var TWO_BARE = new RegExp(
6701
+ "[Tt]i\u1EBFp\\s*tuy\u1EBFn\\s+([A-Z])([A-Z])\\s*(?:,\\s*|v\xE0\\s+)([A-Z])([A-Z])(?!\\p{L})",
6702
+ "gu"
6703
+ );
6704
+ var NAME_BEFORE_TWO = new RegExp(
6705
+ "([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",
6706
+ "gu"
6707
+ );
5948
6708
  var TOUCH_APPOS = new RegExp(
5949
6709
  "^(?: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",
5950
6710
  "u"
@@ -5954,7 +6714,7 @@ var tangentNamedFromExtRule = {
5954
6714
  id: "tangentNamedFromExt",
5955
6715
  priority: 50,
5956
6716
  languages: ["vi"],
5957
- patterns: [PREFILTER42],
6717
+ patterns: [PREFILTER43],
5958
6718
  match(ctx) {
5959
6719
  const par = PAREN_CIRCLE2.exec(ctx.problem);
5960
6720
  const circle = par ? par[1] : "O";
@@ -5983,14 +6743,43 @@ var tangentNamedFromExtRule = {
5983
6743
  });
5984
6744
  }
5985
6745
  }
6746
+ for (const c of ctx.clauses) {
6747
+ if (twoClauses.has(c.id)) continue;
6748
+ TWO_BARE.lastIndex = 0;
6749
+ NAME_BEFORE_TWO.lastIndex = 0;
6750
+ const hits = [...c.text.matchAll(TWO_BARE), ...c.text.matchAll(NAME_BEFORE_TWO)];
6751
+ for (const m of hits) {
6752
+ if (twoClauses.has(c.id)) continue;
6753
+ const ext = m[1];
6754
+ if (m[3] !== ext) continue;
6755
+ const p = m[2];
6756
+ const q = m[4];
6757
+ if (p === q || p === ext || q === ext) continue;
6758
+ twoClauses.add(c.id);
6759
+ whichOf.set(ext, 1);
6760
+ out.push({
6761
+ ruleId: "tangentNamedFromExt",
6762
+ clauseIds: [c.id],
6763
+ intents: [
6764
+ addPoint(p, { kind: "tangentPoint", from: ext, circle, which: 0 }),
6765
+ addPoint(q, { kind: "tangentPoint", from: ext, circle, which: 1 }),
6766
+ connect(ext, p, "segment"),
6767
+ connect(ext, q, "segment")
6768
+ ]
6769
+ });
6770
+ }
6771
+ }
5986
6772
  for (const c of ctx.clauses) {
5987
6773
  if (twoClauses.has(c.id)) continue;
5988
6774
  FROM_TOUCH.lastIndex = 0;
5989
- for (const m of c.text.matchAll(FROM_TOUCH)) {
6775
+ FROM_DRAW_TOUCH.lastIndex = 0;
6776
+ const hits = [...c.text.matchAll(FROM_TOUCH), ...c.text.matchAll(FROM_DRAW_TOUCH)];
6777
+ for (const m of hits) {
5990
6778
  const ext = m[1];
5991
6779
  const p = m[2];
5992
6780
  const q = m[3];
5993
6781
  if (p === q || p === ext || q === ext) continue;
6782
+ if (twoClauses.has(c.id)) continue;
5994
6783
  twoClauses.add(c.id);
5995
6784
  whichOf.set(ext, 1);
5996
6785
  out.push({
@@ -6044,11 +6833,16 @@ var tangentNamedFromExtRule = {
6044
6833
  // src/stamps/geometry-2d/ai/rules/secant.ts
6045
6834
  var CAT_TUYEN = /cát\s*tuyến\s+([A-Z])([A-Z])([A-Z])(?![A-Z])/gu;
6046
6835
  var LINE_THROUGH = new RegExp(
6047
- 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])`,
6836
+ 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])`,
6837
+ "gu"
6838
+ );
6839
+ var CAT_CUTS = new RegExp(
6840
+ 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])`,
6048
6841
  "gu"
6049
6842
  );
6050
6843
  var PAREN_CIRCLE3 = /\(\s*([A-Z])\s*\)/u;
6051
- var PREFILTER43 = /cát\s*tuyến|[Qq]ua\s+(?:điểm\s+)?[A-Z][^.]{0,30}?cắt/u;
6844
+ 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;
6845
+ var PREFILTER44 = /cát\s*tuyến|[Qq]ua\s+(?:điểm\s+)?[A-Z][^.]{0,30}?cắt/u;
6052
6846
  function secantIntents(ext, near, far, circle, theta) {
6053
6847
  if ((/* @__PURE__ */ new Set([ext, near, far])).size !== 3) return null;
6054
6848
  const line = ext + near;
@@ -6062,13 +6856,26 @@ var secantRule = {
6062
6856
  id: "secant",
6063
6857
  priority: 58,
6064
6858
  languages: ["vi"],
6065
- patterns: [PREFILTER43],
6859
+ patterns: [PREFILTER44],
6066
6860
  match(ctx) {
6067
6861
  const par = PAREN_CIRCLE3.exec(ctx.problem);
6068
6862
  const fallbackCircle = par ? par[1] : "O";
6863
+ const extM = EXT_RESOLVE.exec(ctx.problem);
6864
+ const resolvedExt = extM ? extM[1] : void 0;
6069
6865
  const out = [];
6070
6866
  let theta = 2.4;
6071
6867
  for (const c of ctx.clauses) {
6868
+ if (resolvedExt) {
6869
+ CAT_CUTS.lastIndex = 0;
6870
+ for (const m of c.text.matchAll(CAT_CUTS)) {
6871
+ const circle = m[1] ?? fallbackCircle;
6872
+ const intents = secantIntents(resolvedExt, m[2], m[3], circle, theta);
6873
+ if (intents) {
6874
+ out.push({ ruleId: "secant", clauseIds: [c.id], intents });
6875
+ theta += 0.7;
6876
+ }
6877
+ }
6878
+ }
6072
6879
  CAT_TUYEN.lastIndex = 0;
6073
6880
  for (const m of c.text.matchAll(CAT_TUYEN)) {
6074
6881
  const intents = secantIntents(m[1], m[2], m[3], fallbackCircle, theta);
@@ -6093,14 +6900,21 @@ var secantRule = {
6093
6900
 
6094
6901
  // src/stamps/geometry-2d/ai/rules/oppositeRayPoint.ts
6095
6902
  var RE17 = new RegExp(
6096
- 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])`,
6903
+ 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])`,
6097
6904
  "gu"
6098
6905
  );
6099
6906
  var RE_NAME_FIRST = new RegExp(
6100
- 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])`,
6907
+ 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])`,
6101
6908
  "gu"
6102
6909
  );
6910
+ var RE_DISTRIB = new RegExp(
6911
+ 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])`,
6912
+ "u"
6913
+ );
6103
6914
  var CANON = 2.5;
6915
+ function splitNames2(blob) {
6916
+ return blob.split(/\s*,\s*|\s+và\s+/u).map((s) => s.trim()).filter(Boolean);
6917
+ }
6104
6918
  var oppositeRayPointRule = {
6105
6919
  id: "oppositeRayPoint",
6106
6920
  priority: 56,
@@ -6109,6 +6923,35 @@ var oppositeRayPointRule = {
6109
6923
  match(ctx) {
6110
6924
  const out = [];
6111
6925
  for (const c of ctx.clauses) {
6926
+ const dm = RE_DISTRIB.exec(c.text);
6927
+ if (dm) {
6928
+ const names = splitNames2(dm[1]);
6929
+ const rays = splitNames2(dm[2]);
6930
+ if (names.length >= 2 && names.length === rays.length) {
6931
+ const intents = [];
6932
+ let valid3 = true;
6933
+ for (let i = 0; i < names.length; i++) {
6934
+ const a = rays[i][0];
6935
+ const b = rays[i][1];
6936
+ if ((/* @__PURE__ */ new Set([a, b, names[i]])).size !== 3) {
6937
+ valid3 = false;
6938
+ break;
6939
+ }
6940
+ intents.push(
6941
+ addPoint(names[i], {
6942
+ kind: "pointAtDistance",
6943
+ from: b,
6944
+ through: a,
6945
+ distance: { kind: "literal", value: CANON }
6946
+ })
6947
+ );
6948
+ }
6949
+ if (valid3) {
6950
+ out.push({ ruleId: "oppositeRayPoint", clauseIds: [c.id], intents });
6951
+ continue;
6952
+ }
6953
+ }
6954
+ }
6112
6955
  RE17.lastIndex = 0;
6113
6956
  for (const m of c.text.matchAll(RE17)) {
6114
6957
  const a = m[1];
@@ -6164,7 +7007,7 @@ var CENTER3 = /\(\s*([A-Z])\s*[;,)]|tâm\s+([A-Z])(?![A-Za-z])/u;
6164
7007
  function appearsBefore(problem, chordText, d) {
6165
7008
  const pos = problem.indexOf(chordText);
6166
7009
  if (pos < 0) return true;
6167
- return new RegExp(`(?<![A-Za-z])${d}(?![a-z])`, "u").test(problem.slice(0, pos));
7010
+ return new RegExp(`(?<![A-Za-z])${escapeRe(d)}(?![a-z])`, "u").test(problem.slice(0, pos));
6168
7011
  }
6169
7012
  var perpChordAtFootRule = {
6170
7013
  id: "perpChordAtFoot",
@@ -6260,7 +7103,7 @@ var diameterCircleSecantRule = {
6260
7103
  };
6261
7104
 
6262
7105
  // src/stamps/geometry-2d/ai/rules/tangentsAtMeet.ts
6263
- 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;
7106
+ 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;
6264
7107
  var CIRCLE_REF6 = /đường\s*tròn\s*(?:tâm\s+)?\(?\s*([A-ZωΩ])(?:\s*[;,]\s*[Rr])?\s*\)?|\(\s*([A-ZωΩ])\s*\)/u;
6265
7108
  var RE19 = new RegExp(
6266
7109
  "[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])",
@@ -6277,7 +7120,7 @@ var tangentsAtMeetRule = {
6277
7120
  priority: 55,
6278
7121
  // trên intersection(45) — 2 tiếp tuyến + giao trong CÙNG match nên tự đủ thứ tự
6279
7122
  languages: ["vi"],
6280
- patterns: [PREFILTER44],
7123
+ patterns: [PREFILTER45],
6281
7124
  match(ctx) {
6282
7125
  const circle = circleName5(ctx.problem);
6283
7126
  if (!circle) return [];
@@ -6305,7 +7148,7 @@ var tangentsAtMeetRule = {
6305
7148
  };
6306
7149
 
6307
7150
  // src/stamps/geometry-2d/ai/rules/angleBisectorCutsSideCircle.ts
6308
- var PREFILTER45 = /phân\s*giác[^.]{0,40}?cắt/u;
7151
+ var PREFILTER46 = /phân\s*giác[^.]{0,40}?cắt/u;
6309
7152
  var FOOT = new RegExp(
6310
7153
  "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])",
6311
7154
  "u"
@@ -6320,7 +7163,7 @@ var angleBisectorCutsSideCircleRule = {
6320
7163
  priority: 48,
6321
7164
  // sau triangle/circle/cevian; trước intersection(45)
6322
7165
  languages: ["vi"],
6323
- patterns: [PREFILTER45],
7166
+ patterns: [PREFILTER46],
6324
7167
  match(ctx) {
6325
7168
  const out = [];
6326
7169
  for (const c of ctx.clauses) {
@@ -6366,7 +7209,7 @@ var angleBisectorCutsSideCircleRule = {
6366
7209
  };
6367
7210
 
6368
7211
  // src/stamps/geometry-2d/ai/rules/circleCutsArcSecond.ts
6369
- 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;
7212
+ 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;
6370
7213
  var CUTS_ARC = new RegExp(
6371
7214
  "\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]))?",
6372
7215
  "u"
@@ -6381,7 +7224,7 @@ var circleCutsArcSecondRule = {
6381
7224
  priority: 45,
6382
7225
  // sau mọi rule dựng đường tròn + điểm chung
6383
7226
  languages: ["vi"],
6384
- patterns: [PREFILTER46],
7227
+ patterns: [PREFILTER47],
6385
7228
  match(ctx) {
6386
7229
  const out = [];
6387
7230
  for (const c of ctx.clauses) {
@@ -6420,7 +7263,7 @@ var circleCutsArcSecondRule = {
6420
7263
  };
6421
7264
 
6422
7265
  // src/stamps/geometry-2d/ai/rules/angleBisectorCutsCircleLine.ts
6423
- var PREFILTER47 = /[Pp]hân\s*giác[^.]{0,40}?cắt\s+(?:đường\s*tròn|\()/u;
7266
+ var PREFILTER48 = /[Pp]hân\s*giác[^.]{0,40}?cắt\s+(?:đường\s*tròn|\()/u;
6424
7267
  var RE20 = new RegExp(
6425
7268
  "[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]))?",
6426
7269
  "u"
@@ -6433,7 +7276,7 @@ var angleBisectorCutsCircleLineRule = {
6433
7276
  // perpFoot 65 dựng tia/C/H trước).
6434
7277
  priority: 49,
6435
7278
  languages: ["vi"],
6436
- patterns: [PREFILTER47],
7279
+ patterns: [PREFILTER48],
6437
7280
  match(ctx) {
6438
7281
  const out = [];
6439
7282
  for (const c of ctx.clauses) {
@@ -6465,17 +7308,34 @@ var angleBisectorCutsCircleLineRule = {
6465
7308
  };
6466
7309
 
6467
7310
  // src/stamps/geometry-2d/ai/rules/tangentLineNamedAtPoint.ts
6468
- var PREFILTER48 = /(?:trên|thuộc)\s+\(\s*[A-Z][^)]*\)[^.]{0,24}?kẻ\s+tiếp\s*tuyến\s+[a-z]/u;
7311
+ 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;
6469
7312
  var RE21 = new RegExp(
6470
7313
  "(?: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})",
6471
7314
  "gu"
6472
7315
  );
7316
+ var RE_NAMED_OF_AT = new RegExp(
7317
+ "(?:[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])",
7318
+ "gu"
7319
+ );
7320
+ var RE_THROUGH_NAMED = new RegExp(
7321
+ "(?:[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})",
7322
+ "gu"
7323
+ );
7324
+ var PAREN_CIRCLE4 = /\(\s*([A-Z])(?:\s*[;,]\s*[Rr])?\s*\)|đường\s*tròn\s+tâm\s+([A-Z])(?![A-Za-z])/u;
7325
+ function circleDslName(problem, center) {
7326
+ return /đường\s*kính/u.test(problem) ? `${center}_c` : center;
7327
+ }
7328
+ function resolveProblemCircle(problem) {
7329
+ const m = PAREN_CIRCLE4.exec(problem);
7330
+ const center = m?.[1] ?? m?.[2];
7331
+ return center ? circleDslName(problem, center) : void 0;
7332
+ }
6473
7333
  var tangentLineNamedAtPointRule = {
6474
7334
  id: "tangentLineNamedAtPoint",
6475
7335
  priority: 63,
6476
7336
  // cao như tangentAt — đường d phải dựng trước điểm-trên-d
6477
7337
  languages: ["vi"],
6478
- patterns: [PREFILTER48],
7338
+ patterns: [PREFILTER49],
6479
7339
  match(ctx) {
6480
7340
  const out = [];
6481
7341
  for (const c of ctx.clauses) {
@@ -6490,6 +7350,30 @@ var tangentLineNamedAtPointRule = {
6490
7350
  intents: [drawLine(line, "tangentAt", { through, circle })]
6491
7351
  });
6492
7352
  }
7353
+ RE_NAMED_OF_AT.lastIndex = 0;
7354
+ for (const m of c.text.matchAll(RE_NAMED_OF_AT)) {
7355
+ const line = m[1];
7356
+ const circle = circleDslName(ctx.problem, m[2]);
7357
+ const through = m[3];
7358
+ out.push({
7359
+ ruleId: "tangentLineNamedAtPoint",
7360
+ clauseIds: [c.id],
7361
+ intents: [drawLine(line, "tangentAt", { through, circle })]
7362
+ });
7363
+ }
7364
+ RE_THROUGH_NAMED.lastIndex = 0;
7365
+ const probCircle = resolveProblemCircle(ctx.problem);
7366
+ if (probCircle) {
7367
+ for (const m of c.text.matchAll(RE_THROUGH_NAMED)) {
7368
+ const through = m[1];
7369
+ const line = m[2];
7370
+ out.push({
7371
+ ruleId: "tangentLineNamedAtPoint",
7372
+ clauseIds: [c.id],
7373
+ intents: [drawLine(line, "tangentAt", { through, circle: probCircle })]
7374
+ });
7375
+ }
7376
+ }
6493
7377
  }
6494
7378
  return out;
6495
7379
  }
@@ -6535,13 +7419,13 @@ var givenDiameterCircleRule = {
6535
7419
 
6536
7420
  // src/stamps/geometry-2d/ai/rules/multiDiameterCircles.ts
6537
7421
  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;
6538
- var PREFILTER49 = /đường\s*kính[^.]{0,40}?tâm/u;
7422
+ var PREFILTER50 = /đường\s*kính[^.]{0,40}?tâm/u;
6539
7423
  var multiDiameterCirclesRule = {
6540
7424
  id: "multiDiameterCircles",
6541
7425
  priority: 67,
6542
7426
  // như circleDiameter — dựng đầu mút + tâm trước điểm phái sinh
6543
7427
  languages: ["vi"],
6544
- patterns: [PREFILTER49],
7428
+ patterns: [PREFILTER50],
6545
7429
  match(ctx) {
6546
7430
  for (const c of ctx.clauses) {
6547
7431
  const m = RE23.exec(c.text);
@@ -6593,12 +7477,12 @@ function diameterMap(problem) {
6593
7477
  }
6594
7478
  return map;
6595
7479
  }
6596
- 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;
7480
+ 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;
6597
7481
  var cutCirclesDistribRule = {
6598
7482
  id: "cutCirclesDistrib",
6599
7483
  priority: 45,
6600
7484
  languages: ["vi"],
6601
- patterns: [PREFILTER50],
7485
+ patterns: [PREFILTER51],
6602
7486
  match(ctx) {
6603
7487
  const map = diameterMap(ctx.problem);
6604
7488
  const out = [];
@@ -6626,6 +7510,7 @@ var cutCirclesDistribRule = {
6626
7510
  // src/stamps/geometry-2d/ai/rules/givenNamedCircle.ts
6627
7511
  var BARE_PAREN2 = /\(\s*([A-Z])\s*\)/gu;
6628
7512
  var HAS_POINTS_ON = /(?:trên|thuộc)\s+(?:nửa\s+)?(?:đường\s*tròn|cung|\(\s*[A-Z]\s*\))/u;
7513
+ var HAS_TANGENT_OR_SECANT = /tiếp\s*tuyến|cát\s*tuyến/u;
6629
7514
  var givenNamedCircleRule = {
6630
7515
  id: "givenNamedCircle",
6631
7516
  priority: 74,
@@ -6633,7 +7518,7 @@ var givenNamedCircleRule = {
6633
7518
  languages: ["vi"],
6634
7519
  patterns: [/\(\s*[A-Z]\s*\)/u],
6635
7520
  match(ctx) {
6636
- if (!HAS_POINTS_ON.test(ctx.problem)) return [];
7521
+ if (!HAS_POINTS_ON.test(ctx.problem) && !HAS_TANGENT_OR_SECANT.test(ctx.problem)) return [];
6637
7522
  const out = [];
6638
7523
  const seen = /* @__PURE__ */ new Set();
6639
7524
  BARE_PAREN2.lastIndex = 0;
@@ -6641,12 +7526,14 @@ var givenNamedCircleRule = {
6641
7526
  const center = m[1];
6642
7527
  if (seen.has(center)) continue;
6643
7528
  const c = center;
6644
- if (new RegExp(`\\(\\s*${c}\\s*[;,]`, "u").test(ctx.problem)) continue;
6645
- 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;
6646
- 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;
6647
- if (!new RegExp(`${CIRCLE_KW}\\s*\\(\\s*${c}\\s*\\)`, "u").test(ctx.problem)) continue;
7529
+ if (new RegExp(`\\(\\s*${escapeRe(c)}\\s*[;,]`, "u").test(ctx.problem)) continue;
7530
+ 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;
7531
+ 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;
7532
+ const isNamedCircle = new RegExp(`${CIRCLE_KW}\\s*\\(\\s*${escapeRe(c)}\\s*\\)`, "u").test(ctx.problem);
7533
+ const isOpenedBare = new RegExp(`^[Cc]ho\\s+\\(\\s*${escapeRe(c)}\\s*\\)`, "u").test(ctx.problem.trimStart());
7534
+ if (!isNamedCircle && !isOpenedBare) continue;
6648
7535
  seen.add(center);
6649
- const owner = ctx.clauses.find((cl) => new RegExp(`\\(\\s*${c}\\s*\\)`, "u").test(cl.text));
7536
+ const owner = ctx.clauses.find((cl) => new RegExp(`\\(\\s*${escapeRe(c)}\\s*\\)`, "u").test(cl.text));
6650
7537
  out.push({
6651
7538
  ruleId: "givenNamedCircle",
6652
7539
  clauseIds: owner ? [owner.id] : [],
@@ -6658,12 +7545,16 @@ var givenNamedCircleRule = {
6658
7545
  };
6659
7546
 
6660
7547
  // src/stamps/geometry-2d/ai/rules/twoPerpLinesMeet.ts
6661
- 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;
7548
+ 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;
6662
7549
  var RE25 = new RegExp(
6663
7550
  "[\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])",
6664
7551
  "gu"
6665
7552
  );
6666
- var RE_DISTRIB = new RegExp(
7553
+ var RE_DRAW_FIRST = new RegExp(
7554
+ "(?: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])",
7555
+ "gu"
7556
+ );
7557
+ var RE_DISTRIB2 = new RegExp(
6667
7558
  "[\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])",
6668
7559
  "gu"
6669
7560
  );
@@ -6672,7 +7563,7 @@ var twoPerpLinesMeetRule = {
6672
7563
  priority: 49,
6673
7564
  // sau parallelPerp (dựng đường), trước/đủ để S tham chiếu 2 đường
6674
7565
  languages: ["vi"],
6675
- patterns: [PREFILTER51],
7566
+ patterns: [PREFILTER52],
6676
7567
  match(ctx) {
6677
7568
  const out = [];
6678
7569
  const emit = (c, p1, l1, p2, l2, s) => {
@@ -6694,8 +7585,12 @@ var twoPerpLinesMeetRule = {
6694
7585
  for (const m of c.text.matchAll(RE25)) {
6695
7586
  emit(c, m[1], m[2] + m[3], m[4], m[5] + m[6], m[7]);
6696
7587
  }
6697
- RE_DISTRIB.lastIndex = 0;
6698
- for (const m of c.text.matchAll(RE_DISTRIB)) {
7588
+ RE_DRAW_FIRST.lastIndex = 0;
7589
+ for (const m of c.text.matchAll(RE_DRAW_FIRST)) {
7590
+ emit(c, m[1], m[2] + m[3], m[4], m[5] + m[6], m[7]);
7591
+ }
7592
+ RE_DISTRIB2.lastIndex = 0;
7593
+ for (const m of c.text.matchAll(RE_DISTRIB2)) {
6699
7594
  emit(c, m[1], m[3] + m[4], m[2], m[5] + m[6], m[7]);
6700
7595
  }
6701
7596
  }
@@ -6704,27 +7599,57 @@ var twoPerpLinesMeetRule = {
6704
7599
  };
6705
7600
 
6706
7601
  // src/stamps/geometry-2d/ai/rules/circumcircleCutsLine.ts
6707
- 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;
7602
+ 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;
6708
7603
  var RE26 = new RegExp(
6709
7604
  "[\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]))?",
6710
7605
  "gu"
6711
7606
  );
7607
+ var RE_PAREN = new RegExp(
7608
+ "[\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]))?",
7609
+ "gu"
7610
+ );
7611
+ var RE_SET = new RegExp(
7612
+ "\\(([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*\\}",
7613
+ "gu"
7614
+ );
6712
7615
  var circumcircleCutsLineRule = {
6713
7616
  id: "circumcircleCutsLine",
6714
7617
  priority: 58,
6715
7618
  // sau circleTriangle/cevian; trước intersection(45)
6716
7619
  languages: ["vi"],
6717
- patterns: [PREFILTER52],
7620
+ patterns: [PREFILTER53],
6718
7621
  match(ctx) {
6719
7622
  const out = [];
6720
7623
  for (const c of ctx.clauses) {
6721
- RE26.lastIndex = 0;
6722
- for (const m of c.text.matchAll(RE26)) {
7624
+ for (const re of [RE26, RE_PAREN]) {
7625
+ re.lastIndex = 0;
7626
+ for (const m of c.text.matchAll(re)) {
7627
+ const tri = m[1].split("");
7628
+ const line = m[2];
7629
+ const k = m[3];
7630
+ const shared = m[4] ?? line.split("").find((v) => tri.includes(v));
7631
+ if (!shared || tri.includes(k) || line.includes(k)) continue;
7632
+ const w = `w${m[1]}`;
7633
+ out.push({
7634
+ ruleId: "circumcircleCutsLine",
7635
+ clauseIds: [c.id],
7636
+ intents: [
7637
+ drawCircle(w, "through3", { points: tri }),
7638
+ addPoint(k, { kind: "secondIntersection", line, circle: w, other: shared })
7639
+ ]
7640
+ });
7641
+ }
7642
+ }
7643
+ RE_SET.lastIndex = 0;
7644
+ for (const m of c.text.matchAll(RE_SET)) {
6723
7645
  const tri = m[1].split("");
6724
7646
  const line = m[2];
6725
- const k = m[3];
6726
- const shared = m[4] ?? line.split("").find((v) => tri.includes(v));
6727
- if (!shared || tri.includes(k) || line.includes(k)) continue;
7647
+ const set = [m[3], m[4]];
7648
+ const onLine = set.filter((v) => line.includes(v));
7649
+ if (onLine.length !== 1) continue;
7650
+ const shared = onLine[0];
7651
+ const k = set.find((v) => v !== shared);
7652
+ if (tri.includes(k) || line.includes(k)) continue;
6728
7653
  const w = `w${m[1]}`;
6729
7654
  out.push({
6730
7655
  ruleId: "circumcircleCutsLine",
@@ -6754,7 +7679,7 @@ var LINE_TYPES = [
6754
7679
  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;
6755
7680
  var VERTEX_PAIR = /(?<!\p{L})[A-Z][A-Z](?:['′])?(?!\p{L})/gu;
6756
7681
  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;
6757
- var PREFILTER53 = LINE_TYPES.map((t) => t.kw);
7682
+ var PREFILTER54 = LINE_TYPES.map((t) => t.kw);
6758
7683
  function trianglesIn4(text) {
6759
7684
  const out = [];
6760
7685
  TRI_G9.lastIndex = 0;
@@ -6787,7 +7712,7 @@ var lineConcurrencyRule = {
6787
7712
  id: "lineConcurrency",
6788
7713
  priority: 69,
6789
7714
  languages: ["vi"],
6790
- patterns: PREFILTER53,
7715
+ patterns: PREFILTER54,
6791
7716
  match(ctx) {
6792
7717
  const fallbackTri = uniqueTriangle5(ctx.problem);
6793
7718
  const out = [];
@@ -6819,7 +7744,7 @@ var lineConcurrencyRule = {
6819
7744
  };
6820
7745
 
6821
7746
  // src/stamps/geometry-2d/ai/rules/twoCirclesMeet.ts
6822
- var PREFILTER54 = /hai\s+đường\s*tròn[^.]{0,70}?cắt\s+nhau/u;
7747
+ var PREFILTER55 = /hai\s+đường\s*tròn[^.]{0,70}?cắt\s+nhau/u;
6823
7748
  var CIRCLE2 = "\\(\\s*([A-Z]['\u2032]?)(?:\\s*[;,]\\s*(?:[Rr]['\u2032]?|\\d+(?:[.,]\\d+)?\\s*[a-z]*))?\\s*\\)";
6824
7749
  var TWO_MEET2 = new RegExp(
6825
7750
  "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])",
@@ -6832,7 +7757,7 @@ var twoCirclesMeetRule = {
6832
7757
  // phái sinh downstream → dựng sớm. DƯỚI circleRadius (75) cho nhất quán.
6833
7758
  priority: 74,
6834
7759
  languages: ["vi"],
6835
- patterns: [PREFILTER54],
7760
+ patterns: [PREFILTER55],
6836
7761
  match(ctx) {
6837
7762
  const m = TWO_MEET2.exec(ctx.problem);
6838
7763
  if (!m) return [];
@@ -6859,7 +7784,7 @@ var twoCirclesMeetRule = {
6859
7784
  };
6860
7785
 
6861
7786
  // src/stamps/geometry-2d/ai/rules/radius.ts
6862
- var PREFILTER55 = /bán\s*kính\s+[A-Z]{2}/u;
7787
+ var PREFILTER56 = /bán\s*kính\s+[A-Z]{2}/u;
6863
7788
  var DRAW_RADII = new RegExp(
6864
7789
  "(?:[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])",
6865
7790
  "u"
@@ -6870,7 +7795,7 @@ var radiusRule = {
6870
7795
  id: "radius",
6871
7796
  priority: 60,
6872
7797
  languages: ["vi"],
6873
- patterns: [PREFILTER55],
7798
+ patterns: [PREFILTER56],
6874
7799
  match(ctx) {
6875
7800
  const out = [];
6876
7801
  for (const c of ctx.clauses) {
@@ -6894,17 +7819,17 @@ var radiusRule = {
6894
7819
  };
6895
7820
 
6896
7821
  // src/stamps/geometry-2d/ai/rules/perpNamedLine.ts
6897
- var PREFILTER56 = /(?:[Kk]ẻ|[Vv]ẽ|[Dd]ựng)\s+(?:đường\s*thẳng\s+)?[A-Z][a-z]\s*(?:⊥|vuông\s*góc)/u;
7822
+ 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;
6898
7823
  var RE27 = new RegExp(
6899
7824
  // `⊥\\s*` (KHÔNG `\\s+`): OCR hay dính "Ax⊥MN" (vao10:11). Dạng chữ giữ `\\s+`.
6900
- "(?:[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]))?",
7825
+ "(?:[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]))?",
6901
7826
  "gu"
6902
7827
  );
6903
7828
  var perpNamedLineRule = {
6904
7829
  id: "perp-named-line",
6905
7830
  priority: 60,
6906
7831
  languages: ["vi"],
6907
- patterns: [PREFILTER56],
7832
+ patterns: [PREFILTER57],
6908
7833
  match(ctx) {
6909
7834
  const out = [];
6910
7835
  for (const c of ctx.clauses) {
@@ -6930,7 +7855,7 @@ var perpNamedLineRule = {
6930
7855
  };
6931
7856
 
6932
7857
  // src/stamps/geometry-2d/ai/rules/parallelSidePoints.ts
6933
- var PREFILTER57 = /thuộc\s+[A-Z]{2}\s+và\s+[A-Z]{2}\s+sao\s+cho[^.]*?(?:∥|\/\/|song\s*song)/u;
7858
+ var PREFILTER58 = /thuộc\s+[A-Z]{2}\s+và\s+[A-Z]{2}\s+sao\s+cho[^.]*?(?:∥|\/\/|song\s*song)/u;
6934
7859
  var PAR2 = "(?:\u2225|\\/\\/|song\\s*song(?:\\s+v\u1EDBi)?)";
6935
7860
  var RE28 = new RegExp(
6936
7861
  "([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])",
@@ -6940,7 +7865,7 @@ var parallelSidePointsRule = {
6940
7865
  id: "parallel-side-points",
6941
7866
  priority: 55,
6942
7867
  languages: ["vi"],
6943
- patterns: [PREFILTER57],
7868
+ patterns: [PREFILTER58],
6944
7869
  match(ctx) {
6945
7870
  const out = [];
6946
7871
  for (const c of ctx.clauses) {
@@ -6963,36 +7888,370 @@ var parallelSidePointsRule = {
6963
7888
  }
6964
7889
  };
6965
7890
 
6966
- // src/stamps/geometry-2d/ai/rules/parallelThroughCutsCircle.ts
6967
- var DISTRIB4 = new RegExp(
6968
- 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})`,
6969
- "gu"
6970
- );
6971
- var DECL_NAMED = new RegExp(
6972
- 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})`,
6973
- "u"
6974
- );
6975
- var NAMED_CUT_BOTH = new RegExp(
6976
- 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})`,
6977
- "u"
6978
- );
6979
- 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;
6980
- function lineNameOf2(through) {
6981
- return "par" + through;
7891
+ // src/stamps/geometry-2d/ai/intent-builders/shared.ts
7892
+ var SQRT3 = 1.7320508075688772;
7893
+ function triangleCanonical(variant) {
7894
+ switch (variant) {
7895
+ case "equilateral":
7896
+ return [[0, 0], [4, 0], [2, 2 * SQRT3]];
7897
+ case "isoceles-AB":
7898
+ return [[0, 0], [4, 0], [2, 3]];
7899
+ case "isoceles-BC":
7900
+ return [[0, 3], [-2, 0], [2, 0]];
7901
+ case "isoceles-CA":
7902
+ return [[0, 0], [2, 3], [4, 0]];
7903
+ case "right-at-A":
7904
+ return [[0, 0], [4, 0], [0, 3]];
7905
+ case "right-at-B":
7906
+ return [[0, 0], [4, 0], [4, 3]];
7907
+ case "right-at-C":
7908
+ return [[0, 0], [6, 0], [3, 3]];
7909
+ case "any":
7910
+ default:
7911
+ return [[0, 0], [5, 0], [2, 3]];
7912
+ }
6982
7913
  }
6983
- function resolveCircle4(problem) {
6984
- const m = /đường\s*tròn\s*\(\s*([A-Z])\s*\)|\(\s*([A-Z])\s*\)/u.exec(problem);
6985
- if (!m) return void 0;
6986
- return m[1] ?? m[2];
7914
+ function squareCanonical() {
7915
+ return [[0, 0], [4, 0], [4, 4], [0, 4]];
6987
7916
  }
6988
- function secondIntersection2(name, line, circle, other) {
6989
- return addPoint(name, { kind: "secondIntersection", line, circle, other });
7917
+ function rectangleCanonical(variant) {
7918
+ if (variant === "tall") return [[0, 0], [2.5, 0], [2.5, 4], [0, 4]];
7919
+ return [[0, 0], [4, 0], [4, 2.5], [0, 2.5]];
6990
7920
  }
6991
- var parallelThroughCutsCircleRule = {
6992
- id: "parallelThroughCutsCircle",
6993
- priority: 46,
6994
- languages: ["vi"],
6995
- patterns: [PREFILTER58],
7921
+ function rhombusCanonical() {
7922
+ return [[-2, 0], [0, -1.5], [2, 0], [0, 1.5]];
7923
+ }
7924
+ function trapezoidCanonical(variant) {
7925
+ switch (variant) {
7926
+ case "right":
7927
+ return [[0, 0], [5, 0], [3, 3], [0, 3]];
7928
+ case "isoceles":
7929
+ return [[0, 0], [5, 0], [4, 3], [1, 3]];
7930
+ case "general":
7931
+ default:
7932
+ return [[0, 0], [5, 0], [3.5, 3], [1, 3]];
7933
+ }
7934
+ }
7935
+ function parallelogramCanonical() {
7936
+ return [[0, 0], [4, 0], [5, 3], [1, 3]];
7937
+ }
7938
+ function quadrilateralCanonical() {
7939
+ return [[0, 0], [5, 0], [4.5, 3.5], [0.5, 3]];
7940
+ }
7941
+ function addPoint2(s, p) {
7942
+ if (s.pointNames.has(p.name)) {
7943
+ if (p.kind !== "free") {
7944
+ const idx = s.points.findIndex((q) => q.name === p.name);
7945
+ if (idx >= 0 && s.points[idx].kind === "free") s.points[idx] = p;
7946
+ }
7947
+ return;
7948
+ }
7949
+ s.points.push(p);
7950
+ s.pointNames.add(p.name);
7951
+ }
7952
+ var FREE_DEFAULT_SPREAD = [
7953
+ [3, 3],
7954
+ [-3, 3],
7955
+ [3, -3],
7956
+ [-3, -3],
7957
+ [4, 0],
7958
+ [-4, 0],
7959
+ [0, 4],
7960
+ [0, -4]
7961
+ ];
7962
+ function defaultFreeCoord(s) {
7963
+ let count = 0;
7964
+ for (const p of s.points) if (p.kind === "free") count++;
7965
+ return [...FREE_DEFAULT_SPREAD[count % FREE_DEFAULT_SPREAD.length]];
7966
+ }
7967
+ function addShape(s, sh) {
7968
+ if (s.shapeNames.has(sh.name)) return;
7969
+ s.shapes.push(sh);
7970
+ s.shapeNames.add(sh.name);
7971
+ }
7972
+ function uniqueShapeName(s, base) {
7973
+ if (!s.shapeNames.has(base)) return base;
7974
+ let i = 2;
7975
+ while (s.shapeNames.has(`${base}${i}`)) i++;
7976
+ return `${base}${i}`;
7977
+ }
7978
+ function uniquePointName(s, base) {
7979
+ if (!s.pointNames.has(base)) return base;
7980
+ let i = 2;
7981
+ while (s.pointNames.has(`${base}${i}`)) i++;
7982
+ return `${base}${i}`;
7983
+ }
7984
+ function ensureSegment(s, a, b) {
7985
+ const k1 = `${a}-${b}`;
7986
+ const k2 = `${b}-${a}`;
7987
+ const existing = s.segmentByEnds.get(k1) ?? s.segmentByEnds.get(k2);
7988
+ if (existing) return existing;
7989
+ const name = uniqueShapeName(s, `${a}${b}`);
7990
+ addShape(s, { name, kind: "segment", p1: a, p2: b });
7991
+ s.segmentByEnds.set(k1, name);
7992
+ return name;
7993
+ }
7994
+ function resolveLineRefWithFallback(s, ref, anchor) {
7995
+ if (/^[A-Za-z]$/.test(ref) && /^[A-Za-z]$/.test(anchor) && ref !== anchor && s.pointNames.has(ref) && s.pointNames.has(anchor)) {
7996
+ const [a, b] = ref < anchor ? [ref, anchor] : [anchor, ref];
7997
+ return ensureSegment(s, a, b);
7998
+ }
7999
+ return resolveSegmentRef(s, ref);
8000
+ }
8001
+ function resolveSegmentRef(s, ref) {
8002
+ if (s.shapeNames.has(ref)) return ref;
8003
+ if (/^[A-Z][A-Z]$/u.test(ref)) {
8004
+ return ensureSegment(s, ref[0], ref[1]);
8005
+ }
8006
+ if (ref.length === 2 && s.pointNames.has(ref[0]) && s.pointNames.has(ref[1])) {
8007
+ return ensureSegment(s, ref[0], ref[1]);
8008
+ }
8009
+ const dashMatch = ref.match(/^([A-Za-z][A-Za-z0-9'_]*)[-_]?([A-Za-z][A-Za-z0-9'_]*)$/);
8010
+ if (dashMatch && s.pointNames.has(dashMatch[1]) && s.pointNames.has(dashMatch[2])) {
8011
+ return ensureSegment(s, dashMatch[1], dashMatch[2]);
8012
+ }
8013
+ const sub2 = splitKnownPair(s, ref);
8014
+ if (sub2) return ensureSegment(s, sub2[0], sub2[1]);
8015
+ return ref;
8016
+ }
8017
+ function splitKnownPair(s, ref) {
8018
+ for (let i = ref.length - 1; i >= 1; i--) {
8019
+ const a = ref.slice(0, i);
8020
+ const b = ref.slice(i);
8021
+ if (s.pointNames.has(a) && s.pointNames.has(b)) return [a, b];
8022
+ }
8023
+ return null;
8024
+ }
8025
+ function parseEnds(ref) {
8026
+ if (ref.length === 2) return [ref[0], ref[1]];
8027
+ const m = ref.match(/^([A-Za-z][A-Za-z0-9'_]*)[-_]?([A-Za-z][A-Za-z0-9'_]*)$/);
8028
+ if (m) return [m[1], m[2]];
8029
+ return null;
8030
+ }
8031
+ var SHAPE_VARIANTS = {
8032
+ triangle: ["any", "equilateral", "isoceles-AB", "isoceles-BC", "isoceles-CA", "right-at-A", "right-at-B", "right-at-C"],
8033
+ square: ["standard"],
8034
+ rectangle: ["standard", "wide", "tall"],
8035
+ rhombus: ["standard"],
8036
+ trapezoid: ["right", "isoceles", "general"],
8037
+ parallelogram: ["standard"],
8038
+ quadrilateral: ["any"]
8039
+ };
8040
+
8041
+ // src/stamps/geometry-2d/ai/rules/inscribedSquare.ts
8042
+ function sub(p, q) {
8043
+ return [p[0] - q[0], p[1] - q[1]];
8044
+ }
8045
+ function add(p, q) {
8046
+ return [p[0] + q[0], p[1] + q[1]];
8047
+ }
8048
+ function mul(p, k) {
8049
+ return [p[0] * k, p[1] * k];
8050
+ }
8051
+ function dot(p, q) {
8052
+ return p[0] * q[0] + p[1] * q[1];
8053
+ }
8054
+ function cross(p, q) {
8055
+ return p[0] * q[1] - p[1] * q[0];
8056
+ }
8057
+ function len(p) {
8058
+ return Math.hypot(p[0], p[1]);
8059
+ }
8060
+ function norm2(p) {
8061
+ const l = len(p) || 1;
8062
+ return [p[0] / l, p[1] / l];
8063
+ }
8064
+ function inscribedSquareCoords(A, B, C) {
8065
+ const u = norm2(sub(C, B));
8066
+ const a = len(sub(C, B));
8067
+ const tA = dot(sub(A, B), u);
8068
+ const foot = add(B, mul(u, tA));
8069
+ const h = len(sub(A, foot));
8070
+ const s = a * h / (a + h);
8071
+ const n = norm2(sub(A, foot));
8072
+ const BA = sub(B, A);
8073
+ const q0 = -(s * cross(n, BA)) / cross(u, BA);
8074
+ const Q = add(B, mul(u, q0));
8075
+ const P = add(B, mul(u, q0 + s));
8076
+ const M = add(Q, mul(n, s));
8077
+ const N = add(P, mul(n, s));
8078
+ return { M, N, P, Q };
8079
+ }
8080
+ function paramOn(pt, X, Y) {
8081
+ const d = sub(Y, X);
8082
+ return dot(sub(pt, X), d) / (dot(d, d) || 1);
8083
+ }
8084
+ function inscribedSquareParams(A, B, C) {
8085
+ const { M, N, P, Q } = inscribedSquareCoords(A, B, C);
8086
+ return {
8087
+ tM: paramOn(M, A, B),
8088
+ tN: paramOn(N, A, C),
8089
+ tP: paramOn(P, B, C),
8090
+ tQ: paramOn(Q, B, C)
8091
+ };
8092
+ }
8093
+ var PREFILTER59 = /hình\s*vuông[^.]*?(?:trên|∈|thuộc|\bNe\b|\bEe\b)/iu;
8094
+ var SQUARE_LABELS = /hình\s*vuông\s+([A-Z])\s*([A-Z])\s*([A-Z])\s*([A-Z])(?![A-Z])/u;
8095
+ var RIGHT_AT2 = /vuông\s+tại\s+([A-Z])(?![A-Za-z])/u;
8096
+ var RIGHT_BY_IDX2 = ["right-at-A", "right-at-B", "right-at-C"];
8097
+ var MEMBERSHIP_G = /(?:([A-Z])\s*(?:∈|trên|thuộc)|\b([A-Z])e\b)\s*([A-Z])([A-Z])(?![A-Z])/gu;
8098
+ var LIST_MEMBERSHIP_G = /([A-Z])\s*,\s*([A-Z])\s*(?:∈|trên|thuộc)\s*([A-Z])([A-Z])(?![A-Z])/gu;
8099
+ function parseMemberships(text) {
8100
+ const out = [];
8101
+ let m;
8102
+ LIST_MEMBERSHIP_G.lastIndex = 0;
8103
+ const listSpans = [];
8104
+ while ((m = LIST_MEMBERSHIP_G.exec(text)) !== null) {
8105
+ const side = [m[3], m[4]];
8106
+ out.push({ pt: m[1], side });
8107
+ out.push({ pt: m[2], side });
8108
+ listSpans.push([m.index, m.index + m[0].length]);
8109
+ }
8110
+ MEMBERSHIP_G.lastIndex = 0;
8111
+ while ((m = MEMBERSHIP_G.exec(text)) !== null) {
8112
+ if (listSpans.some(([a, b]) => m.index >= a && m.index < b)) continue;
8113
+ const pt = m[1] ?? m[2];
8114
+ out.push({ pt, side: [m[3], m[4]] });
8115
+ }
8116
+ return out;
8117
+ }
8118
+ var inscribedSquareRule = {
8119
+ id: "inscribed-square",
8120
+ priority: 56,
8121
+ languages: ["vi"],
8122
+ patterns: [PREFILTER59],
8123
+ match(ctx) {
8124
+ const out = [];
8125
+ for (const c of ctx.clauses) {
8126
+ const labelM = SQUARE_LABELS.exec(c.text);
8127
+ if (!labelM) continue;
8128
+ const labels = [labelM[1], labelM[2], labelM[3], labelM[4]];
8129
+ if (new Set(labels).size !== 4) continue;
8130
+ const memberships = parseMemberships(c.text);
8131
+ if (memberships.length < 4) continue;
8132
+ const bySide = /* @__PURE__ */ new Map();
8133
+ const ptSide = /* @__PURE__ */ new Map();
8134
+ for (const mem of memberships) {
8135
+ if (!labels.includes(mem.pt)) continue;
8136
+ ptSide.set(mem.pt, mem.side);
8137
+ const key = [...mem.side].sort().join("");
8138
+ const arr = bySide.get(key) ?? [];
8139
+ if (!arr.includes(mem.pt)) arr.push(mem.pt);
8140
+ bySide.set(key, arr);
8141
+ }
8142
+ if (labels.some((l) => !ptSide.has(l))) continue;
8143
+ const sideKeys = [...bySide.keys()];
8144
+ const hypKey = sideKeys.find((k) => (bySide.get(k) ?? []).length === 2);
8145
+ if (!hypKey) continue;
8146
+ const legKeys = sideKeys.filter((k) => k !== hypKey);
8147
+ if (legKeys.length !== 2) continue;
8148
+ if ((bySide.get(legKeys[0]) ?? []).length !== 1) continue;
8149
+ if ((bySide.get(legKeys[1]) ?? []).length !== 1) continue;
8150
+ const triVerts = /* @__PURE__ */ new Set();
8151
+ for (const [, side] of ptSide) side.forEach((v) => triVerts.add(v));
8152
+ if (triVerts.size !== 3) continue;
8153
+ const legVerts = legKeys.map((k) => k.split(""));
8154
+ const apex = legVerts[0].find((v) => legVerts[1].includes(v));
8155
+ if (!apex) continue;
8156
+ const triLabels = [apex, ...[...triVerts].filter((v) => v !== apex).sort()];
8157
+ const explicitRight = collectRightAt(ctx.clauses);
8158
+ if (explicitRight && explicitRight !== apex) continue;
8159
+ const tri = triangleCanonical("right-at-A");
8160
+ const params = inscribedSquareParams(tri[0], tri[1], tri[2]);
8161
+ const placement = assignSquarePlacement(
8162
+ labels,
8163
+ ptSide,
8164
+ bySide.get(hypKey),
8165
+ triLabels[0],
8166
+ // apex (đỉnh góc vuông)
8167
+ triLabels[1],
8168
+ // đỉnh B của tam giác (đầu mút cạnh huyền gần Q)
8169
+ triLabels[2],
8170
+ // đỉnh C của tam giác (đầu mút cạnh huyền gần P)
8171
+ params
8172
+ );
8173
+ if (!placement) continue;
8174
+ const rightIdx = triLabels.indexOf(apex);
8175
+ const variant = RIGHT_BY_IDX2[rightIdx] ?? "right-at-A";
8176
+ const intents = [drawShape("triangle", triLabels, variant)];
8177
+ for (const lbl of labels) {
8178
+ const { of, t } = placement.get(lbl);
8179
+ intents.push(addPoint(lbl, { kind: "onSegment", of, t }));
8180
+ }
8181
+ intents.push(markShape("quadrilateral", labels));
8182
+ const claimed = /* @__PURE__ */ new Set([c.id]);
8183
+ for (const cc of ctx.clauses) {
8184
+ const rm = RIGHT_AT2.exec(cc.text);
8185
+ if (rm && rm[1] === apex) claimed.add(cc.id);
8186
+ }
8187
+ out.push({ ruleId: "inscribed-square", clauseIds: [...claimed], intents });
8188
+ }
8189
+ return out;
8190
+ }
8191
+ };
8192
+ function collectRightAt(clauses) {
8193
+ for (const c of clauses) {
8194
+ const m = RIGHT_AT2.exec(c.text);
8195
+ if (m) return m[1];
8196
+ }
8197
+ return void 0;
8198
+ }
8199
+ function assignSquarePlacement(labels, ptSide, hypPts, triA, triB, triC, params) {
8200
+ const place = /* @__PURE__ */ new Map();
8201
+ const onAB = labels.find((l) => !hypPts.includes(l) && ptSide.get(l).includes(triB));
8202
+ const onAC = labels.find((l) => !hypPts.includes(l) && ptSide.get(l).includes(triC));
8203
+ if (!onAB || !onAC) return null;
8204
+ place.set(onAB, { of: `${triA}${triB}`, t: params.tM });
8205
+ place.set(onAC, { of: `${triA}${triC}`, t: params.tN });
8206
+ const neighborInHyp = (anchor) => {
8207
+ const i = labels.indexOf(anchor);
8208
+ const next = labels[(i + 1) % labels.length];
8209
+ const prev = labels[(i - 1 + labels.length) % labels.length];
8210
+ if (hypPts.includes(next)) return next;
8211
+ if (hypPts.includes(prev)) return prev;
8212
+ return void 0;
8213
+ };
8214
+ const qVtx = neighborInHyp(onAB);
8215
+ const pVtx = neighborInHyp(onAC);
8216
+ const hyp = `${triB}${triC}`;
8217
+ if (qVtx) place.set(qVtx, { of: hyp, t: params.tQ });
8218
+ if (pVtx) place.set(pVtx, { of: hyp, t: params.tP });
8219
+ for (const hp of hypPts) {
8220
+ if (!place.has(hp)) place.set(hp, { of: hyp, t: hp === qVtx ? params.tQ : params.tP });
8221
+ }
8222
+ return labels.every((l) => place.has(l)) ? place : null;
8223
+ }
8224
+
8225
+ // src/stamps/geometry-2d/ai/rules/parallelThroughCutsCircle.ts
8226
+ var DISTRIB4 = new RegExp(
8227
+ 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})`,
8228
+ "gu"
8229
+ );
8230
+ var DECL_NAMED = new RegExp(
8231
+ 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})`,
8232
+ "u"
8233
+ );
8234
+ var NAMED_CUT_BOTH = new RegExp(
8235
+ 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})`,
8236
+ "u"
8237
+ );
8238
+ 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;
8239
+ function lineNameOf2(through) {
8240
+ return "par" + through;
8241
+ }
8242
+ function resolveCircle4(problem) {
8243
+ const m = /đường\s*tròn\s*\(\s*([A-Z])\s*\)|\(\s*([A-Z])\s*\)/u.exec(problem);
8244
+ if (!m) return void 0;
8245
+ return m[1] ?? m[2];
8246
+ }
8247
+ function secondIntersection2(name, line, circle, other) {
8248
+ return addPoint(name, { kind: "secondIntersection", line, circle, other });
8249
+ }
8250
+ var parallelThroughCutsCircleRule = {
8251
+ id: "parallelThroughCutsCircle",
8252
+ priority: 46,
8253
+ languages: ["vi"],
8254
+ patterns: [PREFILTER60],
6996
8255
  match(ctx) {
6997
8256
  const circle = resolveCircle4(ctx.problem);
6998
8257
  if (!circle) return [];
@@ -7068,12 +8327,12 @@ var RE29 = new RegExp(
7068
8327
  "(?:[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])",
7069
8328
  "gu"
7070
8329
  );
7071
- 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];
8330
+ 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];
7072
8331
  var lineThroughCutsTwoLinesRule = {
7073
8332
  id: "lineThroughCutsTwoLines",
7074
8333
  priority: 44,
7075
8334
  languages: ["vi"],
7076
- patterns: PREFILTER59,
8335
+ patterns: PREFILTER61,
7077
8336
  match(ctx) {
7078
8337
  const out = [];
7079
8338
  for (const c of ctx.clauses) {
@@ -7151,54 +8410,651 @@ function detectSpec(problem) {
7151
8410
  a = x1;
7152
8411
  b = y1;
7153
8412
  }
7154
- if (vertex && a && b && a !== b) {
7155
- const onLine = findOnLine(problem, vertex);
7156
- if (onLine) return { vertex, a, b, onLine };
8413
+ if (vertex && a && b && a !== b) {
8414
+ const onLine = findOnLine(problem, vertex);
8415
+ if (onLine) return { vertex, a, b, onLine };
8416
+ }
8417
+ }
8418
+ return null;
8419
+ }
8420
+ var rightAngleViewingRule = {
8421
+ id: "rightAngleViewing",
8422
+ priority: 63,
8423
+ languages: ["vi"],
8424
+ patterns: [
8425
+ /(?:[Gg]óc|∠)[^.;\n]{0,18}90/u,
8426
+ /(?:[Gg]óc|∠)\s*[A-Z]{3}\s*vuông/u,
8427
+ /[A-Z]{2}\s*(?:⊥|vuông\s*góc)\s*[A-Z]{2}/u
8428
+ ],
8429
+ match(ctx) {
8430
+ const spec = detectSpec(ctx.problem);
8431
+ if (!spec) return [];
8432
+ const clauseIds = ctx.clauses.filter(
8433
+ (c) => ANGLE_EQ_RE.test(c.text) || ANGLE_VUONG_RE.test(c.text) || PERP_RE.test(c.text) || findOnLine(c.text, spec.vertex) !== null
8434
+ ).map((c) => c.id);
8435
+ const out = [
8436
+ {
8437
+ ruleId: "rightAngleViewing",
8438
+ clauseIds,
8439
+ intents: [
8440
+ addPoint(spec.vertex, {
8441
+ kind: "rightAngleViewing",
8442
+ a: spec.a,
8443
+ b: spec.b,
8444
+ onLine: spec.onLine
8445
+ })
8446
+ ]
8447
+ }
8448
+ ];
8449
+ return out;
8450
+ }
8451
+ };
8452
+
8453
+ // src/stamps/geometry-2d/ai/rules/collinearPoints.ts
8454
+ 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;
8455
+ var NAMES = "([A-Z](?:['\u2032])?(?:\\s*(?:,|v\xE0)\\s*[A-Z](?:['\u2032])?){2,})";
8456
+ var ON_LINE = 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+(?: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",
8458
+ "u"
8459
+ );
8460
+ var COLLINEAR = new RegExp(
8461
+ "(?:[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",
8462
+ "u"
8463
+ );
8464
+ function parseNames(blob) {
8465
+ return blob.split(/\s*(?:,|và)\s*/u).map((s) => s.trim().replace("\u2032", "'")).filter((s) => /^[A-Z]'?$/u.test(s));
8466
+ }
8467
+ var collinearPointsRule = {
8468
+ id: "collinear-points",
8469
+ priority: 95,
8470
+ // cao: dựng "bộ khung" điểm thẳng hàng sớm (như triangle/quad).
8471
+ languages: ["vi"],
8472
+ patterns: [PREFILTER62],
8473
+ match(ctx) {
8474
+ const out = [];
8475
+ for (const c of ctx.clauses) {
8476
+ const m = ON_LINE.exec(c.text) ?? COLLINEAR.exec(c.text);
8477
+ if (!m) continue;
8478
+ const names = parseNames(m[1]);
8479
+ if (names.length < 3) continue;
8480
+ if (new Set(names).size !== names.length) continue;
8481
+ const first = names[0];
8482
+ const last = names[names.length - 1];
8483
+ const span = 12;
8484
+ const intents = [
8485
+ addPoint(first, { kind: "free", at: [-span / 2, 0] }),
8486
+ addPoint(last, { kind: "free", at: [span / 2, 0] }),
8487
+ // Điểm giữa: onSegment trên đoạn đầu-cuối, t cách đều theo thứ tự liệt kê.
8488
+ ...names.slice(1, -1).map(
8489
+ (nm, i) => addPoint(nm, { kind: "onSegment", of: `${first}${last}`, t: (i + 1) / (names.length - 1) })
8490
+ ),
8491
+ connect(first, last, "segment")
8492
+ ];
8493
+ out.push({ ruleId: "collinear-points", clauseIds: [c.id], intents });
8494
+ }
8495
+ return out;
8496
+ }
8497
+ };
8498
+
8499
+ // src/stamps/geometry-2d/ai/rules/quadDiagonals.ts
8500
+ 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;
8501
+ var QUAD_DECL = /([A-Z])([A-Z])([A-Z])([A-Z])(?![A-Z])\s+là\s+(?:một\s+)?tứ\s*giác/u;
8502
+ var DIAGONALS = new RegExp(
8503
+ "(?: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])",
8504
+ "u"
8505
+ );
8506
+ var quadDiagonalsRule = {
8507
+ id: "quad-diagonals",
8508
+ priority: 99,
8509
+ // ~quad (100): vẽ tứ giác sớm; intersection (45) dựng E sau.
8510
+ languages: ["vi"],
8511
+ patterns: [PREFILTER63],
8512
+ match(ctx) {
8513
+ const out = [];
8514
+ for (const c of ctx.clauses) {
8515
+ const q = QUAD_DECL.exec(c.text);
8516
+ if (!q) continue;
8517
+ const labels = [q[1], q[2], q[3], q[4]];
8518
+ if (new Set(labels).size !== 4) continue;
8519
+ const intents = [drawShape("quadrilateral", labels, "any")];
8520
+ const d = DIAGONALS.exec(c.text);
8521
+ if (d) {
8522
+ const diag1 = d[1];
8523
+ const diag2 = d[2];
8524
+ const e = d[3];
8525
+ const ends = [diag1[0], diag1[1], diag2[0], diag2[1]];
8526
+ const okDiag = new Set(ends).size === 4 && ends.every((x) => labels.includes(x)) && !labels.includes(e);
8527
+ if (okDiag) {
8528
+ intents.push(connect(diag1[0], diag1[1], "segment"));
8529
+ intents.push(connect(diag2[0], diag2[1], "segment"));
8530
+ intents.push(addPoint(e, { kind: "intersection", of: [diag1, diag2] }));
8531
+ }
8532
+ }
8533
+ out.push({ ruleId: "quad-diagonals", clauseIds: [c.id], intents });
8534
+ }
8535
+ return out;
8536
+ }
8537
+ };
8538
+
8539
+ // src/stamps/geometry-2d/ai/rules/twoCirclesTangent.ts
8540
+ var PREFILTER64 = /hai\s+[Đđ]ư[ờơ]ng\s*tròn[^.]{0,70}?tiếp\s*xúc/u;
8541
+ var CIRCLE3 = "\\(\\s*([A-Z]['\u2032]?)(?:\\s*[;,]\\s*(?:[Rr]['\u2032]?|\\d+(?:[.,]\\d+)?\\s*[a-z]*))?\\s*\\)";
8542
+ var TWO_TANGENT = new RegExp(
8543
+ "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])",
8544
+ "u"
8545
+ );
8546
+ var norm3 = (s) => s.replace(/′/g, "'");
8547
+ var twoCirclesTangentRule = {
8548
+ id: "two-circles-tangent",
8549
+ // TRÊN chord/externalPoint, cạnh twoCirclesMeet (74): tiếp điểm A là gốc nhiều
8550
+ // phái sinh → dựng sớm.
8551
+ priority: 74,
8552
+ languages: ["vi"],
8553
+ patterns: [PREFILTER64],
8554
+ match(ctx) {
8555
+ const m = TWO_TANGENT.exec(ctx.problem);
8556
+ if (!m) return [];
8557
+ const c1 = norm3(m[1]);
8558
+ const c2 = norm3(m[2]);
8559
+ const mode = m[3];
8560
+ const a = m[4];
8561
+ if (mode === "trong") return [];
8562
+ if (c1 === c2) return [];
8563
+ if (a === c1 || a === c2) return [];
8564
+ const r2 = 2;
8565
+ const declId = ctx.clauses.find((c) => /tiếp\s*xúc/u.test(c.text))?.id;
8566
+ return [
8567
+ {
8568
+ ruleId: "two-circles-tangent",
8569
+ clauseIds: declId === void 0 ? [] : [declId],
8570
+ intents: [
8571
+ // 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.
8572
+ addPoint(a, { kind: "free", at: [0, 0] }),
8573
+ addPoint(c1, { kind: "free", at: [-3, 0] }),
8574
+ addPoint(c2, { kind: "free", at: [r2, 0] }),
8575
+ drawCircle(`${c1}_t`, "centerThrough", { center: c1, through: a }),
8576
+ drawCircle(`${c2}_t`, "centerThrough", { center: c2, through: a })
8577
+ ]
8578
+ }
8579
+ ];
8580
+ }
8581
+ };
8582
+
8583
+ // src/stamps/geometry-2d/ai/rules/commonTangent.ts
8584
+ var PREFILTER65 = /[Tt]iếp\s*tuyến\s+chung/u;
8585
+ var TANGENT_GLUE = /[Tt]iếp\s*tuyến\s+chung\s+(?:(?:ngoài|trong)\s+)?([A-Z])([A-Z])(?![A-Za-z])/u;
8586
+ var TANGENT_NAMED_BEFORE2 = /([A-Z])([A-Z])(?![A-Za-z])\s+là\s+tiếp\s*tuyến\s+chung/u;
8587
+ var INTERNAL = /tiếp\s*tuyến\s+chung\s+trong/u;
8588
+ var CIRCLE_G = /\(\s*([A-Z]['′]?)(?:\s*[;,]\s*(?:[Rr]['′]?|\d+(?:[.,]\d+)?\s*[a-z]*))?\s*\)/gu;
8589
+ var CIRC2 = "\\(\\s*([A-Z]['\u2032]?)(?:\\s*[;,]\\s*(?:[Rr]['\u2032]?|\\d+(?:[.,]\\d+)?\\s*[a-z]*))?\\s*\\)";
8590
+ var TANGENT_TOUCH = new RegExp(
8591
+ "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])",
8592
+ "u"
8593
+ );
8594
+ var norm4 = (s) => s.replace(/′/g, "'");
8595
+ function membership(problem, pt) {
8596
+ const re = new RegExp(
8597
+ 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]?)",
8598
+ "u"
8599
+ );
8600
+ const m = re.exec(problem);
8601
+ return m ? norm4(m[1]) : void 0;
8602
+ }
8603
+ var commonTangentRule = {
8604
+ id: "common-tangent",
8605
+ // Cạnh twoCirclesTangent (74): 2 đtròn + tiếp điểm là gốc nhiều phái sinh.
8606
+ priority: 73,
8607
+ languages: ["vi"],
8608
+ patterns: [PREFILTER65],
8609
+ match(ctx) {
8610
+ const p = ctx.problem;
8611
+ let t1;
8612
+ let t2;
8613
+ let c1;
8614
+ let c2;
8615
+ const mt = TANGENT_TOUCH.exec(p);
8616
+ if (mt) {
8617
+ c1 = norm4(mt[1]);
8618
+ c2 = norm4(mt[2]);
8619
+ t1 = norm4(mt[3]);
8620
+ t2 = norm4(mt[4]);
8621
+ } else {
8622
+ const mg = TANGENT_GLUE.exec(p);
8623
+ if (mg) {
8624
+ t1 = mg[1];
8625
+ t2 = mg[2];
8626
+ } else {
8627
+ const mb = TANGENT_NAMED_BEFORE2.exec(p);
8628
+ if (mb) {
8629
+ t1 = mb[1];
8630
+ t2 = mb[2];
8631
+ }
8632
+ }
8633
+ }
8634
+ if (!t1 || !t2 || t1 === t2) return [];
8635
+ const m1 = c1 ? void 0 : membership(p, t1);
8636
+ const m2 = c2 ? void 0 : membership(p, t2);
8637
+ if (c1 && c2) ; else if (m1 && m2 && m1 !== m2) {
8638
+ c1 = m1;
8639
+ c2 = m2;
8640
+ } else {
8641
+ const seen = [];
8642
+ CIRCLE_G.lastIndex = 0;
8643
+ let cm;
8644
+ while ((cm = CIRCLE_G.exec(p)) !== null) {
8645
+ const nm = norm4(cm[1]);
8646
+ if (!seen.includes(nm)) seen.push(nm);
8647
+ if (seen.length === 2) break;
8648
+ }
8649
+ if (seen.length === 2) {
8650
+ [c1, c2] = seen;
8651
+ }
8652
+ }
8653
+ if (!c1 || !c2 || c1 === c2) return [];
8654
+ if (t1 === c1 || t1 === c2 || t2 === c1 || t2 === c2) return [];
8655
+ const variant = INTERNAL.test(p) ? "internal" : "external";
8656
+ const r1 = 3;
8657
+ const r2 = 2;
8658
+ const declId = ctx.clauses.find((c) => /tiếp\s*tuyến\s+chung/u.test(c.text))?.id;
8659
+ const intents = [
8660
+ addPoint(c1, { kind: "free", at: [0, 0] }),
8661
+ addPoint(c2, { kind: "free", at: [12, 0] }),
8662
+ drawCircle(c1, "centerRadius", { center: c1, radius: r1 }),
8663
+ drawCircle(c2, "centerRadius", { center: c2, radius: r2 }),
8664
+ addPoint(t1, { kind: "commonTangentPoint", circles: [c1, c2], on: 0, variant, side: 0 }),
8665
+ addPoint(t2, { kind: "commonTangentPoint", circles: [c1, c2], on: 1, variant, side: 0 }),
8666
+ connect(t1, t2, "segment")
8667
+ ];
8668
+ return [
8669
+ {
8670
+ ruleId: "common-tangent",
8671
+ clauseIds: declId === void 0 ? [] : [declId],
8672
+ intents
8673
+ }
8674
+ ];
8675
+ }
8676
+ };
8677
+
8678
+ // src/stamps/geometry-2d/ai/rules/twoCirclesCenterRadiusMeet.ts
8679
+ var CENTER_RADIUS = new RegExp(
8680
+ "([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])",
8681
+ "gu"
8682
+ );
8683
+ var MEET = new RegExp(
8684
+ "[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])",
8685
+ "u"
8686
+ );
8687
+ var PREFILTER66 = /làm\s+tâm[^.]{0,40}?bán\s*kính/u;
8688
+ var twoCirclesCenterRadiusMeetRule = {
8689
+ id: "two-circles-center-radius-meet",
8690
+ // Cạnh twoCirclesMeet (74): 2 đtròn + giao điểm là gốc phái sinh.
8691
+ priority: 73,
8692
+ languages: ["vi"],
8693
+ patterns: [PREFILTER66],
8694
+ match(ctx) {
8695
+ const p = ctx.problem;
8696
+ CENTER_RADIUS.lastIndex = 0;
8697
+ const circles = [];
8698
+ let cm;
8699
+ while ((cm = CENTER_RADIUS.exec(p)) !== null) {
8700
+ const center = cm[1];
8701
+ const a = cm[2];
8702
+ const b = cm[3];
8703
+ if (a !== center || b === center) continue;
8704
+ circles.push({ center, through: b });
8705
+ }
8706
+ if (circles.length !== 2) return [];
8707
+ const [k1, k2] = circles;
8708
+ if (k1.center === k2.center) return [];
8709
+ if (k1.through !== k2.through) return [];
8710
+ const mm = MEET.exec(p);
8711
+ if (!mm) return [];
8712
+ const d = mm[1];
8713
+ if (d === k1.center || d === k2.center || d === k1.through) return [];
8714
+ 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);
8715
+ const intents = [
8716
+ drawCircle(k1.center, "centerThrough", { center: k1.center, through: k1.through }),
8717
+ drawCircle(k2.center, "centerThrough", { center: k2.center, through: k2.through }),
8718
+ addPoint(d, { kind: "circleIntersection", c1: k1.center, c2: k2.center, which: 1 })
8719
+ ];
8720
+ return [
8721
+ {
8722
+ ruleId: "two-circles-center-radius-meet",
8723
+ clauseIds,
8724
+ intents
8725
+ }
8726
+ ];
8727
+ }
8728
+ };
8729
+
8730
+ // src/stamps/geometry-2d/ai/rules/rightAngleVertexCircles.ts
8731
+ var PREFILTER67 = /góc\s+vuông\s+[a-z][A-Z][a-z]/u;
8732
+ var RIGHT_ANGLE = /góc\s+vuông\s+([a-z])([A-Z])([a-z])(?![\p{L}])/u;
8733
+ var TWO_ON_RAYS = new RegExp(
8734
+ "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]",
8735
+ "u"
8736
+ );
8737
+ var CIRCLE_CUTS_RAY = new RegExp(
8738
+ "[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])",
8739
+ "gu"
8740
+ );
8741
+ var rightAngleVertexCirclesRule = {
8742
+ id: "right-angle-vertex-circles",
8743
+ priority: 76,
8744
+ // dựng bộ khung góc + điểm sớm (trên twoCirclesMeet/tangent 74).
8745
+ languages: ["vi"],
8746
+ patterns: [PREFILTER67],
8747
+ match(ctx) {
8748
+ const ra = RIGHT_ANGLE.exec(ctx.problem);
8749
+ if (!ra) return [];
8750
+ const o = ra[2];
8751
+ const xRay = ra[1];
8752
+ const yRay = ra[3];
8753
+ let pI = "";
8754
+ let pK = "";
8755
+ for (const c of ctx.clauses) {
8756
+ const t = TWO_ON_RAYS.exec(c.text);
8757
+ if (t) {
8758
+ pI = t[1];
8759
+ pK = t[2];
8760
+ break;
8761
+ }
8762
+ }
8763
+ if (!pI || !pK || pI === pK || pI === o || pK === o) return [];
8764
+ const xEnd = xRay.toUpperCase() === o ? `${o}x` : xRay.toUpperCase();
8765
+ const yEnd = yRay.toUpperCase() === o ? `${o}y` : yRay.toUpperCase();
8766
+ const used = /* @__PURE__ */ new Set([o, pI, pK]);
8767
+ const ax = used.has(xEnd) ? "X9" : xEnd;
8768
+ const ay = used.has(yEnd) ? "Y9" : yEnd;
8769
+ const intents = [
8770
+ addPoint(o, { kind: "free", at: [0, 0] }),
8771
+ addPoint(ax, { kind: "free", at: [8, 0] }),
8772
+ addPoint(ay, { kind: "free", at: [0, 8] }),
8773
+ addPoint(pI, { kind: "onSegment", of: `${o}${ax}`, t: 0.35 }),
8774
+ addPoint(pK, { kind: "onSegment", of: `${o}${ay}`, t: 0.35 }),
8775
+ connect(o, ax, "segment"),
8776
+ connect(o, ay, "segment")
8777
+ ];
8778
+ for (const c of ctx.clauses) {
8779
+ CIRCLE_CUTS_RAY.lastIndex = 0;
8780
+ for (const m of c.text.matchAll(CIRCLE_CUTS_RAY)) {
8781
+ const center = m[1];
8782
+ const rayVertex = m[4];
8783
+ const rayDir = m[5];
8784
+ const hit = m[6];
8785
+ if (![pI, pK, o].includes(center)) continue;
8786
+ if (hit === center || hit === o) continue;
8787
+ const rayAxisEnd = rayDir.toUpperCase() === xRay.toUpperCase() ? ax : rayDir.toUpperCase() === yRay.toUpperCase() ? ay : "";
8788
+ if (!rayAxisEnd || rayVertex !== o) continue;
8789
+ const otherCenter = center === pI ? pK : pI;
8790
+ const circ = `${center}_c`;
8791
+ intents.push(drawCircle(circ, "centerThrough", { center, through: otherCenter }));
8792
+ intents.push(
8793
+ addPoint(hit, { kind: "secondIntersection", line: `${o}${rayAxisEnd}`, circle: circ, other: o })
8794
+ );
8795
+ }
8796
+ }
8797
+ return [{ ruleId: "right-angle-vertex-circles", clauseIds: ctx.clauses.map((c) => c.id), intents }];
8798
+ }
8799
+ };
8800
+
8801
+ // src/stamps/geometry-2d/ai/rules/namedLine.ts
8802
+ var LC = String.raw`([a-z]{1,2}[0-9]?)(?!\p{L})`;
8803
+ 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])`;
8804
+ var PT3 = String.raw`([A-Z])(?![A-Za-z])`;
8805
+ var VERB = String.raw`(?:[Cc]ho|[Vv]ẽ|[Kk]ẻ|[Dd]ựng)\s+(?:một\s+)?${DUONG_KW}\s*thẳng\s+`;
8806
+ var DECL = String.raw`(?:(?:và|[Cc]ho|[Vv]ẽ|[Kk]ẻ|[Dd]ựng)\s+)?(?:một\s+)?${DUONG_KW}\s*thẳng\s+`;
8807
+ var PERP = new RegExp(
8808
+ 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,
8809
+ "gu"
8810
+ );
8811
+ var PARA_AFTER = new RegExp(
8812
+ 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,
8813
+ "gu"
8814
+ );
8815
+ var PARA_BEFORE = new RegExp(
8816
+ 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,
8817
+ "gu"
8818
+ );
8819
+ var THRU_TWO = new RegExp(
8820
+ VERB + LC + String.raw`\s+(?:đi\s+)?qua\s+(?:hai\s+điểm\s+)?` + PT3 + String.raw`\s+và\s+(?:điểm\s+)?` + PT3,
8821
+ "gu"
8822
+ );
8823
+ var SECANT_LINE = new RegExp(
8824
+ 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,
8825
+ "gu"
8826
+ );
8827
+ var FREE_DECL = new RegExp(
8828
+ 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,
8829
+ // tiếp xúc. Cho phép ranh giới câu / "và" / "," / "(".
8830
+ String.raw`(?!\s*(?:⊥|//|vuông\s*góc|song\s*song|(?:đi\s+)?qua|cắt|tiếp\s*xúc|tại|ở)\b)`,
8831
+ "gu"
8832
+ );
8833
+ function lineReferenced(problem, name) {
8834
+ const esc = escapeRe(name);
8835
+ const N = String.raw`(?<![\p{L}\d])${esc}(?![\p{L}\d])`;
8836
+ const REFS = [
8837
+ // trước "d": "⊥ d", "song song d", "thuộc/trên/nằm trên d", "cắt … d", "của d"
8838
+ 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"),
8839
+ // sau "d": "d cắt", "d tại", "d lấy", "d, "
8840
+ new RegExp(N + String.raw`\s+(?:cắt|tại|ở|lấy)`, "u")
8841
+ ];
8842
+ return REFS.some((re) => re.test(problem));
8843
+ }
8844
+ var PREFILTER68 = new RegExp(
8845
+ String.raw`${DUONG_KW}\s*thẳng\s+[a-z]{1,2}[0-9]?(?![\p{L}])`,
8846
+ "u"
8847
+ );
8848
+ var namedLineRule = {
8849
+ id: "named-line",
8850
+ // Cao (63) như tangentLineNamedAtPoint — đường <d> phải dựng TRƯỚC điểm-trên-d
8851
+ // / intersection tham chiếu d.
8852
+ priority: 63,
8853
+ languages: ["vi"],
8854
+ patterns: [PREFILTER68],
8855
+ match(ctx) {
8856
+ const out = [];
8857
+ const push = (clauseId, intents) => {
8858
+ out.push({ ruleId: "named-line", clauseIds: [clauseId], intents });
8859
+ };
8860
+ for (const c of ctx.clauses) {
8861
+ const seen = /* @__PURE__ */ new Set();
8862
+ PERP.lastIndex = 0;
8863
+ for (const m of c.text.matchAll(PERP)) {
8864
+ const line = m[1];
8865
+ const to = m[2].replace(/\s+/g, "");
8866
+ const anchor = m[3];
8867
+ if (seen.has(line)) continue;
8868
+ seen.add(line);
8869
+ push(c.id, [drawLine(line, "perpThrough", { through: anchor, to })]);
8870
+ }
8871
+ PARA_AFTER.lastIndex = 0;
8872
+ for (const m of c.text.matchAll(PARA_AFTER)) {
8873
+ const line = m[1];
8874
+ const to = m[2].replace(/\s+/g, "");
8875
+ const anchor = m[3];
8876
+ if (seen.has(line) || to.includes(anchor)) continue;
8877
+ seen.add(line);
8878
+ push(c.id, [drawLine(line, "parallelThrough", { through: anchor, to })]);
8879
+ }
8880
+ PARA_BEFORE.lastIndex = 0;
8881
+ for (const m of c.text.matchAll(PARA_BEFORE)) {
8882
+ const anchor = m[1];
8883
+ const line = m[2];
8884
+ const to = m[3].replace(/\s+/g, "");
8885
+ if (seen.has(line) || to.includes(anchor)) continue;
8886
+ seen.add(line);
8887
+ push(c.id, [drawLine(line, "parallelThrough", { through: anchor, to })]);
8888
+ }
8889
+ THRU_TWO.lastIndex = 0;
8890
+ for (const m of c.text.matchAll(THRU_TWO)) {
8891
+ const line = m[1];
8892
+ const a = m[2];
8893
+ const b = m[3];
8894
+ if (seen.has(line) || a === b) continue;
8895
+ seen.add(line);
8896
+ push(c.id, [drawLine(line, "lineThrough", { points: [a, b] })]);
8897
+ }
8898
+ SECANT_LINE.lastIndex = 0;
8899
+ for (const m of c.text.matchAll(SECANT_LINE)) {
8900
+ const line = m[1];
8901
+ const a = m[2];
8902
+ const b = m[3];
8903
+ if (seen.has(line) || a === b) continue;
8904
+ seen.add(line);
8905
+ push(c.id, [drawLine(line, "lineThrough", { points: [a, b] })]);
8906
+ }
8907
+ FREE_DECL.lastIndex = 0;
8908
+ for (const m of c.text.matchAll(FREE_DECL)) {
8909
+ const line = m[1];
8910
+ if (seen.has(line) || !lineReferenced(ctx.problem, line)) continue;
8911
+ seen.add(line);
8912
+ const p1 = `${line}_p1`;
8913
+ const p2 = `${line}_p2`;
8914
+ push(c.id, [
8915
+ addPoint(p1, { kind: "free" }),
8916
+ addPoint(p2, { kind: "free" }),
8917
+ drawLine(line, "lineThrough", { points: [p1, p2] })
8918
+ ]);
8919
+ }
8920
+ }
8921
+ return out;
8922
+ }
8923
+ };
8924
+
8925
+ // src/stamps/geometry-2d/ai/rules/bisectorsMeet.ts
8926
+ var ANG_HEAD = "(?:\u2220\\s*|g\xF3c\\s+)([A-Z])([A-Z])([A-Z])(?![A-Z])";
8927
+ var ANG_TAIL = "(?:\u2220\\s*|g\xF3c\\s+)?([A-Z])([A-Z])([A-Z])(?![A-Z])";
8928
+ var RE30 = new RegExp(
8929
+ "[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])",
8930
+ "gu"
8931
+ );
8932
+ var PREFILTER69 = /[Pp]hân\s*giác[^.]{0,80}?(?:cắt|giao)\s+nhau\s+(?:tại|ở)/u;
8933
+ var bisectorsMeetRule = {
8934
+ id: "bisectorsMeet",
8935
+ // Trên intersection (45) để các tia phân giác (draw-line) + điểm giao gom cùng
8936
+ // 1 match; intent-layer order-retry topo lo thứ tự build. 49 = ngang
8937
+ // twoPerpLinesMeet (cùng họ "2 đường đặt-tên giao nhau").
8938
+ priority: 49,
8939
+ languages: ["vi"],
8940
+ patterns: [PREFILTER69],
8941
+ match(ctx) {
8942
+ const out = [];
8943
+ for (const c of ctx.clauses) {
8944
+ RE30.lastIndex = 0;
8945
+ for (const m of c.text.matchAll(RE30)) {
8946
+ const a1 = [m[1], m[2], m[3]];
8947
+ const a2 = [m[4], m[5], m[6]];
8948
+ const p = m[7];
8949
+ const n1 = `bis${a1[0]}${a1[1]}${a1[2]}`;
8950
+ const n2 = `bis${a2[0]}${a2[1]}${a2[2]}`;
8951
+ const intents = [
8952
+ drawLine(n1, "angleBisector", { p1: a1[0], vertex: a1[1], p2: a1[2] }),
8953
+ drawLine(n2, "angleBisector", { p1: a2[0], vertex: a2[1], p2: a2[2] })
8954
+ ];
8955
+ const angleLetters = /* @__PURE__ */ new Set([...a1, ...a2]);
8956
+ if (n1 !== n2 && !angleLetters.has(p)) {
8957
+ intents.push(addPoint(p, { kind: "intersection", of: [n1, n2] }));
8958
+ }
8959
+ out.push({ ruleId: "bisectorsMeet", clauseIds: [c.id], intents });
8960
+ }
7157
8961
  }
8962
+ return out;
7158
8963
  }
7159
- return null;
8964
+ };
8965
+
8966
+ // src/stamps/geometry-2d/ai/rules/hexagon.ts
8967
+ var HEX = "([A-Z])([A-Z])([A-Z])([A-Z])([A-Z])([A-Z])(?![A-Z])";
8968
+ var HEXAGON_DECL = new RegExp("[Ll]\u1EE5c\\s+gi\xE1c\\s+" + HEX, "gu");
8969
+ var PREFILTER70 = /[Ll]ục\s+giác/u;
8970
+ var HEX_RADIUS = 5;
8971
+ var HEX_APOTHEM = HEX_RADIUS * Math.cos(Math.PI / 6);
8972
+ var HEX_THETAS = Array.from(
8973
+ { length: 6 },
8974
+ (_, i) => Math.PI / 2 - i * Math.PI / 3
8975
+ );
8976
+ function hexVertexCoord(i) {
8977
+ const t = HEX_THETAS[i];
8978
+ return [
8979
+ +(HEX_RADIUS * Math.cos(t)).toFixed(4),
8980
+ +(HEX_RADIUS * Math.sin(t)).toFixed(4)
8981
+ ];
7160
8982
  }
7161
- var rightAngleViewingRule = {
7162
- id: "rightAngleViewing",
7163
- priority: 63,
8983
+ var CENTER4 = "(?:\\(\\s*([A-Z])\\s*\\)|t\xE2m\\s+([A-Z]))";
8984
+ var HEX_CIRCUMSCRIBES = new RegExp(
8985
+ "^[\\s,]*ngo\u1EA1i\\s*ti\u1EBFp\\s+(?:(?:" + CIRCLE_KW + ")\\s*)?" + CENTER4,
8986
+ "u"
8987
+ );
8988
+ var HEX_INSCRIBED_BY = new RegExp(
8989
+ "(?:" + CIRCLE_KW + ")\\s*" + CENTER4 + "\\s*n\u1ED9i\\s*ti\u1EBFp\\s+$",
8990
+ "u"
8991
+ );
8992
+ function detectInscribedCircle(text, start, afterEnd) {
8993
+ const a = HEX_CIRCUMSCRIBES.exec(text.slice(afterEnd));
8994
+ if (a) return a[1] ?? a[2] ?? "O";
8995
+ const b = HEX_INSCRIBED_BY.exec(text.slice(0, start));
8996
+ if (b) return b[1] ?? b[2] ?? "O";
8997
+ return void 0;
8998
+ }
8999
+ var hexagonRule = {
9000
+ id: "hexagon",
9001
+ priority: 100,
9002
+ // ngang quad (đa giác cơ sở) — gate riêng prefilter "lục giác".
7164
9003
  languages: ["vi"],
7165
- patterns: [
7166
- /(?:[Gg]óc|∠)[^.;\n]{0,18}90/u,
7167
- /(?:[Gg]óc|∠)\s*[A-Z]{3}\s*vuông/u,
7168
- /[A-Z]{2}\s*(?:⊥|vuông\s*góc)\s*[A-Z]{2}/u
7169
- ],
9004
+ patterns: [PREFILTER70],
7170
9005
  match(ctx) {
7171
- const spec = detectSpec(ctx.problem);
7172
- if (!spec) return [];
7173
- const clauseIds = ctx.clauses.filter(
7174
- (c) => ANGLE_EQ_RE.test(c.text) || ANGLE_VUONG_RE.test(c.text) || PERP_RE.test(c.text) || findOnLine(c.text, spec.vertex) !== null
7175
- ).map((c) => c.id);
7176
- const out = [
7177
- {
7178
- ruleId: "rightAngleViewing",
7179
- clauseIds,
7180
- intents: [
7181
- addPoint(spec.vertex, {
7182
- kind: "rightAngleViewing",
7183
- a: spec.a,
7184
- b: spec.b,
7185
- onLine: spec.onLine
7186
- })
7187
- ]
9006
+ const out = [];
9007
+ for (const c of ctx.clauses) {
9008
+ HEXAGON_DECL.lastIndex = 0;
9009
+ for (const m of c.text.matchAll(HEXAGON_DECL)) {
9010
+ const labels = [m[1], m[2], m[3], m[4], m[5], m[6]];
9011
+ const start = m.index ?? 0;
9012
+ const afterEnd = start + m[0].length;
9013
+ const vertices = labels.map(
9014
+ (lbl, i) => addPoint(lbl, { kind: "free", at: hexVertexCoord(i) })
9015
+ );
9016
+ const center = detectInscribedCircle(c.text, start, afterEnd);
9017
+ const intents = [...vertices];
9018
+ if (center !== void 0) {
9019
+ const centerName = escapeRe(center) === center ? center : "O";
9020
+ intents.unshift(
9021
+ addPoint(centerName, {
9022
+ kind: "circumcenter",
9023
+ of: [labels[0], labels[2], labels[4]]
9024
+ })
9025
+ );
9026
+ intents.push(
9027
+ drawCircle(centerName, "centerRadius", {
9028
+ center: centerName,
9029
+ radius: HEX_APOTHEM
9030
+ })
9031
+ );
9032
+ }
9033
+ intents.push(markShape("polygon", labels));
9034
+ out.push({ ruleId: "hexagon", clauseIds: [c.id], intents });
7188
9035
  }
7189
- ];
9036
+ }
7190
9037
  return out;
7191
9038
  }
7192
9039
  };
7193
9040
 
7194
9041
  // src/stamps/geometry-2d/ai/rules/registry.ts
7195
9042
  var RULES = [
9043
+ namedLineRule,
9044
+ collinearPointsRule,
9045
+ quadDiagonalsRule,
9046
+ twoCirclesTangentRule,
9047
+ commonTangentRule,
9048
+ twoCirclesCenterRadiusMeetRule,
9049
+ rightAngleVertexCirclesRule,
7196
9050
  circumcircleCutsLineRule,
7197
9051
  lineConcurrencyRule,
7198
9052
  parallelThroughCutsCircleRule,
7199
9053
  lineThroughCutsTwoLinesRule,
7200
9054
  rightAngleViewingRule,
7201
9055
  triangleRule,
9056
+ inscribedSquareRule,
9057
+ hexagonRule,
7202
9058
  quadRule,
7203
9059
  connectRule,
7204
9060
  midpointRule,
@@ -7211,6 +9067,7 @@ var RULES = [
7211
9067
  circleTriangleRule,
7212
9068
  tangentFromExtRule,
7213
9069
  externalPointRule,
9070
+ externalPointAtRadiusRule,
7214
9071
  arcMidpointRule,
7215
9072
  excenterRule,
7216
9073
  parallelPerpRule,
@@ -7250,6 +9107,7 @@ var RULES = [
7250
9107
  tangentAtCutsLinesRule,
7251
9108
  diameterEndpointRule,
7252
9109
  circleThroughTwoCutsSidesRule,
9110
+ circleCenterRadiusCutsSidesRule,
7253
9111
  circumcircleNamedDistribRule,
7254
9112
  circumcirclePairMeetRule,
7255
9113
  tangentCircleAtPointRule,
@@ -7278,7 +9136,8 @@ var RULES = [
7278
9136
  twoCirclesMeetRule,
7279
9137
  radiusRule,
7280
9138
  perpNamedLineRule,
7281
- parallelSidePointsRule
9139
+ parallelSidePointsRule,
9140
+ bisectorsMeetRule
7282
9141
  ];
7283
9142
  var ALL_RULES = [...RULES].sort(
7284
9143
  (a, b) => b.priority - a.priority
@@ -7293,20 +9152,329 @@ function runRules(ctx) {
7293
9152
  }
7294
9153
 
7295
9154
  // src/stamps/geometry-2d/ai/deterministic/normalizeText.ts
7296
- var TRIANGLE_SYMBOL = /[Δ∆]\s*(?=[A-Z])/gu;
9155
+ var TRIANGLE_SYMBOL = /[Δ∆]\s*(?=[A-Z])/gu;
7297
9156
  var CIRCLE_SYNONYM = /vòng\s+tròn/giu;
9157
+ var CIRCLE_PAREN_RESTORE = /(đường\s*tròn\s+)([A-Z])(['′]?)\s*;\s*([Rr])(['′]?)(?![A-Za-z'′])/gu;
7298
9158
  var QUAD_SYMBOL = /[◊▱□]\s*(?=[A-Z])/gu;
9159
+ var MIDPOINT_TYPO = /trung\s*điển(?!\p{L})/gu;
7299
9160
  var SQRT_NOISE = /√/gu;
7300
9161
  var PRIME_VARIANT = /([A-Za-z0-9])[’′´]/gu;
9162
+ var DIAERESIS_O = /(?<!\p{Ll})Ö(?!\p{Ll})/gu;
9163
+ var INTERSECT_PAREN = /(\([A-Z]{3}\))\s+N\s+([A-Z]{2})(\s*=\s*\{)/gu;
7301
9164
  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;
7302
9165
  var GLUE_LABEL_WORD = /([A-Z])(sao|nằm|lần|thuộc|cắt|của|đến|trên|tại|và)/gu;
9166
+ var GLUE_RAYTOKEN_WORD = /([A-Z][a-z])(tại|cắt|và|của|đến|trên|thuộc)(?![\p{L}])/gu;
9167
+ var GLUE_VOCAB = [
9168
+ "\u0111\u01B0\u1EDDng",
9169
+ "ngo\u1EA1i",
9170
+ "trung",
9171
+ "ph\xE2n",
9172
+ "\u0111i\u1EC3m",
9173
+ "gi\xE1c",
9174
+ "tr\xF2n",
9175
+ "c\u1EA1nh",
9176
+ "k\xEDnh",
9177
+ "n\u1ED9i",
9178
+ "ti\u1EBFp",
9179
+ "cung",
9180
+ "d\xE2y",
9181
+ "tam",
9182
+ "t\u1EE9",
9183
+ "g\xF3c",
9184
+ "vu\xF4ng",
9185
+ "c\xE2n",
9186
+ "nh\u1ECDn",
9187
+ "n\u1EEDa",
9188
+ "\u0111o\u1EA1n",
9189
+ "h\xECnh",
9190
+ "qua",
9191
+ "c\u1EAFt",
9192
+ "l\u1EA1i",
9193
+ "c\xE1c",
9194
+ "v\xE0",
9195
+ "t\u1EA1i",
9196
+ "m\u1ED9t",
9197
+ "ba",
9198
+ "b\u1EA5t",
9199
+ "k\xEC",
9200
+ "k\u1EF3",
9201
+ "c\xF3",
9202
+ "\u0111i",
9203
+ "v\u1EDBi",
9204
+ "l\u1EA5y",
9205
+ "c\u1EE7a",
9206
+ "l\u1EA7n",
9207
+ "l\u01B0\u1EE3t",
9208
+ "th\u1EE9",
9209
+ "hai",
9210
+ "g\u1ECDi",
9211
+ "k\u1EBB",
9212
+ "v\u1EBD",
9213
+ "cho",
9214
+ "t\xE2m",
9215
+ "b\xE1n",
9216
+ // 'là' (hệ từ) + từ-vựng hình-chiếu/⊥ — tách glue "làtrung"/"làhình"/
9217
+ // "chiếucủa"/"xuốngđường" (OCR HHP dày). Whitelist↔whitelist nên 'là' chỉ tách
9218
+ // khi theo NGAY bởi một từ-khoá hình-học khác (an toàn, regression-gated).
9219
+ "l\xE0",
9220
+ "chi\u1EBFu",
9221
+ "xu\u1ED1ng",
9222
+ "l\xEAn",
9223
+ "\u0111\u1ED1i",
9224
+ "x\u1EE9ng",
9225
+ "song",
9226
+ "th\u1EB3ng"
9227
+ ];
9228
+ var GLUE_VOCAB_SORTED = [...GLUE_VOCAB].sort((a, b) => b.length - a.length);
9229
+ var GLUE_ALT = GLUE_VOCAB_SORTED.join("|");
9230
+ var GLUE_VOCAB_RE = new RegExp(`(${GLUE_ALT})(?=${GLUE_ALT})`, "gu");
9231
+ var GLUE_VOCAB_LABEL = new RegExp(
9232
+ `(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])`,
9233
+ "gu"
9234
+ );
9235
+ 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;
9236
+ var GLUE_LABEL_VOCAB = new RegExp(`([A-Z])(${GLUE_ALT})(?![\\p{L}])`, "gu");
9237
+ var GLUE_PAREN_OPEN = /(\p{Ll})\(/gu;
9238
+ var GLUE_PAREN_CLOSE = /\)(\p{L})/gu;
9239
+ function deglueMultiWord(s) {
9240
+ let out = s.replace(CAP_GLUE, "$1 $2");
9241
+ for (let k = 0; k < 8; k++) {
9242
+ const next = out.replace(GLUE_VOCAB_RE, "$1 ");
9243
+ if (next === out) break;
9244
+ out = next;
9245
+ }
9246
+ out = out.replace(GLUE_VOCAB_LABEL, "$1 $2").replace(GLUE_LABEL_VOCAB, "$1 $2").replace(GLUE_PAREN_OPEN, "$1 (").replace(GLUE_PAREN_CLOSE, ") $1");
9247
+ return out;
9248
+ }
7303
9249
  function normalizeProblemText(problem) {
7304
- 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");
9250
+ 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");
9251
+ if (!/\(\s*[A-Z]/u.test(s)) s = s.replace(CIRCLE_PAREN_RESTORE, "$1($2$3;$4$5)");
9252
+ return deglueMultiWord(
9253
+ 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")
9254
+ );
9255
+ }
9256
+
9257
+ // src/stamps/geometry-2d/ai/deterministic/correctUserInput.ts
9258
+ function foldVietnamese(s) {
9259
+ return s.toLowerCase().replace(/đ/g, "d").normalize("NFD").replace(/[̀-ͯ]/g, "");
9260
+ }
9261
+ function levenshtein(a, b) {
9262
+ const m = a.length;
9263
+ const n = b.length;
9264
+ if (m === 0) return n;
9265
+ if (n === 0) return m;
9266
+ let prev = new Array(n + 1);
9267
+ for (let j = 0; j <= n; j++) prev[j] = j;
9268
+ for (let i = 1; i <= m; i++) {
9269
+ const cur = new Array(n + 1);
9270
+ cur[0] = i;
9271
+ for (let j = 1; j <= n; j++) {
9272
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
9273
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);
9274
+ }
9275
+ prev = cur;
9276
+ }
9277
+ return prev[n];
9278
+ }
9279
+ var CORRECTION_VOCAB = [
9280
+ // circle / line core
9281
+ "\u0111\u01B0\u1EDDng",
9282
+ "tr\xF2n",
9283
+ "b\xE1n",
9284
+ "k\xEDnh",
9285
+ "v\xF2ng",
9286
+ // polygon / triangle (TAM before TÂM, THANG before THẲNG — see collision-winner warning above)
9287
+ "tam",
9288
+ "t\xE2m",
9289
+ "thang",
9290
+ "th\u1EB3ng",
9291
+ "gi\xE1c",
9292
+ "t\u1EE9",
9293
+ "g\xF3c",
9294
+ "c\u1EA1nh",
9295
+ "\u0111o\u1EA1n",
9296
+ "h\xECnh",
9297
+ "ch\u1EEF",
9298
+ "nh\u1EADt",
9299
+ "vu\xF4ng",
9300
+ "c\xE2n",
9301
+ "nh\u1ECDn",
9302
+ "\u0111\u1EC1u",
9303
+ "thoi",
9304
+ "b\xECnh",
9305
+ "h\xE0nh",
9306
+ // chord / arc / point
9307
+ "d\xE2y",
9308
+ "cung",
9309
+ "\u0111i\u1EC3m",
9310
+ "tia",
9311
+ "n\u1EEDa",
9312
+ // tangent / inscribed
9313
+ "ti\u1EBFp",
9314
+ "tuy\u1EBFn",
9315
+ "n\u1ED9i",
9316
+ "ngo\u1EA1i",
9317
+ "x\xFAc",
9318
+ // cevians / projection
9319
+ "trung",
9320
+ "tr\u1EF1c",
9321
+ "cao",
9322
+ "ph\xE2n",
9323
+ "chi\u1EBFu",
9324
+ "\u0111\u1ED1i",
9325
+ "x\u1EE9ng",
9326
+ // verbs / connectors phổ biến trong đề
9327
+ "cho",
9328
+ "g\u1ECDi",
9329
+ "v\u1EBD",
9330
+ "k\u1EBB",
9331
+ "l\u1EA5y",
9332
+ "qua",
9333
+ "c\u1EAFt",
9334
+ "tr\xEAn",
9335
+ "d\u01B0\u1EDBi",
9336
+ "\u0111\u1EBFn",
9337
+ // 'đi' ĐÃ GỠ: "di"→"đi" net-harmful trên corpus (regress 7 bài locus "di động"
9338
+ // → "đi động"). Từ TRẦN 2 ký tự đa-nghĩa: rủi ro cao, lợi ích thấp.
9339
+ "song",
9340
+ "v\u1EDBi",
9341
+ "c\u1EE7a",
9342
+ "v\xE0",
9343
+ "l\xE0",
9344
+ "c\xF3",
9345
+ "n\u1EB1m",
9346
+ "thu\u1ED9c",
9347
+ "t\u1EA1i",
9348
+ "m\u1ED9t",
9349
+ "hai",
9350
+ "ba",
9351
+ "c\xE1c",
9352
+ "l\u1EA7n",
9353
+ "l\u01B0\u1EE3t",
9354
+ "th\u1EE9",
9355
+ "b\u1EA5t",
9356
+ "k\xEC",
9357
+ "k\u1EF3",
9358
+ "trong",
9359
+ "ngo\xE0i",
9360
+ "l\xEAn",
9361
+ "xu\u1ED1ng"
9362
+ ];
9363
+ var FOLDED_VOCAB = (() => {
9364
+ const m = /* @__PURE__ */ new Map();
9365
+ for (const w of CORRECTION_VOCAB) {
9366
+ const f = foldVietnamese(w);
9367
+ if (!m.has(f)) m.set(f, w);
9368
+ }
9369
+ return m;
9370
+ })();
9371
+ var LETTERS_ONLY = /^\p{L}+$/u;
9372
+ var UPPER_LABEL = /^\p{Lu}{1,4}$/u;
9373
+ var HAS_UPPER = /\p{Lu}/u;
9374
+ function classifyToken(token) {
9375
+ if (!LETTERS_ONLY.test(token)) return "protected";
9376
+ if (UPPER_LABEL.test(token)) return "protected";
9377
+ return HAS_UPPER.test(token) ? "upper" : "lower";
9378
+ }
9379
+ var DEFAULT_CORRECT_CONFIG = {
9380
+ structure: true,
9381
+ accents: true,
9382
+ // Tầng 3 (fuzzy typo) MẶC ĐỊNH TẮT: trên corpus thật nó băm từ TRẦN hợp lệ
9383
+ // ("nhau"→"nhật", "giao"→"giác", "di"→"đi") vì vocab hình-học trùng không-gian
9384
+ // âm với văn xuôi Việt → net-harmful (đo qua diag-all). Vẫn cài đặt đầy đủ + bật
9385
+ // được qua flag (mutation test / opt-in tương lai). accents+structure đủ an toàn.
9386
+ typo: false,
9387
+ maxTypoDistance: 1,
9388
+ minTypoLen: 4
9389
+ };
9390
+ var SYMBOL_MAP = [
9391
+ // "độ" có dấu = độ-góc rõ ràng → luôn đổi. "do" trần mơ hồ (do đó/do dai/lý do)
9392
+ // → 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
9393
+ // băm "2 do dai"→"2° dai". "90 do"/"90 do." vẫn đổi.
9394
+ [/(\d+)\s*độ(?!\p{L})/giu, "$1\xB0"],
9395
+ [/(\d+)\s*do(?!\s*\p{L})/giu, "$1\xB0"]
9396
+ ];
9397
+ function applyStructure(s) {
9398
+ let out = s;
9399
+ for (const [re, to] of SYMBOL_MAP) out = out.replace(re, to);
9400
+ return out.replace(/\s+/g, " ").trim();
9401
+ }
9402
+ function applyLeadingCase(canonical, original) {
9403
+ if (/^\p{Lu}/u.test(original)) {
9404
+ return canonical.charAt(0).toUpperCase() + canonical.slice(1);
9405
+ }
9406
+ return canonical;
9407
+ }
9408
+ function fuzzyLookup(folded, cfg) {
9409
+ if (folded.length < cfg.minTypoLen) return null;
9410
+ let best = null;
9411
+ let bestDist = cfg.maxTypoDistance + 1;
9412
+ let tie = false;
9413
+ for (const [key, canonical] of FOLDED_VOCAB) {
9414
+ if (canonical === key) continue;
9415
+ if (Math.abs(key.length - folded.length) > cfg.maxTypoDistance) continue;
9416
+ const d = levenshtein(folded, key);
9417
+ if (d < bestDist) {
9418
+ bestDist = d;
9419
+ best = canonical;
9420
+ tie = false;
9421
+ } else if (d === bestDist) {
9422
+ tie = true;
9423
+ }
9424
+ }
9425
+ if (best === null || bestDist > cfg.maxTypoDistance || tie) return null;
9426
+ return best;
9427
+ }
9428
+ function letterFold(token) {
9429
+ return foldVietnamese(token).replace(/[^a-z]+/g, "");
9430
+ }
9431
+ var DISAMBIG = {
9432
+ // "tam giác" giữ "tam"; còn lại (tâm O, tâm đường tròn…) → "tâm".
9433
+ tam: (_p, n) => n === "giac" ? "tam" : "t\xE2m",
9434
+ // "hình thang" giữ "thang"; còn lại (đường thẳng, thẳng hàng…) → "thẳng".
9435
+ thang: (p, _n) => p === "hinh" ? "thang" : "th\u1EB3ng",
9436
+ // "ngoại tiếp" → "ngoại"; còn lại (nằm ngoài, điểm ngoài…) → "ngoài".
9437
+ ngoai: (_p, n) => n === "tiep" ? "ngo\u1EA1i" : "ngo\xE0i"
9438
+ };
9439
+ function correctToken(token, cfg, prevFold = "", nextFold = "") {
9440
+ const klass = classifyToken(token);
9441
+ if (klass === "protected") return token;
9442
+ const folded = foldVietnamese(token);
9443
+ if (folded !== token.toLowerCase()) return token;
9444
+ if (cfg.accents) {
9445
+ const dis = DISAMBIG[folded];
9446
+ if (dis) return applyLeadingCase(dis(prevFold, nextFold), token);
9447
+ const exact = FOLDED_VOCAB.get(folded);
9448
+ if (exact) return applyLeadingCase(exact, token);
9449
+ }
9450
+ if (cfg.typo && klass === "lower") {
9451
+ const near = fuzzyLookup(folded, cfg);
9452
+ if (near) return near;
9453
+ }
9454
+ return token;
9455
+ }
9456
+ function correctUserInput(input, cfg = DEFAULT_CORRECT_CONFIG) {
9457
+ let s = input;
9458
+ if (cfg.structure) s = applyStructure(s);
9459
+ if (!cfg.accents && !cfg.typo) return s;
9460
+ const parts = s.split(/(\s+)/);
9461
+ const wordIdx = [];
9462
+ for (let i = 0; i < parts.length; i++) {
9463
+ if (parts[i] !== "" && !/\s/.test(parts[i])) wordIdx.push(i);
9464
+ }
9465
+ const out = parts.slice();
9466
+ for (let w = 0; w < wordIdx.length; w++) {
9467
+ const i = wordIdx[w];
9468
+ const prevFold = w > 0 ? letterFold(parts[wordIdx[w - 1]]) : "";
9469
+ const nextFold = w < wordIdx.length - 1 ? letterFold(parts[wordIdx[w + 1]]) : "";
9470
+ out[i] = correctToken(parts[i], cfg, prevFold, nextFold);
9471
+ }
9472
+ return out.join("");
7305
9473
  }
7306
9474
 
7307
9475
  // src/stamps/geometry-2d/ai/deterministic/runDeterministicIntents.ts
7308
9476
  function collectDeterministic(rawProblem) {
7309
- const problem = normalizeProblemText(rawProblem);
9477
+ const problem = normalizeProblemText(correctUserInput(rawProblem));
7310
9478
  const clauses = segmentClauses(problem);
7311
9479
  const drawableClauses = clauses.filter((c) => c.hasGeometry);
7312
9480
  let drawableProblem = problem;
@@ -7355,7 +9523,7 @@ var SHAPE_TRI = /tam giác\s+([A-Z]{3})(?![A-Z])/gu;
7355
9523
  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;
7356
9524
  function collectExpectedNames(problem) {
7357
9525
  const names = /* @__PURE__ */ new Set();
7358
- const add = (re, groups) => {
9526
+ const add2 = (re, groups) => {
7359
9527
  re.lastIndex = 0;
7360
9528
  let m;
7361
9529
  while ((m = re.exec(problem)) !== null) {
@@ -7365,9 +9533,9 @@ function collectExpectedNames(problem) {
7365
9533
  }
7366
9534
  }
7367
9535
  };
7368
- add(NAMED_INTRO, [[1, 2]]);
7369
- add(NAMED_LA, [[1, 2]]);
7370
- add(NAMED_LANLUOT, [[1, 2], [3, 4]]);
9536
+ add2(NAMED_INTRO, [[1, 2]]);
9537
+ add2(NAMED_LA, [[1, 2]]);
9538
+ add2(NAMED_LANLUOT, [[1, 2], [3, 4]]);
7371
9539
  for (const re of [SHAPE_TRI, SHAPE_QUAD]) {
7372
9540
  re.lastIndex = 0;
7373
9541
  let m;
@@ -7618,6 +9786,12 @@ function rewriteCircleRefs(intent, rename) {
7618
9786
  if (newC1 !== c.c1 || newC2 !== c.c2) {
7619
9787
  return { ...intent, constraint: { ...c, c1: newC1, c2: newC2 } };
7620
9788
  }
9789
+ } else if (c.kind === "commonTangentPoint") {
9790
+ const n0 = rename.get(c.circles[0]) ?? c.circles[0];
9791
+ const n1 = rename.get(c.circles[1]) ?? c.circles[1];
9792
+ if (n0 !== c.circles[0] || n1 !== c.circles[1]) {
9793
+ return { ...intent, constraint: { ...c, circles: [n0, n1] } };
9794
+ }
7621
9795
  } else if (c.kind === "intersection") {
7622
9796
  const newOf = c.of.map((r) => rename.get(r) ?? r);
7623
9797
  if (newOf[0] !== c.of[0] || newOf[1] !== c.of[1]) {
@@ -7685,14 +9859,14 @@ function repairCircleIntersections(points, shapes) {
7685
9859
  const targetD = (lower + upper) / 2;
7686
9860
  let dx = mover.x - anchor[0];
7687
9861
  let dy = mover.y - anchor[1];
7688
- const len = Math.hypot(dx, dy);
7689
- if (len < 1e-9) {
9862
+ const len2 = Math.hypot(dx, dy);
9863
+ if (len2 < 1e-9) {
7690
9864
  dx = 1;
7691
9865
  dy = 0;
7692
9866
  }
7693
- const norm2 = Math.hypot(dx, dy) || 1;
7694
- mover.x = anchor[0] + targetD * dx / norm2;
7695
- mover.y = anchor[1] + targetD * dy / norm2;
9867
+ const norm5 = Math.hypot(dx, dy) || 1;
9868
+ mover.x = anchor[0] + targetD * dx / norm5;
9869
+ mover.y = anchor[1] + targetD * dy / norm5;
7696
9870
  }
7697
9871
  }
7698
9872
 
@@ -7880,156 +10054,6 @@ var IntentBuilderError = class extends Error {
7880
10054
  }
7881
10055
  };
7882
10056
 
7883
- // src/stamps/geometry-2d/ai/intent-builders/shared.ts
7884
- var SQRT3 = 1.7320508075688772;
7885
- function triangleCanonical(variant) {
7886
- switch (variant) {
7887
- case "equilateral":
7888
- return [[0, 0], [4, 0], [2, 2 * SQRT3]];
7889
- case "isoceles-AB":
7890
- return [[0, 0], [4, 0], [2, 3]];
7891
- case "isoceles-BC":
7892
- return [[0, 3], [-2, 0], [2, 0]];
7893
- case "isoceles-CA":
7894
- return [[0, 0], [2, 3], [4, 0]];
7895
- case "right-at-A":
7896
- return [[0, 0], [4, 0], [0, 3]];
7897
- case "right-at-B":
7898
- return [[0, 0], [4, 0], [4, 3]];
7899
- case "right-at-C":
7900
- return [[0, 0], [6, 0], [3, 3]];
7901
- case "any":
7902
- default:
7903
- return [[0, 0], [5, 0], [2, 3]];
7904
- }
7905
- }
7906
- function squareCanonical() {
7907
- return [[0, 0], [4, 0], [4, 4], [0, 4]];
7908
- }
7909
- function rectangleCanonical(variant) {
7910
- if (variant === "tall") return [[0, 0], [2.5, 0], [2.5, 4], [0, 4]];
7911
- return [[0, 0], [4, 0], [4, 2.5], [0, 2.5]];
7912
- }
7913
- function rhombusCanonical() {
7914
- return [[-2, 0], [0, -1.5], [2, 0], [0, 1.5]];
7915
- }
7916
- function trapezoidCanonical(variant) {
7917
- switch (variant) {
7918
- case "right":
7919
- return [[0, 0], [5, 0], [3, 3], [0, 3]];
7920
- case "isoceles":
7921
- return [[0, 0], [5, 0], [4, 3], [1, 3]];
7922
- case "general":
7923
- default:
7924
- return [[0, 0], [5, 0], [3.5, 3], [1, 3]];
7925
- }
7926
- }
7927
- function parallelogramCanonical() {
7928
- return [[0, 0], [4, 0], [5, 3], [1, 3]];
7929
- }
7930
- function quadrilateralCanonical() {
7931
- return [[0, 0], [5, 0], [4.5, 3.5], [0.5, 3]];
7932
- }
7933
- function addPoint2(s, p) {
7934
- if (s.pointNames.has(p.name)) {
7935
- if (p.kind !== "free") {
7936
- const idx = s.points.findIndex((q) => q.name === p.name);
7937
- if (idx >= 0 && s.points[idx].kind === "free") s.points[idx] = p;
7938
- }
7939
- return;
7940
- }
7941
- s.points.push(p);
7942
- s.pointNames.add(p.name);
7943
- }
7944
- var FREE_DEFAULT_SPREAD = [
7945
- [3, 3],
7946
- [-3, 3],
7947
- [3, -3],
7948
- [-3, -3],
7949
- [4, 0],
7950
- [-4, 0],
7951
- [0, 4],
7952
- [0, -4]
7953
- ];
7954
- function defaultFreeCoord(s) {
7955
- let count = 0;
7956
- for (const p of s.points) if (p.kind === "free") count++;
7957
- return [...FREE_DEFAULT_SPREAD[count % FREE_DEFAULT_SPREAD.length]];
7958
- }
7959
- function addShape(s, sh) {
7960
- if (s.shapeNames.has(sh.name)) return;
7961
- s.shapes.push(sh);
7962
- s.shapeNames.add(sh.name);
7963
- }
7964
- function uniqueShapeName(s, base) {
7965
- if (!s.shapeNames.has(base)) return base;
7966
- let i = 2;
7967
- while (s.shapeNames.has(`${base}${i}`)) i++;
7968
- return `${base}${i}`;
7969
- }
7970
- function uniquePointName(s, base) {
7971
- if (!s.pointNames.has(base)) return base;
7972
- let i = 2;
7973
- while (s.pointNames.has(`${base}${i}`)) i++;
7974
- return `${base}${i}`;
7975
- }
7976
- function ensureSegment(s, a, b) {
7977
- const k1 = `${a}-${b}`;
7978
- const k2 = `${b}-${a}`;
7979
- const existing = s.segmentByEnds.get(k1) ?? s.segmentByEnds.get(k2);
7980
- if (existing) return existing;
7981
- const name = uniqueShapeName(s, `${a}${b}`);
7982
- addShape(s, { name, kind: "segment", p1: a, p2: b });
7983
- s.segmentByEnds.set(k1, name);
7984
- return name;
7985
- }
7986
- function resolveLineRefWithFallback(s, ref, anchor) {
7987
- if (/^[A-Za-z]$/.test(ref) && /^[A-Za-z]$/.test(anchor) && ref !== anchor && s.pointNames.has(ref) && s.pointNames.has(anchor)) {
7988
- const [a, b] = ref < anchor ? [ref, anchor] : [anchor, ref];
7989
- return ensureSegment(s, a, b);
7990
- }
7991
- return resolveSegmentRef(s, ref);
7992
- }
7993
- function resolveSegmentRef(s, ref) {
7994
- if (s.shapeNames.has(ref)) return ref;
7995
- if (/^[A-Z][A-Z]$/u.test(ref)) {
7996
- return ensureSegment(s, ref[0], ref[1]);
7997
- }
7998
- if (ref.length === 2 && s.pointNames.has(ref[0]) && s.pointNames.has(ref[1])) {
7999
- return ensureSegment(s, ref[0], ref[1]);
8000
- }
8001
- const dashMatch = ref.match(/^([A-Za-z][A-Za-z0-9'_]*)[-_]?([A-Za-z][A-Za-z0-9'_]*)$/);
8002
- if (dashMatch && s.pointNames.has(dashMatch[1]) && s.pointNames.has(dashMatch[2])) {
8003
- return ensureSegment(s, dashMatch[1], dashMatch[2]);
8004
- }
8005
- const sub = splitKnownPair(s, ref);
8006
- if (sub) return ensureSegment(s, sub[0], sub[1]);
8007
- return ref;
8008
- }
8009
- function splitKnownPair(s, ref) {
8010
- for (let i = ref.length - 1; i >= 1; i--) {
8011
- const a = ref.slice(0, i);
8012
- const b = ref.slice(i);
8013
- if (s.pointNames.has(a) && s.pointNames.has(b)) return [a, b];
8014
- }
8015
- return null;
8016
- }
8017
- function parseEnds(ref) {
8018
- if (ref.length === 2) return [ref[0], ref[1]];
8019
- const m = ref.match(/^([A-Za-z][A-Za-z0-9'_]*)[-_]?([A-Za-z][A-Za-z0-9'_]*)$/);
8020
- if (m) return [m[1], m[2]];
8021
- return null;
8022
- }
8023
- var SHAPE_VARIANTS = {
8024
- triangle: ["any", "equilateral", "isoceles-AB", "isoceles-BC", "isoceles-CA", "right-at-A", "right-at-B", "right-at-C"],
8025
- square: ["standard"],
8026
- rectangle: ["standard", "wide", "tall"],
8027
- rhombus: ["standard"],
8028
- trapezoid: ["right", "isoceles", "general"],
8029
- parallelogram: ["standard"],
8030
- quadrilateral: ["any"]
8031
- };
8032
-
8033
10057
  // src/stamps/geometry-2d/ai/intent-builders/draw-shape.ts
8034
10058
  var buildDrawShape = (s, intent) => {
8035
10059
  const labels = intent.labels;
@@ -8302,6 +10326,25 @@ var buildPointAtDistance = (s, intent) => {
8302
10326
  addPoint2(s, { name: intent.name, kind: "pointAtDistance", from: c.from, through: c.through, distance: c.distance });
8303
10327
  };
8304
10328
 
10329
+ // src/stamps/geometry-2d/ai/intent-builders/add-point/commonTangentPoint.ts
10330
+ var buildCommonTangentPoint = (s, intent) => {
10331
+ const c = intent.constraint;
10332
+ if (c.kind !== "commonTangentPoint") return;
10333
+ const isCircle = (name) => {
10334
+ const sh = s.shapes.find((x) => x.name === name);
10335
+ return !!sh && CIRCLE_KINDS.has(sh.kind);
10336
+ };
10337
+ if (!isCircle(c.circles[0]) || !isCircle(c.circles[1])) return;
10338
+ addPoint2(s, {
10339
+ name: intent.name,
10340
+ kind: "commonTangentPoint",
10341
+ circles: [c.circles[0], c.circles[1]],
10342
+ on: c.on,
10343
+ variant: c.variant,
10344
+ side: c.side
10345
+ });
10346
+ };
10347
+
8305
10348
  // src/stamps/geometry-2d/ai/intent-builders/add-point/externalToCircle.ts
8306
10349
  var buildExternalToCircle = (s, intent) => {
8307
10350
  const c = intent.constraint;
@@ -8371,6 +10414,7 @@ var ADD_POINT_BUILDERS = {
8371
10414
  reflectPoint: buildReflectPoint,
8372
10415
  reflectLine: buildReflectLine,
8373
10416
  pointAtDistance: buildPointAtDistance,
10417
+ commonTangentPoint: buildCommonTangentPoint,
8374
10418
  externalToCircle: buildExternalToCircle,
8375
10419
  onCircle: buildOnCircle,
8376
10420
  mixtilinearPoint: buildMixtilinearPoint
@@ -8995,6 +11039,8 @@ function constraintKey(c) {
8995
11039
  return `externalToCircle:${c.circle}`;
8996
11040
  case "onCircle":
8997
11041
  return `onCircle:${c.circle}:${c.theta ?? ""}`;
11042
+ case "commonTangentPoint":
11043
+ return `commonTangentPoint:${c.circles.join(",")}:${c.on}:${c.variant}:${c.side}`;
8998
11044
  }
8999
11045
  }
9000
11046
  function samePrefix(a, b, depth) {
@@ -9022,11 +11068,11 @@ function verifyGeometry(intents, dsl) {
9022
11068
  const other2 = apex === "A" ? pC : apex === "B" ? pC : pB;
9023
11069
  const v1 = [other1[0] - apexP[0], other1[1] - apexP[1]];
9024
11070
  const v2 = [other2[0] - apexP[0], other2[1] - apexP[1]];
9025
- const dot = v1[0] * v2[0] + v1[1] * v2[1];
9026
- if (Math.abs(dot) > 1e-6) {
11071
+ const dot2 = v1[0] * v2[0] + v1[1] * v2[1];
11072
+ if (Math.abs(dot2) > 1e-6) {
9027
11073
  issues.push({
9028
11074
  axis: "wrong",
9029
- detail: `triangle ${A}${B}${C} variant=right-at-${apex} kh\xF4ng vu\xF4ng (dot=${dot.toFixed(3)})`
11075
+ detail: `triangle ${A}${B}${C} variant=right-at-${apex} kh\xF4ng vu\xF4ng (dot=${dot2.toFixed(3)})`
9030
11076
  });
9031
11077
  }
9032
11078
  }
@@ -9074,7 +11120,7 @@ function buildAndTranspile(intents) {
9074
11120
  return second.ok ? second : first;
9075
11121
  }
9076
11122
  function tryDeterministicFigure(rawProblem) {
9077
- const problem = normalizeProblemText(rawProblem);
11123
+ const problem = normalizeProblemText(correctUserInput(rawProblem));
9078
11124
  const det = runDeterministicIntents(problem);
9079
11125
  if (!det.ok) return { ok: false, reason: det.reason, coverage: det.coverage };
9080
11126
  const built = buildAndTranspile(
@@ -9149,7 +11195,7 @@ function describeDeterministicMiss(miss) {
9149
11195
  // src/stamps/geometry-2d/ai/deterministic/partialFigure.ts
9150
11196
  var MAX_SALVAGE_ITERS = 6;
9151
11197
  function tryPartialFigure(rawProblem) {
9152
- const problem = normalizeProblemText(rawProblem);
11198
+ const problem = normalizeProblemText(correctUserInput(rawProblem));
9153
11199
  const part = tryPartialDeterministic(problem);
9154
11200
  if (part.detIntents.length === 0) return null;
9155
11201
  const intents = resolveCircleNameCollisions(normalizeIntents(part.detIntents, problem));
@@ -9533,6 +11579,16 @@ var AddPointIntentZ = z.object({
9533
11579
  z.object({ kind: z.literal("segmentLength"), p1: LabelZ, p2: LabelZ, scale: z.number().positive().optional(), offset: z.number().optional() }),
9534
11580
  z.object({ kind: z.literal("literal"), value: z.number().positive(), scale: z.number().positive().optional(), offset: z.number().optional() })
9535
11581
  ])
11582
+ }),
11583
+ // Tiếp điểm tiếp tuyến CHUNG 2 đường tròn (spec mục A). circles = 2 tên đtròn;
11584
+ // on = tiếp điểm trên đtròn 0 hay 1; variant = ngoài/trong; side = chọn 1 trong
11585
+ // 2 tiếp tuyến cùng loại.
11586
+ z.object({
11587
+ kind: z.literal("commonTangentPoint"),
11588
+ circles: z.tuple([LabelZ, LabelZ]),
11589
+ on: z.union([z.literal(0), z.literal(1)]),
11590
+ variant: z.enum(["external", "internal"]),
11591
+ side: z.union([z.literal(0), z.literal(1)])
9536
11592
  })
9537
11593
  ])
9538
11594
  });
@@ -9583,8 +11639,8 @@ var DrawLineIntentZ = z.object({
9583
11639
  });
9584
11640
  var MarkShapeIntentZ = z.object({
9585
11641
  op: z.literal("mark-shape"),
9586
- shape: z.enum(["triangle", "quadrilateral"]),
9587
- labels: z.array(LabelZ).min(3).max(4)
11642
+ shape: z.enum(["triangle", "quadrilateral", "polygon"]),
11643
+ labels: z.array(LabelZ).min(3).max(8)
9588
11644
  });
9589
11645
  var IntentZ = z.discriminatedUnion("op", [
9590
11646
  DrawShapeIntentZ,
@@ -9896,7 +11952,7 @@ ho\u1EB7c
9896
11952
  - quadrilateral: any
9897
11953
 
9898
11954
  ## Constraint kinds (cho add-point)
9899
- midpoint, perpFoot, centroid, circumcenter, incenter, orthocenter, intersection, onSegment, free, secondIntersection, circleIntersection, tangencyPoint, tangentPoint, angleBisectorFoot, externalAngleBisectorFoot, arcMidpoint, reflectPoint, reflectLine, excenter, rightAngleViewing, pointAtDistance
11955
+ midpoint, perpFoot, centroid, circumcenter, incenter, orthocenter, intersection, onSegment, free, secondIntersection, circleIntersection, tangencyPoint, tangentPoint, angleBisectorFoot, externalAngleBisectorFoot, arcMidpoint, reflectPoint, reflectLine, excenter, rightAngleViewing, pointAtDistance, commonTangentPoint
9900
11956
 
9901
11957
  ## Style enum (cho connect)
9902
11958
  segment, line, ray, perpBisector