Skip to content

Commit 0624bff

Browse files
committed
Replacing location_at(planar) with (x_dir)
1 parent 8c17183 commit 0624bff

File tree

2 files changed

+104
-8
lines changed

2 files changed

+104
-8
lines changed

src/build123d/topology/one_d.py

+52-7
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,11 @@
120120
from OCP.HLRBRep import HLRBRep_Algo, HLRBRep_HLRToShape
121121
from OCP.ShapeAnalysis import ShapeAnalysis_FreeBounds
122122
from OCP.ShapeFix import ShapeFix_Shape, ShapeFix_Wireframe
123-
from OCP.Standard import Standard_Failure, Standard_NoSuchObject
123+
from OCP.Standard import (
124+
Standard_Failure,
125+
Standard_NoSuchObject,
126+
Standard_ConstructionError,
127+
)
124128
from OCP.TColStd import (
125129
TColStd_Array1OfReal,
126130
TColStd_HArray1OfBoolean,
@@ -511,7 +515,8 @@ def location_at(
511515
distance: float,
512516
position_mode: PositionMode = PositionMode.PARAMETER,
513517
frame_method: FrameMethod = FrameMethod.FRENET,
514-
planar: bool = False,
518+
planar: bool | None = None,
519+
x_dir: VectorLike | None = None,
515520
) -> Location:
516521
"""Locations along curve
517522
@@ -522,8 +527,18 @@ def location_at(
522527
position_mode (PositionMode, optional): position calculation mode.
523528
Defaults to PositionMode.PARAMETER.
524529
frame_method (FrameMethod, optional): moving frame calculation method.
530+
The FRENET frame can “twist” or flip unexpectedly, especially near flat
531+
spots. The CORRECTED frame behaves more like a “camera dolly” or
532+
sweep profile would — it's smoother and more stable.
525533
Defaults to FrameMethod.FRENET.
526-
planar (bool, optional): planar mode. Defaults to False.
534+
planar (bool, optional): planar mode. Defaults to None.
535+
x_dir (VectorLike, optional): override the x_dir to help with plane
536+
creation along a 1D shape. Must be perpendicalar to shapes tangent.
537+
Defaults to None.
538+
539+
.. deprecated::
540+
The `planar` parameter is deprecated and will be removed in a future release.
541+
Use `x_dir` to specify orientation instead.
527542
528543
Returns:
529544
Location: A Location object representing local coordinate system
@@ -550,23 +565,45 @@ def location_at(
550565
pnt = curve.Value(param)
551566

552567
transformation = gp_Trsf()
553-
if planar:
568+
if planar is not None:
569+
warnings.warn(
570+
"The 'planar' parameter is deprecated and will be removed in a future version. "
571+
"Use 'x_dir' to control orientation instead.",
572+
DeprecationWarning,
573+
stacklevel=2,
574+
)
575+
if planar is not None and planar:
554576
transformation.SetTransformation(
555577
gp_Ax3(pnt, gp_Dir(0, 0, 1), gp_Dir(normal.XYZ())), gp_Ax3()
556578
)
579+
elif x_dir is not None:
580+
try:
581+
582+
transformation.SetTransformation(
583+
gp_Ax3(pnt, gp_Dir(tangent.XYZ()), Vector(x_dir).to_dir()), gp_Ax3()
584+
)
585+
except Standard_ConstructionError:
586+
raise ValueError(
587+
f"Unable to create location with given x_dir {x_dir}. "
588+
f"x_dir must be perpendicular to shape's tangent "
589+
f"{tuple(Vector(tangent))}."
590+
)
591+
557592
else:
558593
transformation.SetTransformation(
559594
gp_Ax3(pnt, gp_Dir(tangent.XYZ()), gp_Dir(normal.XYZ())), gp_Ax3()
560595
)
596+
loc = Location(TopLoc_Location(transformation))
561597

562-
return Location(TopLoc_Location(transformation))
598+
return loc
563599

564600
def locations(
565601
self,
566602
distances: Iterable[float],
567603
position_mode: PositionMode = PositionMode.PARAMETER,
568604
frame_method: FrameMethod = FrameMethod.FRENET,
569-
planar: bool = False,
605+
planar: bool | None = None,
606+
x_dir: VectorLike | None = None,
570607
) -> list[Location]:
571608
"""Locations along curve
572609
@@ -579,13 +616,21 @@ def locations(
579616
frame_method (FrameMethod, optional): moving frame calculation method.
580617
Defaults to FrameMethod.FRENET.
581618
planar (bool, optional): planar mode. Defaults to False.
619+
x_dir (VectorLike, optional): override the x_dir to help with plane
620+
creation along a 1D shape. Must be perpendicalar to shapes tangent.
621+
Defaults to None.
622+
623+
.. deprecated::
624+
The `planar` parameter is deprecated and will be removed in a future release.
625+
Use `x_dir` to specify orientation instead.
582626
583627
Returns:
584628
list[Location]: A list of Location objects representing local coordinate
585629
systems at the specified distances.
586630
"""
587631
return [
588-
self.location_at(d, position_mode, frame_method, planar) for d in distances
632+
self.location_at(d, position_mode, frame_method, planar, x_dir)
633+
for d in distances
589634
]
590635

591636
def normal(self) -> Vector:

tests/test_direct_api/test_mixin1_d.py

+52-1
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,16 @@
2929
import math
3030
import unittest
3131

32-
from build123d.build_enums import CenterOf, GeomType, PositionMode, Side, SortBy
32+
from build123d.build_enums import (
33+
CenterOf,
34+
FrameMethod,
35+
GeomType,
36+
PositionMode,
37+
Side,
38+
SortBy,
39+
)
3340
from build123d.geometry import Axis, Location, Plane, Vector
41+
from build123d.objects_curve import Polyline
3442
from build123d.objects_part import Box, Cylinder
3543
from build123d.topology import Compound, Edge, Face, Wire
3644

@@ -201,6 +209,18 @@ def test_location_at(self):
201209
self.assertAlmostEqual(loc.position, (0, 1, 0), 5)
202210
self.assertAlmostEqual(loc.orientation, (0, -90, -90), 5)
203211

212+
def test_location_at_x_dir(self):
213+
path = Polyline((-50, -40), (50, -40), (50, 40), (-50, 40), close=True)
214+
l1 = path.location_at(0)
215+
l2 = path.location_at(0, x_dir=(0, 1, 0))
216+
self.assertAlmostEqual(l1.position, l2.position, 5)
217+
self.assertAlmostEqual(l1.z_axis, l2.z_axis, 5)
218+
self.assertNotEqual(l1.x_axis, l2.x_axis, 5)
219+
self.assertAlmostEqual(l2.x_axis, Axis(path @ 0, (0, 1, 0)), 5)
220+
221+
with self.assertRaises(ValueError):
222+
path.location_at(0, x_dir=(1, 0, 0))
223+
204224
def test_locations(self):
205225
locs = Edge.make_circle(1).locations([i / 4 for i in range(4)])
206226
self.assertAlmostEqual(locs[0].position, (1, 0, 0), 5)
@@ -212,6 +232,37 @@ def test_locations(self):
212232
self.assertAlmostEqual(locs[3].position, (0, -1, 0), 5)
213233
self.assertAlmostEqual(locs[3].orientation, (0, 90, 90), 5)
214234

235+
def test_location_at_corrected_frenet(self):
236+
# A polyline with sharp corners — problematic for classic Frenet
237+
path = Polyline((0, 0), (10, 0), (10, 10), (0, 10))
238+
239+
# Request multiple locations along the curve
240+
locations = [
241+
path.location_at(t, frame_method=FrameMethod.CORRECTED)
242+
for t in [0.0, 0.25, 0.5, 0.75, 1.0]
243+
]
244+
# Ensure all locations were created and have consistent orientation
245+
self.assertTrue(
246+
all(
247+
locations[0].x_axis.direction == l.x_axis.direction
248+
for l in locations[1:]
249+
)
250+
)
251+
252+
# Check that Z-axis is approximately orthogonal to X-axis
253+
for loc in locations:
254+
self.assertLess(abs(loc.z_axis.direction.dot(loc.x_axis.direction)), 1e-6)
255+
256+
# Check continuity of rotation (not flipping wildly)
257+
# Check angle between x_axes doesn't flip more than ~90 degrees
258+
angles = []
259+
for i in range(len(locations) - 1):
260+
a1 = locations[i].x_axis.direction
261+
a2 = locations[i + 1].x_axis.direction
262+
angle = a1.get_angle(a2)
263+
angles.append(angle)
264+
self.assertTrue(all(abs(angle) < 90 for angle in angles))
265+
215266
def test_project(self):
216267
target = Face.make_rect(10, 10, Plane.XY.rotated((0, 45, 0)))
217268
circle = Edge.make_circle(1).locate(Location((0, 0, 10)))

0 commit comments

Comments
 (0)