diff --git a/backend/engine/cdsl_engine/runtime_types.py b/backend/engine/cdsl_engine/runtime_types.py index a43eb976..2d85f548 100644 --- a/backend/engine/cdsl_engine/runtime_types.py +++ b/backend/engine/cdsl_engine/runtime_types.py @@ -355,6 +355,111 @@ class ThreadSpec: ) +@dataclass(frozen=True) +class BendLeg: + """One straight wing of a bent sheet-metal chain (runtime-neutral). + + ``leg_mm`` is the straight mid-plane distance from the previous fold vertex + to the fold vertex leaving this wing (the unfolded chord length between the + two surrounding fold vertices). When the wing has a following wing, + ``bend_angle_deg`` is the required *interior* angle between the two wings + (0 < angle < 180, 90 = a right-angle bend), ``inner_radius_mm`` the inner + bend-surface fillet radius (>= 0; the outer radius is always + ``inner_radius_mm + thickness_mm``) and ``side`` the fold direction + (+1 / -1). A trailing wing carries no fold; stray fold fields on the last + wing are ignored for tolerance. + """ + + leg_mm: float + bend_angle_deg: float | None = None + inner_radius_mm: float = 0.0 + side: int = 1 + + +@dataclass(frozen=True) +class BendSpec: + """Runtime-neutral definition of a sheet-metal bend (``bend_add``). + + The part is described by its mid-plane centreline: ``chain`` lists the + straight wings joined by equal-thickness bend corners. ``thickness_mm`` + and ``width_mm`` are the sheet thickness and the full length along the fold + (width) axis. ``frame.origin_mm`` anchors the start of the first wing's + mid-plane path, ``frame.x_dir`` is the fold/width axis and + ``frame.normal`` is the mid-plane normal of the first wing; the first wing + extends along ``normal x x_dir``. + + The spec deliberately contains no OCC planes or shapes. The adapter turns + this definition into a bent solid keeping source-contract parsing separate + from B-rep work (same pattern as :class:`ThreadSpec`). + """ + + thickness_mm: float + width_mm: float + frame: PlaneSpec + chain: tuple[BendLeg, ...] + + @classmethod + def from_feature(cls, params: dict[str, Any]) -> "BendSpec": + try: + thickness = float(params.get("thickness_mm") or 0.0) + width = float(params.get("width_mm") or 0.0) + except (TypeError, ValueError) as error: + raise ValueError("bend thickness/width must be numeric") from error + if thickness <= 0 or width <= 0: + raise ValueError("bend requires positive thickness_mm and width_mm in millimetres") + raw_chain = params.get("chain") + if not isinstance(raw_chain, (list, tuple)) or not raw_chain: + raise ValueError("bend requires a non-empty chain of wing segments") + chain: list[BendLeg] = [] + for index, raw_leg in enumerate(raw_chain): + if not isinstance(raw_leg, dict): + raise ValueError(f"bend chain[{index}] must be an object with leg_mm") + try: + leg = float(raw_leg.get("leg_mm") or 0.0) + except (TypeError, ValueError) as error: + raise ValueError(f"bend chain[{index}].leg_mm must be numeric") from error + if leg <= 0: + raise ValueError(f"bend chain[{index}].leg_mm must be positive") + bend_angle: float | None = None + if index < len(raw_chain) - 1: + raw_angle = raw_leg.get("bend_angle_deg") + if raw_angle is None: + raise ValueError( + f"bend chain[{index}] needs bend_angle_deg for the fold towards the next wing" + ) + try: + bend_angle = float(raw_angle) + except (TypeError, ValueError) as error: + raise ValueError(f"bend chain[{index}].bend_angle_deg must be numeric") from error + if not 0 < bend_angle < 180: + raise ValueError("bend interior angle must be between 0 and 180 degrees") + radius = float(raw_leg.get("inner_radius_mm") or 0.0) + raw_side = raw_leg.get("side") + side = 1 if raw_side is None else int(raw_side) + if radius < 0: + raise ValueError(f"bend chain[{index}].inner_radius_mm must be non-negative") + if side not in (1, -1): + raise ValueError(f"bend chain[{index}].side must be +1 or -1") + chain.append(BendLeg( + leg_mm=leg, + bend_angle_deg=bend_angle, + inner_radius_mm=radius, + side=side, + )) + raw_frame = params.get("frame") + if isinstance(raw_frame, dict): + frame = PlaneSpec.from_mapping(raw_frame) + else: + # frame 缺省:首翼沿世界 +X 延伸、厚度沿 +Y、折痕沿 +Z(贴 XY 平面)。 + frame = PlaneSpec( + origin_mm=(0.0, 0.0, 0.0), + x_dir=(0.0, 0.0, 1.0), + y_dir=(1.0, 0.0, 0.0), + normal=(0.0, 1.0, 0.0), + ) + return cls(thickness_mm=thickness, width_mm=width, frame=frame, chain=tuple(chain)) + + @dataclass(frozen=True) class RuntimeDiagnostic: code: str