Spaces:
Running
Running
| """Canonical Standard Geometry Constructors (P1). | |
| Provides hierarchical, intuitive construction strategies for standard 2D/3D shapes | |
| and solids before falling back to generic numerical optimization. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| import math | |
| from typing import Any, Dict, List, Optional, Set, Tuple | |
| import numpy as np | |
| from .models import Point, Constraint | |
| from .validator import GeometryValidator | |
| logger = logging.getLogger(__name__) | |
| class StandardGeometryConstructor: | |
| """ | |
| Hierarchical and canonical geometry constructor for standard 2D and 3D shapes. | |
| Constructs well-formed, intuitive default representations on canonical planes (z=0 for 3D bases) | |
| while strictly preserving mathematical lengths and explicit user coordinates. | |
| """ | |
| def __init__(self): | |
| self.validator = GeometryValidator(tolerance=0.02) | |
| def try_construct( | |
| self, | |
| points: List[Point], | |
| constraints: List[Constraint], | |
| solids_meta: List[Dict[str, Any]], | |
| is_3d: bool = False, | |
| ) -> Optional[Dict[str, Any]]: | |
| """ | |
| Attempts hierarchical canonical construction. | |
| Returns engine result dict if successful and fully validated, else None. | |
| """ | |
| # If user gave explicit coordinates for multiple points, let the general solver handle it | |
| explicit_pts = {p.id: p for p in points if p.x is not None or p.y is not None or p.z is not None} | |
| if len(explicit_pts) >= 2: | |
| return None | |
| point_ids = [p.id for p in points] | |
| lengths: Dict[Tuple[str, str], float] = {} | |
| for c in constraints: | |
| if c.type == "length" and len(c.targets) == 2: | |
| p1, p2 = c.targets[0], c.targets[1] | |
| val = float(c.value) | |
| lengths[(p1, p2)] = val | |
| lengths[(p2, p1)] = val | |
| def get_len(p1: str, p2: str, default: float = 5.0) -> float: | |
| return lengths.get((p1, p2), default) | |
| # ========================================================================= | |
| # 1. 3D SOLIDS CANONICAL CONSTRUCTORS | |
| # ========================================================================= | |
| if is_3d: | |
| # --------------------------------------------------------------------- | |
| # A. PYRAMID (S_ABCD or S_ABC) | |
| # --------------------------------------------------------------------- | |
| pyramid_solid = next((s for s in solids_meta if s.get("type") == "pyramid"), None) | |
| if pyramid_solid: | |
| apex = pyramid_solid.get("apex") | |
| base = pyramid_solid.get("base", []) | |
| if apex and len(base) in (3, 4): | |
| coords: Dict[str, List[float]] = {} | |
| # 1. Construct Base on z=0 | |
| if len(base) == 4: | |
| # Quadrilateral base: Square / Rectangle | |
| pA, pB, pC, pD = base[0], base[1], base[2], base[3] | |
| side_ab = get_len(pA, pB, 6.0) | |
| side_bc = get_len(pB, pC, side_ab) | |
| coords[pA] = [0.0, 0.0, 0.0] | |
| coords[pB] = [side_ab, 0.0, 0.0] | |
| coords[pC] = [side_ab, side_bc, 0.0] | |
| coords[pD] = [0.0, side_bc, 0.0] | |
| else: | |
| # Triangular base | |
| pA, pB, pC = base[0], base[1], base[2] | |
| side_ab = get_len(pA, pB, 6.0) | |
| side_bc = get_len(pB, pC, side_ab) | |
| side_ca = get_len(pC, pA, side_ab) | |
| # Equilateral or general triangle in z=0 | |
| coords[pA] = [0.0, 0.0, 0.0] | |
| coords[pB] = [side_ab, 0.0, 0.0] | |
| # Solve C_x, C_y in z=0 | |
| cos_A = (side_ab**2 + side_ca**2 - side_bc**2) / (2 * side_ab * side_ca + 1e-9) | |
| cos_A = max(-1.0, min(1.0, cos_A)) | |
| sin_A = math.sqrt(max(0.0, 1.0 - cos_A**2)) | |
| coords[pC] = [side_ca * cos_A, side_ca * sin_A, 0.0] | |
| # 2. Determine Foot of Altitude O | |
| # Check if explicit center / foot constraint exists | |
| foot_id = None | |
| for c in constraints: | |
| if c.type in ("center", "centroid") and len(c.targets) >= 2: | |
| if c.targets[0] in point_ids and set(c.targets[1:]).issubset(set(base)): | |
| foot_id = c.targets[0] | |
| break | |
| elif c.type in ("perp_plane", "height", "altitude") and len(c.targets) >= 2: | |
| if c.targets[0] == apex and c.targets[1] in point_ids: | |
| foot_id = c.targets[1] | |
| break | |
| base_vecs = [np.array(coords[bp]) for bp in base] | |
| mean_center = np.mean(base_vecs, axis=0) | |
| if foot_id and foot_id not in coords: | |
| coords[foot_id] = [float(mean_center[0]), float(mean_center[1]), 0.0] | |
| # 3. Determine Height / Apex S | |
| height = None | |
| if foot_id: | |
| height = lengths.get((apex, foot_id)) | |
| if height is None: | |
| # Check lateral edge length | |
| lateral_len = get_len(apex, base[0], None) | |
| if lateral_len is not None: | |
| r_foot = float(np.linalg.norm(coords[base[0]] - mean_center)) | |
| if lateral_len > r_foot: | |
| height = math.sqrt(lateral_len**2 - r_foot**2) | |
| if height is None: | |
| height = 8.0 | |
| apex_x = coords[foot_id][0] if foot_id and foot_id in coords else mean_center[0] | |
| apex_y = coords[foot_id][1] if foot_id and foot_id in coords else mean_center[1] | |
| coords[apex] = [float(apex_x), float(apex_y), float(height)] | |
| # 4. Resolve any additional auxiliary points (midpoints, sections, point_on) | |
| self._resolve_auxiliary_points(coords, constraints, point_ids) | |
| # Validate construction | |
| engine_res = {"coordinates": coords, "solids": solids_meta, "drawing_phases": []} | |
| val = self.validator.validate(engine_res, constraints, is_3d=True) | |
| if val.is_valid: | |
| logger.info("[StandardGeometryConstructor] Canonical Pyramid construction SUCCESS.") | |
| return engine_res | |
| # --------------------------------------------------------------------- | |
| # B. PRISM / CUBE / CUBOID | |
| # --------------------------------------------------------------------- | |
| prism_solid = next((s for s in solids_meta if s.get("type") in ("prism", "cube", "cuboid")), None) | |
| if prism_solid: | |
| b1 = prism_solid.get("base1", []) | |
| b2 = prism_solid.get("base2", []) | |
| s_type = prism_solid.get("type") | |
| if len(b1) == len(b2) and len(b1) in (3, 4): | |
| coords: Dict[str, List[float]] = {} | |
| height = get_len(b1[0], b2[0], 6.0) | |
| if len(b1) == 4: | |
| side_a = get_len(b1[0], b1[1], 5.0) | |
| side_b = side_a if s_type == "cube" else get_len(b1[1], b1[2], 4.0) | |
| if s_type == "cube": | |
| height = side_a | |
| coords[b1[0]] = [0.0, 0.0, 0.0] | |
| coords[b1[1]] = [side_a, 0.0, 0.0] | |
| coords[b1[2]] = [side_a, side_b, 0.0] | |
| coords[b1[3]] = [0.0, side_b, 0.0] | |
| else: | |
| side_a = get_len(b1[0], b1[1], 5.0) | |
| coords[b1[0]] = [0.0, 0.0, 0.0] | |
| coords[b1[1]] = [side_a, 0.0, 0.0] | |
| coords[b1[2]] = [side_a / 2.0, side_a * math.sqrt(3) / 2.0, 0.0] | |
| # Translate Base 2 along +Z | |
| for p1, p2 in zip(b1, b2): | |
| coords[p2] = [coords[p1][0], coords[p1][1], float(height)] | |
| self._resolve_auxiliary_points(coords, constraints, point_ids) | |
| engine_res = {"coordinates": coords, "solids": solids_meta, "drawing_phases": []} | |
| val = self.validator.validate(engine_res, constraints, is_3d=True) | |
| if val.is_valid: | |
| logger.info(f"[StandardGeometryConstructor] Canonical {s_type} construction SUCCESS.") | |
| return engine_res | |
| # ========================================================================= | |
| # 2. 2D POLYGON CANONICAL CONSTRUCTORS | |
| # ========================================================================= | |
| else: | |
| poly_constraint = next( | |
| (c for c in constraints if c.type in ("square", "rectangle", "equilateral_triangle", "right_triangle")), | |
| None, | |
| ) | |
| if poly_constraint: | |
| c_type = poly_constraint.type | |
| targets = poly_constraint.targets | |
| coords: Dict[str, List[float]] = {} | |
| if c_type == "square" and len(targets) >= 4: | |
| pA, pB, pC, pD = targets[:4] | |
| side = get_len(pA, pB, 6.0) | |
| coords[pA] = [0.0, 0.0, 0.0] | |
| coords[pB] = [side, 0.0, 0.0] | |
| coords[pC] = [side, side, 0.0] | |
| coords[pD] = [0.0, side, 0.0] | |
| elif c_type == "rectangle" and len(targets) >= 4: | |
| pA, pB, pC, pD = targets[:4] | |
| side_a = get_len(pA, pB, 8.0) | |
| side_b = get_len(pB, pC, 6.0) | |
| coords[pA] = [0.0, 0.0, 0.0] | |
| coords[pB] = [side_a, 0.0, 0.0] | |
| coords[pC] = [side_a, side_b, 0.0] | |
| coords[pD] = [0.0, side_b, 0.0] | |
| elif c_type == "equilateral_triangle" and len(targets) >= 3: | |
| pA, pB, pC = targets[:3] | |
| side = get_len(pA, pB, 6.0) | |
| coords[pA] = [0.0, 0.0, 0.0] | |
| coords[pB] = [side, 0.0, 0.0] | |
| coords[pC] = [side / 2.0, side * math.sqrt(3) / 2.0, 0.0] | |
| elif c_type == "right_triangle" and len(targets) >= 3: | |
| pA, pB, pC = targets[:3] | |
| side_ab = get_len(pA, pB, 6.0) | |
| side_bc = get_len(pB, pC, 8.0) | |
| coords[pB] = [0.0, 0.0, 0.0] | |
| coords[pA] = [side_ab, 0.0, 0.0] | |
| coords[pC] = [0.0, side_bc, 0.0] | |
| if coords: | |
| self._resolve_auxiliary_points(coords, constraints, point_ids) | |
| engine_res = {"coordinates": coords, "solids": solids_meta, "drawing_phases": []} | |
| val = self.validator.validate(engine_res, constraints, is_3d=False) | |
| if val.is_valid: | |
| logger.info(f"[StandardGeometryConstructor] Canonical 2D {c_type} construction SUCCESS.") | |
| return engine_res | |
| return None | |
| def _resolve_auxiliary_points( | |
| self, | |
| coords: Dict[str, List[float]], | |
| constraints: List[Constraint], | |
| point_ids: List[str], | |
| ): | |
| """Resolves midpoint, section, center, and point_on auxiliary points iteratively.""" | |
| for _ in range(3): | |
| for c in constraints: | |
| c_type = c.type | |
| targets = c.targets | |
| val = c.value | |
| if c_type == "midpoint" and len(targets) == 3: | |
| pM, pA, pB = targets[0], targets[1], targets[2] | |
| if pM not in coords and pA in coords and pB in coords: | |
| vA = np.array(coords[pA]) | |
| vB = np.array(coords[pB]) | |
| coords[pM] = list((vA + vB) / 2.0) | |
| elif c_type == "section" and len(targets) == 3: | |
| pE, pA, pC = targets[0], targets[1], targets[2] | |
| if pE not in coords and pA in coords and pC in coords: | |
| vA = np.array(coords[pA]) | |
| vC = np.array(coords[pC]) | |
| k = float(val) | |
| coords[pE] = list(vA + k * (vC - vA)) | |
| elif c_type in ("center", "centroid") and len(targets) >= 3: | |
| pO = targets[0] | |
| poly_pts = targets[1:] | |
| if pO not in coords and all(p in coords for p in poly_pts): | |
| poly_vecs = [np.array(coords[p]) for p in poly_pts] | |
| coords[pO] = list(np.mean(poly_vecs, axis=0)) | |
| elif c_type == "point_on" and len(targets) == 3: | |
| pP, pA, pB = targets[0], targets[1], targets[2] | |
| if pP not in coords and pA in coords and pB in coords: | |
| # If length AP is given | |
| len_ap = next( | |
| ( | |
| float(cc.value) | |
| for cc in constraints | |
| if cc.type == "length" | |
| and set(cc.targets[:2]) == {pP, pA} | |
| ), | |
| None, | |
| ) | |
| vA = np.array(coords[pA]) | |
| vB = np.array(coords[pB]) | |
| total_len = float(np.linalg.norm(vB - vA)) | |
| if len_ap is not None and total_len > 1e-4: | |
| t = len_ap / total_len | |
| coords[pP] = list(vA + t * (vB - vA)) | |