Merge pull request '增加了thread几何基础操作' (#9) from ganjihong into main
Reviewed-on: #9
This commit was merged in pull request #9.
This commit is contained in:
@@ -7,7 +7,8 @@ from typing import Any, Iterable
|
||||
|
||||
from build123d import Axis, Compound, Edge, Face, Helix, Location, Plane, ShapeList, Solid, Vector, Wire, export_step
|
||||
|
||||
from .runtime_types import AxisSpec, HoleSpec, PlaneSpec, TopologyRecord, Vector3, canonical_plane_signature
|
||||
from .parametric_thread import build_thread_solid
|
||||
from .runtime_types import AxisSpec, HoleSpec, PlaneSpec, ThreadSpec, TopologyRecord, Vector3, canonical_plane_signature
|
||||
|
||||
|
||||
def _vector(value: list[float] | tuple[float, float, float]) -> Vector:
|
||||
@@ -354,6 +355,26 @@ class Build123dGeometryAdapter:
|
||||
raise ValueError("hole has no positions")
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def thread_solid(spec: ThreadSpec) -> Any:
|
||||
"""Build a threaded solid segment anchored on ``spec.axis``.
|
||||
|
||||
The parametric generator constructs the thread in a local +Z frame
|
||||
spanning ``[0, length_mm]``. This gate rotates that frame so the
|
||||
thread axis lands on ``spec.axis.direction`` with
|
||||
``spec.axis.origin_mm`` at the leading end face, keeping placement in
|
||||
the adapter and geometry construction in ``parametric_thread.py``.
|
||||
"""
|
||||
solid = build_thread_solid(spec)
|
||||
direction = _vector(spec.axis.direction)
|
||||
if abs(direction.X) <= 1e-9 and abs(direction.Y) <= 1e-9:
|
||||
# 轴沿 ±Z:X 方向任意即可,螺纹起始相位绕轴无意义。
|
||||
frame_x = Vector(1.0, 0.0, 0.0)
|
||||
else:
|
||||
frame_x = Vector(0.0, 0.0, 1.0).cross(direction).normalized()
|
||||
plane = Plane(origin=_vector(spec.axis.origin_mm), x_dir=frame_x, z_dir=direction)
|
||||
return solid.moved(Location(plane))
|
||||
|
||||
@staticmethod
|
||||
def fillet(body: Any, radius_mm: float, edges: Iterable[Edge]) -> Any:
|
||||
# 对指定边以给定半径做圆角。
|
||||
|
||||
@@ -29,11 +29,16 @@ _ACTIVE_BODY_REQUIRED = frozenset({
|
||||
})
|
||||
_BODY_MUTATING_ATOMICS = frozenset({
|
||||
"extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind",
|
||||
"revolve_add", "revolve_cut", "sphere_add", *_HOLE_ATOMICS, "fillet", "chamfer",
|
||||
"revolve_add", "revolve_cut", "sphere_add", "thread_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.
|
||||
_REPLAYABLE_ATOMICS = _BODY_MUTATING_ATOMICS | frozenset({"pattern_linear", "pattern_mirror"})
|
||||
# Context-only features have no geometry definition to instance. thread_add is
|
||||
# excluded: pattern translation does not yet move its parametric axis, so a
|
||||
# replayed thread would silently re-fuse at the original location.
|
||||
_REPLAYABLE_ATOMICS = (
|
||||
_BODY_MUTATING_ATOMICS - frozenset({"thread_add"})
|
||||
) | frozenset({"pattern_linear", "pattern_mirror"})
|
||||
_SUPPORTED_EXTENTS = frozenset({
|
||||
"blind", "mid_plane", "through_all", "through_all_both", "through_all_and_blind",
|
||||
"up_to_surface", "up_to_vertex", "offset_from_surface", "through_next", "up_to_body",
|
||||
@@ -427,7 +432,7 @@ class CapabilityAnalyzer:
|
||||
body_available = True
|
||||
body_producers = {
|
||||
"extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind",
|
||||
"revolve_add", "revolve_cut", "sphere_add",
|
||||
"revolve_add", "revolve_cut", "sphere_add", "thread_add",
|
||||
}
|
||||
document_blockers: list[RuntimeDiagnostic] = []
|
||||
if not any(node.atomic_id in body_producers for node in plan):
|
||||
|
||||
@@ -117,6 +117,20 @@
|
||||
"required": ["radius_mm", "center_mm"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"threadAddParams": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"major_diameter_mm": {"$ref": "#/$defs/positive"},
|
||||
"minor_diameter_mm": {"$ref": "#/$defs/positive"},
|
||||
"pitch_mm": {"$ref": "#/$defs/positive"},
|
||||
"length_mm": {"$ref": "#/$defs/positive"},
|
||||
"axis": {"$ref": "#/$defs/axis"},
|
||||
"angle_deg": {"type": "number", "exclusiveMinimum": 0, "exclusiveMaximum": 180},
|
||||
"lefthand": {"type": "boolean"}
|
||||
},
|
||||
"required": ["major_diameter_mm", "minor_diameter_mm", "pitch_mm", "length_mm", "axis"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"holeBaseParams": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -318,7 +332,7 @@
|
||||
"required": ["type", "contours"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"feature_atomic_ids": {"enum": ["extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "revolve_add", "revolve_cut", "hole_blind", "hole_countersink", "hole_counterbore", "sphere_add", "fillet", "chamfer", "pattern_linear", "pattern_mirror", "reference_plane", "reference_axis", "hole_wizard"]},
|
||||
"feature_atomic_ids": {"enum": ["extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "revolve_add", "revolve_cut", "hole_blind", "hole_countersink", "hole_counterbore", "sphere_add", "thread_add", "fillet", "chamfer", "pattern_linear", "pattern_mirror", "reference_plane", "reference_axis", "hole_wizard"]},
|
||||
"feature": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -341,6 +355,7 @@
|
||||
{"if": {"properties": {"atomic_id": {"const": "revolve_add"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/revolveParams"}}}},
|
||||
{"if": {"properties": {"atomic_id": {"const": "revolve_cut"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/revolveParams"}}}},
|
||||
{"if": {"properties": {"atomic_id": {"const": "sphere_add"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/sphereParams"}}}},
|
||||
{"if": {"properties": {"atomic_id": {"const": "thread_add"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/threadAddParams"}}}},
|
||||
{"if": {"properties": {"atomic_id": {"const": "hole_blind"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/holeBlindParams"}}}},
|
||||
{"if": {"properties": {"atomic_id": {"const": "hole_countersink"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/holeCountersinkParams"}}}},
|
||||
{"if": {"properties": {"atomic_id": {"const": "hole_counterbore"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/holeCounterboreParams"}}}},
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"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"]},
|
||||
"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"]},
|
||||
"thread_add": {"atomic_id":"thread_add","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"forbidden"},"author_params_schema":{"type":"object","properties":{"major_diameter_mm":{"type":"number","exclusiveMinimum":0},"minor_diameter_mm":{"type":"number","exclusiveMinimum":0},"pitch_mm":{"type":"number","exclusiveMinimum":0},"length_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},"angle_deg":{"type":"number","exclusiveMinimum":0,"exclusiveMaximum":180},"lefthand":{"type":"boolean"}},"required":["major_diameter_mm","minor_diameter_mm","pitch_mm","length_mm","axis"],"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"]},
|
||||
"reference_plane": {"atomic_id":"reference_plane","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"forbidden"},"author_params_schema":{"type":"object","properties":{"plane":{"type":"object","properties":{"origin_mm":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"x_dir":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"normal":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["origin_mm","x_dir","normal"],"additionalProperties":false}},"required":["plane"],"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":["requires_active_solid","reference_plane_nonzero_normal"],"candidate_verifiers":[]},
|
||||
"reference_axis": {"atomic_id":"reference_axis","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"forbidden"},"author_params_schema":{"type":"object","properties":{"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":["axis"],"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":["requires_active_solid","reference_axis_nonzero_direction"],"candidate_verifiers":[]},
|
||||
"hole_wizard": {
|
||||
|
||||
@@ -11,7 +11,8 @@ 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, Vector3,
|
||||
AxisSpec, CapabilityResult, FeaturePlanNode, FeatureResult, HoleSpec, PlaneSpec,
|
||||
ThreadSpec, Vector3,
|
||||
RuntimeDiagnostic, SelectorResolution, TopologyRecord, TopologyRegistry,
|
||||
vector_add, vector_cross, vector_dot, vector_scale, vector_subtract, vector_unit,
|
||||
)
|
||||
@@ -23,6 +24,7 @@ ALL_ATOMIC_IDS = frozenset({
|
||||
"revolve_add", "revolve_cut", "hole_blind", "hole_countersink",
|
||||
"hole_counterbore", "sphere_add", "reference_plane", "reference_axis",
|
||||
"hole_wizard", "fillet", "chamfer", "pattern_linear", "pattern_mirror",
|
||||
"thread_add",
|
||||
})
|
||||
|
||||
|
||||
@@ -83,6 +85,7 @@ class GeometryAdapter(Protocol):
|
||||
def fuse(self, body: Any | None, solid: Any) -> Any: ...
|
||||
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 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: ...
|
||||
@@ -510,6 +513,23 @@ def _execute_sphere(node: FeaturePlanNode, session: ExecutionSession) -> Feature
|
||||
return session.result(node)
|
||||
|
||||
|
||||
def _execute_thread(node: FeaturePlanNode, session: ExecutionSession) -> FeatureResult:
|
||||
# 螺纹特征(thread_add)执行入口:按规格生成参数化螺纹段并并入当前主体。
|
||||
# 1. 解析并校验尺寸/牙距/轴,非法输入抛出带具体原因的 ValueError。
|
||||
spec = ThreadSpec.from_feature(node.atomic_id, node.params)
|
||||
# 2. 由适配器门面生成沿 spec.axis 放置的外螺纹实心段。
|
||||
solid = session.adapter.thread_solid(spec)
|
||||
# 3. 与当前主体做布尔并(fuse)后登记为新主体,并返回该特征的结果对象。
|
||||
session.register_body(node.feature_id, session.adapter.fuse(session.body, solid), replay_node=node)
|
||||
return session.result(node)
|
||||
|
||||
|
||||
def _thread_executor(node: FeaturePlanNode, session: ExecutionSession, sketch: dict[str, Any] | None) -> FeatureResult:
|
||||
# 螺纹不需要草图平面,丢弃该参数后执行。
|
||||
del sketch
|
||||
return _execute_thread(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")
|
||||
@@ -1021,6 +1041,7 @@ EXECUTORS: dict[str, ExecutorFunction] = {
|
||||
"reference_plane": _reference_plane_executor,
|
||||
"reference_axis": _reference_axis_executor,
|
||||
"sphere_add": _sphere_executor,
|
||||
"thread_add": _thread_executor,
|
||||
"extrude_add_blind": _primary_executor,
|
||||
"extrude_add_two_sided": _primary_executor,
|
||||
"extrude_cut_blind": _primary_executor,
|
||||
|
||||
@@ -273,6 +273,66 @@ class HoleSpec:
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ThreadSpec:
|
||||
"""Runtime-neutral definition of a parametric screw thread.
|
||||
|
||||
The spec deliberately contains no OCC planes or shapes. The adapter turns
|
||||
this definition into a threaded solid segment keeping source-contract
|
||||
parsing separate from B-rep work.
|
||||
|
||||
``axis.origin_mm`` anchors the leading end face of the threaded segment and
|
||||
``axis.direction`` is the outward thread axis; ``angle_deg`` is the full
|
||||
flank angle (60 for ISO metric V threads, 30 for trapezoidal leadscrews).
|
||||
``internal`` selects the cutting form used by later thread_cut support.
|
||||
"""
|
||||
|
||||
major_diameter_mm: float
|
||||
minor_diameter_mm: float
|
||||
pitch_mm: float
|
||||
length_mm: float
|
||||
axis: AxisSpec
|
||||
angle_deg: float = 60.0
|
||||
internal: bool = False
|
||||
lefthand: bool = False
|
||||
|
||||
@classmethod
|
||||
def from_feature(cls, atomic_id: str, params: dict[str, Any]) -> "ThreadSpec":
|
||||
try:
|
||||
major = float(params.get("major_diameter_mm") or 0.0)
|
||||
minor = float(params.get("minor_diameter_mm") or 0.0)
|
||||
pitch = float(params.get("pitch_mm") or 0.0)
|
||||
length = float(params.get("length_mm") or 0.0)
|
||||
except (TypeError, ValueError) as error:
|
||||
raise ValueError("thread dimensions must be numeric") from error
|
||||
if major <= 0 or minor <= 0 or pitch <= 0 or length <= 0:
|
||||
raise ValueError("thread requires positive major/minor/pitch/length in millimetres")
|
||||
if minor >= major:
|
||||
raise ValueError("thread minor diameter must be smaller than the major diameter")
|
||||
raw_axis = params.get("axis")
|
||||
if not isinstance(raw_axis, dict):
|
||||
raise ValueError("thread requires an axis definition")
|
||||
angle = 60.0
|
||||
raw_angle = params.get("angle_deg")
|
||||
if raw_angle is not None:
|
||||
try:
|
||||
angle = float(raw_angle)
|
||||
except (TypeError, ValueError) as error:
|
||||
raise ValueError("thread angle_deg must be numeric") from error
|
||||
if not 0 < angle < 180:
|
||||
raise ValueError("thread angle_deg must be between 0 and 180")
|
||||
return cls(
|
||||
major_diameter_mm=major,
|
||||
minor_diameter_mm=minor,
|
||||
pitch_mm=pitch,
|
||||
length_mm=length,
|
||||
axis=AxisSpec.from_mapping(raw_axis),
|
||||
angle_deg=angle,
|
||||
internal=bool(params.get("internal", False)),
|
||||
lefthand=bool(params.get("lefthand", False)),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RuntimeDiagnostic:
|
||||
code: str
|
||||
|
||||
Reference in New Issue
Block a user