From 7f6d44249b83bc0506c0ba97b77da83450d2f8a4 Mon Sep 17 00:00:00 2001 From: gumyr Date: Fri, 21 Nov 2025 14:18:48 -0500 Subject: [PATCH 1/3] Added GCPnts_UniformDeflection to positions --- src/build123d/topology/one_d.py | 47 ++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/src/build123d/topology/one_d.py b/src/build123d/topology/one_d.py index 27f9b11..d416be0 100644 --- a/src/build123d/topology/one_d.py +++ b/src/build123d/topology/one_d.py @@ -90,7 +90,11 @@ from OCP.BRepProj import BRepProj_Projection from OCP.BRepTools import BRepTools, BRepTools_WireExplorer from OCP.GC import GC_MakeArcOfCircle, GC_MakeArcOfEllipse from OCP.GccEnt import GccEnt_unqualified, GccEnt_Position -from OCP.GCPnts import GCPnts_AbscissaPoint +from OCP.GCPnts import ( + GCPnts_AbscissaPoint, + GCPnts_QuasiUniformDeflection, + GCPnts_UniformDeflection, +) from OCP.Geom import ( Geom_BezierCurve, Geom_BSplineCurve, @@ -116,6 +120,9 @@ from OCP.GeomAbs import ( GeomAbs_C0, GeomAbs_C1, GeomAbs_C2, + GeomAbs_C3, + GeomAbs_CN, + GeomAbs_C1, GeomAbs_G1, GeomAbs_G2, GeomAbs_JoinType, @@ -1178,22 +1185,50 @@ class Mixin1D(Shape[TOPODS]): def positions( self, - distances: Iterable[float], + distances: Iterable[float] | None = None, position_mode: PositionMode = PositionMode.PARAMETER, + deflection: float | None = None, ) -> list[Vector]: """Positions along curve Generate positions along the underlying curve Args: - distances (Iterable[float]): distance or parameter values - position_mode (PositionMode, optional): position calculation mode. - Defaults to PositionMode.PARAMETER. + distances (Iterable[float] | None, optional): distance or parameter values. + Defaults to None. + 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: 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() + 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( self, face: Face, direction: VectorLike, closest: bool = True From 2d82b2ca5cde5b1ad50e23ac2eb9ededd388f2fe Mon Sep 17 00:00:00 2001 From: gumyr Date: Tue, 25 Nov 2025 11:27:17 -0500 Subject: [PATCH 2/3] Adding tests for positions with deflection --- src/build123d/topology/one_d.py | 9 +++- tests/test_direct_api/test_mixin1_d.py | 63 +++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/build123d/topology/one_d.py b/src/build123d/topology/one_d.py index d416be0..231b844 100644 --- a/src/build123d/topology/one_d.py +++ b/src/build123d/topology/one_d.py @@ -373,7 +373,10 @@ class Mixin1D(Shape[TOPODS]): def _to_param(edge_wire: Mixin1D, value: float | VectorLike, name: str) -> float: """Convert a float or VectorLike into a curve parameter.""" if isinstance(value, (int, float)): - return float(value) + if edge_wire.is_forward: + return float(value) + else: + return 1.0 - float(value) try: point = Vector(value) except TypeError as exc: @@ -1209,7 +1212,9 @@ class Mixin1D(Shape[TOPODS]): 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() + discretizer: ( + GCPnts_UniformDeflection | GCPnts_QuasiUniformDeflection + ) = GCPnts_UniformDeflection() else: discretizer = GCPnts_QuasiUniformDeflection() diff --git a/tests/test_direct_api/test_mixin1_d.py b/tests/test_direct_api/test_mixin1_d.py index 1d7791b..efd4989 100644 --- a/tests/test_direct_api/test_mixin1_d.py +++ b/tests/test_direct_api/test_mixin1_d.py @@ -28,6 +28,7 @@ license: import math import unittest +from unittest.mock import patch from build123d.build_enums import ( CenterOf, @@ -106,13 +107,73 @@ class TestMixin1D(unittest.TestCase): 5, ) - def test_positions(self): + def test_positions_with_distances(self): e = Edge.make_line((0, 0, 0), (1, 1, 1)) distances = [i / 4 for i in range(3)] pts = e.positions(distances) for i, position in enumerate(pts): 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.edge.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): self.assertAlmostEqual( Edge.make_circle(1, start_angle=0, end_angle=90).tangent_at(1.0), From 82aa0aa36724aa6b6a437e3eaa7cf2ac7963ae26 Mon Sep 17 00:00:00 2001 From: gumyr Date: Tue, 25 Nov 2025 11:39:39 -0500 Subject: [PATCH 3/3] Updating positions tests --- src/build123d/topology/one_d.py | 5 +---- tests/test_direct_api/test_mixin1_d.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/build123d/topology/one_d.py b/src/build123d/topology/one_d.py index 231b844..f76f6bc 100644 --- a/src/build123d/topology/one_d.py +++ b/src/build123d/topology/one_d.py @@ -373,10 +373,7 @@ class Mixin1D(Shape[TOPODS]): def _to_param(edge_wire: Mixin1D, value: float | VectorLike, name: str) -> float: """Convert a float or VectorLike into a curve parameter.""" if isinstance(value, (int, float)): - if edge_wire.is_forward: - return float(value) - else: - return 1.0 - float(value) + return float(value) try: point = Vector(value) except TypeError as exc: diff --git a/tests/test_direct_api/test_mixin1_d.py b/tests/test_direct_api/test_mixin1_d.py index efd4989..864711b 100644 --- a/tests/test_direct_api/test_mixin1_d.py +++ b/tests/test_direct_api/test_mixin1_d.py @@ -166,7 +166,7 @@ class TestMixin1D(unittest.TestCase): def test_positions_deflection_failure(self): e = Edge.make_circle(1.0) - with patch("build123d.edge.GCPnts_UniformDeflection") as MockDefl: + with patch("build123d.topology.one_d.GCPnts_UniformDeflection") as MockDefl: instance = MockDefl.return_value instance.IsDone.return_value = False instance.NbPoints.return_value = 0