thread 功能的主要功能实现
This commit is contained in:
@@ -0,0 +1,166 @@
|
|||||||
|
"""Parametric screw-thread generator built on build123d geometry.
|
||||||
|
|
||||||
|
Strategy
|
||||||
|
--------
|
||||||
|
A screw thread is a helical prism: one trapezoidal tooth profile swept along a
|
||||||
|
helix using the OCC Frenet frame. ``is_frenet=True`` keeps the profile
|
||||||
|
orientation constant along a straight helix (the curvature vector always
|
||||||
|
points at the axis), which is exactly the configuration a machined thread has:
|
||||||
|
the flank is a true helical surface with a constant axial pitch.
|
||||||
|
|
||||||
|
The generator builds a single seamless "tooth ribbon" (one pitch wide per
|
||||||
|
loop) plus the core cylinder, fuses them, and trims both ends flush at
|
||||||
|
``z in [0, length_mm]``. The tooth root is sunk slightly below the core
|
||||||
|
radius so the boolean union has a clean volume overlap instead of a pair of
|
||||||
|
coincident faces (which OCC cannot fuse reliably).
|
||||||
|
|
||||||
|
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``;
|
||||||
|
frame placement/rotation to ``spec.axis`` is the adapter's responsibility.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
from build123d import Edge, Face, Location, Plane, ShapeList, Solid, Vector, Wire
|
||||||
|
|
||||||
|
from .runtime_types import ThreadSpec
|
||||||
|
|
||||||
|
#: How far below the nominal core radius the tooth root extends (mm, clamped).
|
||||||
|
#: The extra overlap guarantees the root cylinder union is a clean volume
|
||||||
|
#: boolean rather than a coincident-face attachment.
|
||||||
|
_ROOT_OVERLAP_MM = 0.15
|
||||||
|
#: Minimum surviving flat on the tooth crest before the flanks would overlap.
|
||||||
|
_MIN_CREST_HALF_WIDTH_MM = 0.02
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_spec(spec: ThreadSpec) -> tuple[float, float, float, float, float]:
|
||||||
|
"""Range-check a spec and return geometry parameters.
|
||||||
|
|
||||||
|
Returns ``(crest_radius, root_radius, sink, flank_throw, flank_half_tan)``
|
||||||
|
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.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:
|
||||||
|
raise ValueError("thread minor diameter must be smaller than the major diameter")
|
||||||
|
if spec.pitch_mm <= 0:
|
||||||
|
raise ValueError("thread pitch must be positive")
|
||||||
|
if spec.length_mm <= 0:
|
||||||
|
raise ValueError("thread length must be positive")
|
||||||
|
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
|
||||||
|
root_radius = spec.minor_diameter_mm / 2.0
|
||||||
|
depth_radius = crest_radius - root_radius
|
||||||
|
if depth_radius <= 0:
|
||||||
|
raise ValueError("thread major diameter must exceed the minor diameter")
|
||||||
|
|
||||||
|
sink = min(_ROOT_OVERLAP_MM, 0.25 * depth_radius, 0.1 * spec.pitch_mm)
|
||||||
|
full_depth = depth_radius + sink # from sunk root up to the crest
|
||||||
|
flank_half_tan = math.tan(math.radians(spec.angle_deg / 2.0))
|
||||||
|
flank_throw = full_depth * flank_half_tan
|
||||||
|
return crest_radius, root_radius, sink, flank_throw, flank_half_tan
|
||||||
|
|
||||||
|
|
||||||
|
def _build_z_aligned(spec: ThreadSpec) -> Solid:
|
||||||
|
"""Construct an external thread along +Z spanning ``z in [0, length_mm]``.
|
||||||
|
|
||||||
|
Local frame: z = thread axis, the leading end face sits at ``z = 0`` and
|
||||||
|
starts inside a tooth valley so the first crest rises cleanly off the end
|
||||||
|
face. Helix pitch runs right-handed (or left-handed when ``lefthand``).
|
||||||
|
"""
|
||||||
|
crest_radius, root_radius, sink, flank_throw, _flank_half_tan = _validate_spec(spec)
|
||||||
|
pitch = spec.pitch_mm
|
||||||
|
|
||||||
|
# Tooth geometry in the axial cross-section (z = axial, r = radial).
|
||||||
|
# One full tooth occupies a pitch-wide interval centered on the crest flat;
|
||||||
|
# the flank horizontal throw is `full_depth * tan(half_angle)`.
|
||||||
|
crest_half_width = pitch / 2.0 - flank_throw
|
||||||
|
if crest_half_width < _MIN_CREST_HALF_WIDTH_MM:
|
||||||
|
raise ValueError(f"thread pitch is too small for the given depth and flank angle (flank throw {flank_throw:.4f} mm must stay below pitch/2)")
|
||||||
|
|
||||||
|
# Overshoot both ends by one pitch so the trimmed faces land in full
|
||||||
|
# material; the crest centre sits at the helix start phase (z = 0), which
|
||||||
|
# also puts the z = 0 end face through full crest material after trimming.
|
||||||
|
helix_height = spec.length_mm + 2.0 * pitch
|
||||||
|
helix = Edge.make_helix(
|
||||||
|
pitch=pitch,
|
||||||
|
height=helix_height,
|
||||||
|
radius=root_radius,
|
||||||
|
lefthand=spec.lefthand,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Profile vertices (z, r) -> world (x = r, y = 0, z). Order is counter
|
||||||
|
# clockwise in the (z, r) plane: bottom edge first, then crest right,
|
||||||
|
# crest flat, crest left back down. The bottom edge spans the full pitch
|
||||||
|
# so consecutive helical loops share an identical seam line.
|
||||||
|
sunk_root = root_radius - sink
|
||||||
|
pts = [
|
||||||
|
Vector(sunk_root, 0.0, -pitch / 2.0),
|
||||||
|
Vector(sunk_root, 0.0, pitch / 2.0),
|
||||||
|
Vector(crest_radius, 0.0, crest_half_width),
|
||||||
|
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))])
|
||||||
|
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.
|
||||||
|
# (The sunken tooth roots overlap it so the union below is clean.)
|
||||||
|
# The profile spans z in [-pitch/2, +pitch/2] around the helix start, so
|
||||||
|
# the core begins at -pitch/2 and covers the shell plus one extra pitch.
|
||||||
|
core_height = helix_height + pitch
|
||||||
|
core = Solid.make_cylinder(root_radius, core_height, Plane(origin=(0.0, 0.0, -pitch / 2.0)))
|
||||||
|
|
||||||
|
fused = core.fuse(ribbon)
|
||||||
|
|
||||||
|
# 平移半个牙距,使 z = 0 端面落在牙谷中心:首尾端面无半牙、端面圆盘完整,
|
||||||
|
# 裁切后 [0, length_mm] 内牙顶平台数稳定为 length/pitch。
|
||||||
|
fused = fused.moved(Location((0.0, 0.0, -pitch / 2.0)))
|
||||||
|
|
||||||
|
# Trim both ends flush to z in [0, length_mm] with an oversized box.
|
||||||
|
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)),
|
||||||
|
)
|
||||||
|
intersected = fused.intersect(clamp)
|
||||||
|
if isinstance(intersected, ShapeList):
|
||||||
|
candidates = list(intersected)
|
||||||
|
elif intersected is not None:
|
||||||
|
candidates = [intersected]
|
||||||
|
else:
|
||||||
|
candidates = []
|
||||||
|
members: list[Solid] = []
|
||||||
|
for candidate in candidates:
|
||||||
|
if isinstance(candidate, Solid):
|
||||||
|
members.append(candidate)
|
||||||
|
else:
|
||||||
|
members.extend(candidate.solids())
|
||||||
|
if not members:
|
||||||
|
raise ValueError("thread end trim produced no solid")
|
||||||
|
# 端部裁齐应保持单一主体;若 OCC 留下浮点碎屑,取体积最大的实心主体。
|
||||||
|
trimmed = members[0] if len(members) == 1 else max(members, key=lambda shape: shape.volume)
|
||||||
|
return trimmed
|
||||||
|
|
||||||
|
|
||||||
|
def build_thread_solid(spec: ThreadSpec) -> Solid:
|
||||||
|
"""Build one external-threaded solid segment for ``spec``.
|
||||||
|
|
||||||
|
Returns a solid whose thread axis is +Z and whose leading end face is at
|
||||||
|
``z = 0``. The caller (geometry adapter) is responsible for placing the
|
||||||
|
solid at ``spec.axis``.
|
||||||
|
"""
|
||||||
|
result = _build_z_aligned(spec)
|
||||||
|
if not result.solids():
|
||||||
|
raise ValueError("thread generation produced no solid")
|
||||||
|
return result
|
||||||
Reference in New Issue
Block a user