Spaces:
Sleeping
Sleeping
| import re | |
| import logging | |
| from typing import List, Tuple, Dict, Any | |
| from .models import Point, Constraint | |
| logger = logging.getLogger(__name__) | |
| 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]] = [] | |
| segments: List[List[str]] = [] | |
| lines_ext: List[List[str]] = [] | |
| rays: List[List[str]] = [] | |
| is_3d = False | |
| logger.info("==[DSLParser] Parsing DSL input==") | |
| logger.debug(f"[DSLParser] Raw DSL:\n{text}") | |
| lines = text.strip().split('\n') | |
| for line in lines: | |
| line = line.strip() | |
| if not line or line.startswith('//') or line.startswith('#'): | |
| continue | |
| # POINT(A) or POINT(A, 0, 0, 5) | |
| m = re.match(r'POINT\((\w+)(?:,\s*([\d\.-]+),\s*([\d\.-]+)(?:,\s*([\d\.-]+))?)?\)', line) | |
| if m: | |
| name = m.group(1) | |
| 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 | |
| # z=0 with x,y is still the xy-plane; only treat as 3D when z is meaningfully non-zero. | |
| # Otherwise POINT(A,0,0,0) incorrectly forced is_3d and broke 2D engine paths. | |
| if z is not None and abs(z) > 1e-9: | |
| is_3d = True | |
| points[name] = Point(id=name, x=x, y=y, z=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) | |
| m = re.match(r'LENGTH\((\w+),\s*([\d\.]+)\)', line) | |
| if m: | |
| target, value = m.group(1), float(m.group(2)) | |
| pts = [target[i:i+1] for i in range(len(target))] | |
| constraints.append(Constraint(type='length', targets=pts, value=value)) | |
| logger.debug(f"[DSLParser] + LENGTH: {pts} = {value}") | |
| continue | |
| # ANGLE(A, 90) or ANGLE(A, 90deg) | |
| m = re.match(r'ANGLE\((\w+),\s*([\d\.]+)(?:deg)?\)', line) | |
| if m: | |
| target, value = m.group(1), float(m.group(2)) | |
| constraints.append(Constraint(type='angle', targets=[target], value=value)) | |
| logger.debug(f"[DSLParser] + ANGLE: vertex={target}, degrees={value}") | |
| continue | |
| # PARALLEL(AB, CD) | |
| m = re.match(r'PARALLEL\((\w+),\s*(\w+)\)', line) | |
| if m: | |
| seg1, seg2 = m.group(1), m.group(2) | |
| constraints.append(Constraint(type='parallel', targets=list(seg1) + list(seg2), value=0)) | |
| logger.debug(f"[DSLParser] + PARALLEL: {seg1} || {seg2}") | |
| continue | |
| # PERPENDICULAR(AB, CD) | |
| m = re.match(r'PERPENDICULAR\((\w+),\s*(\w+)\)', line) | |
| if m: | |
| seg1, seg2 = m.group(1), m.group(2) | |
| constraints.append(Constraint(type='perpendicular', targets=list(seg1) + list(seg2), value=0)) | |
| logger.debug(f"[DSLParser] + PERPENDICULAR: {seg1} _|_ {seg2}") | |
| continue | |
| # MIDPOINT(M, AB) — M is midpoint of AB | |
| m = re.match(r'MIDPOINT\((\w+),\s*(\w+)\)', line) | |
| if m: | |
| mid, seg = m.group(1), m.group(2) | |
| if mid not in points: | |
| points[mid] = Point(id=mid) | |
| pts = [mid] + [seg[i:i+1] for i in range(len(seg))] | |
| constraints.append(Constraint(type='midpoint', targets=pts, value=0)) | |
| logger.debug(f"[DSLParser] + MIDPOINT: {mid} = mid({seg})") | |
| continue | |
| # SECTION(E, A, C, 0.66) — E lies on AC s.t. AE = 0.66 * AC | |
| m = re.match(r'SECTION\((\w+),\s*(\w+),\s*(\w+),\s*([\d\.-]+)\)', line) | |
| if m: | |
| target, p1, p2, k = m.group(1), m.group(2), m.group(3), float(m.group(4)) | |
| if target not in points: | |
| points[target] = Point(id=target) | |
| constraints.append(Constraint(type='section', targets=[target, p1, p2], value=k)) | |
| logger.debug(f"[DSLParser] + SECTION: {target} = {p1} + {k}({p2}-{p1})") | |
| continue | |
| # CIRCLE(O, r) | |
| m = re.match(r'CIRCLE\((\w+),\s*([\d\.]+)\)', line) | |
| if m: | |
| center, radius = m.group(1), float(m.group(2)) | |
| if center not in points: | |
| points[center] = Point(id=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 | |
| # POLYGON_ORDER(A, B, C, D) — thứ tự nối điểm để vẽ đa giác | |
| m = re.match(r'POLYGON_ORDER\(([^)]+)\)', line) | |
| if m: | |
| polygon_order = [p.strip() for p in m.group(1).split(',')] | |
| logger.debug(f"[DSLParser] + POLYGON_ORDER: {polygon_order}") | |
| continue | |
| # SEGMENT(M, N) — đoạn thẳng phụ cần vẽ | |
| m = re.match(r'SEGMENT\((\w+),\s*(\w+)\)', line) | |
| if m: | |
| p1, p2 = m.group(1), m.group(2) | |
| 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) — infinite line | |
| m = re.match(r'LINE\((\w+),\s*(\w+)\)', line) | |
| if m: | |
| p1, p2 = m.group(1), m.group(2) | |
| 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) — ray AB starting at A | |
| m = re.match(r'RAY\((\w+),\s*(\w+)\)', line) | |
| if m: | |
| p1, p2 = m.group(1), m.group(2) | |
| rays.append([p1, p2]) | |
| constraints.append(Constraint(type='ray', targets=[p1, p2], value=0)) | |
| logger.debug(f"[DSLParser] + RAY: {p1}->{p2}") | |
| 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": | |
| if not polygon_order: polygon_order = list(targets) | |
| elif pt_type == "PYRAMID": | |
| # S_ABCD -> S is apex, ABCD is base | |
| if "_" in targets: | |
| apex, base = targets.split("_") | |
| # Add segments from apex to all base points | |
| for p in base: | |
| segments.append([apex, p]) | |
| constraints.append(Constraint(type='segment', targets=[apex, p], value=0)) | |
| if not polygon_order: polygon_order = list(base) | |
| elif pt_type == "PRISM": | |
| # ABC_DEF -> two bases | |
| if "_" in targets: | |
| b1, b2 = targets.split("_") | |
| for p1, p2 in zip(b1, b2): | |
| segments.append([p1, p2]) | |
| constraints.append(Constraint(type='segment', targets=[p1, p2], value=0)) | |
| logger.debug(f"[DSLParser] + {pt_type}: {targets}") | |
| continue | |
| # SPHERE(O, r) | |
| m = re.match(r'SPHERE\((\w+),\s*([\d\.]+)\)', line) | |
| if m: | |
| is_3d = True | |
| center, radius = m.group(1), float(m.group(2)) | |
| if center not in points: | |
| points[center] = Point(id=center) | |
| constraints.append(Constraint(type='sphere', targets=[center], value=radius)) | |
| logger.debug(f"[DSLParser] + SPHERE: center={center}, r={radius}") | |
| 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: | |
| # Some targets might be values or comma-separated strings (handled elsewhere), | |
| # but most are single-character point IDs. | |
| if isinstance(pid, str) and len(pid) == 1 and pid not in points: | |
| points[pid] = Point(id=pid) | |
| logger.debug(f"[DSLParser] ! Auto-declared missing point from constraint: {pid}") | |
| # Attach metadata to a synthetic constraint for downstream use | |
| if polygon_order: | |
| constraints.append(Constraint(type='polygon_order', targets=polygon_order, value=0)) | |
| elif explicit_point_ids: | |
| # Re-use polygon_order as a carrier for explicit points IF no real order was specified | |
| constraints.append(Constraint(type='explicit_points', targets=explicit_point_ids, value=0)) | |
| # Add auxiliary metadata for lines and rays | |
| 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)) | |
| return list(points.values()), constraints, is_3d | |