Replacing Mixin1D.discretize with enhanced Minxin1D.positions
Some checks failed
benchmarks / benchmarks (macos-14, 3.12) (push) Has been cancelled
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Has been cancelled
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Has been cancelled
benchmarks / benchmarks (windows-latest, 3.12) (push) Has been cancelled
Upload coverage reports to Codecov / run (push) Has been cancelled
pylint / lint (3.10) (push) Has been cancelled
Run type checker / typecheck (3.10) (push) Has been cancelled
Run type checker / typecheck (3.13) (push) Has been cancelled
Wheel building and publishing / Build wheel on ubuntu-latest (push) Has been cancelled
tests / tests (macos-14, 3.10) (push) Has been cancelled
tests / tests (macos-14, 3.13) (push) Has been cancelled
tests / tests (macos-15-intel, 3.10) (push) Has been cancelled
tests / tests (macos-15-intel, 3.13) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.10) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.13) (push) Has been cancelled
tests / tests (windows-latest, 3.10) (push) Has been cancelled
tests / tests (windows-latest, 3.13) (push) Has been cancelled
Wheel building and publishing / upload_pypi (push) Has been cancelled

This commit is contained in:
gumyr 2025-11-25 13:26:36 -05:00
commit a8fc16b344
2 changed files with 100 additions and 36 deletions

View file

