Replacing location_at(planar) with (x_dir)

This commit is contained in:
gumyr 2025-03-21 15:29:27 -04:00
parent 8c171837ee
commit 0624bff82e
2 changed files with 104 additions and 8 deletions

View file

@ -120,7 +120,11 @@ from OCP.HLRAlgo import HLRAlgo_Projector
from OCP.HLRBRep import HLRBRep_Algo, HLRBRep_HLRToShape
from OCP.ShapeAnalysis import ShapeAnalysis_FreeBounds
from OCP.ShapeFix import ShapeFix_Shape, ShapeFix_Wireframe
from OCP.Standard import Standard_Failure, Standard_NoSuchObject
from OCP.Standard import (
Standard_Failure,
Standard_NoSuchObject,
Standard_ConstructionError,
)
from OCP.TColStd import (
TColStd_Array1OfReal,
TColStd_HArray1OfBoolean,
@ -511,7 +515,8 @@ class Mixin1D(Shape):
distance: float,
position_mode: PositionMode = PositionMode.PARAMETER,
frame_method: FrameMethod = FrameMethod.FRENET,
planar: bool = False,
planar: bool | None = None,
x_dir: VectorLike | None = None,
) -> Location:
"""Locations along curve
@ -522,8 +527,18 @@ class Mixin1D(Shape):
position_mode (PositionMode, optional): position calculation mode.
Defaults to PositionMode.PARAMETER.
frame_method (FrameMethod, optional): moving frame calculation method.
The FRENET frame can twist or flip unexpectedly, especially near flat
spots. The CORRECTED frame behaves more like a camera dolly or
sweep profile would it's smoother and more stable.
Defaults to FrameMethod.FRENET.
planar (bool, optional): planar mode. Defaults to False.
planar (bool, optional): planar mode. Defaults to None.
x_dir (VectorLike, optional): override the x_dir to help with plane
creation along a 1D shape. Must be perpendicalar to shapes tangent.
Defaults to None.
.. deprecated::
The `planar` parameter is deprecated and will be removed in a future release.
Use `x_dir` to specify orientation instead.
Returns:
Location: A Location object representing local coordinate system
@ -550,23 +565,45 @@ class Mixin1D(Shape):
pnt = curve.Value(param)
transformation = gp_Trsf()
if planar:
if planar is not None:
warnings.warn(
"The 'planar' parameter is deprecated and will be removed in a future version. "
"Use 'x_dir' to control orientation instead.",
DeprecationWarning,
stacklevel=2,
)
if planar is not None and planar:
transformation.SetTransformation(
gp_Ax3(pnt, gp_Dir(0, 0, 1), gp_Dir(normal.XYZ())), gp_Ax3()
)
elif x_dir is not None:
try:
transformation.SetTransformation(
gp_Ax3(pnt, gp_Dir(tangent.XYZ()), Vector(x_dir).to_dir()), gp_Ax3()
)
except Standard_ConstructionError:
raise ValueError(
f"Unable to create location with given x_dir {x_dir}. "
f"x_dir must be perpendicular to shape's tangent "
f"{tuple(Vector(tangent))}."
)
else:
transformation.SetTransformation(
gp_Ax3(pnt, gp_Dir(tangent.XYZ()), gp_Dir(normal.XYZ())), gp_Ax3()
)
loc = Location(TopLoc_Location(transformation))
return Location(TopLoc_Location(transformation))
return loc
def locations(
self,
distances: Iterable[float],
position_mode: PositionMode = PositionMode.PARAMETER,
frame_method: FrameMethod = FrameMethod.FRENET,
planar: bool = False,
planar: bool | None = None,
x_dir: VectorLike | None = None,
) -> list[Location]:
"""Locations along curve
@ -579,13 +616,21 @@ class Mixin1D(Shape):
frame_method (FrameMethod, optional): moving frame calculation method.
Defaults to FrameMethod.FRENET.
planar (bool, optional): planar mode. Defaults to False.
x_dir (VectorLike, optional): override the x_dir to help with plane
creation along a 1D shape. Must be perpendicalar to shapes tangent.
Defaults to None.
.. deprecated::
The `planar` parameter is deprecated and will be removed in a future release.
Use `x_dir` to specify orientation instead.
Returns:
list[Location]: A list of Location objects representing local coordinate
systems at the specified distances.
"""
return [
self.location_at(d, position_mode, frame_method, planar) for d in distances
self.location_at(d, position_mode, frame_method, planar, x_dir)
for d in distances
]
def normal(self) -> Vector:

View file

@ -29,8 +29,16 @@ license:
import math
import unittest
from build123d.build_enums import CenterOf, GeomType, PositionMode, Side, SortBy
from build123d.build_enums import (
CenterOf,
FrameMethod,
GeomType,
PositionMode,
Side,
SortBy,
)
from build123d.geometry import Axis, Location, Plane, Vector
from build123d.objects_curve import Polyline
from build123d.objects_part import Box, Cylinder
from build123d.topology import Compound, Edge, Face, Wire
@ -201,6 +209,18 @@ class TestMixin1D(unittest.TestCase):
self.assertAlmostEqual(loc.position, (0, 1, 0), 5)
self.assertAlmostEqual(loc.orientation, (0, -90, -90), 5)
def test_location_at_x_dir(self):
path = Polyline((-50, -40), (50, -40), (50, 40), (-50, 40), close=True)
l1 = path.location_at(0)
l2 = path.location_at(0, x_dir=(0, 1, 0))
self.assertAlmostEqual(l1.position, l2.position, 5)
self.assertAlmostEqual(l1.z_axis, l2.z_axis, 5)
self.assertNotEqual(l1.x_axis, l2.x_axis, 5)
self.assertAlmostEqual(l2.x_axis, Axis(path @ 0, (0, 1, 0)), 5)
with self.assertRaises(ValueError):
path.location_at(0, x_dir=(1, 0, 0))
def test_locations(self):
locs = Edge.make_circle(1).locations([i / 4 for i in range(4)])
self.assertAlmostEqual(locs[0].position, (1, 0, 0), 5)
@ -212,6 +232,37 @@ class TestMixin1D(unittest.TestCase):
self.assertAlmostEqual(locs[3].position, (0, -1, 0), 5)
self.assertAlmostEqual(locs[3].orientation, (0, 90, 90), 5)
def test_location_at_corrected_frenet(self):
# A polyline with sharp corners — problematic for classic Frenet
path = Polyline((0, 0), (10, 0), (10, 10), (0, 10))
# Request multiple locations along the curve
locations = [
path.location_at(t, frame_method=FrameMethod.CORRECTED)
for t in [0.0, 0.25, 0.5, 0.75, 1.0]
]
# Ensure all locations were created and have consistent orientation
self.assertTrue(
all(
locations[0].x_axis.direction == l.x_axis.direction
for l in locations[1:]
)
)
# Check that Z-axis is approximately orthogonal to X-axis
for loc in locations:
self.assertLess(abs(loc.z_axis.direction.dot(loc.x_axis.direction)), 1e-6)
# Check continuity of rotation (not flipping wildly)
# Check angle between x_axes doesn't flip more than ~90 degrees
angles = []
for i in range(len(locations) - 1):
a1 = locations[i].x_axis.direction
a2 = locations[i + 1].x_axis.direction
angle = a1.get_angle(a2)
angles.append(angle)
self.assertTrue(all(abs(angle) < 90 for angle in angles))
def test_project(self):
target = Face.make_rect(10, 10, Plane.XY.rotated((0, 45, 0)))
circle = Edge.make_circle(1).locate(Location((0, 0, 10)))