提交N3 功能的改动

This commit is contained in:
2026-09-07 19:30:56 +08:00
parent a208ea5c8b
commit 00b50b1f31
6 changed files with 95 additions and 20 deletions
@@ -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:
@@ -378,6 +379,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:
# 对指定边以给定半径做圆角。
+9 -9
View File
@@ -38,15 +38,15 @@ _BODY_MUTATING_ATOMICS = frozenset({
"extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind",
"extrude_cut_through",
"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",
@@ -312,7 +312,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")
@@ -341,7 +341,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")
@@ -465,7 +465,7 @@ class CapabilityAnalyzer:
body_producers = {
"extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind",
"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",
+34 -1
View File
@@ -144,6 +144,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": {
@@ -389,7 +421,7 @@
"required": ["type", "contours"],
"additionalProperties": false
},
"feature_atomic_ids": {"enum": ["extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "extrude_cut_through", "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_through", "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": {
@@ -415,6 +447,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"}}}},
@@ -15,6 +15,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"]},
+21 -1
View File
@@ -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",
})
@@ -89,6 +90,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: ...
@@ -600,6 +602,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")
@@ -1344,6 +1363,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,
+8 -8
View File
@@ -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": 770,
"selectors_sha256": "7b0e2142effc71bf2e3011654f020062665103cad5a6e219a323d1ee17887c89"
"selector_count": 771,
"selectors_sha256": "2c0f76833cd08592b71ef3a5b5b1548b649ac485e2b2388358426f815c21a047"
},
"p4": {
"selector_count": 825,
"selectors_sha256": "fa9bce14dc72170bd92ea8cb274490e1df733a85a8647de5f13cc39d8b1e945d"
"selector_count": 826,
"selectors_sha256": "c031552f9ae29f03ca58545d90dc8b16e2b5963f0923ff075e3724f066a2bf3d"
},
"p6": {
"selector_count": 892,
"selectors_sha256": "4f925d00636f471f517ccfbd1c030bc6fda8fa172332399964d80618f698ce89"
"selector_count": 893,
"selectors_sha256": "de44a06e55aa21940e9ccb02d0deaba90bb7669d8fb93a62bf976c903a18c42f"
}
}
}