提交关于N5功能,“involute_gear渐开线齿廓与齿条专用生成”的改动
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
"""Parametric equal-thickness sheet-metal bend generator (``bend_add``).
|
||||
|
||||
A bent sheet is an equal-thickness ribbon around its mid-plane centreline:
|
||||
straight wings joined by tangent circular-arc corners of radius
|
||||
``r_m = r_i + t/2``. We walk the wing chain in the local XY bending plane
|
||||
(first wing along +X), round each fold vertex with ``fillet_2d``, offset the
|
||||
centreline by ``+t/2`` / ``-t/2``, close both offset curves with square end
|
||||
caps and extrude by ``width_mm`` along +Z. Cross-section area is exactly
|
||||
``thickness * centreline_length`` so the closed-form volume is
|
||||
``t * w * L_mid`` with ``L_mid = sum(leg) - sum(2 r_m cot(alpha/2)) +
|
||||
sum(r_m (pi - alpha))`` (alpha = interior angle in radians). Geometric-
|
||||
equivalence only: no K-factor flattening, so the volume deliberately differs
|
||||
from the flat blank. Local frame: +X = first wing, +Y = thickness normal,
|
||||
+Z = fold (width) axis, first wing starts at the origin. The module reads
|
||||
only ``runtime_types.BendSpec`` and returns an OCC ``Solid``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
from build123d import Edge, Face, Side, Solid, Vector, Wire
|
||||
|
||||
try: # build123d >= 0.9 exposes Kind; keep the import optional.
|
||||
from build123d import Kind as _Kind
|
||||
|
||||
_KIND_ARC = _Kind.ARC
|
||||
except ImportError: # pragma: no cover
|
||||
_KIND_ARC = None
|
||||
|
||||
from .runtime_types import BendLeg, BendSpec
|
||||
|
||||
|
||||
def _unit(angle_deg: float) -> tuple[float, float]:
|
||||
angle = math.radians(angle_deg)
|
||||
return math.cos(angle), math.sin(angle)
|
||||
|
||||
|
||||
def _perp(value: Vector) -> Vector:
|
||||
return Vector(-value.Y, value.X, 0.0)
|
||||
|
||||
|
||||
def bend_folds(spec: BendSpec) -> tuple[list[float], list[tuple[float, float, float, int]]]:
|
||||
"""Return ``(legs, folds)``; ``folds[i] = (angle, r_i, r_m, side)``."""
|
||||
if not spec.chain:
|
||||
raise ValueError("bend chain must contain at least one wing")
|
||||
legs = [float(leg.leg_mm) for leg in spec.chain]
|
||||
folds: list[tuple[float, float, float, int]] = []
|
||||
for index in range(len(spec.chain) - 1):
|
||||
leg: BendLeg = spec.chain[index]
|
||||
if leg.bend_angle_deg is None:
|
||||
raise ValueError(f"bend chain[{index}] needs bend_angle_deg towards the next wing")
|
||||
angle = float(leg.bend_angle_deg)
|
||||
if not 0 < angle < 180:
|
||||
raise ValueError("bend interior angle must be between 0 and 180 degrees")
|
||||
radius = float(leg.inner_radius_mm)
|
||||
if radius < 0:
|
||||
raise ValueError("bend inner radius must be non-negative")
|
||||
folds.append((angle, radius, radius + spec.thickness_mm / 2.0, int(leg.side)))
|
||||
return legs, folds
|
||||
|
||||
|
||||
def fold_vertices(legs: list[float], folds: list[tuple[float, float, float, int]]) -> list[Vector]:
|
||||
"""Fold-vertex polyline (both end points included) in the XY plane."""
|
||||
angle = 0.0
|
||||
points = [Vector(0.0, 0.0, 0.0)]
|
||||
x = y = 0.0
|
||||
for index in range(len(legs)):
|
||||
dx, dy = _unit(angle)
|
||||
x += dx * legs[index]
|
||||
y += dy * legs[index]
|
||||
points.append(Vector(x, y, 0.0))
|
||||
if index < len(folds):
|
||||
bend_angle_deg, _r_i, _r_m, side = folds[index]
|
||||
angle += float(side) * (180.0 - bend_angle_deg)
|
||||
return points
|
||||
|
||||
|
||||
def mid_path_length_mm(spec: BendSpec) -> float:
|
||||
"""Exact centreline length of the bent part (mm)."""
|
||||
legs, folds = bend_folds(spec)
|
||||
total = sum(legs)
|
||||
for bend_angle_deg, _r_i, mid_radius, _side in folds:
|
||||
delta = math.radians(180.0 - bend_angle_deg)
|
||||
cut = mid_radius / math.tan(math.radians(bend_angle_deg) / 2.0)
|
||||
total -= 2.0 * cut
|
||||
total += mid_radius * delta
|
||||
return total
|
||||
|
||||
|
||||
def validate_chain(spec: BendSpec) -> float:
|
||||
"""Range-check a bend spec; return the centreline length.
|
||||
|
||||
Every wing must keep a positive straight portion after its neighbouring
|
||||
bend corners consume their tangent lengths (otherwise corners overlap).
|
||||
"""
|
||||
if spec.thickness_mm <= 0 or spec.width_mm <= 0:
|
||||
raise ValueError("bend requires positive thickness_mm and width_mm")
|
||||
legs, folds = bend_folds(spec)
|
||||
cuts: list[float] = []
|
||||
for bend_angle_deg, _r_i, mid_radius, _side in folds:
|
||||
cuts.append(mid_radius / math.tan(math.radians(bend_angle_deg) / 2.0))
|
||||
for index in range(len(legs)):
|
||||
left = cuts[index - 1] if index > 0 else 0.0
|
||||
right = cuts[index] if index < len(folds) else 0.0
|
||||
if legs[index] - left - right <= 0:
|
||||
raise ValueError(
|
||||
f"bend wing {index} (leg {legs[index]:.4f} mm) is consumed by the adjacent "
|
||||
f"bend radii (needs > {left + right:.4f} mm)"
|
||||
)
|
||||
return mid_path_length_mm(spec)
|
||||
|
||||
|
||||
def _centerline_wire(legs, folds):
|
||||
"""Tangent-continuous filleted centreline wire in the XY plane."""
|
||||
points = fold_vertices(legs, folds)
|
||||
wire = Wire([Edge.make_line(points[i], points[i + 1]) for i in range(len(points) - 1)])
|
||||
for index, (bend_angle_deg, _r_i, mid_radius, _side) in enumerate(folds):
|
||||
if mid_radius <= 0:
|
||||
continue
|
||||
target = points[index + 1]
|
||||
selected = [
|
||||
vertex for vertex in wire.vertices()
|
||||
if (vertex.X - target.X) ** 2 + (vertex.Y - target.Y) ** 2 < 1e-9
|
||||
]
|
||||
if not selected:
|
||||
raise ValueError(f"bend corner {index} vertex not found")
|
||||
try:
|
||||
wire = wire.fillet_2d(mid_radius, selected)
|
||||
except Exception as error: # pragma: no cover - defensive
|
||||
raise ValueError(
|
||||
f"bend corner {index} fillet of radius {mid_radius:.4f} mm failed"
|
||||
) from error
|
||||
return wire, points
|
||||
|
||||
|
||||
def build_bend_solid(spec: BendSpec) -> Solid:
|
||||
"""Build the bent sheet in the local frame (+Z = width axis).
|
||||
|
||||
The first wing's mid-plane runs from the origin along +X; thickness spans
|
||||
+/- t/2 across the XY mid-plane and the width spans z in [0, width_mm].
|
||||
The caller (geometry adapter) places the solid at ``spec.frame``.
|
||||
"""
|
||||
thickness = float(spec.thickness_mm)
|
||||
width = float(spec.width_mm)
|
||||
legs, folds = bend_folds(spec)
|
||||
validate_chain(spec)
|
||||
half = thickness / 2.0
|
||||
wire, points = _centerline_wire(legs, folds)
|
||||
left = wire.offset_2d(half, kind=_KIND_ARC, side=Side.LEFT, closed=False)
|
||||
right = wire.offset_2d(half, kind=_KIND_ARC, side=Side.RIGHT, closed=False)
|
||||
start_tangent = (points[1] - points[0]).normalized()
|
||||
end_tangent = (points[-1] - points[-2]).normalized()
|
||||
cap_start = Edge.make_line(points[0] + _perp(start_tangent) * half, points[0] - _perp(start_tangent) * half)
|
||||
cap_end = Edge.make_line(points[-1] + _perp(end_tangent) * half, points[-1] - _perp(end_tangent) * half)
|
||||
closed = Wire([*left.edges(), cap_end, *right.edges(), cap_start])
|
||||
return Solid.extrude(Face(closed), Vector(0.0, 0.0, width))
|
||||
Reference in New Issue
Block a user