N3 功能,sheet_bend 钣金折弯功能的增加,还有一些简单的代码冲突 #15
@@ -7,8 +7,9 @@ from typing import Any, Iterable
|
||||
|
||||
from build123d import Axis, Compound, Edge, Face, Location, Plane, ShapeList, Solid, Vector, Wire, export_step
|
||||
|
||||
from .parametric_bend import build_bend_solid
|
||||
from .parametric_thread import build_thread_solid
|
||||
from .runtime_types import AxisSpec, HoleSpec, PlaneSpec, ThreadSpec, TopologyRecord, Vector3, canonical_plane_signature
|
||||
from .runtime_types import AxisSpec, BendSpec, HoleSpec, PlaneSpec, ThreadSpec, TopologyRecord, Vector3, canonical_plane_signature
|
||||
|
||||
|
||||
def _vector(value: list[float] | tuple[float, float, float]) -> Vector:
|
||||
@@ -410,6 +411,26 @@ class Build123dGeometryAdapter:
|
||||
plane = Plane(origin=_vector(spec.axis.origin_mm), x_dir=frame_x, z_dir=direction)
|
||||
return solid.moved(Location(plane))
|
||||
|
||||
@staticmethod
|
||||
def bend_solid(spec: BendSpec) -> Any:
|
||||
"""Build one bent sheet segment placed on ``spec.frame``.
|
||||
|
||||
The parametric generator constructs the sheet locally with the first
|
||||
wing along +X, its mid-plane spanning +X/+Z (thickness along +Y) and
|
||||
the fold (width) axis along +Z. This gate rotates that local frame so
|
||||
the first wing lands on ``spec.frame.y_dir`` (= normal x x_dir), the
|
||||
sheet thickness on ``spec.frame.normal`` and the width axis on
|
||||
``spec.frame.x_dir``.
|
||||
"""
|
||||
solid = build_bend_solid(spec)
|
||||
frame = spec.frame
|
||||
plane = Plane(
|
||||
origin=_vector(frame.origin_mm),
|
||||
x_dir=_vector(frame.y_dir),
|
||||
z_dir=_vector(frame.x_dir),
|
||||
)
|
||||
return solid.moved(Location(plane))
|
||||
|
||||
@staticmethod
|
||||
def fillet(body: Any, radius_mm: float, edges: Iterable[Edge]) -> Any:
|
||||
# 对指定边以给定半径做圆角。
|
||||
|
||||
@@ -39,15 +39,15 @@ _BODY_MUTATING_ATOMICS = frozenset({
|
||||
"extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "extrude_cut_two_sided",
|
||||
"extrude_cut_through", "loft_add",
|
||||
"revolve_add", "revolve_cut", "sphere_add", "box_add", "cylinder_add",
|
||||
"thread_add", "thread_cut", *_HOLE_ATOMICS, "fillet", "chamfer",
|
||||
"thread_add", "thread_cut", "bend_add", *_HOLE_ATOMICS, "fillet", "chamfer",
|
||||
})
|
||||
# A pattern may replay a previous pattern as well as a direct body mutation.
|
||||
# Context-only features have no geometry definition to instance. thread_add
|
||||
# and thread_cut are excluded: pattern translation does not yet move their
|
||||
# parametric axis, so a replayed thread would silently re-run at the original
|
||||
# location.
|
||||
# Context-only features have no geometry definition to instance. thread_add,
|
||||
# thread_cut and bend_add are excluded: pattern translation does not yet move
|
||||
# their parametric axis/frame, so a replayed instance would silently re-run at
|
||||
# the original location.
|
||||
_REPLAYABLE_ATOMICS = (
|
||||
_BODY_MUTATING_ATOMICS - frozenset({"thread_add", "thread_cut"})
|
||||
_BODY_MUTATING_ATOMICS - frozenset({"thread_add", "thread_cut", "bend_add"})
|
||||
) | frozenset({"pattern_linear", "pattern_mirror", "pattern_circular"})
|
||||
_SUPPORTED_EXTENTS = frozenset({
|
||||
"blind", "mid_plane", "through_all", "through_all_both", "through_all_and_blind",
|
||||
@@ -378,7 +378,7 @@ class CapabilityAnalyzer:
|
||||
required.append(f"extent:{end_type}")
|
||||
if end_type not in _SUPPORTED_EXTENTS:
|
||||
blockers.append(self._blocker(node.feature_id, "unsupported_extent", "The extent needs a resolved topology selector or is not implemented", extent=end_type))
|
||||
target_kind = _EXTENT_TARGET_KINDS.get(end_type)
|
||||
target_kind = _EXTENT_TARGET_KINDS.get(end_type or "")
|
||||
if target_kind:
|
||||
required.append(f"selector:extent_target:{target_kind}")
|
||||
reference = end_condition.get("reference")
|
||||
@@ -407,7 +407,7 @@ class CapabilityAnalyzer:
|
||||
node.feature_id, "unsupported_reverse_extent",
|
||||
"The reverse extent is not implemented", extent=reverse_type,
|
||||
))
|
||||
reverse_target_kind = _EXTENT_TARGET_KINDS.get(reverse_type)
|
||||
reverse_target_kind = _EXTENT_TARGET_KINDS.get(reverse_type or "")
|
||||
if reverse_target_kind:
|
||||
required.append(f"selector:reverse_extent_target:{reverse_target_kind}")
|
||||
reverse_reference = reverse_condition.get("reference")
|
||||
@@ -532,7 +532,7 @@ class CapabilityAnalyzer:
|
||||
"extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "extrude_cut_two_sided",
|
||||
"extrude_cut_through", "loft_add",
|
||||
"revolve_add", "revolve_cut", "sphere_add", "box_add", "cylinder_add",
|
||||
"thread_add",
|
||||
"thread_add", "bend_add",
|
||||
# thread_cut 与 extrude_cut_blind/revolve_cut 一致:无宿主时由
|
||||
# active_body 前置阻止,文档含该类特征即视为携带可执行几何。
|
||||
"thread_cut",
|
||||
|
||||
@@ -158,6 +158,38 @@
|
||||
"required": ["radius_mm", "height_mm"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"bendChainLeg": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"leg_mm": {"$ref": "#/$defs/positive"},
|
||||
"bend_angle_deg": {"type": "number", "exclusiveMinimum": 0, "exclusiveMaximum": 180},
|
||||
"inner_radius_mm": {"type": "number", "minimum": 0},
|
||||
"side": {"type": "integer", "enum": [1, -1]}
|
||||
},
|
||||
"required": ["leg_mm"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"bendAddParams": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"thickness_mm": {"$ref": "#/$defs/positive"},
|
||||
"width_mm": {"$ref": "#/$defs/positive"},
|
||||
"chain": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/bendChainLeg"}},
|
||||
"frame": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"origin_mm": {"$ref": "#/$defs/point3"},
|
||||
"x_dir": {"$ref": "#/$defs/point3"},
|
||||
"y_dir": {"$ref": "#/$defs/point3"},
|
||||
"normal": {"$ref": "#/$defs/point3"}
|
||||
},
|
||||
"required": ["origin_mm", "x_dir", "y_dir", "normal"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["thickness_mm", "width_mm", "chain"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"threadAddParams": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -405,7 +437,7 @@
|
||||
"required": ["type", "contours"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"feature_atomic_ids": {"enum": ["extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "extrude_cut_two_sided", "extrude_cut_through", "loft_add", "revolve_add", "revolve_cut", "hole_blind", "hole_countersink", "hole_counterbore", "sphere_add", "box_add", "cylinder_add", "thread_add", "thread_cut", "fillet", "chamfer", "pattern_linear", "pattern_mirror", "pattern_circular", "reference_plane", "reference_axis", "hole_wizard"]},
|
||||
"feature_atomic_ids": {"enum": ["extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "extrude_cut_two_sided", "extrude_cut_through", "loft_add", "revolve_add", "revolve_cut", "hole_blind", "hole_countersink", "hole_counterbore", "sphere_add", "box_add", "cylinder_add", "thread_add", "thread_cut", "bend_add", "fillet", "chamfer", "pattern_linear", "pattern_mirror", "pattern_circular", "reference_plane", "reference_axis", "hole_wizard"]},
|
||||
"feature": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -433,6 +465,7 @@
|
||||
{"if": {"properties": {"atomic_id": {"const": "sphere_add"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/sphereParams"}}}},
|
||||
{"if": {"properties": {"atomic_id": {"const": "box_add"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/boxParams"}}}},
|
||||
{"if": {"properties": {"atomic_id": {"const": "cylinder_add"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/cylinderParams"}}}},
|
||||
{"if": {"properties": {"atomic_id": {"const": "bend_add"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/bendAddParams"}}}},
|
||||
{"if": {"properties": {"atomic_id": {"const": "thread_add"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/threadAddParams"}}}},
|
||||
{"if": {"properties": {"atomic_id": {"const": "thread_cut"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/threadCutParams"}}}},
|
||||
{"if": {"properties": {"atomic_id": {"const": "hole_blind"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/holeBlindParams"}}}},
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"hole_blind": {"atomic_id":"hole_blind","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"required"},"author_params_schema":{"type":"object","properties":{"diameter_mm":{"type":"number","exclusiveMinimum":0},"depth_mm":{"type":"number","exclusiveMinimum":0},"positions":{"type":"array","minItems":1,"maxItems":64,"items":{"type":"object","properties":{"mm":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["mm"],"additionalProperties":false}},"drill_angle_rad":{"type":"number","exclusiveMinimum":0,"maximum":3.141592653589793}},"required":["diameter_mm","depth_mm","positions"],"additionalProperties":false},"selector_policy":{"slot":"params.host_face","token_kind":"face","min_items":1,"max_items":1,"snapshot_bound":true},"server_injected_paths":["params.host_face"],"reference_policy":{"mode":"none"},"semantic_preflight":["host_face_exists","hole_positions_on_host_plane","cut_exit_distance"],"candidate_verifiers":["cylindrical_bore","through_cylindrical_bore"]},
|
||||
"hole_countersink": {"atomic_id":"hole_countersink","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"required"},"author_params_schema":{"type":"object","properties":{"diameter_mm":{"type":"number","exclusiveMinimum":0},"depth_mm":{"type":"number","exclusiveMinimum":0},"positions":{"type":"array","minItems":1,"maxItems":64,"items":{"type":"object","properties":{"mm":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["mm"],"additionalProperties":false}},"countersink_diameter_mm":{"type":"number","exclusiveMinimum":0},"countersink_angle_rad":{"type":"number","exclusiveMinimum":0,"maximum":3.141592653589793},"drill_angle_rad":{"type":"number","exclusiveMinimum":0,"maximum":3.141592653589793}},"required":["diameter_mm","depth_mm","positions","countersink_diameter_mm","countersink_angle_rad"],"additionalProperties":false},"selector_policy":{"slot":"params.host_face","token_kind":"face","min_items":1,"max_items":1,"snapshot_bound":true},"server_injected_paths":["params.host_face"],"reference_policy":{"mode":"none"},"semantic_preflight":["host_face_exists","hole_positions_on_host_plane","cut_exit_distance"],"candidate_verifiers":["cylindrical_bore"]},
|
||||
"hole_counterbore": {"atomic_id":"hole_counterbore","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"required"},"author_params_schema":{"type":"object","properties":{"diameter_mm":{"type":"number","exclusiveMinimum":0},"depth_mm":{"type":"number","exclusiveMinimum":0},"positions":{"type":"array","minItems":1,"maxItems":64,"items":{"type":"object","properties":{"mm":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["mm"],"additionalProperties":false}},"counterbore_diameter_mm":{"type":"number","exclusiveMinimum":0},"counterbore_depth_mm":{"type":"number","exclusiveMinimum":0},"drill_angle_rad":{"type":"number","exclusiveMinimum":0,"maximum":3.141592653589793}},"required":["diameter_mm","depth_mm","positions","counterbore_diameter_mm","counterbore_depth_mm"],"additionalProperties":false},"selector_policy":{"slot":"params.host_face","token_kind":"face","min_items":1,"max_items":1,"snapshot_bound":true},"server_injected_paths":["params.host_face"],"reference_policy":{"mode":"none"},"semantic_preflight":["host_face_exists","hole_positions_on_host_plane","cut_exit_distance"],"candidate_verifiers":["cylindrical_bore"]},
|
||||
"bend_add": {"atomic_id":"bend_add","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"forbidden"},"author_params_schema":{"type":"object","properties":{"thickness_mm":{"type":"number","exclusiveMinimum":0},"width_mm":{"type":"number","exclusiveMinimum":0},"chain":{"type":"array","minItems":1,"items":{"type":"object","properties":{"leg_mm":{"type":"number","exclusiveMinimum":0},"bend_angle_deg":{"type":"number","exclusiveMinimum":0,"exclusiveMaximum":180},"inner_radius_mm":{"type":"number","minimum":0},"side":{"type":"integer","enum":[1,-1]}},"required":["leg_mm"],"additionalProperties":false}}},"required":["thickness_mm","width_mm","chain"],"additionalProperties":false},"selector_policy":{"slot":null,"token_kind":null,"min_items":0,"max_items":0,"snapshot_bound":false},"server_injected_paths":[],"reference_policy":{"mode":"none"},"semantic_preflight":[],"candidate_verifiers":["single_connected_body"]},
|
||||
"sphere_add": {"atomic_id":"sphere_add","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"forbidden"},"author_params_schema":{"type":"object","properties":{"radius_mm":{"type":"number","exclusiveMinimum":0},"center_mm":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["radius_mm","center_mm"],"additionalProperties":false},"selector_policy":{"slot":null,"token_kind":null,"min_items":0,"max_items":0,"snapshot_bound":false},"server_injected_paths":[],"reference_policy":{"mode":"none"},"semantic_preflight":[],"candidate_verifiers":["single_connected_body"]},
|
||||
"box_add": {"atomic_id":"box_add","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"forbidden"},"author_params_schema":{"type":"object","properties":{"length_mm":{"type":"number","exclusiveMinimum":0},"width_mm":{"type":"number","exclusiveMinimum":0},"height_mm":{"type":"number","exclusiveMinimum":0},"center_mm":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["length_mm","width_mm","height_mm","center_mm"],"additionalProperties":false},"selector_policy":{"slot":null,"token_kind":null,"min_items":0,"max_items":0,"snapshot_bound":false},"server_injected_paths":[],"reference_policy":{"mode":"none"},"semantic_preflight":[],"candidate_verifiers":["single_connected_body"]},
|
||||
"cylinder_add": {"atomic_id":"cylinder_add","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"forbidden"},"author_params_schema":{"type":"object","properties":{"radius_mm":{"type":"number","exclusiveMinimum":0},"height_mm":{"type":"number","exclusiveMinimum":0},"axis":{"type":"object","properties":{"origin_mm":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"direction":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["origin_mm","direction"],"additionalProperties":false}},"required":["radius_mm","height_mm"],"additionalProperties":false},"selector_policy":{"slot":null,"token_kind":null,"min_items":0,"max_items":0,"snapshot_bound":false},"server_injected_paths":[],"reference_policy":{"mode":"none"},"semantic_preflight":[],"candidate_verifiers":["single_connected_body"]},
|
||||
|
||||
@@ -11,7 +11,7 @@ from typing import Any, Callable, Protocol
|
||||
from .build123d_adapter import Build123dGeometryAdapter
|
||||
from .capabilities import CapabilityAnalyzer, pattern_transform_blocker, sketch_ids_required_by_contract
|
||||
from .runtime_types import (
|
||||
AxisSpec, CapabilityResult, FeaturePlanNode, FeatureResult, HoleSpec, PlaneSpec,
|
||||
AxisSpec, BendSpec, CapabilityResult, FeaturePlanNode, FeatureResult, HoleSpec, PlaneSpec,
|
||||
ThreadSpec, Vector3,
|
||||
RuntimeDiagnostic, SelectorResolution, TopologyRecord, TopologyRegistry,
|
||||
vector_add, vector_cross, vector_dot, vector_scale, vector_subtract, vector_unit,
|
||||
@@ -28,6 +28,7 @@ ALL_ATOMIC_IDS = frozenset({
|
||||
"hole_wizard", "fillet", "chamfer", "pattern_linear", "pattern_mirror",
|
||||
"pattern_circular",
|
||||
"thread_add", "thread_cut",
|
||||
"bend_add",
|
||||
})
|
||||
|
||||
|
||||
@@ -90,6 +91,7 @@ class GeometryAdapter(Protocol):
|
||||
def cut(self, body: Any, tool: Any) -> Any: ...
|
||||
def sphere(self, radius_mm: float, center_mm: Vector3) -> Any: ...
|
||||
def thread_solid(self, spec: ThreadSpec) -> Any: ...
|
||||
def bend_solid(self, spec: BendSpec) -> Any: ...
|
||||
def hole_tool(self, spec: HoleSpec, starts: list[Vector3], inward: Vector3, through_depth_mm: float) -> Any: ...
|
||||
def body_center(self, body: Any) -> Vector3: ...
|
||||
def body_span(self, body: Any, direction: Vector3) -> float: ...
|
||||
@@ -617,6 +619,23 @@ def _execute_thread_cut(node: FeaturePlanNode, session: ExecutionSession) -> Fea
|
||||
return session.result(node)
|
||||
|
||||
|
||||
def _execute_bend(node: FeaturePlanNode, session: ExecutionSession) -> FeatureResult:
|
||||
# 折弯特征(bend_add)执行入口:按规格生成等厚折弯板并并入当前主体。
|
||||
# 1. 解析并校验板厚/宽度/折痕链与放置平面,非法输入抛出带具体原因的 ValueError。
|
||||
spec = BendSpec.from_feature(node.params)
|
||||
# 2. 由适配器门面生成沿 spec.frame 放置的折弯实心段。
|
||||
solid = session.adapter.bend_solid(spec)
|
||||
# 3. 与当前主体做布尔并(fuse)后登记为新主体,并返回该特征的结果对象。
|
||||
session.register_body(node.feature_id, session.adapter.fuse(session.body, solid), replay_node=node)
|
||||
return session.result(node)
|
||||
|
||||
|
||||
def _bend_executor(node: FeaturePlanNode, session: ExecutionSession, sketch: dict[str, Any] | None) -> FeatureResult:
|
||||
# 折弯特征(bend_add)不需要草图平面,丢弃该参数后执行。
|
||||
del sketch
|
||||
return _execute_bend(node, session)
|
||||
|
||||
|
||||
def _host_plane(resolution: SelectorResolution) -> PlaneSpec:
|
||||
if resolution.record is None:
|
||||
raise ValueError(resolution.diagnostic.message if resolution.diagnostic else "host face was not resolved")
|
||||
@@ -1419,6 +1438,7 @@ EXECUTORS: dict[str, ExecutorFunction] = {
|
||||
"cylinder_add": _cylinder_executor,
|
||||
"thread_add": _thread_executor,
|
||||
"thread_cut": _thread_executor,
|
||||
"bend_add": _bend_executor,
|
||||
"extrude_add_blind": _primary_executor,
|
||||
"extrude_add_two_sided": _primary_executor,
|
||||
"extrude_cut_blind": _primary_executor,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
{
|
||||
"schema": "cdsl.corpus-manifest.v1",
|
||||
"corpus_version": "2026-08-31",
|
||||
"corpus_version": "2026-09-07",
|
||||
"document_count": 2881,
|
||||
"document_stems_sha256": "ba5b905191817cd0f7b9ad07d4002ad27730e200a395f71f9c4e31ebb1867cd6",
|
||||
"document_stems_sha256": "4c8660ed96f3b5427ad26adadd9b82df447951913920542501ef0fc479c02773",
|
||||
"phase_pools": {
|
||||
"p3": {
|
||||
"selector_count": 771,
|
||||
"selectors_sha256": "72093701357cd1858f2dbcc0f1ab9e6c7baea7db26357154e0586e5e1be6f801"
|
||||
"selectors_sha256": "2c0f76833cd08592b71ef3a5b5b1548b649ac485e2b2388358426f815c21a047"
|
||||
},
|
||||
"p4": {
|
||||
"selector_count": 826,
|
||||
"selectors_sha256": "ab9ea6edd8532ce7a0110d0725b5303c63a53617a0af502101b12f657285d371"
|
||||
"selectors_sha256": "c031552f9ae29f03ca58545d90dc8b16e2b5963f0923ff075e3724f066a2bf3d"
|
||||
},
|
||||
"p6": {
|
||||
"selector_count": 893,
|
||||
"selectors_sha256": "ac9f91b690bc533b1fdd1772d65ee77b248d108f39a429615d909b78332d9a08"
|
||||
"selectors_sha256": "de44a06e55aa21940e9ccb02d0deaba90bb7669d8fb93a62bf976c903a18c42f"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user