sanjuhs's picture
Upload folder using huggingface_hub
6de1b61 verified
from __future__ import annotations
from typing import Any
from .cadquery_builder import build_cadquery_artifact
from .models import Design, Feature, Hole, ToolAction
from .solver3d import solve_3d_linear_elasticity
def _feature_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"}:
return "freeform_object"
return "bracket"
def _design_bounds(design: Design) -> dict[str, float]:
xs = [0.0, design.base_length_mm, design.load_point_x_mm]
ys = [-design.base_width_mm / 2, design.base_width_mm / 2, design.load_point_y_mm]
zs = [0.0, design.base_thickness_mm]
for feature in design.features:
xs.extend([feature.x, feature.x2])
ys.extend([feature.y, feature.y2])
zs.extend([feature.height, feature.radius])
return {
"min_x": round(min(xs), 3),
"max_x": round(max(xs), 3),
"min_y": round(min(ys), 3),
"max_y": round(max(ys), 3),
"min_z": 0.0,
"max_z": round(max(zs), 3),
}
def design_family_template(family: str) -> Design:
if family.startswith("blank_"):
design = design_family_template(family.removeprefix("blank_"))
design.title = "Blank " + design.title
design.rationale = "Initialized as a blank family envelope; later tool calls add visible CAD features."
design.features = []
return design
if family == "wall_hook":
return Design(
title="Wall-mounted J hook for 120 N hanging load",
rationale="A compact wall plate carries two mounting bolts while a thick J-shaped hook tube carries the hanging load at its curled tip.",
material="aluminum_6061",
load_newtons=120,
load_point_x_mm=62,
load_point_y_mm=0,
base_length_mm=74,
base_width_mm=54,
base_thickness_mm=6,
fixed_holes=[Hole(x=10, y=-15, radius=3), Hole(x=10, y=15, radius=3)],
features=[
Feature(type="hook_curve", x=12, y=0, x2=68, y2=0, width=8, height=34, radius=10, note="round J hook tube"),
Feature(type="boss", x=62, y=0, height=1, radius=4, note="hook lip/load contact proxy"),
],
expected_failure_mode="Bending at hook root and mounting-hole bearing stress.",
action_plan=["Create wall mounting plate", "Extrude curved hook tube", "Thicken root boss", "Run FEA proxy", "Commit hook geometry"],
)
if family == "torque_clamp":
return Design(
title="Split clamp fixture for 120 Nm shaft torque",
rationale="Two jaws wrap a shaft proxy while a fixed root resists torque through a compact shear path.",
material="aluminum_6061",
load_newtons=120,
load_point_x_mm=68,
load_point_y_mm=0,
base_length_mm=92,
base_width_mm=58,
base_thickness_mm=8,
fixed_holes=[Hole(x=12, y=-18, radius=3), Hole(x=12, y=18, radius=3)],
features=[
Feature(type="clamp_jaw", x=34, y=-18, x2=78, y2=-18, width=10, height=18, radius=9, note="lower clamp jaw"),
Feature(type="clamp_jaw", x=34, y=18, x2=78, y2=18, width=10, height=18, radius=9, note="upper clamp jaw"),
Feature(type="boss", x=66, y=0, height=12, radius=12, note="shaft torque proxy"),
],
expected_failure_mode="Jaw bending and root shear under torque proxy load.",
action_plan=["Create split clamp jaws", "Place shaft proxy", "Add fixed root holes", "Run torque proxy FEA", "Commit clamp"],
)
if family == "motor_stator":
return Design(
title="12-slot axial motor stator concept",
rationale="A circular stator ring with twelve teeth gives a recognizable motor design that can later connect to electromagnetic and thermal solvers.",
material="steel_1018",
load_newtons=80,
load_point_x_mm=52,
load_point_y_mm=0,
base_length_mm=96,
base_width_mm=96,
base_thickness_mm=8,
fixed_holes=[],
features=[
Feature(type="stator_ring", x=48, y=0, width=14, height=8, radius=34, note="lamination ring"),
Feature(type="stator_tooth", x=48, y=0, width=9, height=18, radius=12, note="12 radial teeth"),
Feature(type="boss", x=48, y=0, height=8, radius=6, note="center shaft/load proxy"),
],
expected_failure_mode="Thermal expansion and tooth/root stress are future solver targets.",
action_plan=["Create stator ring", "Add radial teeth", "Add center shaft proxy", "Run structural proxy", "Commit stator"],
)
if family == "chair":
return Design(
title="Simple full chair with backrest",
rationale="A rectangular seat panel, four splayed legs, crossbars, and a backrest give a simple recognizable chair for load and deflection tests.",
material="aluminum_6061",
load_newtons=700,
load_point_x_mm=45,
load_point_y_mm=0,
base_length_mm=90,
base_width_mm=70,
base_thickness_mm=6,
fixed_holes=[],
features=[
Feature(type="seat_panel", x=45, y=0, width=90, height=6, radius=0, note="seat panel"),
Feature(type="chair_leg", x=14, y=-24, x2=6, y2=-32, width=6, height=55, radius=0, note="front left leg"),
Feature(type="chair_leg", x=76, y=-24, x2=84, y2=-32, width=6, height=55, radius=0, note="front right leg"),
Feature(type="chair_leg", x=14, y=24, x2=6, y2=32, width=6, height=55, radius=0, note="rear left leg"),
Feature(type="chair_leg", x=76, y=24, x2=84, y2=32, width=6, height=55, radius=0, note="rear right leg"),
Feature(type="chair_back", x=45, y=31, width=84, height=44, radius=0, note="upright backrest panel"),
Feature(type="chair_crossbar", x=45, y=-30, width=84, height=4, radius=0, note="front leg crossbar"),
],
expected_failure_mode="Leg buckling and seat deflection under distributed downward load.",
action_plan=["Create seat", "Add four legs", "Add backrest", "Apply distributed load proxy", "Run structural proxy", "Commit chair"],
)
if family == "table":
return Design(
title="Small freeform table",
rationale="A tabletop plus individually added legs and stretchers forms a table from primitive CAD features.",
material="aluminum_6061",
load_newtons=500,
load_point_x_mm=50,
load_point_y_mm=0,
base_length_mm=100,
base_width_mm=70,
base_thickness_mm=6,
fixed_holes=[],
features=[
Feature(type="tabletop", x=50, y=0, width=100, height=6, radius=4, note="small tabletop panel"),
Feature(type="table_leg", x=12, y=-28, width=6, height=48, radius=3, note="table leg"),
Feature(type="table_leg", x=88, y=-28, width=6, height=48, radius=3, note="table leg"),
Feature(type="table_leg", x=12, y=28, width=6, height=48, radius=3, note="table leg"),
Feature(type="table_leg", x=88, y=28, width=6, height=48, radius=3, note="table leg"),
],
expected_failure_mode="Tabletop bending and leg buckling under downward load.",
action_plan=["Create tabletop", "Add requested legs", "Add stretchers", "Apply distributed load proxy", "Run structural proxy"],
)
if family == "freeform_object":
return Design(
title="Freeform primitive CAD object",
rationale="A blank primitive grammar object; the agent composes panels, tubes, curves, feet, and decorative elements from the prompt.",
material="aluminum_6061",
load_newtons=120,
load_point_x_mm=50,
load_point_y_mm=0,
base_length_mm=100,
base_width_mm=70,
base_thickness_mm=6,
fixed_holes=[],
features=[],
expected_failure_mode="Depends on primitive layout and load path.",
action_plan=["Compose primitive CAD features", "Apply load", "Run FEA", "Iterate"],
)
return Design(
title=f"{family.replace('_', ' ').title()}",
rationale="Initialized from a ribbed cantilever design family template.",
fixed_holes=[Hole(x=12, y=-13, radius=3), Hole(x=12, y=13, radius=3)],
features=[
Feature(type="boss", x=90, y=0, height=7, radius=6, note="default load boss"),
],
)
def apply_action(design: Design | None, action: ToolAction, prompt: str = "") -> tuple[Design | None, dict[str, Any]]:
"""Apply one agent tool action to the parametric design state."""
if action.tool == "create_design_family":
family = action.params.get("family", "ribbed_cantilever_bracket")
design = design_family_template(str(family))
prompt_text = prompt.lower()
if "chair" in str(family) and (
"curv" in prompt_text
or "round" in prompt_text
or "organic" in prompt_text
or "sweep" in prompt_text
or "arched" in prompt_text
or "bent" in prompt_text
or " flowing" in prompt_text
or prompt_text.strip().startswith("flow ")
):
design.title = "Blank curvy full chair with arched backrest"
design.rationale = "Initialized as a curvy chair envelope; later tools add rounded seat, splayed tubular legs, curved crossbars, and an arched backrest."
return design, {"valid": True, "message": f"Created design family {family}."}
if design is None:
return design, {"valid": False, "error": f"Tool {action.tool} requires an active design."}
if action.tool == "set_material":
design.material = action.params.get("material", design.material)
return design, {"valid": True, "changed_parameters": ["material"]}
if action.tool == "set_envelope":
for key, attr in [("length_mm", "base_length_mm"), ("width_mm", "base_width_mm"), ("thickness_mm", "base_thickness_mm")]:
if key in action.params:
setattr(design, attr, float(action.params[key]))
return design, {"valid": True, "changed_parameters": ["base_length_mm", "base_width_mm", "base_thickness_mm"]}
if action.tool == "set_load":
vector = action.params.get("vector_n", [0, 0, -design.load_newtons])
point = action.params.get("point_mm") or action.params.get("point") or [design.load_point_x_mm, design.load_point_y_mm, design.base_thickness_mm]
design.load_newtons = abs(float(vector[2] if len(vector) > 2 else vector[-1]))
design.load_point_x_mm = float(point[0])
design.load_point_y_mm = float(point[1])
return design, {"valid": True, "changed_parameters": ["load_newtons", "load_point_x_mm", "load_point_y_mm"]}
if action.tool == "add_mount_hole":
center = action.params.get("center", [action.params.get("x", 12), action.params.get("y", 0)])
design.fixed_holes.append(Hole(x=float(center[0]), y=float(center[1]), radius=float(action.params.get("radius_mm", action.params.get("radius", 3)))))
return design, {"valid": True, "changed_parameters": ["fixed_holes"]}
if action.tool == "add_rib":
start = action.params.get("start", [action.params.get("x", 10), action.params.get("y", 0), 0])
end = action.params.get("end", [action.params.get("x2", 90), action.params.get("y2", 0), 10])
design.features.append(
Feature(
type="rib",
x=float(start[0]),
y=float(start[1]),
x2=float(end[0]),
y2=float(end[1]),
width=float(action.params.get("width_mm", action.params.get("width", 4))),
height=float(action.params.get("height_mm", action.params.get("height", max(end[2] if len(end) > 2 else 12, 1)))),
note=str(action.params.get("id", "agent rib")),
)
)
return design, {"valid": True, "changed_parameters": ["features"]}
if action.tool == "add_lightening_hole":
center = action.params.get("center", [action.params.get("x", 50), action.params.get("y", 0), 0])
design.features.append(
Feature(
type="lightening_hole",
x=float(center[0]),
y=float(center[1]),
radius=float(action.params.get("radius_mm", action.params.get("radius", 4))),
note=str(action.params.get("id", "agent lightening hole")),
)
)
return design, {"valid": True, "changed_parameters": ["features"]}
if action.tool == "add_feature":
params = dict(action.params)
feature_type = str(params.pop("type"))
design.features.append(
Feature(
type=feature_type, # type: ignore[arg-type]
x=float(params.get("x", 0)),
y=float(params.get("y", 0)),
x2=float(params.get("x2", 0)),
y2=float(params.get("y2", 0)),
width=float(params.get("width", params.get("width_mm", 0))),
height=float(params.get("height", params.get("height_mm", 0))),
radius=float(params.get("radius", params.get("radius_mm", 0))),
note=str(params.get("note", params.get("id", feature_type))),
)
)
return design, {"valid": True, "changed_parameters": ["features"], "added_feature": feature_type}
if action.tool == "observe_design":
return design, {
"valid": True,
"observation": {
"family": _feature_family(design),
"feature_count": len(design.features),
"fixed_hole_count": len(design.fixed_holes),
"bounds_mm": _design_bounds(design),
"load_point_mm": [design.load_point_x_mm, design.load_point_y_mm, design.base_thickness_mm],
"material": design.material,
},
}
if action.tool == "measure_clearance":
bounds = _design_bounds(design)
return design, {
"valid": True,
"measurement": {
"bounds_mm": bounds,
"envelope_length_mm": round(bounds["max_x"] - bounds["min_x"], 3),
"envelope_width_mm": round(bounds["max_y"] - bounds["min_y"], 3),
"feature_count": len(design.features),
"load_is_inside_nominal_envelope": 0 <= design.load_point_x_mm <= design.base_length_mm
and -design.base_width_mm / 2 <= design.load_point_y_mm <= design.base_width_mm / 2,
},
}
if action.tool == "check_constraints":
family = _feature_family(design)
missing: list[str] = []
if family == "chair":
for required in ["seat_panel", "chair_leg", "chair_back"]:
if not any(feature.type == required for feature in design.features):
missing.append(required)
if sum(1 for feature in design.features if feature.type == "chair_leg") < 4:
missing.append("four_chair_legs")
if family == "torque_clamp" and sum(1 for feature in design.features if feature.type == "clamp_jaw") < 2:
missing.append("two_split_clamp_jaws")
if family == "motor_stator" and not any(feature.type == "stator_tooth" for feature in design.features):
missing.append("radial_stator_teeth")
return design, {"valid": True, "family": family, "constraint_status": "pass" if not missing else "needs_repair", "missing": missing}
if action.tool == "visual_snapshot":
return design, {
"valid": True,
"snapshot": {
"view": action.params.get("view", "isometric"),
"camera": action.params.get("camera", "auto"),
"note": "Renderer should inspect this view for identity, load contact, and disconnected geometry.",
"family": _feature_family(design),
"bounds_mm": _design_bounds(design),
},
}
if action.tool == "critique_geometry":
family = _feature_family(design)
notes = []
if family == "torque_clamp":
notes.append("Blue cylinder is shaft/load proxy; green arcs and jaws should visibly wrap it with a small split gap.")
if family == "chair":
notes.append("Seat, four legs, crossbars, and backrest should be visible from isometric and side views.")
if family == "motor_stator":
notes.append("Stator should read as a flat toothed annulus with visible center bore and radial slots.")
if family == "wall_hook":
notes.append("Hook tip must be the load contact; load arrow should terminate on the curved hook.")
return design, {"valid": True, "critique": notes or ["Generic bracket load path should remain connected."]}
if action.tool == "run_fea":
return design, {"valid": True, "simulation": solve_3d_linear_elasticity(design, prompt)}
if action.tool == "export_cadquery":
return design, build_cadquery_artifact(design)
if action.tool == "commit_design":
return design, {"valid": True, "committed": True, "simulation": solve_3d_linear_elasticity(design, prompt)}
return design, {"valid": False, "error": f"Unknown tool: {action.tool}"}
def run_actions(actions: list[ToolAction], prompt: str = "", initial_design: Design | None = None) -> dict[str, Any]:
design: Design | None = initial_design
trace = []
last_result: dict[str, Any] = {}
for idx, action in enumerate(actions, start=1):
design, result = apply_action(design, action, prompt)
last_result = result
trace.append({"step": idx, "action": action.model_dump(), "result": result, "design": design.model_dump() if design else None})
if result.get("committed"):
break
return {"design": design.model_dump() if design else None, "last_result": last_result, "trace": trace}