@ -120,6 +120,9 @@ from OCP.GeomAbs import (
GeomAbs_C0, GeomAbs_C0,
GeomAbs_C1, GeomAbs_C1,
GeomAbs_C2, GeomAbs_C2,
GeomAbs_C3,
GeomAbs_CN,
GeomAbs_C1,
GeomAbs_G1, GeomAbs_G1,
GeomAbs_G2, GeomAbs_G2,
GeomAbs_JoinType, GeomAbs_JoinType,
@ -545,31 +548,6 @@ class Mixin1D(Shape[TOPODS]):
return result return result
def discretize(self, deflection: float = 0.1, quasi=True) -> list[Vector]:
"""Discretize the shape into a list of points"""
if self.wrapped is None:
raise ValueError("Cannot discretize an empty shape")
curve = self.geom_adaptor()
if quasi:
discretizer = GCPnts_QuasiUniformDeflection()
else:
discretizer = GCPnts_UniformDeflection()
discretizer.Initialize(
curve,
deflection,
curve.FirstParameter(),
curve.LastParameter(),
)
assert discretizer.IsDone()
return [
Vector(v.X(), v.Y(), v.Z())
for v in (
curve.Value(discretizer.Parameter(i))
for i in range(1, discretizer.NbPoints() + 1)
)
]
def curvature_comb( def curvature_comb(
self, count: int = 100, max_tooth_size: float | None = None self, count: int = 100, max_tooth_size: float | None = None
) -> ShapeList[Edge]: ) -> ShapeList[Edge]:
@ -1207,22 +1185,52 @@ class Mixin1D(Shape[TOPODS]):
def positions( def positions(
self, self,
distances: Iterable[float], distances: Iterable[float] | None = None,
position_mode: PositionMode = PositionMode.PARAMETER, position_mode: PositionMode = PositionMode.PARAMETER,
deflection: float | None = None,
) -> list[Vector]: ) -> list[Vector]:
"""Positions along curve """Positions along curve
Generate positions along the underlying curve Generate positions along the underlying curve
Args: Args:
distances (Iterable[float]): distance or parameter values distances (Iterable[float] | None, optional): distance or parameter values.
position_mode (PositionMode, optional): position calculation mode. Defaults to None.
Defaults to PositionMode.PARAMETER. position_mode (PositionMode, optional): position calculation mode only applies
when using distances. Defaults to PositionMode.PARAMETER.
deflection (float | None, optional): maximum deflection between the curve and
the polygon that results from the computed points. Defaults to None.
Returns: Returns:
list[Vector]: positions along curve list[Vector]: positions along curve
""" """
return [self.position_at(d, position_mode) for d in distances] if deflection is not None:
curve: BRepAdaptor_Curve | BRepAdaptor_CompCurve = self.geom_adaptor()
# GCPnts_UniformDeflection provides the best results but is limited
if curve.Continuity() in (GeomAbs_C2, GeomAbs_C3, GeomAbs_CN):
discretizer: (
GCPnts_UniformDeflection | GCPnts_QuasiUniformDeflection
) = GCPnts_UniformDeflection()
else:
discretizer = GCPnts_QuasiUniformDeflection()
discretizer.Initialize(
curve,
deflection,
curve.FirstParameter(),
curve.LastParameter(),
)
if not discretizer.IsDone() or discretizer.NbPoints() == 0:
raise RuntimeError("Deflection calculation failed")
return [
Vector(curve.Value(discretizer.Parameter(i + 1)))
for i in range(discretizer.NbPoints())
]
elif distances is not None:
return [self.position_at(d, position_mode) for d in distances]
else:
raise ValueError("Either distances or deflection must be provided")
def project( def project(
self, face: Face, direction: VectorLike, closest: bool = True self, face: Face, direction: VectorLike, closest: bool = True

View file

@ -28,6 +28,7 @@ license:
import math import math
import unittest import unittest
from unittest.mock import patch
from build123d.build_enums import ( from build123d.build_enums import (
CenterOf, CenterOf,
@ -106,13 +107,73 @@ class TestMixin1D(unittest.TestCase):
5, 5,
) )
def test_positions(self): def test_positions_with_distances(self):
e = Edge.make_line((0, 0, 0), (1, 1, 1)) e = Edge.make_line((0, 0, 0), (1, 1, 1))
distances = [i / 4 for i in range(3)] distances = [i / 4 for i in range(3)]
pts = e.positions(distances) pts = e.positions(distances)
for i, position in enumerate(pts): for i, position in enumerate(pts):
self.assertAlmostEqual(position, (i / 4, i / 4, i / 4), 5) self.assertAlmostEqual(position, (i / 4, i / 4, i / 4), 5)
def test_positions_deflection_line(self):
"""Deflection sampling on a straight line should yield exactly 2 points."""
e = Edge.make_line((0, 0, 0), (10, 0, 0))
pts = e.positions(deflection=0.1)
self.assertEqual(len(pts), 2)
self.assertAlmostEqual(pts[0], (0, 0, 0), 7)
self.assertAlmostEqual(pts[1], (10, 0, 0), 7)
def test_positions_deflection_circle(self):
"""Deflection on a C2 curve (circle) should produce multiple points."""
radius = 5
e = Edge.make_circle(radius)
pts = e.positions(deflection=0.1)
# Should produce more than just two points
self.assertGreater(len(pts), 2)
# Endpoints should match curve endpoints
first, last = pts[0], pts[-1]
curve = e.geom_adaptor()
p0 = Vector(curve.Value(curve.FirstParameter()))
p1 = Vector(curve.Value(curve.LastParameter()))
self.assertAlmostEqual(first, p0, 7)
self.assertAlmostEqual(last, p1, 7)
def test_positions_deflection_resolution(self):
"""Smaller deflection tolerance should produce more points."""
e = Edge.make_circle(10)
pts_coarse = e.positions(deflection=0.5)
pts_fine = e.positions(deflection=0.05)
self.assertGreater(len(pts_fine), len(pts_coarse))
def test_positions_deflection_C0_curve(self):
"""C0 spline should use QuasiUniformDeflection and still succeed."""
e = Polyline((0, 0), (1, 2), (2, 0))._to_bspline() # C0
pts = e.positions(deflection=0.1)
self.assertGreater(len(pts), 2)
def test_positions_missing_arguments(self):
e = Edge.make_line((0, 0, 0), (1, 0, 0))
with self.assertRaises(ValueError):
e.positions()
def test_positions_deflection_failure(self):
e = Edge.make_circle(1.0)
with patch("build123d.topology.one_d.GCPnts_UniformDeflection") as MockDefl:
instance = MockDefl.return_value
instance.IsDone.return_value = False
instance.NbPoints.return_value = 0
with self.assertRaises(RuntimeError):
e.positions(deflection=0.1)
def test_tangent_at(self): def test_tangent_at(self):
self.assertAlmostEqual( self.assertAlmostEqual(
Edge.make_circle(1, start_angle=0, end_angle=90).tangent_at(1.0), Edge.make_circle(1, start_angle=0, end_angle=90).tangent_at(1.0),
@ -368,11 +429,6 @@ class TestMixin1D(unittest.TestCase):
self.assertAlmostEqual(common.z_dir.Y, 0, 5) self.assertAlmostEqual(common.z_dir.Y, 0, 5)
self.assertAlmostEqual(common.z_dir.Z, 0, 5) self.assertAlmostEqual(common.z_dir.Z, 0, 5)
def test_discretize(self):
edge = Edge.make_circle(2, start_angle=0, end_angle=180)
points = edge.discretize(0.1)
self.assertEqual(len(points), 6)
def test_edge_volume(self): def test_edge_volume(self):
edge = Edge.make_line((0, 0), (1, 1)) edge = Edge.make_line((0, 0), (1, 1))
self.assertAlmostEqual(edge.volume, 0, 5) self.assertAlmostEqual(edge.volume, 0, 5)