Add bulk UV sampler, TKHelix and MakeSphere (8.0.1.2)
- n3xd_ocp.sample.face_grid: a face's UV grid of points and outward normals in one GIL-free call, sampling exactly where np.linspace does so a caller's fitted surface does not move. - n3xd_ocp.helix: OCCT 8.0's TKHelix, which upstream does not bind at all. Takes Python lists rather than NCollection_Array1. Two things the header does not say, both found by probing: SetParameters wants N+1 diameters for N segments (one per boundary, so a taper interpolates), and the builder is right-hand only -- a negative pitch is error status 12, not a mirrored helix. - Bind BRepPrimAPI_MakeSphere and give inventory.py an EXTRA_SYMBOLS addendum for symbols no app source imports. assay's gen_flow_fixtures has been unrunnable since the 10C cutover for want of it; the gap was wider than --check, since sigdiff is inventory-driven too. Gates: 105 tests, 139/139 symbols, sigdiff clean, ASAN clean, wheel self-contained with no libGL/libX11 DT_NEEDED. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HbTQ2HYWQwdtGmGJwypt6Z
This commit is contained in:
118
tests/test_ext_helix.py
Normal file
118
tests/test_ext_helix.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""n3xd_ocp.helix — OCCT 8.0's TKHelix builder.
|
||||
|
||||
The toolkit is new in 8.0 and has no upstream binding to compare against, so
|
||||
these are property checks rather than a parity gate: the wire has to be a real
|
||||
helix of the requested pitch and turn count, and the failure modes have to
|
||||
surface as Python exceptions rather than a silently empty shape.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import pytest
|
||||
|
||||
import n3xd_ocp
|
||||
from OCP.BRepGProp import BRepGProp
|
||||
from OCP.GeomAbs import GeomAbs_C1, GeomAbs_C2
|
||||
from OCP.gp import gp_Ax3, gp_Dir, gp_Pnt
|
||||
from OCP.GProp import GProp_GProps
|
||||
from OCP.TopAbs import TopAbs_EDGE
|
||||
from OCP.TopExp import TopExp
|
||||
from OCP.TopTools import TopTools_IndexedMapOfShape
|
||||
|
||||
AXIS = gp_Ax3(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1), gp_Dir(1, 0, 0))
|
||||
|
||||
|
||||
def _length(shape) -> float:
|
||||
props = GProp_GProps()
|
||||
BRepGProp.LinearProperties_s(shape, props)
|
||||
return props.Mass()
|
||||
|
||||
|
||||
def _edge_count(shape) -> int:
|
||||
edges = TopTools_IndexedMapOfShape()
|
||||
TopExp.MapShapes_s(shape, TopAbs_EDGE, edges)
|
||||
return edges.Extent()
|
||||
|
||||
|
||||
def test_single_segment_has_the_right_arc_length():
|
||||
"""One turn of a helix is sqrt(circumference^2 + pitch^2) long."""
|
||||
diameter, pitch, turns = 8.0, 1.25, 1.0
|
||||
wire, reached = n3xd_ocp.helix.pure_helix(AXIS, diameter, [pitch], [turns])
|
||||
expected = turns * math.hypot(math.pi * diameter, pitch)
|
||||
assert _length(wire) == pytest.approx(expected, rel=1e-4)
|
||||
assert reached > 0.0
|
||||
assert _edge_count(wire) >= 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("turns", [0.5, 3.0, 24.0])
|
||||
def test_arc_length_scales_with_the_turn_count(turns):
|
||||
diameter, pitch = 8.0, 1.25
|
||||
wire, _reached = n3xd_ocp.helix.pure_helix(AXIS, diameter, [pitch], [turns])
|
||||
expected = turns * math.hypot(math.pi * diameter, pitch)
|
||||
assert _length(wire) == pytest.approx(expected, rel=1e-4)
|
||||
|
||||
|
||||
def test_variable_pitch_segments_compose():
|
||||
"""The array form is the reason this is bound at all — a spring whose
|
||||
pitch changes partway is one wire, not two."""
|
||||
diameter = 10.0
|
||||
wire, _reached = n3xd_ocp.helix.pure_helix(
|
||||
AXIS, diameter, [2.0, 5.0], [3.0, 2.0]
|
||||
)
|
||||
expected = 3.0 * math.hypot(math.pi * diameter, 2.0) + 2.0 * math.hypot(
|
||||
math.pi * diameter, 5.0
|
||||
)
|
||||
assert _length(wire) == pytest.approx(expected, rel=1e-3)
|
||||
|
||||
|
||||
def test_tapered_diameters_via_the_builder():
|
||||
"""Two segments, so three boundary diameters: 10 -> 8 tapering, then 8 -> 8
|
||||
straight. A tapered thread (NPT) is the reason the array form is bound."""
|
||||
builder = n3xd_ocp.helix.BuilderHelix()
|
||||
builder.set_parameters(AXIS, [10.0, 8.0, 8.0], [2.0, 2.0], [2.0, 2.0])
|
||||
builder.set_approx_parameters(1.0e-4, 8, GeomAbs_C1)
|
||||
builder.perform()
|
||||
assert builder.error_status() == 0
|
||||
# The straight half is exact; the tapered half is longer than a cylinder of
|
||||
# its smaller diameter and shorter than one of its larger.
|
||||
straight = 2.0 * math.hypot(math.pi * 8.0, 2.0)
|
||||
largest = 2.0 * math.hypot(math.pi * 10.0, 2.0)
|
||||
total = _length(builder.shape())
|
||||
assert straight * 2 < total < straight + largest
|
||||
|
||||
|
||||
def test_the_n_plus_one_diameter_rule_is_enforced():
|
||||
"""OCCT wants N+1 diameters for N segments and raises
|
||||
Standard_ConstructionError otherwise; the binding checks it first so the
|
||||
message names the shape it wanted."""
|
||||
builder = n3xd_ocp.helix.BuilderHelix()
|
||||
with pytest.raises(ValueError, match="N\\+1 diameters"):
|
||||
builder.set_parameters(AXIS, [8.0], [1.25], [1.0])
|
||||
builder.set_parameters(AXIS, [8.0, 8.0], [1.25], [1.0]) # accepted
|
||||
|
||||
|
||||
def test_continuity_is_selectable():
|
||||
wire_c1, _ = n3xd_ocp.helix.pure_helix(
|
||||
AXIS, 8.0, [1.25], [2.0], continuity=GeomAbs_C1
|
||||
)
|
||||
wire_c2, _ = n3xd_ocp.helix.pure_helix(
|
||||
AXIS, 8.0, [1.25], [2.0], continuity=GeomAbs_C2
|
||||
)
|
||||
assert _length(wire_c1) == pytest.approx(_length(wire_c2), rel=1e-3)
|
||||
|
||||
|
||||
def test_mismatched_segment_arrays_are_rejected():
|
||||
with pytest.raises(ValueError):
|
||||
n3xd_ocp.helix.pure_helix(AXIS, 8.0, [1.0, 2.0], [1.0])
|
||||
with pytest.raises(ValueError):
|
||||
n3xd_ocp.helix.pure_helix(AXIS, 8.0, [], [])
|
||||
|
||||
|
||||
def test_builder_rejects_ragged_parameters():
|
||||
builder = n3xd_ocp.helix.BuilderHelix()
|
||||
with pytest.raises(ValueError):
|
||||
builder.set_parameters(AXIS, [8.0, 8.0], [1.0], [1.0, 2.0])
|
||||
with pytest.raises(ValueError):
|
||||
builder.set_parameters(AXIS, [8.0, 8.0], [], [])
|
||||
Reference in New Issue
Block a user