math-solver / solver /dsl_parser.py
Cuong2004
Deploy API from GitHub Actions
1993d5c
Raw History Blame Contribute Delete
57.3 kB
import re
import logging
from typing import List, Tuple, Dict, Any
from .models import Point, Constraint
logger = logging.getLogger(__name__)
def _parse_point_tokens(s: str) -> List[str]:
"""
Parse a string of point names into a list of strings.
Handles:
- 'A, B, C' -> ['A', 'B', 'C']
- 'ABCD' -> ['A', 'B', 'C', 'D']
- 'A1B1C1D1' -> ['A1', 'B1', 'C1', 'D1']
- "A'B'C'D'" -> ["A'", "B'", "C'", "D'"]
- 'A_1, B_1' -> ['A_1', 'B_1']
"""
s = s.strip()
if not s:
return []
if ',' in s:
return [p.strip() for p in s.split(',') if p.strip()]
tokens = re.findall(r"[A-Za-z][0-9_']*", s)
return tokens if tokens else [s]
def _eval_val_expr(raw_val: str, default: float = 4.0) -> float:
"""Safely evaluates numeric constants or expressions like 'a', '2*a', '4*sqrt(2)' into floats."""
if not raw_val:
return default
s = str(raw_val).strip().strip('"\'')
try:
return float(s)
except ValueError:
pass
import math
clean = re.sub(r'(?<=\d)a\b', '*4.0', s)
clean = re.sub(r'\ba\b', '4.0', clean)
clean = clean.replace('sqrt', 'math.sqrt').replace('pi', 'math.pi')
try:
return float(eval(clean, {"__builtins__": None, "math": math}))
except Exception:
return default
def _add_poly_segments(pts: List[str], target_segments: List[List[str]], constraints: List[Constraint]):
"""Add cyclic segments for a polygon (e.g. A-B, B-C, C-D, D-A)."""
if len(pts) < 2:
return
for i in range(len(pts)):
p1 = pts[i]
p2 = pts[(i + 1) % len(pts)] if len(pts) > 2 else pts[1]
if [p1, p2] not in target_segments and [p2, p1] not in target_segments:
target_segments.append([p1, p2])
constraints.append(Constraint(type='segment', targets=[p1, p2], value=0))
if len(pts) == 2:
break
class DSLParser:
def parse(self, text: str) -> Tuple[List[Point], List[Constraint], bool]:
"""Parse DSL text into points and constraints. Stateless per call."""
points: Dict[str, Point] = {}
explicit_point_ids: List[str] = []
constraints: List[Constraint] = []
polygon_order: List[str] = []
circles: List[Dict[str, Any]] = []
solids: List[Dict[str, Any]] = []
segments: List[List[str]] = []
lines_ext: List[List[str]] = []
rays: List[List[str]] = []
is_3d = False
logger.info("==[DSLParser] Parsing DSL input (v5.2)==")
logger.debug(f"[DSLParser] Raw DSL:\n{text}")
def ensure_point(pid: str, x: float = None, y: float = None, z: float = None) -> Point:
if pid not in points:
points[pid] = Point(id=pid, x=x, y=y, z=z)
else:
if x is not None: points[pid].x = x
if y is not None: points[pid].y = y
if z is not None: points[pid].z = z
return points[pid]
lines = text.strip().split('\n')
for line in lines:
line = line.strip()
if not line or line.startswith('//') or line.startswith('#'):
continue
# Normalize procedural assignment syntax: P = FUNC(...) -> FUNC(P, ...)
assign_m = re.match(r'^([A-Za-z0-9\']+)\s*=\s*([A-Za-z0-9_]+)\((.*)\)$', line)
if assign_m:
var_name, func_name, inner_args = assign_m.group(1), assign_m.group(2), assign_m.group(3).strip()
tokens = [t.strip() for t in inner_args.split(',') if t.strip()]
if not tokens or tokens[0] != var_name:
line = f"{func_name}({var_name}, {inner_args})" if inner_args else f"{func_name}({var_name})"
else:
line = f"{func_name}({inner_args})"
# POINT(A) or POINT(A, 0, 0, 5) or POINT(A1, 10, 0, 0)
m = re.match(r'POINT\(([^,)]+)(?:,\s*([\d\.-]+),\s*([\d\.-]+)(?:,\s*([\d\.-]+))?)?\)', line)
if m:
name = m.group(1).strip()
x = float(m.group(2)) if m.group(2) else None
y = float(m.group(3)) if m.group(3) else None
z = float(m.group(4)) if m.group(4) else None
if z is not None and abs(z) > 1e-9:
is_3d = True
ensure_point(name, x, y, z)
if name not in explicit_point_ids:
explicit_point_ids.append(name)
logger.debug(f"[DSLParser] + POINT: {name} ({x}, {y}, {z})")
continue
# LENGTH(AB, 5) or LENGTH(A, B, 5) or LENGTH(AB, a) or LENGTH(SA, 2*a)
m = re.match(r'LENGTH\(([^,]+),\s*(?:([^,]+),\s*)?([^)]+)\)', line)
if m:
if m.group(2):
p1, p2, val_str = m.group(1).strip(), m.group(2).strip(), m.group(3).strip()
val = _eval_val_expr(val_str, 4.0)
pts = [p1, p2]
else:
target, val_str = m.group(1).strip(), m.group(3).strip()
val = _eval_val_expr(val_str, 4.0)
pts = _parse_point_tokens(target)
if len(pts) >= 2:
ensure_point(pts[0])
ensure_point(pts[1])
constraints.append(Constraint(type='length', targets=pts[:2], value=val))
logger.debug(f"[DSLParser] + LENGTH: {pts[:2]} = {val}")
continue
# LENGTH_EQUAL(AB, CD) or EQUAL_LENGTH(AB, CD)
m = re.match(r'(?:LENGTH_EQUAL|EQUAL_LENGTH)\(([^,]+),\s*([^)]+)\)', line)
if m:
seg1 = _parse_point_tokens(m.group(1))
seg2 = _parse_point_tokens(m.group(2))
if len(seg1) >= 2 and len(seg2) >= 2:
for p in seg1[:2] + seg2[:2]: ensure_point(p)
constraints.append(Constraint(type='length_equal', targets=seg1[:2] + seg2[:2], value=0))
logger.debug(f"[DSLParser] + LENGTH_EQUAL: {seg1[:2]} == {seg2[:2]}")
continue
# POINT_ON(P, AB) or POINT_ON(P, A, B) or ON_SEGMENT(P, AB) or POINT_ON_LINE(P, AB)
m = re.match(r'(?:POINT_ON|ON_SEGMENT|POINT_ON_LINE|POINT_ON_SEGMENT)\(([^,]+),\s*(?:([^,]+),\s*([^)]+)|([^)]+))\)', line)
if m:
target = m.group(1).strip()
if m.group(2) and m.group(3):
p1, p2 = m.group(2).strip(), m.group(3).strip()
else:
seg_pts = _parse_point_tokens(m.group(4))
p1, p2 = seg_pts[0], seg_pts[1] if len(seg_pts) > 1 else 'B'
ensure_point(target)
ensure_point(p1)
ensure_point(p2)
constraints.append(Constraint(type='point_on', targets=[target, p1, p2], value=0))
logger.debug(f"[DSLParser] + POINT_ON: {target} on ({p1}, {p2})")
continue
# HEIGHT(S, O, ABCD) or ALTITUDE(S, O, ABCD) or HEIGHT(S, O, ABCD, 10)
m = re.match(r'(?:HEIGHT|ALTITUDE)\(([^,]+),\s*([^,]+),\s*([^,)]+)(?:,\s*([\d\.]+))?\)', line)
if m:
is_3d = True
s_apex, o_foot = m.group(1).strip(), m.group(2).strip()
base_pts = _parse_point_tokens(m.group(3))
val = float(m.group(4)) if m.group(4) else 0.0
ensure_point(s_apex)
ensure_point(o_foot)
for p in base_pts: ensure_point(p)
constraints.append(Constraint(type='height', targets=[s_apex, o_foot] + base_pts, value=val))
logger.debug(f"[DSLParser] + HEIGHT: {s_apex}{o_foot} _|_ plane({base_pts}) (val={val})")
continue
# FOOT(H, P, AB) or FOOT_OF_PERPENDICULAR(H, P, AB)
m = re.match(r'(?:FOOT|FOOT_OF_PERPENDICULAR)\(([^,]+),\s*([^,]+),\s*(?:([^,]+),\s*([^)]+)|([^)]+))\)', line)
if m:
pH, pP = m.group(1).strip(), m.group(2).strip()
if m.group(3) and m.group(4):
pA, pB = m.group(3).strip(), m.group(4).strip()
else:
seg_pts = _parse_point_tokens(m.group(5))
pA, pB = seg_pts[0], seg_pts[1] if len(seg_pts) > 1 else 'B'
ensure_point(pH)
ensure_point(pP)
ensure_point(pA)
ensure_point(pB)
constraints.append(Constraint(type='foot', targets=[pH, pP, pA, pB], value=0))
logger.debug(f"[DSLParser] + FOOT: {pH} foot of {pP} on {pA}{pB}")
continue
# FOOT_PLANE(H, P, ABC)
m = re.match(r'FOOT_PLANE\(([^,]+),\s*([^,]+),\s*([^)]+)\)', line)
if m:
is_3d = True
pH, pP = m.group(1).strip(), m.group(2).strip()
plane_pts = _parse_point_tokens(m.group(3))
ensure_point(pH)
ensure_point(pP)
for p in plane_pts: ensure_point(p)
constraints.append(Constraint(type='foot_plane', targets=[pH, pP] + plane_pts, value=0))
logger.debug(f"[DSLParser] + FOOT_PLANE: {pH} foot of {pP} on plane({plane_pts})")
continue
# MEDIAN(A, M, BC)
m = re.match(r'MEDIAN\(([^,]+),\s*([^,]+),\s*(?:([^,]+),\s*([^)]+)|([^)]+))\)', line)
if m:
pA, pM = m.group(1).strip(), m.group(2).strip()
if m.group(3) and m.group(4):
pB, pC = m.group(3).strip(), m.group(4).strip()
else:
seg_pts = _parse_point_tokens(m.group(5))
pB, pC = seg_pts[0], seg_pts[1] if len(seg_pts) > 1 else 'C'
ensure_point(pA)
ensure_point(pM)
ensure_point(pB)
ensure_point(pC)
constraints.append(Constraint(type='median', targets=[pA, pM, pB, pC], value=0))
logger.debug(f"[DSLParser] + MEDIAN: {pA}{pM} to {pB}{pC}")
continue
# CENTER(O, ABCD) or CENTER(O, ABC) or CENTROID(G, ABC)
m = re.match(r'(?:CENTER|CENTROID)\(([^,]+),\s*([^)]+)\)', line)
if m:
c_pt = m.group(1).strip()
poly_pts = _parse_point_tokens(m.group(2))
ensure_point(c_pt)
for p in poly_pts: ensure_point(p)
constraints.append(Constraint(type='center', targets=[c_pt] + poly_pts, value=0))
logger.debug(f"[DSLParser] + CENTER: {c_pt} center of {poly_pts}")
continue
# RIGHT_TRIANGLE(ABC, B) or RIGHT_TRIANGLE(ABC)
m = re.match(r'RIGHT_TRIANGLE\(([^,)]+)(?:,\s*([^)]+))?\)', line)
if m:
pts = _parse_point_tokens(m.group(1))
if len(pts) == 3:
for p in pts: ensure_point(p)
_add_poly_segments(pts, segments, constraints)
if not polygon_order: polygon_order = list(pts)
right_v = m.group(2).strip() if m.group(2) else pts[1]
other_v = [p for p in pts if p != right_v]
if len(other_v) == 2:
constraints.append(Constraint(type='perpendicular', targets=[right_v, other_v[0], right_v, other_v[1]], value=0))
logger.debug(f"[DSLParser] + RIGHT_TRIANGLE: {pts} right at {right_v}")
continue
# EQUILATERAL_TRIANGLE(ABC) or EQUILATERAL_TRIANGLE(ABC, a)
m = re.match(r'EQUILATERAL_TRIANGLE\(([^,)]+)(?:,\s*([\d\.]+))?\)', line)
if m:
pts = _parse_point_tokens(m.group(1))
side_val = float(m.group(2)) if m.group(2) else None
if len(pts) == 3:
for p in pts: ensure_point(p)
_add_poly_segments(pts, segments, constraints)
if not polygon_order: polygon_order = list(pts)
constraints.append(Constraint(type='length_equal', targets=[pts[0], pts[1], pts[1], pts[2]], value=0))
constraints.append(Constraint(type='length_equal', targets=[pts[1], pts[2], pts[2], pts[0]], value=0))
if side_val is not None:
constraints.append(Constraint(type='length', targets=[pts[0], pts[1]], value=side_val))
logger.debug(f"[DSLParser] + EQUILATERAL_TRIANGLE: {pts}")
continue
# ISOSCELES_TRIANGLE(ABC, A) or ISOSCELES_TRIANGLE(ABC)
m = re.match(r'ISOSCELES_TRIANGLE\(([^,)]+)(?:,\s*([^)]+))?\)', line)
if m:
pts = _parse_point_tokens(m.group(1))
if len(pts) == 3:
for p in pts: ensure_point(p)
_add_poly_segments(pts, segments, constraints)
if not polygon_order: polygon_order = list(pts)
apex_v = m.group(2).strip() if m.group(2) else pts[0]
base_v = [p for p in pts if p != apex_v]
if len(base_v) == 2:
constraints.append(Constraint(type='length_equal', targets=[apex_v, base_v[0], apex_v, base_v[1]], value=0))
logger.debug(f"[DSLParser] + ISOSCELES_TRIANGLE: {pts} apex at {apex_v}")
continue
# ANGLE(A, 90) or ANGLE(A, B, C, 90) or ANGLE(ABC, 90deg)
m = re.match(r'ANGLE\((.+)\)', line)
if m:
raw_args = [a.strip() for a in m.group(1).split(',')]
if len(raw_args) >= 2:
val_str = raw_args[-1].replace('deg', '').strip()
try:
val = float(val_str)
target_args = raw_args[:-1]
if len(target_args) == 1:
pts = _parse_point_tokens(target_args[0])
else:
pts = target_args
for p in pts: ensure_point(p)
constraints.append(Constraint(type='angle', targets=pts, value=val))
logger.debug(f"[DSLParser] + ANGLE: vertex/targets={pts}, degrees={val}")
except ValueError:
pass
continue
# PERPENDICULAR_PLANE(SO, ABCD) or LINE_PERP_PLANE(SO, ABC)
m = re.match(r'(?:PERPENDICULAR_PLANE|LINE_PERP_PLANE)\(([^,]+),\s*([^)]+)\)', line)
if m:
is_3d = True
line_pts = _parse_point_tokens(m.group(1))
plane_pts = _parse_point_tokens(m.group(2))
for p in line_pts + plane_pts: ensure_point(p)
constraints.append(Constraint(type='perp_plane', targets=line_pts[:2] + plane_pts, value=0))
logger.debug(f"[DSLParser] + PERPENDICULAR_PLANE: line={line_pts[:2]} _|_ plane={plane_pts}")
continue
# COPLANAR(A, B, C, D) or COPLANAR(ABCD)
m = re.match(r'COPLANAR\(([^)]+)\)', line)
if m:
is_3d = True
pts = _parse_point_tokens(m.group(1))
for p in pts: ensure_point(p)
if len(pts) >= 4:
constraints.append(Constraint(type='coplanar', targets=pts, value=0))
logger.debug(f"[DSLParser] + COPLANAR: {pts}")
continue
# POINT_ON_PLANE(P, ABC) or POINT_ON_PLANE(P, A, B, C)
m = re.match(r'POINT_ON_PLANE\(([^,]+),\s*([^)]+)\)', line)
if m:
is_3d = True
p_target = m.group(1).strip()
plane_pts = _parse_point_tokens(m.group(2))
ensure_point(p_target)
for p in plane_pts: ensure_point(p)
constraints.append(Constraint(type='point_on_plane', targets=[p_target] + plane_pts, value=0))
logger.debug(f"[DSLParser] + POINT_ON_PLANE: {p_target} on plane {plane_pts}")
continue
# PARALLEL(AB, CD)
m = re.match(r'PARALLEL\(([^,]+),\s*([^)]+)\)', line)
if m:
seg1_pts = _parse_point_tokens(m.group(1))
seg2_pts = _parse_point_tokens(m.group(2))
if len(seg1_pts) >= 2 and len(seg2_pts) >= 2:
for p in seg1_pts[:2] + seg2_pts[:2]: ensure_point(p)
constraints.append(Constraint(type='parallel', targets=seg1_pts[:2] + seg2_pts[:2], value=0))
logger.debug(f"[DSLParser] + PARALLEL: {seg1_pts[:2]} || {seg2_pts[:2]}")
continue
# PERPENDICULAR(AB, CD)
m = re.match(r'PERPENDICULAR\(([^,]+),\s*([^)]+)\)', line)
if m:
seg1_pts = _parse_point_tokens(m.group(1))
seg2_pts = _parse_point_tokens(m.group(2))
if len(seg1_pts) >= 2 and len(seg2_pts) >= 2:
for p in seg1_pts[:2] + seg2_pts[:2]: ensure_point(p)
constraints.append(Constraint(type='perpendicular', targets=seg1_pts[:2] + seg2_pts[:2], value=0))
logger.debug(f"[DSLParser] + PERPENDICULAR: {seg1_pts[:2]} _|_ {seg2_pts[:2]}")
continue
# MIDPOINT(M, AB) or MIDPOINT(M, A, B)
m = re.match(r'MIDPOINT\(([^,]+),\s*(?:([^,]+),\s*([^)]+)|([^)]+))\)', line)
if m:
mid = m.group(1).strip()
if m.group(2) and m.group(3):
p1, p2 = m.group(2).strip(), m.group(3).strip()
else:
seg_pts = _parse_point_tokens(m.group(4))
p1, p2 = seg_pts[0], seg_pts[1] if len(seg_pts) > 1 else 'B'
ensure_point(mid)
ensure_point(p1)
ensure_point(p2)
constraints.append(Constraint(type='midpoint', targets=[mid, p1, p2], value=0))
logger.debug(f"[DSLParser] + MIDPOINT: {mid} = mid({p1}, {p2})")
continue
# SECTION(E, A, C, 0.66)
m = re.match(r'SECTION\(([^,]+),\s*([^,]+),\s*([^,]+),\s*([\d\.-]+)\)', line)
if m:
target, p1, p2, k = m.group(1).strip(), m.group(2).strip(), m.group(3).strip(), float(m.group(4))
ensure_point(target)
ensure_point(p1)
ensure_point(p2)
constraints.append(Constraint(type='section', targets=[target, p1, p2], value=k))
logger.debug(f"[DSLParser] + SECTION: {target} = {p1} + {k}({p2}-{p1})")
continue
# INTERSECT_LINES(P, AB, CD) or INTERSECT_LINES(P, A, B, C, D)
m = re.match(r'INTERSECT_LINES\(([^,]+),\s*(?:([^,]+),\s*([^,]+),\s*([^,]+),\s*([^)]+)|([^,]+),\s*([^)]+))\)', line)
if m:
p_out = m.group(1).strip()
if m.group(2):
pA, pB, pC, pD = m.group(2).strip(), m.group(3).strip(), m.group(4).strip(), m.group(5).strip()
else:
seg1 = _parse_point_tokens(m.group(6))
seg2 = _parse_point_tokens(m.group(7))
pA, pB = seg1[0], seg1[1] if len(seg1) > 1 else 'B'
pC, pD = seg2[0], seg2[1] if len(seg2) > 1 else 'D'
for p in [p_out, pA, pB, pC, pD]: ensure_point(p)
constraints.append(Constraint(type='intersect_lines', targets=[p_out, pA, pB, pC, pD], value=0))
logger.debug(f"[DSLParser] + INTERSECT_LINES: {p_out} = {pA}{pB} x {pC}{pD}")
continue
# INTERSECT_LINE_PLANE(P, Line, Plane)
m = re.match(r'INTERSECT_LINE_PLANE\(([^,]+),\s*([^,]+),\s*([^)]+)\)', line)
if m:
is_3d = True
p_out = m.group(1).strip()
line_pts = _parse_point_tokens(m.group(2))
plane_pts = _parse_point_tokens(m.group(3))
for p in [p_out] + line_pts + plane_pts: ensure_point(p)
constraints.append(Constraint(type='intersect_line_plane', targets=[p_out] + line_pts[:2] + plane_pts, value=0))
logger.debug(f"[DSLParser] + INTERSECT_LINE_PLANE: {p_out} = line({line_pts[:2]}) x plane({plane_pts})")
continue
# INCENTER(I, A, B, C) or INCENTER(I, ABC)
m = re.match(r'INCENTER\(([^,]+),\s*(?:([^,]+),\s*([^,]+),\s*([^)]+)|([^)]+))\)', line)
if m:
pI = m.group(1).strip()
if m.group(2):
pA, pB, pC = m.group(2).strip(), m.group(3).strip(), m.group(4).strip()
else:
pts = _parse_point_tokens(m.group(5))
pA, pB, pC = pts[0], pts[1], pts[2]
for p in [pI, pA, pB, pC]: ensure_point(p)
constraints.append(Constraint(type='incenter', targets=[pI, pA, pB, pC], value=0))
logger.debug(f"[DSLParser] + INCENTER: {pI} of ({pA}, {pB}, {pC})")
continue
# CIRCUMCENTER(O, A, B, C) or CIRCUMCENTER(O, ABC)
m = re.match(r'CIRCUMCENTER\(([^,]+),\s*(?:([^,]+),\s*([^,]+),\s*([^)]+)|([^)]+))\)', line)
if m:
pO = m.group(1).strip()
if m.group(2):
pA, pB, pC = m.group(2).strip(), m.group(3).strip(), m.group(4).strip()
else:
pts = _parse_point_tokens(m.group(5))
pA, pB, pC = pts[0], pts[1], pts[2]
for p in [pO, pA, pB, pC]: ensure_point(p)
constraints.append(Constraint(type='circumcenter', targets=[pO, pA, pB, pC], value=0))
logger.debug(f"[DSLParser] + CIRCUMCENTER: {pO} of ({pA}, {pB}, {pC})")
continue
# ORTHOCENTER(H, A, B, C) or ORTHOCENTER(H, ABC)
m = re.match(r'ORTHOCENTER\(([^,]+),\s*(?:([^,]+),\s*([^,]+),\s*([^)]+)|([^)]+))\)', line)
if m:
pH = m.group(1).strip()
if m.group(2):
pA, pB, pC = m.group(2).strip(), m.group(3).strip(), m.group(4).strip()
else:
pts = _parse_point_tokens(m.group(5))
pA, pB, pC = pts[0], pts[1], pts[2]
for p in [pH, pA, pB, pC]: ensure_point(p)
constraints.append(Constraint(type='orthocenter', targets=[pH, pA, pB, pC], value=0))
logger.debug(f"[DSLParser] + ORTHOCENTER: {pH} of ({pA}, {pB}, {pC})")
continue
# ANGLE_BISECTOR(D, A, B, C) -> D on BC, bisecting angle BAC
m = re.match(r'ANGLE_BISECTOR\(([^,]+),\s*([^,]+),\s*([^,]+),\s*([^)]+)\)', line)
if m:
pD, pA, pB, pC = m.group(1).strip(), m.group(2).strip(), m.group(3).strip(), m.group(4).strip()
for p in [pD, pA, pB, pC]: ensure_point(p)
constraints.append(Constraint(type='angle_bisector', targets=[pD, pA, pB, pC], value=0))
logger.debug(f"[DSLParser] + ANGLE_BISECTOR: {pA}{pD} bisects angle {pB}{pA}{pC}")
continue
# PROJECT_POINT_PLANE(H, P, Plane)
m = re.match(r'PROJECT_POINT_PLANE\(([^,]+),\s*([^,]+),\s*([^)]+)\)', line)
if m:
is_3d = True
pH, pP = m.group(1).strip(), m.group(2).strip()
plane_pts = _parse_point_tokens(m.group(3))
ensure_point(pH)
ensure_point(pP)
for p in plane_pts: ensure_point(p)
constraints.append(Constraint(type='foot_plane', targets=[pH, pP] + plane_pts, value=0))
logger.debug(f"[DSLParser] + PROJECT_POINT_PLANE: {pH} of {pP} on plane({plane_pts})")
continue
# PROJECT_POINT_LINE(H, P, AB)
m = re.match(r'PROJECT_POINT_LINE\(([^,]+),\s*([^,]+),\s*(?:([^,]+),\s*([^)]+)|([^)]+))\)', line)
if m:
pH, pP = m.group(1).strip(), m.group(2).strip()
if m.group(3) and m.group(4):
pA, pB = m.group(3).strip(), m.group(4).strip()
else:
seg_pts = _parse_point_tokens(m.group(5))
pA, pB = seg_pts[0], seg_pts[1] if len(seg_pts) > 1 else 'B'
for p in [pH, pP, pA, pB]: ensure_point(p)
constraints.append(Constraint(type='foot', targets=[pH, pP, pA, pB], value=0))
logger.debug(f"[DSLParser] + PROJECT_POINT_LINE: {pH} of {pP} on {pA}{pB}")
continue
# ANGLE_RELATION(A, B, C, D, E, F, ratio) or ANGLE_RELATION(ABC, DEF, ratio)
m = re.match(r'ANGLE_RELATION\((.+)\)', line)
if m:
raw_args = [a.strip() for a in m.group(1).split(',')]
if len(raw_args) >= 3:
ratio = float(raw_args[-1])
if len(raw_args) == 3:
ang1_pts = _parse_point_tokens(raw_args[0])
ang2_pts = _parse_point_tokens(raw_args[1])
else:
ang1_pts = raw_args[:3]
ang2_pts = raw_args[3:-1]
for p in ang1_pts + ang2_pts: ensure_point(p)
constraints.append(Constraint(type='angle_relation', targets=ang1_pts[:3] + ang2_pts[:3], value=ratio))
logger.debug(f"[DSLParser] + ANGLE_RELATION: angle({ang1_pts[:3]}) = {ratio} * angle({ang2_pts[:3]})")
continue
# ANGLE_SUM(A, B, C, D, E, F, target) or ANGLE_SUM(ABC, DEF, target)
m = re.match(r'ANGLE_SUM\((.+)\)', line)
if m:
raw_args = [a.strip() for a in m.group(1).split(',')]
if len(raw_args) >= 3:
target_val = float(raw_args[-1].replace('deg', ''))
if len(raw_args) == 3:
ang1_pts = _parse_point_tokens(raw_args[0])
ang2_pts = _parse_point_tokens(raw_args[1])
else:
ang1_pts = raw_args[:3]
ang2_pts = raw_args[3:-1]
for p in ang1_pts + ang2_pts: ensure_point(p)
constraints.append(Constraint(type='angle_sum', targets=ang1_pts[:3] + ang2_pts[:3], value=target_val))
logger.debug(f"[DSLParser] + ANGLE_SUM: angle({ang1_pts[:3]}) + angle({ang2_pts[:3]}) = {target_val}")
continue
# INSIDE_SEGMENT(M, A, B) or INSIDE_SEGMENT(M, AB)
m = re.match(r'INSIDE_SEGMENT\(([^,]+),\s*(?:([^,]+),\s*([^)]+)|([^)]+))\)', line)
if m:
pM = m.group(1).strip()
if m.group(2) and m.group(3):
pA, pB = m.group(2).strip(), m.group(3).strip()
else:
pts = _parse_point_tokens(m.group(4))
pA, pB = pts[0], pts[1] if len(pts) > 1 else 'B'
for p in [pM, pA, pB]: ensure_point(p)
constraints.append(Constraint(type='inside_segment', targets=[pM, pA, pB], value=0))
logger.debug(f"[DSLParser] + INSIDE_SEGMENT: {pM} strictly inside ({pA}, {pB})")
continue
# ON_RAY(M, A, B) or ON_RAY(M, AB)
m = re.match(r'ON_RAY\(([^,]+),\s*(?:([^,]+),\s*([^)]+)|([^)]+))\)', line)
if m:
pM = m.group(1).strip()
if m.group(2) and m.group(3):
pA, pB = m.group(2).strip(), m.group(3).strip()
else:
pts = _parse_point_tokens(m.group(4))
pA, pB = pts[0], pts[1] if len(pts) > 1 else 'B'
for p in [pM, pA, pB]: ensure_point(p)
constraints.append(Constraint(type='on_ray', targets=[pM, pA, pB], value=0))
logger.debug(f"[DSLParser] + ON_RAY: {pM} on ray ({pA}->{pB})")
continue
# ON_OPPOSITE_RAY(M, A, B) or ON_OPPOSITE_RAY(M, AB)
m = re.match(r'ON_OPPOSITE_RAY\(([^,]+),\s*(?:([^,]+),\s*([^)]+)|([^)]+))\)', line)
if m:
pM = m.group(1).strip()
if m.group(2) and m.group(3):
pA, pB = m.group(2).strip(), m.group(3).strip()
else:
pts = _parse_point_tokens(m.group(4))
pA, pB = pts[0], pts[1] if len(pts) > 1 else 'B'
for p in [pM, pA, pB]: ensure_point(p)
constraints.append(Constraint(type='on_opposite_ray', targets=[pM, pA, pB], value=0))
logger.debug(f"[DSLParser] + ON_OPPOSITE_RAY: {pM} on opposite ray ({pA}->{pB})")
continue
# RATIO_SEGMENT(A, M, B, ratio)
m = re.match(r'RATIO_SEGMENT\(([^,]+),\s*([^,]+),\s*([^,]+),\s*([\d\.-]+)\)', line)
if m:
pA, pM, pB, ratio = m.group(1).strip(), m.group(2).strip(), m.group(3).strip(), float(m.group(4))
for p in [pA, pM, pB]: ensure_point(p)
constraints.append(Constraint(type='ratio_segment', targets=[pA, pM, pB], value=ratio))
logger.debug(f"[DSLParser] + RATIO_SEGMENT: {pA}{pM} / {pM}{pB} = {ratio}")
continue
# ANGLE_LINE_PLANE(Line, Plane, deg)
m = re.match(r'ANGLE_LINE_PLANE\(([^,]+),\s*([^,]+),\s*([\d\.-]+)(?:deg)?\)', line)
if m:
is_3d = True
line_pts = _parse_point_tokens(m.group(1))
plane_pts = _parse_point_tokens(m.group(2))
deg_val = float(m.group(3))
for p in line_pts[:2] + plane_pts: ensure_point(p)
constraints.append(Constraint(type='angle_line_plane', targets=line_pts[:2] + plane_pts, value=deg_val))
logger.debug(f"[DSLParser] + ANGLE_LINE_PLANE: line({line_pts[:2]}), plane({plane_pts}) = {deg_val} deg")
continue
# DIHEDRAL_ANGLE(Plane1, Plane2, deg) or ANGLE_PLANES(...)
m = re.match(r'(?:DIHEDRAL_ANGLE|ANGLE_PLANES)\(([^,]+),\s*([^,]+),\s*([\d\.-]+)(?:deg)?\)', line)
if m:
is_3d = True
p1_pts = _parse_point_tokens(m.group(1))
p2_pts = _parse_point_tokens(m.group(2))
deg_val = float(m.group(3))
for p in p1_pts + p2_pts: ensure_point(p)
constraints.append(Constraint(type='dihedral_angle', targets=p1_pts[:3] + p2_pts[:3], value=deg_val))
logger.debug(f"[DSLParser] + DIHEDRAL_ANGLE: plane({p1_pts[:3]}), plane({p2_pts[:3]}) = {deg_val} deg")
continue
# DISTANCE_SKEW_LINES(AB, CD, val)
m = re.match(r'DISTANCE_SKEW_LINES\(([^,]+),\s*([^,]+),\s*([\d\.-]+)\)', line)
if m:
is_3d = True
seg1 = _parse_point_tokens(m.group(1))
seg2 = _parse_point_tokens(m.group(2))
val = float(m.group(3))
for p in seg1[:2] + seg2[:2]: ensure_point(p)
constraints.append(Constraint(type='distance_skew_lines', targets=seg1[:2] + seg2[:2], value=val))
logger.debug(f"[DSLParser] + DISTANCE_SKEW_LINES: {seg1[:2]} - {seg2[:2]} = {val}")
continue
# DISTANCE_POINT_PLANE(P, Plane, val)
m = re.match(r'DISTANCE_POINT_PLANE\(([^,]+),\s*([^,]+),\s*([\d\.-]+)\)', line)
if m:
is_3d = True
p_target = m.group(1).strip()
plane_pts = _parse_point_tokens(m.group(2))
val = float(m.group(3))
ensure_point(p_target)
for p in plane_pts: ensure_point(p)
constraints.append(Constraint(type='distance_point_plane', targets=[p_target] + plane_pts, value=val))
logger.debug(f"[DSLParser] + DISTANCE_POINT_PLANE: {p_target} to plane({plane_pts}) = {val}")
continue
# PLANE_EQ(alpha, A, B, C, D)
m = re.match(r'PLANE_EQ\(([^,]+),\s*([\d\.-]+),\s*([\d\.-]+),\s*([\d\.-]+),\s*([\d\.-]+)\)', line)
if m:
is_3d = True
name = m.group(1).strip()
A_c, B_c, C_c, D_c = float(m.group(2)), float(m.group(3)), float(m.group(4)), float(m.group(5))
constraints.append(Constraint(type='plane_eq', targets=[name, str(A_c), str(B_c), str(C_c), str(D_c)], value=0))
logger.debug(f"[DSLParser] + PLANE_EQ: {name} => {A_c}x + {B_c}y + {C_c}z + {D_c} = 0")
continue
# SPHERE_EQ(S, center, R)
m = re.match(r'SPHERE_EQ\(([^,]+),\s*([^,]+),\s*([\d\.]+)\)', line)
if m:
is_3d = True
s_name, center, radius = m.group(1).strip(), m.group(2).strip(), float(m.group(3))
ensure_point(center)
constraints.append(Constraint(type='sphere_eq', targets=[s_name, center], value=radius))
solids.append({"type": "sphere", "name": s_name, "center": center, "radius": radius})
logger.debug(f"[DSLParser] + SPHERE_EQ: {s_name} center={center}, r={radius}")
continue
# TANGENT_PLANE_SPHERE(Plane, Sphere)
m = re.match(r'TANGENT_PLANE_SPHERE\(([^,]+),\s*([^)]+)\)', line)
if m:
is_3d = True
p_name, s_name = m.group(1).strip(), m.group(2).strip()
constraints.append(Constraint(type='tangent_plane_sphere', targets=[p_name, s_name], value=0))
logger.debug(f"[DSLParser] + TANGENT_PLANE_SPHERE: plane {p_name} tangent to sphere {s_name}")
continue
# CIRCLE(O, r)
m = re.match(r'CIRCLE\(([^,]+),\s*([\d\.]+)\)', line)
if m:
center, radius = m.group(1).strip(), float(m.group(2))
ensure_point(center)
constraints.append(Constraint(type='circle', targets=[center], value=radius))
circles.append({"center": center, "radius": radius})
logger.debug(f"[DSLParser] + CIRCLE: center={center}, r={radius}")
continue
# SPHERE(O, r)
m = re.match(r'SPHERE\(([^,]+),\s*([\d\.]+)\)', line)
if m:
is_3d = True
center, radius = m.group(1).strip(), float(m.group(2))
ensure_point(center)
constraints.append(Constraint(type='sphere', targets=[center], value=radius))
solids.append({"type": "sphere", "center": center, "radius": radius})
logger.debug(f"[DSLParser] + SPHERE: center={center}, r={radius}")
continue
# CONE(S, O, r) or CONE(S, O, r, h) or CONE(S_O, r) or CONE(S_O, r, h)
m4 = re.match(r'CONE\(([A-Za-z0-9\']+),\s*([A-Za-z0-9\']+),\s*([\d\.]+)(?:,\s*([\d\.]+))?\)', line)
m2 = re.match(r'CONE\(([A-Za-z0-9\']+(?:_[A-Za-z0-9\']+)?),\s*([\d\.]+)(?:,\s*([\d\.]+))?\)', line)
if m4 and not ('_' in m4.group(1) and m4.group(2).replace('.', '', 1).isdigit()):
is_3d = True
apex, center = m4.group(1).strip(), m4.group(2).strip()
radius = float(m4.group(3))
height = float(m4.group(4)) if m4.group(4) else None
ensure_point(apex)
ensure_point(center)
segments.append([apex, center])
constraints.append(Constraint(type='segment', targets=[apex, center], value=0))
constraints.append(Constraint(type='cone', targets=[apex, center], value=radius))
if height is not None:
constraints.append(Constraint(type='length', targets=[apex, center], value=height))
solids.append({"type": "cone", "apex": apex, "center": center, "radius": radius, "height": height})
logger.debug(f"[DSLParser] + CONE: apex={apex}, center={center}, r={radius}, h={height}")
continue
elif m2:
is_3d = True
raw_pts = m2.group(1).strip()
if '_' in raw_pts:
apex, center = [p.strip() for p in raw_pts.split('_', 1)]
else:
apex, center = raw_pts, 'O'
radius = float(m2.group(2))
height = float(m2.group(3)) if m2.group(3) else None
ensure_point(apex)
ensure_point(center)
segments.append([apex, center])
constraints.append(Constraint(type='segment', targets=[apex, center], value=0))
constraints.append(Constraint(type='cone', targets=[apex, center], value=radius))
if height is not None:
constraints.append(Constraint(type='length', targets=[apex, center], value=height))
solids.append({"type": "cone", "apex": apex, "center": center, "radius": radius, "height": height})
logger.debug(f"[DSLParser] + CONE: apex={apex}, center={center}, r={radius}, h={height}")
continue
# CYLINDER(O1, O2, r) or CYLINDER(O1_O2, r)
m = re.match(r'CYLINDER\(([^,]+)(?:,\s*([^,]+))?,\s*([\d\.]+)\)', line)
if m:
is_3d = True
if m.group(2):
c1, c2 = m.group(1).strip(), m.group(2).strip()
elif '_' in m.group(1):
c1, c2 = [p.strip() for p in m.group(1).split('_', 1)]
else:
c1, c2 = 'O1', 'O2'
radius = float(m.group(3))
ensure_point(c1)
ensure_point(c2)
segments.append([c1, c2])
constraints.append(Constraint(type='segment', targets=[c1, c2], value=0))
constraints.append(Constraint(type='cylinder', targets=[c1, c2], value=radius))
solids.append({"type": "cylinder", "center1": c1, "center2": c2, "radius": radius})
logger.debug(f"[DSLParser] + CYLINDER: c1={c1}, c2={c2}, r={radius}")
continue
# TETRAHEDRON(ABCD) or TETRAHEDRON(ABCD, a)
m = re.match(r'TETRAHEDRON\(([^,)]+)(?:,\s*([\d\.]+))?\)', line)
if m:
is_3d = True
pts = _parse_point_tokens(m.group(1))
side_val = float(m.group(2)) if m.group(2) else None
for p in pts: ensure_point(p)
if len(pts) >= 4:
pA, pB, pC, pD = pts[0], pts[1], pts[2], pts[3]
# Generate all 6 edges
for p1, p2 in [(pA, pB), (pA, pC), (pA, pD), (pB, pC), (pC, pD), (pD, pB)]:
if [p1, p2] not in segments and [p2, p1] not in segments:
segments.append([p1, p2])
constraints.append(Constraint(type='segment', targets=[p1, p2], value=0))
if side_val is not None:
constraints.append(Constraint(type='length', targets=[p1, p2], value=side_val))
if not polygon_order: polygon_order = [pB, pC, pD]
solids.append({"type": "tetrahedron", "apex": pA, "base": [pB, pC, pD], "points": pts[:4]})
logger.debug(f"[DSLParser] + TETRAHEDRON: {pts[:4]}")
continue
# CUBE(ABCD_A1B1C1D1) or CUBE(ABCD_EFGH) or CUBE(..., a)
m = re.match(r'CUBE\(([^,)]+)(?:,\s*([\d\.]+))?\)', line)
if m:
is_3d = True
targets = m.group(1).strip()
side_val = float(m.group(2)) if m.group(2) else None
if '_' in targets:
b1_raw, b2_raw = targets.split('_', 1)
b1 = _parse_point_tokens(b1_raw)
b2 = _parse_point_tokens(b2_raw)
else:
all_pts = _parse_point_tokens(targets)
b1, b2 = all_pts[:4], all_pts[4:8]
for p in b1 + b2: ensure_point(p)
_add_poly_segments(b1, segments, constraints)
_add_poly_segments(b2, segments, constraints)
for p1, p2 in zip(b1, b2):
if [p1, p2] not in segments and [p2, p1] not in segments:
segments.append([p1, p2])
constraints.append(Constraint(type='segment', targets=[p1, p2], value=0))
if len(b1) >= 2 and len(b2) >= 2:
for i in range(1, len(b1)):
constraints.append(Constraint(type='parallel', targets=[b1[0], b2[0], b1[i], b2[i]], value=0))
constraints.append(Constraint(type='length_equal', targets=[b1[0], b2[0], b1[i], b2[i]], value=0))
constraints.append(Constraint(type='perpendicular', targets=[b1[0], b2[0], b1[0], b1[1]], value=0))
if len(b1) >= 3:
constraints.append(Constraint(type='perpendicular', targets=[b1[0], b2[0], b1[0], b1[2]], value=0))
if side_val is not None:
for p1, p2 in zip(b1, b1[1:] + b1[:1]):
constraints.append(Constraint(type='length', targets=[p1, p2], value=side_val))
if b1 and b2:
constraints.append(Constraint(type='length', targets=[b1[0], b2[0]], value=side_val))
if not polygon_order: polygon_order = list(b1)
solids.append({"type": "cube", "base1": b1, "base2": b2, "points": b1 + b2})
logger.debug(f"[DSLParser] + CUBE: base1={b1}, base2={b2}")
continue
# CUBOID(ABCD_A1B1C1D1) or PARALLELEPIPED(...)
m = re.match(r'(?:CUBOID|PARALLELEPIPED)\(([^)]+)\)', line)
if m:
is_3d = True
targets = m.group(1).strip()
if '_' in targets:
b1_raw, b2_raw = targets.split('_', 1)
b1 = _parse_point_tokens(b1_raw)
b2 = _parse_point_tokens(b2_raw)
else:
all_pts = _parse_point_tokens(targets)
b1, b2 = all_pts[:4], all_pts[4:8]
for p in b1 + b2: ensure_point(p)
_add_poly_segments(b1, segments, constraints)
_add_poly_segments(b2, segments, constraints)
for p1, p2 in zip(b1, b2):
if [p1, p2] not in segments and [p2, p1] not in segments:
segments.append([p1, p2])
constraints.append(Constraint(type='segment', targets=[p1, p2], value=0))
if len(b1) >= 2 and len(b2) >= 2:
for i in range(1, len(b1)):
constraints.append(Constraint(type='parallel', targets=[b1[0], b2[0], b1[i], b2[i]], value=0))
constraints.append(Constraint(type='length_equal', targets=[b1[0], b2[0], b1[i], b2[i]], value=0))
constraints.append(Constraint(type='perpendicular', targets=[b1[0], b2[0], b1[0], b1[1]], value=0))
if len(b1) >= 3:
constraints.append(Constraint(type='perpendicular', targets=[b1[0], b2[0], b1[0], b1[2]], value=0))
if not polygon_order: polygon_order = list(b1)
solids.append({"type": "cuboid", "base1": b1, "base2": b2, "points": b1 + b2})
logger.debug(f"[DSLParser] + CUBOID: base1={b1}, base2={b2}")
continue
# FRUSTUM(ABCD_EFGH) or TRUNCATED_PYRAMID(ABCD_EFGH)
m = re.match(r'(?:FRUSTUM|TRUNCATED_PYRAMID)\(([^)]+)\)', line)
if m:
is_3d = True
targets = m.group(1).strip()
if '_' in targets:
b1_raw, b2_raw = targets.split('_', 1)
b1 = _parse_point_tokens(b1_raw)
b2 = _parse_point_tokens(b2_raw)
else:
all_pts = _parse_point_tokens(targets)
n = len(all_pts) // 2
b1, b2 = all_pts[:n], all_pts[n:]
for p in b1 + b2: ensure_point(p)
_add_poly_segments(b1, segments, constraints)
_add_poly_segments(b2, segments, constraints)
for p1, p2 in zip(b1, b2):
if [p1, p2] not in segments and [p2, p1] not in segments:
segments.append([p1, p2])
constraints.append(Constraint(type='segment', targets=[p1, p2], value=0))
if not polygon_order: polygon_order = list(b1)
solids.append({"type": "frustum", "base1": b1, "base2": b2, "points": b1 + b2})
logger.debug(f"[DSLParser] + FRUSTUM: base1={b1}, base2={b2}")
continue
# POLYGON_ORDER(A, B, C, D)
m = re.match(r'POLYGON_ORDER\(([^)]+)\)', line)
if m:
polygon_order = _parse_point_tokens(m.group(1))
logger.debug(f"[DSLParser] + POLYGON_ORDER: {polygon_order}")
continue
# SEGMENT(M, N) or SEGMENT(MN)
m = re.match(r'SEGMENT\(([^,]+)(?:,\s*([^)]+))?\)', line)
if m:
if m.group(2):
p1, p2 = m.group(1).strip(), m.group(2).strip()
else:
pts = _parse_point_tokens(m.group(1))
p1, p2 = pts[0], pts[1] if len(pts) > 1 else 'B'
ensure_point(p1)
ensure_point(p2)
segments.append([p1, p2])
constraints.append(Constraint(type='segment', targets=[p1, p2], value=0))
logger.debug(f"[DSLParser] + SEGMENT: {p1}—{p2}")
continue
# LINE(A, B)
m = re.match(r'LINE\(([^,]+)(?:,\s*([^)]+))?\)', line)
if m:
if m.group(2):
p1, p2 = m.group(1).strip(), m.group(2).strip()
else:
pts = _parse_point_tokens(m.group(1))
p1, p2 = pts[0], pts[1] if len(pts) > 1 else 'B'
ensure_point(p1)
ensure_point(p2)
lines_ext.append([p1, p2])
constraints.append(Constraint(type='line', targets=[p1, p2], value=0))
logger.debug(f"[DSLParser] + LINE: {p1}-{p2}")
continue
# RAY(A, B)
m = re.match(r'RAY\(([^,]+)(?:,\s*([^)]+))?\)', line)
if m:
if m.group(2):
p1, p2 = m.group(1).strip(), m.group(2).strip()
else:
pts = _parse_point_tokens(m.group(1))
p1, p2 = pts[0], pts[1] if len(pts) > 1 else 'B'
ensure_point(p1)
ensure_point(p2)
rays.append([p1, p2])
constraints.append(Constraint(type='ray', targets=[p1, p2], value=0))
logger.debug(f"[DSLParser] + RAY: {p1}->{p2}")
continue
# SQUARE(ABCD)
m = re.match(r'SQUARE\(([^)]+)\)', line)
if m:
pts = _parse_point_tokens(m.group(1))
if len(pts) == 4:
pA, pB, pC, pD = pts
for p in pts: ensure_point(p)
_add_poly_segments(pts, segments, constraints)
if not polygon_order: polygon_order = list(pts)
constraints.append(Constraint(type='perpendicular', targets=[pA, pB, pA, pD], value=0))
constraints.append(Constraint(type='perpendicular', targets=[pB, pA, pB, pC], value=0))
constraints.append(Constraint(type='parallel', targets=[pA, pB, pD, pC], value=0))
constraints.append(Constraint(type='parallel', targets=[pA, pD, pB, pC], value=0))
constraints.append(Constraint(type='length_equal', targets=[pA, pB, pB, pC], value=0))
constraints.append(Constraint(type='length_equal', targets=[pB, pC, pC, pD], value=0))
constraints.append(Constraint(type='length_equal', targets=[pC, pD, pD, pA], value=0))
if is_3d:
constraints.append(Constraint(type='coplanar', targets=[pA, pB, pC, pD], value=0))
logger.debug(f"[DSLParser] + SQUARE: {pts}")
continue
# RECTANGLE(ABCD)
m = re.match(r'RECTANGLE\(([^)]+)\)', line)
if m:
pts = _parse_point_tokens(m.group(1))
if len(pts) == 4:
pA, pB, pC, pD = pts
for p in pts: ensure_point(p)
_add_poly_segments(pts, segments, constraints)
if not polygon_order: polygon_order = list(pts)
constraints.append(Constraint(type='perpendicular', targets=[pA, pB, pA, pD], value=0))
constraints.append(Constraint(type='perpendicular', targets=[pB, pA, pB, pC], value=0))
constraints.append(Constraint(type='parallel', targets=[pA, pB, pD, pC], value=0))
constraints.append(Constraint(type='parallel', targets=[pA, pD, pB, pC], value=0))
constraints.append(Constraint(type='length_equal', targets=[pA, pB, pD, pC], value=0))
constraints.append(Constraint(type='length_equal', targets=[pA, pD, pB, pC], value=0))
if is_3d:
constraints.append(Constraint(type='coplanar', targets=[pA, pB, pC, pD], value=0))
logger.debug(f"[DSLParser] + RECTANGLE: {pts}")
continue
# PARALLELOGRAM(ABCD)
m = re.match(r'PARALLELOGRAM\(([^)]+)\)', line)
if m:
pts = _parse_point_tokens(m.group(1))
if len(pts) == 4:
pA, pB, pC, pD = pts
for p in pts: ensure_point(p)
_add_poly_segments(pts, segments, constraints)
if not polygon_order: polygon_order = list(pts)
constraints.append(Constraint(type='parallel', targets=[pA, pB, pD, pC], value=0))
constraints.append(Constraint(type='parallel', targets=[pA, pD, pB, pC], value=0))
constraints.append(Constraint(type='length_equal', targets=[pA, pB, pD, pC], value=0))
constraints.append(Constraint(type='length_equal', targets=[pA, pD, pB, pC], value=0))
if is_3d:
constraints.append(Constraint(type='coplanar', targets=[pA, pB, pC, pD], value=0))
logger.debug(f"[DSLParser] + PARALLELOGRAM: {pts}")
continue
# TRIANGLE(ABC) / PYRAMID(S_ABCD) / PRISM(ABC_DEF)
m = re.match(r'(TRIANGLE|PYRAMID|PRISM)\(([^)]+)\)', line)
if m:
pt_type = m.group(1)
targets = m.group(2)
if pt_type in ["PYRAMID", "PRISM"]:
is_3d = True
if pt_type == "TRIANGLE":
pts = _parse_point_tokens(targets)
for p in pts: ensure_point(p)
_add_poly_segments(pts, segments, constraints)
if not polygon_order: polygon_order = list(pts)
elif pt_type == "PYRAMID":
# S_ABCD or S, ABCD -> S is apex, ABCD is base
if "_" in targets:
apex_raw, base_raw = targets.split("_", 1)
elif "," in targets:
apex_raw, base_raw = targets.split(",", 1)
else:
tokens = _parse_point_tokens(targets)
apex_raw = tokens[0] if tokens else "S"
base_raw = "".join(tokens[1:]) if len(tokens) > 1 else ""
apex = apex_raw.strip()
base = _parse_point_tokens(base_raw)
ensure_point(apex)
for p in base: ensure_point(p)
# Add segments from apex to all base points
for p in base:
if [apex, p] not in segments and [p, apex] not in segments:
segments.append([apex, p])
constraints.append(Constraint(type='segment', targets=[apex, p], value=0))
# Also add base polygon segments
_add_poly_segments(base, segments, constraints)
if not polygon_order: polygon_order = list(base)
solids.append({"type": "pyramid", "apex": apex, "base": base, "points": [apex] + base})
elif pt_type == "PRISM":
# ABC_DEF or ABC, DEF -> two bases
if "_" in targets:
b1_raw, b2_raw = targets.split("_", 1)
elif "," in targets:
b1_raw, b2_raw = targets.split(",", 1)
else:
tokens = _parse_point_tokens(targets)
half = len(tokens) // 2
b1_raw = "".join(tokens[:half])
b2_raw = "".join(tokens[half:])
b1 = _parse_point_tokens(b1_raw)
b2 = _parse_point_tokens(b2_raw)
for p in b1 + b2: ensure_point(p)
# Add base 1 segments
_add_poly_segments(b1, segments, constraints)
# Add base 2 segments
_add_poly_segments(b2, segments, constraints)
# Add lateral edges
for p1, p2 in zip(b1, b2):
if [p1, p2] not in segments and [p2, p1] not in segments:
segments.append([p1, p2])
constraints.append(Constraint(type='segment', targets=[p1, p2], value=0))
if len(b1) >= 2 and len(b2) >= 2:
for i in range(1, len(b1)):
constraints.append(Constraint(type='parallel', targets=[b1[0], b2[0], b1[i], b2[i]], value=0))
constraints.append(Constraint(type='length_equal', targets=[b1[0], b2[0], b1[i], b2[i]], value=0))
has_explicit_altitude = any(kw in text for kw in ("PROJECT_POINT_PLANE", "FOOT_PLANE", "HEIGHT", "ALTITUDE"))
if not has_explicit_altitude:
constraints.append(Constraint(type='perpendicular', targets=[b1[0], b2[0], b1[0], b1[1]], value=0))
if len(b1) >= 3:
constraints.append(Constraint(type='perpendicular', targets=[b1[0], b2[0], b1[0], b1[2]], value=0))
if not polygon_order: polygon_order = list(b1)
solids.append({"type": "prism", "base1": b1, "base2": b2, "points": b1 + b2})
logger.debug(f"[DSLParser] + {pt_type}: {targets}")
continue
logger.warning(f"[DSLParser] ? Unrecognized DSL line: '{line}'")
logger.info(
"[DSLParser] Parsed %d points, %d constraints, is_3d=%s.",
len(points),
len(constraints),
is_3d,
)
# Safety sweep: Ensure all points referenced in constraints actually exist in the points dictionary
for c in constraints:
for pid in c.targets:
if isinstance(pid, str) and pid not in points and not pid.replace('.', '', 1).isdigit():
points[pid] = Point(id=pid)
logger.debug(f"[DSLParser] ! Auto-declared missing point from constraint: {pid}")
# Attach metadata to synthetic constraints for downstream use
if polygon_order:
constraints.append(Constraint(type='polygon_order', targets=polygon_order, value=0))
elif explicit_point_ids:
constraints.append(Constraint(type='explicit_points', targets=explicit_point_ids, value=0))
if lines_ext:
constraints.append(Constraint(type='lines_metadata', targets=[",".join(l) for l in lines_ext], value=0))
if rays:
constraints.append(Constraint(type='rays_metadata', targets=[",".join(l) for l in rays], value=0))
if solids:
import json
constraints.append(Constraint(type='solids_metadata', targets=[json.dumps(s) for s in solids], value=0))
# Pass through ConstraintCompiler for semantic audit & derived constraint expansion (P0)
from .compiler import ConstraintCompiler
compiler = ConstraintCompiler()
compiled_points, compiled_constraints, is_3d = compiler.compile(points, constraints, is_3d)
return compiled_points, compiled_constraints, is_3d