Files
cdsl-cad/backend/app/services/flange_sleeve_template.py
T
2026-08-25 11:01:59 +08:00

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