from __future__ import annotations import math import re import time from pathlib import Path from typing import Any from .models import Design def _slug(value: str) -> str: return re.sub(r"[^a-z0-9]+", "_", value.lower()).strip("_")[:64] or "design" def _family(design: Design) -> str: feature_types = {feature.type for feature in design.features} if "tabletop" in feature_types: return "table" if "stator_ring" in feature_types: return "motor_stator" if "seat_panel" in feature_types: return "chair" if "clamp_jaw" in feature_types: return "torque_clamp" if "hook_curve" in feature_types: return "wall_hook" if feature_types & {"generic_panel", "support_tube", "curved_tube", "flat_foot", "armrest", "headrest", "table_leg"}: return "freeform_object" return "bracket" def _box(cq: Any, x: float, y: float, z: float, cx: float, cy: float, cz: float): return cq.Workplane("XY").box(x, y, z).translate((cx, cy, cz)) def _cylinder_x(cq: Any, radius: float, length: float, cx: float, cy: float, cz: float): return cq.Workplane("YZ").circle(radius).extrude(length).translate((cx - length / 2, cy, cz)) def _cylinder_y(cq: Any, radius: float, length: float, cx: float, cy: float, cz: float): return cq.Workplane("XZ").circle(radius).extrude(length).translate((cx, cy - length / 2, cz)) def _cylinder_z(cq: Any, radius: float, height: float, cx: float, cy: float, cz: float): return cq.Workplane("XY").circle(radius).extrude(height).translate((cx, cy, cz - height / 2)) def _merge(parts: list[Any]): body = parts[0] for part in parts[1:]: body = body.union(part) return body def _box_between_xy(cq: Any, x1: float, y1: float, x2: float, y2: float, width: float, height: float, z: float): length = max(math.hypot(x2 - x1, y2 - y1), 1) part = _box(cq, length, max(width, 1), max(height, 1), (x1 + x2) / 2, (y1 + y2) / 2, z) return part.rotate(((x1 + x2) / 2, (y1 + y2) / 2, z), ((x1 + x2) / 2, (y1 + y2) / 2, z + 1), math.degrees(math.atan2(y2 - y1, x2 - x1))) def _primitive_part(cq: Any, design: Design, feature: Any, ops: list[dict[str, Any]]): if feature.type in {"tabletop", "generic_panel"}: depth = max(design.base_width_mm, 18) z = 52 if feature.type == "tabletop" else max(feature.height, 4) / 2 ops.append({"op": "box_extrude", "part": feature.type, "center_xy_mm": [feature.x, feature.y], "size_mm": [max(feature.width, 24), depth, max(feature.height, 4)]}) return _box(cq, max(feature.width, 24), depth, max(feature.height, 4), feature.x or design.base_length_mm / 2, feature.y, z) if feature.type == "table_leg": height = max(feature.height, 24) radius = max(feature.radius, feature.width / 2, 2.4) ops.append({"op": "cylinder_extrude", "part": "table_leg", "center_xy_mm": [feature.x, feature.y], "height_mm": height, "radius_mm": radius}) return _cylinder_z(cq, radius, height, feature.x, feature.y, height / 2) if feature.type == "support_tube": z = max(feature.height, 8) ops.append({"op": "box_between", "part": "support_tube", "from_xy_mm": [feature.x, feature.y], "to_xy_mm": [feature.x2, feature.y2]}) return _box_between_xy(cq, feature.x, feature.y, feature.x2, feature.y2, max(feature.width, feature.radius * 2, 3), max(feature.radius * 2, 3), z) if feature.type == "curved_tube": z = max(feature.height, 18) ops.append({"op": "segmented_curve_proxy", "part": "curved_tube", "from_xy_mm": [feature.x, feature.y], "to_xy_mm": [feature.x2, feature.y2]}) return _box_between_xy(cq, feature.x, feature.y, feature.x2, feature.y2, max(feature.width, feature.radius * 2, 3), max(feature.radius * 2, 3), z) if feature.type == "flat_foot": ops.append({"op": "box_extrude", "part": "flat_foot", "center_xy_mm": [feature.x, feature.y]}) return _box(cq, max(feature.width, 16), max(feature.radius * 2.4, 8), max(feature.height, 2.5), feature.x, feature.y, max(feature.height, 2.5) / 2) return None def _build_stator(cq: Any, design: Design, ops: list[dict[str, Any]]): ring = next((item for item in design.features if item.type == "stator_ring"), None) tooth = next((item for item in design.features if item.type == "stator_tooth"), None) cx = ring.x if ring else design.base_length_mm / 2 cy = ring.y if ring else 0 outer = ring.radius + ring.width if ring else 46 inner = max((tooth.radius if tooth else 18), 12) height = max(ring.height if ring else design.base_thickness_mm, 4) body = cq.Workplane("XY").circle(outer).circle(inner).extrude(height) ops.append({"op": "sketch_annulus", "outer_radius_mm": outer, "inner_radius_mm": inner, "height_mm": height}) tooth_count = 12 for idx in range(tooth_count): angle = 360 * idx / tooth_count length = max((tooth.height if tooth else 18), 8) width = max((tooth.width if tooth else 8), 3) radial = inner + length / 2 local = cq.Workplane("XY").box(length, width, height).translate((radial, 0, height / 2)) local = local.rotate((0, 0, 0), (0, 0, 1), angle).translate((cx, cy, 0)) body = body.union(local) ops.append({"op": "union_radial_tooth", "index": idx + 1, "angle_deg": angle, "length_mm": length, "width_mm": width}) body = body.translate((cx, cy, 0)) ops.append({"op": "export_ready_body", "family": "motor_stator", "note": "flat annular stator, not a torus placeholder"}) return body def _build_chair(cq: Any, design: Design, ops: list[dict[str, Any]]): parts = [] seat_z = 48 parts.append(_box(cq, design.base_length_mm, design.base_width_mm, design.base_thickness_mm, design.base_length_mm / 2, 0, seat_z)) ops.append({"op": "box_extrude", "part": "seat_panel", "size_mm": [design.base_length_mm, design.base_width_mm, design.base_thickness_mm]}) for leg in [item for item in design.features if item.type == "chair_leg"]: height = max(leg.height, 35) parts.append(_box(cq, max(leg.width, 4), max(leg.width, 4), height, leg.x, leg.y, height / 2)) ops.append({"op": "extrude_leg", "part": leg.note, "top_xy_mm": [leg.x, leg.y], "height_mm": height}) parts.append(_box(cq, design.base_length_mm, 5, 42, design.base_length_mm / 2, design.base_width_mm / 2 - 3, seat_z + 22)) parts.append(_box(cq, 5, 5, 54, 10, design.base_width_mm / 2 - 3, seat_z + 25)) parts.append(_box(cq, 5, 5, 54, design.base_length_mm - 10, design.base_width_mm / 2 - 3, seat_z + 25)) ops.append({"op": "add_backrest", "parts": ["back_panel", "left_back_post", "right_back_post"]}) decorative = [item for item in design.features if item.type == "decorative_curve"] if decorative: back_y = design.base_width_mm / 2 + 0.5 cx = design.base_length_mm / 2 cz = seat_z + 38 if any("flower" in item.note.lower() or "petal" in item.note.lower() for item in decorative): parts.append(_cylinder_y(cq, 3.2, 1.8, cx, back_y, cz)) for idx in range(6): angle = math.tau * idx / 6 px = cx + math.cos(angle) * 10 pz = cz + math.sin(angle) * 10 parts.append(_cylinder_y(cq, 5.5, 1.6, px, back_y, pz)) ops.append({"op": "union_flower_petal_disc", "index": idx + 1, "center_xz_mm": [round(px, 3), round(pz, 3)]}) parts.append(_cylinder_y(cq, 1.6, 1.6, cx - 2, back_y, seat_z + 18)) ops.append({"op": "add_backrest_flower_pattern", "note": "decorative raised petal discs on backrest"}) else: for idx, item in enumerate(decorative, start=1): parts.append(_cylinder_y(cq, max(item.radius, 2), 1.4, item.x or cx, back_y, seat_z + 24 + idx * 8)) ops.append({"op": "union_decorative_curve_proxy", "index": idx, "note": item.note}) for item in [feature for feature in design.features if feature.type in {"armrest", "headrest", "flat_foot", "support_tube", "curved_tube"}]: primitive = _primitive_part(cq, design, item, ops) if primitive is not None: parts.append(primitive) ops.append({"op": "export_ready_body", "family": "chair"}) return _merge(parts) def _build_table(cq: Any, design: Design, ops: list[dict[str, Any]]): parts = [] for feature in design.features: primitive = _primitive_part(cq, design, feature, ops) if primitive is not None: parts.append(primitive) if not parts: parts.append(_box(cq, design.base_length_mm, design.base_width_mm, design.base_thickness_mm, design.base_length_mm / 2, 0, 52)) ops.append({"op": "box_extrude", "part": "fallback_tabletop"}) ops.append({"op": "export_ready_body", "family": "table"}) return _merge(parts) def _build_freeform(cq: Any, design: Design, ops: list[dict[str, Any]]): parts = [] for feature in design.features: primitive = _primitive_part(cq, design, feature, ops) if primitive is not None: parts.append(primitive) if not parts: parts.append(_box(cq, design.base_length_mm, design.base_width_mm, design.base_thickness_mm, design.base_length_mm / 2, 0, design.base_thickness_mm / 2)) ops.append({"op": "box_extrude", "part": "fallback_freeform_panel"}) ops.append({"op": "export_ready_body", "family": "freeform_object"}) return _merge(parts) def _build_clamp(cq: Any, design: Design, ops: list[dict[str, Any]]): parts = [ _box(cq, 16, design.base_width_mm, 28, 8, 0, 14), _cylinder_x(cq, 11, 58, 58, 0, 22), ] ops.append({"op": "box_extrude", "part": "fixed_root", "size_mm": [16, design.base_width_mm, 28]}) ops.append({"op": "cylinder_extrude", "part": "shaft_bore_proxy", "axis": "x", "radius_mm": 11, "length_mm": 58}) for y in [-18, 18]: parts.append(_box(cq, 54, 10, 15, 53, y, 22)) ops.append({"op": "box_extrude", "part": "split_clamp_jaw", "center_y_mm": y, "size_mm": [54, 10, 15]}) parts.append(_cylinder_x(cq, 14, 12, 82, 0, 22)) ops.append({"op": "add_end_collar", "radius_mm": 14, "note": "visual collar for torque/load application"}) ops.append({"op": "export_ready_body", "family": "torque_clamp"}) return _merge(parts) def _build_hook(cq: Any, design: Design, ops: list[dict[str, Any]]): wall_h = max(design.base_width_mm, 50) parts = [_box(cq, 8, 26, wall_h, 4, 0, wall_h / 2)] ops.append({"op": "box_extrude", "part": "wall_plate", "size_mm": [8, 26, wall_h]}) tube = 4 root_z = wall_h * 0.58 points = [(8, root_z), (32, root_z), (52, root_z - 8), (58, root_z - 28), (44, root_z - 38), (35, root_z - 24)] for idx, ((x1, z1), (x2, z2)) in enumerate(zip(points, points[1:]), start=1): length = math.hypot(x2 - x1, z2 - z1) angle = math.degrees(math.atan2(z2 - z1, x2 - x1)) segment = _cylinder_x(cq, tube, length, (x1 + x2) / 2, 0, (z1 + z2) / 2) segment = segment.rotate(((x1 + x2) / 2, 0, (z1 + z2) / 2), ((x1 + x2) / 2, 1, (z1 + z2) / 2), -angle) parts.append(segment) ops.append({"op": "sweep_hook_segment", "index": idx, "from_xz_mm": [x1, z1], "to_xz_mm": [x2, z2], "tube_radius_mm": tube}) ops.append({"op": "export_ready_body", "family": "wall_hook"}) return _merge(parts) def _build_bracket(cq: Any, design: Design, ops: list[dict[str, Any]]): body = _box(cq, design.base_length_mm, design.base_width_mm, design.base_thickness_mm, design.base_length_mm / 2, 0, design.base_thickness_mm / 2) ops.append({"op": "box_extrude", "part": "base_plate", "size_mm": [design.base_length_mm, design.base_width_mm, design.base_thickness_mm]}) for feature in design.features: if feature.type == "rib": length = max(math.hypot(feature.x2 - feature.x, feature.y2 - feature.y), 1) rib = _box(cq, length, max(feature.width, 1), max(feature.height, 1), (feature.x + feature.x2) / 2, (feature.y + feature.y2) / 2, design.base_thickness_mm + max(feature.height, 1) / 2) rib = rib.rotate((feature.x, feature.y, design.base_thickness_mm), (feature.x, feature.y, design.base_thickness_mm + 1), math.degrees(math.atan2(feature.y2 - feature.y, feature.x2 - feature.x))) body = body.union(rib) ops.append({"op": "union_rib", "from_xy_mm": [feature.x, feature.y], "to_xy_mm": [feature.x2, feature.y2]}) if feature.type == "boss": body = body.union(_cylinder_z(cq, max(feature.radius, 1), max(feature.height, 1), feature.x, feature.y, design.base_thickness_mm + max(feature.height, 1) / 2)) ops.append({"op": "union_boss", "center_xy_mm": [feature.x, feature.y], "radius_mm": feature.radius}) ops.append({"op": "export_ready_body", "family": "bracket"}) return body def build_cadquery_artifact(design: Design, output_root: str | Path = "runs") -> dict[str, Any]: """Build an actual CadQuery body and export manufacturable artifacts.""" operations: list[dict[str, Any]] = [] family = _family(design) output_dir = Path(output_root) / f"{int(time.time() * 1000)}_{_slug(design.title)}" output_dir.mkdir(parents=True, exist_ok=True) try: import cadquery as cq builders = { "motor_stator": _build_stator, "chair": _build_chair, "table": _build_table, "freeform_object": _build_freeform, "torque_clamp": _build_clamp, "wall_hook": _build_hook, "bracket": _build_bracket, } body = builders[family](cq, design, operations) step_path = output_dir / "design.step" stl_path = output_dir / "design.stl" cq.exporters.export(body, str(step_path)) cq.exporters.export(body, str(stl_path)) return { "valid": True, "family": family, "engine": "cadquery", "operations": operations, "step_path": str(step_path), "stl_path": str(stl_path), } except Exception as exc: # pragma: no cover - keeps agent loop alive if CAD kernel fails. operations.append({"op": "cadquery_error", "error": str(exc)}) return { "valid": False, "family": family, "engine": "cadquery", "operations": operations, "error": str(exc), }