内螺纹 thread_cut 算子的增加
This commit is contained in:
@@ -14,6 +14,16 @@ loop) plus the core cylinder, fuses them, and trims both ends flush at
|
||||
radius so the boolean union has a clean volume overlap instead of a pair of
|
||||
coincident faces (which OCC cannot fuse reliably).
|
||||
|
||||
``spec.internal=False`` builds an external thread (thread_add): a solid rod
|
||||
whose crest envelope is ``major_diameter_mm``. ``spec.internal=True`` builds
|
||||
an internal-thread cutting tool: the identical helical-rod topology but with
|
||||
the crest radius over-sized by ``INTERNAL_CUT_OVERLAP_MM`` so that
|
||||
``body.cut(tool)`` removes a clean helical groove from the host wall instead
|
||||
of collapsing on coincident faces. External threads may add plain root-radius
|
||||
end shanks (``relief_length_mm``, total length then becomes
|
||||
``length_mm + 2 * relief_length_mm``) and crest/root fillet radii; the
|
||||
internal cutting form accepts fillets but not relief.
|
||||
|
||||
The module stays independent of the CDSL runtime: it only reads
|
||||
``runtime_types.ThreadSpec`` (a build123d-free data class) and returns an OCC
|
||||
``Solid``. Construction happens in a local +Z frame anchored at ``z = 0``;
|
||||
@@ -24,7 +34,7 @@ from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
from build123d import Edge, Face, Location, Plane, ShapeList, Solid, Vector, Wire
|
||||
from build123d import Compound, Edge, Face, Location, Plane, ShapeList, Solid, Vector, Wire
|
||||
|
||||
from .runtime_types import ThreadSpec
|
||||
|
||||
@@ -34,6 +44,11 @@ from .runtime_types import ThreadSpec
|
||||
_ROOT_OVERLAP_MM = 0.15
|
||||
#: Minimum surviving flat on the tooth crest before the flanks would overlap.
|
||||
_MIN_CREST_HALF_WIDTH_MM = 0.02
|
||||
#: Internal-thread cutting tool over-size beyond the nominal major radius (mm).
|
||||
#: ``body.cut(tool)`` removes a helical groove whose crest envelope has to
|
||||
#: penetrate the host wall by a thin material layer; an exact-fit tool would
|
||||
#: place coincident faces inside OCC's boolean and fail unpredictably.
|
||||
INTERNAL_CUT_OVERLAP_MM = 0.02
|
||||
|
||||
|
||||
def _validate_spec(spec: ThreadSpec) -> tuple[float, float, float, float, float]:
|
||||
@@ -43,8 +58,8 @@ def _validate_spec(spec: ThreadSpec) -> tuple[float, float, float, float, float]
|
||||
where ``flank_throw`` is the horizontal flank run per tooth side and
|
||||
``flank_half_tan`` is ``tan(flank_half_angle)``.
|
||||
"""
|
||||
if spec.internal:
|
||||
raise ValueError("internal threads are not implemented yet (thread_cut)")
|
||||
if spec.internal and spec.relief_length_mm > 0:
|
||||
raise ValueError("relief_length_mm is only supported on external threads (thread_add)")
|
||||
if spec.major_diameter_mm <= 0 or spec.minor_diameter_mm <= 0:
|
||||
raise ValueError("thread diameters must be positive")
|
||||
if spec.minor_diameter_mm >= spec.major_diameter_mm:
|
||||
@@ -56,7 +71,12 @@ def _validate_spec(spec: ThreadSpec) -> tuple[float, float, float, float, float]
|
||||
if not 0 < spec.angle_deg < 180:
|
||||
raise ValueError("thread angle_deg must be between 0 and 180")
|
||||
|
||||
crest_radius = spec.major_diameter_mm / 2.0
|
||||
if spec.internal:
|
||||
# 内螺纹刀具:牙顶必须比名义 major 大一个薄材料层,body.cut 才能
|
||||
# 切入宿主孔壁完成布尔差,而不是在 coincident faces 上退化。
|
||||
crest_radius = spec.major_diameter_mm / 2.0 + INTERNAL_CUT_OVERLAP_MM
|
||||
else:
|
||||
crest_radius = spec.major_diameter_mm / 2.0
|
||||
root_radius = spec.minor_diameter_mm / 2.0
|
||||
depth_radius = crest_radius - root_radius
|
||||
if depth_radius <= 0:
|
||||
@@ -109,6 +129,7 @@ def _build_z_aligned(spec: ThreadSpec) -> Solid:
|
||||
Vector(crest_radius, 0.0, -crest_half_width),
|
||||
]
|
||||
wire = Wire([Edge.make_line(pts[index], pts[(index + 1) % len(pts)]) for index in range(len(pts))])
|
||||
wire = _apply_profile_fillets(wire, spec, crest_radius, sunk_root, pitch, crest_half_width)
|
||||
ribbon = Solid.sweep(section=Face(wire), path=helix, make_solid=True, is_frenet=True)
|
||||
|
||||
# Core cylinder at the nominal minor radius spanning the whole helix.
|
||||
@@ -120,18 +141,31 @@ def _build_z_aligned(spec: ThreadSpec) -> Solid:
|
||||
|
||||
fused = core.fuse(ribbon)
|
||||
|
||||
if spec.relief_length_mm > 0:
|
||||
# 端部收尾(relief):螺纹有效段保持 length_mm 不变,置于总长中部
|
||||
# z ∈ [relief, relief + length],两端各附一个牙根半径的光杆段,总长 =
|
||||
# length + 2 * relief。光杆与芯柱做实体重叠后由 trim 裁出干净的纯光杆端面。
|
||||
return _build_external_with_relief(spec, fused, root_radius, crest_radius, pitch)
|
||||
|
||||
# 平移半个牙距,使 z = 0 端面落在牙谷中心:首尾端面无半牙、端面圆盘完整,
|
||||
# 裁切后 [0, length_mm] 内牙顶平台数稳定为 length/pitch。
|
||||
fused = fused.moved(Location((0.0, 0.0, -pitch / 2.0)))
|
||||
return _trim_thread_z(fused, 0.0, spec.length_mm, crest_radius)
|
||||
|
||||
# Trim both ends flush to z in [0, length_mm] with an oversized box.
|
||||
|
||||
def _trim_thread_z(fused: Solid | Compound, z0: float, z1: float, crest_radius: float) -> Solid:
|
||||
"""Intersect ``fused`` with an oversized box clamped to ``z in [z0, z1]``.
|
||||
|
||||
A threaded solid may leave OCC float slivers at the trim planes; the
|
||||
largest surviving solid is returned as the canonical body.
|
||||
"""
|
||||
trim_margin = 2.0
|
||||
half_span = crest_radius + trim_margin
|
||||
clamp = Solid.make_box(
|
||||
2.0 * half_span,
|
||||
2.0 * half_span,
|
||||
spec.length_mm,
|
||||
Plane(origin=(-half_span, -half_span, 0.0)),
|
||||
z1 - z0,
|
||||
Plane(origin=(-half_span, -half_span, z0)),
|
||||
)
|
||||
intersected = fused.intersect(clamp)
|
||||
if isinstance(intersected, ShapeList):
|
||||
@@ -153,6 +187,83 @@ def _build_z_aligned(spec: ThreadSpec) -> Solid:
|
||||
return trimmed
|
||||
|
||||
|
||||
def _build_external_with_relief(
|
||||
spec: ThreadSpec,
|
||||
aligned: Solid | Compound,
|
||||
root_radius: float,
|
||||
crest_radius: float,
|
||||
pitch: float,
|
||||
) -> Solid:
|
||||
"""Build an externally threaded rod with plain root-radius end shanks.
|
||||
|
||||
The threaded portion keeps its full ``length_mm`` and sits in the middle of
|
||||
the part: ``z in [relief, relief + length_mm]``. A plain shank of radius
|
||||
``root_radius`` (the thread root/minor radius) extends over ``z in
|
||||
[0, relief]`` and ``[relief + length_mm, total]``, so the total part length
|
||||
is ``length_mm + 2 * relief_length_mm``.
|
||||
|
||||
``aligned`` is the fused core+ribbon *before* the valley-centring shift.
|
||||
The build first trims a clean valley-centred thread over ``[0, length_mm]``
|
||||
(identical phase and tooth count to the plain build), then shifts it up by
|
||||
``relief`` so both end planes land inside tooth valleys. Each shank
|
||||
cylinder overlaps the threaded core by ``_ROOT_OVERLAP_MM`` so both fuses
|
||||
are volume booleans, and the final trim turns the two end planes into clean
|
||||
plain discs at ``z = 0`` and ``z = total``.
|
||||
"""
|
||||
relief = spec.relief_length_mm
|
||||
length = spec.length_mm
|
||||
overlap = _ROOT_OVERLAP_MM
|
||||
total = length + 2.0 * relief
|
||||
thread = _trim_thread_z(aligned.moved(Location((0.0, 0.0, -pitch / 2.0))), 0.0, length, crest_radius)
|
||||
thread = thread.moved(Location((0.0, 0.0, relief)))
|
||||
bottom = Solid.make_cylinder(root_radius, relief + overlap, Plane(origin=(0.0, 0.0, 0.0)))
|
||||
top = Solid.make_cylinder(
|
||||
root_radius,
|
||||
relief + overlap,
|
||||
Plane(origin=(0.0, 0.0, relief + length - overlap)),
|
||||
)
|
||||
fused = thread.fuse(bottom).fuse(top)
|
||||
return _trim_thread_z(fused, 0.0, total, crest_radius)
|
||||
|
||||
|
||||
def _apply_profile_fillets(
|
||||
wire: Wire,
|
||||
spec: ThreadSpec,
|
||||
crest_radius: float,
|
||||
sunk_root: float,
|
||||
pitch: float,
|
||||
crest_half_width: float,
|
||||
) -> Wire:
|
||||
"""Round the tooth crest/root corners of the axial cross-section.
|
||||
|
||||
A crest/root radius that is too large for the flank lengths makes OCC's
|
||||
``fillet_2d`` fail; the profile then falls back to the sharp-cornered
|
||||
trapezoid instead of blocking the whole build (AGENTS: never destroy a
|
||||
buildable model over a cosmetic detail).
|
||||
"""
|
||||
if spec.crest_radius_mm <= 0 and spec.root_radius_mm <= 0:
|
||||
return wire
|
||||
try:
|
||||
if spec.crest_radius_mm > 0:
|
||||
crest_vertices = [
|
||||
vertex for vertex in wire.vertices()
|
||||
if vertex.X >= crest_radius - 1e-6 and abs(vertex.Z) <= crest_half_width + 1e-6
|
||||
]
|
||||
if crest_vertices:
|
||||
wire = wire.fillet_2d(spec.crest_radius_mm, crest_vertices)
|
||||
if spec.root_radius_mm > 0:
|
||||
root_vertices = [
|
||||
vertex for vertex in wire.vertices()
|
||||
if vertex.X <= sunk_root + 1e-6 and abs(vertex.Z) >= pitch / 2.0 - 1e-6
|
||||
]
|
||||
if root_vertices:
|
||||
wire = wire.fillet_2d(spec.root_radius_mm, root_vertices)
|
||||
except Exception:
|
||||
# 清根圆角过大导致截面退化:保留尖角梯形,不阻断生成。
|
||||
pass
|
||||
return wire
|
||||
|
||||
|
||||
def build_thread_solid(spec: ThreadSpec) -> Solid:
|
||||
"""Build one external-threaded solid segment for ``spec``.
|
||||
|
||||
@@ -164,3 +275,20 @@ def build_thread_solid(spec: ThreadSpec) -> Solid:
|
||||
if not result.solids():
|
||||
raise ValueError("thread generation produced no solid")
|
||||
return result
|
||||
|
||||
|
||||
def build_thread_cut_tool(spec: ThreadSpec) -> Solid:
|
||||
"""Build the internal-thread cutting tool that ``thread_cut`` subtracts.
|
||||
|
||||
The tool is exactly the ``internal=True`` thread form: the identical
|
||||
helical-rod topology as ``thread_add`` but with the crest envelope
|
||||
over-sized by ``INTERNAL_CUT_OVERLAP_MM`` beyond the nominal major radius.
|
||||
``host.cut(tool)`` then removes a clean full-depth helical groove from the
|
||||
host bore wall instead of collapsing on a pair of coincident faces (which
|
||||
OCC cannot cut reliably). This is the geometry-side entry point of the
|
||||
``thread_cut`` atomic; the caller (geometry adapter) places the resulting
|
||||
+Z-aligned tool at ``spec.axis`` exactly like an external thread segment.
|
||||
"""
|
||||
if not spec.internal:
|
||||
raise ValueError("build_thread_cut_tool requires an internal-thread spec (internal=True)")
|
||||
return build_thread_solid(spec)
|
||||
|
||||
Reference in New Issue
Block a user