274 lines
12 KiB
Python
274 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import copy
|
|
import math
|
|
import re
|
|
from typing import Any
|
|
|
|
|
|
TEMPLATE_ID = "flange_sleeve_v1"
|
|
|
|
# These defaults describe the pictured family, but the model may override every
|
|
# dimension that affects the visible form. The backend owns all CDSL plumbing.
|
|
DEFAULT_PLAN: dict[str, float | str] = {
|
|
"name": "Parameterized flange sleeve",
|
|
"flange_width_mm": 120.0,
|
|
"flange_height_mm": 120.0,
|
|
"flange_thickness_mm": 14.0,
|
|
"corner_chamfer_mm": 10.0,
|
|
"tube_outer_diameter_mm": 70.0,
|
|
"tube_straight_length_mm": 95.0,
|
|
"tip_outer_diameter_mm": 62.0,
|
|
"tip_length_mm": 28.0,
|
|
"bore_diameter_mm": 46.0,
|
|
"boss_outer_diameter_mm": 82.0,
|
|
"boss_height_mm": 5.0,
|
|
"mount_hole_diameter_mm": 12.0,
|
|
"mount_counterbore_diameter_mm": 24.0,
|
|
"mount_counterbore_depth_mm": 5.0,
|
|
"mount_hole_u_mm": 42.0,
|
|
"mount_hole_v_mm": 42.0,
|
|
}
|
|
|
|
_DIMENSION_FIELDS = tuple(key for key in DEFAULT_PLAN if key != "name")
|
|
_PART_ID = re.compile(r"^[A-Za-z0-9_-]{3,80}$")
|
|
|
|
|
|
def flange_sleeve_plan_schema() -> dict[str, Any]:
|
|
"""Return the compact semantic plan accepted by the flange-sleeve tool."""
|
|
properties: dict[str, Any] = {
|
|
"template": {"const": TEMPLATE_ID},
|
|
"name": {"type": "string", "minLength": 1, "maxLength": 120},
|
|
"part_id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{3,80}$"},
|
|
}
|
|
for field in _DIMENSION_FIELDS:
|
|
minimum = 0 if field == "corner_chamfer_mm" else 0.001
|
|
properties[field] = {"type": "number", "exclusiveMinimum": minimum} if minimum else {
|
|
"type": "number", "minimum": 0,
|
|
}
|
|
return {
|
|
"type": "object",
|
|
"properties": properties,
|
|
"required": ["template"],
|
|
"additionalProperties": False,
|
|
}
|
|
|
|
|
|
def normalize_flange_sleeve_plan(plan: Any) -> dict[str, float | str]:
|
|
"""Validate only semantic dimensions; do not accept CDSL implementation data."""
|
|
if not isinstance(plan, dict):
|
|
raise ValueError("flange sleeve plan must be a JSON object")
|
|
if plan.get("template") != TEMPLATE_ID:
|
|
raise ValueError(f"flange sleeve plan template must be {TEMPLATE_ID}")
|
|
allowed = {"template", "part_id", *DEFAULT_PLAN}
|
|
unknown = sorted(str(key) for key in plan if key not in allowed)
|
|
if unknown:
|
|
raise ValueError(f"flange sleeve plan has unsupported fields: {', '.join(unknown)}")
|
|
|
|
normalized = copy.deepcopy(DEFAULT_PLAN)
|
|
for field in _DIMENSION_FIELDS:
|
|
if field not in plan:
|
|
continue
|
|
value = plan[field]
|
|
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(float(value)):
|
|
raise ValueError(f"flange sleeve plan {field} must be a finite number")
|
|
number = float(value)
|
|
if number < 0 if field == "corner_chamfer_mm" else number <= 0:
|
|
comparator = "non-negative" if field == "corner_chamfer_mm" else "greater than zero"
|
|
raise ValueError(f"flange sleeve plan {field} must be {comparator}")
|
|
normalized[field] = number
|
|
|
|
name = str(plan.get("name") or normalized["name"]).strip()
|
|
if not name:
|
|
raise ValueError("flange sleeve plan name must not be empty")
|
|
normalized["name"] = name[:120]
|
|
part_id = str(plan.get("part_id") or "flange_sleeve_template")
|
|
if not _PART_ID.fullmatch(part_id):
|
|
raise ValueError("flange sleeve plan part_id must use letters, numbers, underscores, or hyphens")
|
|
normalized["part_id"] = part_id
|
|
_validate_dimensions(normalized)
|
|
return normalized
|
|
|
|
|
|
def _validate_dimensions(plan: dict[str, float | str]) -> None:
|
|
number = lambda field: float(plan[field])
|
|
width, height = number("flange_width_mm"), number("flange_height_mm")
|
|
thickness, chamfer = number("flange_thickness_mm"), number("corner_chamfer_mm")
|
|
tube_od, tip_od, bore = number("tube_outer_diameter_mm"), number("tip_outer_diameter_mm"), number("bore_diameter_mm")
|
|
boss_od = number("boss_outer_diameter_mm")
|
|
hole_od, counterbore_od = number("mount_hole_diameter_mm"), number("mount_counterbore_diameter_mm")
|
|
counterbore_depth = number("mount_counterbore_depth_mm")
|
|
hole_u, hole_v = number("mount_hole_u_mm"), number("mount_hole_v_mm")
|
|
|
|
if chamfer * 2 >= min(width, height):
|
|
raise ValueError("flange sleeve plan corner_chamfer_mm must be less than half the flange side")
|
|
if not bore < min(tube_od, tip_od):
|
|
raise ValueError("flange sleeve plan bore_diameter_mm must be smaller than both tube diameters")
|
|
if not tube_od <= boss_od <= min(width, height):
|
|
raise ValueError("flange sleeve plan boss_outer_diameter_mm must be between tube diameter and flange side")
|
|
if counterbore_od < hole_od:
|
|
raise ValueError("flange sleeve plan mount_counterbore_diameter_mm must not be smaller than mount_hole_diameter_mm")
|
|
if counterbore_depth > thickness:
|
|
raise ValueError("flange sleeve plan mount_counterbore_depth_mm must not exceed flange_thickness_mm")
|
|
|
|
radius = counterbore_od / 2
|
|
if abs(hole_u) + radius >= width / 2 or abs(hole_v) + radius >= height / 2:
|
|
raise ValueError("flange sleeve plan mounting counterbores must remain inside the flange boundary")
|
|
if chamfer and abs(hole_u) > width / 2 - chamfer and abs(hole_v) > height / 2 - chamfer:
|
|
edge_clearance = (width / 2 - abs(hole_u)) + (height / 2 - abs(hole_v))
|
|
if edge_clearance < chamfer + radius * math.sqrt(2):
|
|
raise ValueError("flange sleeve plan mounting counterbores intersect the corner chamfers")
|
|
|
|
|
|
def _x_plane(offset_mm: float, normal_x: float = 1.0) -> dict[str, list[float]]:
|
|
return {
|
|
"origin_mm": [offset_mm, 0.0, 0.0],
|
|
"x_dir": [0.0, 1.0, 0.0],
|
|
"normal": [normal_x, 0.0, 0.0],
|
|
}
|
|
|
|
|
|
def _flange_profile(width: float, height: float, chamfer: float) -> dict[str, Any]:
|
|
if chamfer == 0:
|
|
return {"type": "rectangle", "center": [0.0, 0.0], "width_mm": width, "height_mm": height}
|
|
half_width, half_height = width / 2, height / 2
|
|
return {
|
|
"type": "polygon",
|
|
"vertices": [
|
|
[-half_width + chamfer, -half_height],
|
|
[half_width - chamfer, -half_height],
|
|
[half_width, -half_height + chamfer],
|
|
[half_width, half_height - chamfer],
|
|
[half_width - chamfer, half_height],
|
|
[-half_width + chamfer, half_height],
|
|
[-half_width, half_height - chamfer],
|
|
[-half_width, -half_height + chamfer],
|
|
],
|
|
}
|
|
|
|
|
|
def build_flange_sleeve_cdsl(plan: Any) -> tuple[dict[str, Any], dict[str, float | str]]:
|
|
"""Build a schema-valid CDSL flange sleeve from semantic, editable parameters."""
|
|
values = normalize_flange_sleeve_plan(plan)
|
|
number = lambda field: float(values[field])
|
|
width, height = number("flange_width_mm"), number("flange_height_mm")
|
|
thickness, chamfer = number("flange_thickness_mm"), number("corner_chamfer_mm")
|
|
tube_radius, tip_radius, bore_radius = number("tube_outer_diameter_mm") / 2, number("tip_outer_diameter_mm") / 2, number("bore_diameter_mm") / 2
|
|
straight, tip_length = number("tube_straight_length_mm"), number("tip_length_mm")
|
|
boss_radius, boss_height = number("boss_outer_diameter_mm") / 2, number("boss_height_mm")
|
|
hole_radius, counterbore_radius = number("mount_hole_diameter_mm") / 2, number("mount_counterbore_diameter_mm") / 2
|
|
hole_u, hole_v = number("mount_hole_u_mm"), number("mount_hole_v_mm")
|
|
hole_centers = [[u, v] for u in (-hole_u, hole_u) for v in (-hole_v, hole_v)]
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1",
|
|
"schema_version": "1.3.0",
|
|
"part_id": str(values["part_id"]),
|
|
"kind": "part",
|
|
"meta": {"name": str(values["name"]), "units": "mm"},
|
|
"geometry": {"sketches": [
|
|
{
|
|
"id": "flange_outline",
|
|
"workplane": _x_plane(-thickness),
|
|
"profile": _flange_profile(width, height, chamfer),
|
|
},
|
|
{
|
|
"id": "sleeve_profile",
|
|
"workplane": {
|
|
"origin_mm": [0.0, 0.0, 0.0],
|
|
"x_dir": [1.0, 0.0, 0.0],
|
|
"normal": [0.0, 0.0, 1.0],
|
|
},
|
|
"profile": {
|
|
"type": "polygon",
|
|
"vertices": [
|
|
[0.0, bore_radius],
|
|
[0.0, tube_radius],
|
|
[straight, tube_radius],
|
|
[straight + tip_length, tip_radius],
|
|
[straight + tip_length, bore_radius],
|
|
],
|
|
},
|
|
},
|
|
{
|
|
"id": "front_boss_ring",
|
|
"workplane": _x_plane(0.0),
|
|
"profile": {
|
|
"type": "annulus",
|
|
"inner_radius_mm": bore_radius,
|
|
"outer_radius_mm": boss_radius,
|
|
"center": [0.0, 0.0],
|
|
},
|
|
},
|
|
{
|
|
"id": "center_bore",
|
|
"workplane": _x_plane(-thickness),
|
|
"profile": {"type": "circle", "radius_mm": bore_radius, "center": [0.0, 0.0]},
|
|
},
|
|
{
|
|
"id": "mount_holes",
|
|
"workplane": _x_plane(-thickness),
|
|
"profile": {
|
|
"type": "circles",
|
|
"items": [{"radius_mm": hole_radius, "center": center} for center in hole_centers],
|
|
},
|
|
},
|
|
{
|
|
"id": "mount_counterbores",
|
|
"workplane": _x_plane(0.0, -1.0),
|
|
"profile": {
|
|
"type": "circles",
|
|
"items": [{"radius_mm": counterbore_radius, "center": center} for center in hole_centers],
|
|
},
|
|
},
|
|
]},
|
|
"features": [
|
|
{
|
|
"id": "flange_plate",
|
|
"atomic_id": "extrude_add_blind",
|
|
"depends_on": [],
|
|
"sketch_id": "flange_outline",
|
|
"params": {"distance_mm": thickness},
|
|
},
|
|
{
|
|
"id": "sleeve_body",
|
|
"atomic_id": "revolve_add",
|
|
"depends_on": ["flange_plate"],
|
|
"sketch_id": "sleeve_profile",
|
|
"params": {
|
|
"angle_deg": 360.0,
|
|
"axis": {"origin_mm": [0.0, 0.0, 0.0], "direction": [1.0, 0.0, 0.0]},
|
|
},
|
|
},
|
|
{
|
|
"id": "front_boss",
|
|
"atomic_id": "extrude_add_blind",
|
|
"depends_on": ["flange_plate", "sleeve_body"],
|
|
"sketch_id": "front_boss_ring",
|
|
"params": {"distance_mm": boss_height},
|
|
},
|
|
{
|
|
"id": "center_bore_cut",
|
|
"atomic_id": "extrude_cut_blind",
|
|
"depends_on": ["flange_plate", "sleeve_body", "front_boss"],
|
|
"sketch_id": "center_bore",
|
|
"params": {"distance_mm": thickness + straight + tip_length + boss_height + 1.0},
|
|
},
|
|
{
|
|
"id": "mount_hole_cuts",
|
|
"atomic_id": "extrude_cut_blind",
|
|
"depends_on": ["center_bore_cut"],
|
|
"sketch_id": "mount_holes",
|
|
"params": {"distance_mm": thickness + 1.0},
|
|
},
|
|
{
|
|
"id": "mount_counterbore_cuts",
|
|
"atomic_id": "extrude_cut_blind",
|
|
"depends_on": ["mount_hole_cuts"],
|
|
"sketch_id": "mount_counterbores",
|
|
"params": {"distance_mm": number("mount_counterbore_depth_mm")},
|
|
},
|
|
],
|
|
}
|
|
return cdsl, values
|