Skip to content

Commit b86542b

Browse files
jacobrkerstetterpre-commit-ci[bot]pyansys-ci-botRobPasMue
authored
feat: NURBS and TrimmedCurve enhancements (#1994)
Co-authored-by: Jacob Kerstetter <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <[email protected]> Co-authored-by: Roberto Pastor Muela <[email protected]>
1 parent bb8011b commit b86542b

File tree

7 files changed

+249
-21
lines changed

7 files changed

+249
-21
lines changed

doc/changelog.d/1994.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Nurbs and trimmedcurve enhancements

src/ansys/geometry/core/math/vector.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ def transform(self, matrix: "Matrix44") -> "Vector3D":
159159
transformation matrix and return a new ``Vector3D`` object representing the
160160
transformed vector.
161161
"""
162-
vector_4x1 = np.append(self, 1)
162+
vector_4x1 = np.append(self, 0)
163163
result_4x1 = matrix * vector_4x1
164164
result_vector = Vector3D(result_4x1[0:3])
165165
return result_vector

src/ansys/geometry/core/shapes/curves/circle.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ def transformed_copy(self, matrix: Matrix44) -> "Circle":
163163
new_point = self.origin.transform(matrix)
164164
new_reference = self._reference.transform(matrix)
165165
new_axis = self._axis.transform(matrix)
166+
166167
return Circle(
167168
new_point,
168169
self.radius,

src/ansys/geometry/core/shapes/curves/nurbs.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from typing import TYPE_CHECKING, Optional
2626

2727
from beartype import beartype as check_input_types
28+
import numpy as np
2829

2930
from ansys.geometry.core.math import Matrix44, Point3D
3031
from ansys.geometry.core.math.vector import Vector3D
@@ -141,6 +142,54 @@ def from_control_points(
141142

142143
return curve
143144

145+
@classmethod
146+
@check_input_types
147+
def fit_curve_from_points(
148+
cls,
149+
points: list[Point3D],
150+
degree: int,
151+
) -> "NURBSCurve":
152+
"""Fit a NURBS curve to a set of points.
153+
154+
Parameters
155+
----------
156+
points : list[Point3D]
157+
Points to fit the curve to.
158+
degree : int
159+
Degree of the curve.
160+
161+
Returns
162+
-------
163+
NURBSCurve
164+
Fitted NURBS curve.
165+
166+
"""
167+
from geomdl import fitting
168+
169+
# Convert points to a format suitable for the fitting function
170+
converted_points = []
171+
for pt in points:
172+
pt_raw = [*pt]
173+
converted_points.append(pt_raw)
174+
175+
# Fit the curve to the points
176+
curve = fitting.interpolate_curve(converted_points, degree)
177+
178+
# Construct the NURBSCurve object
179+
nurbs_curve = cls()
180+
nurbs_curve._nurbs_curve.degree = curve.degree
181+
nurbs_curve._nurbs_curve.ctrlpts = [Point3D(entry) for entry in curve.ctrlpts]
182+
nurbs_curve._nurbs_curve.knotvector = curve.knotvector
183+
nurbs_curve._nurbs_curve.weights = curve.weights
184+
185+
# Verify the curve is valid
186+
try:
187+
nurbs_curve._nurbs_curve._check_variables()
188+
except ValueError as e:
189+
raise ValueError(f"Invalid NURBS curve: {e}")
190+
191+
return nurbs_curve
192+
144193
def __eq__(self, other: "NURBSCurve") -> bool:
145194
"""Determine if two curves are equal."""
146195
if not isinstance(other, NURBSCurve):
@@ -164,8 +213,8 @@ def parameterization(self) -> Parameterization:
164213
Information about how the NURBS curve is parameterized.
165214
"""
166215
return Parameterization(
167-
ParamType.OTHER,
168216
ParamForm.OTHER,
217+
ParamType.OTHER,
169218
Interval(start=self._nurbs_curve.domain[0], end=self._nurbs_curve.domain[1]),
170219
)
171220

@@ -182,7 +231,12 @@ def transformed_copy(self, matrix: Matrix44) -> "NURBSCurve":
182231
NURBSCurve
183232
Transformed copy of the curve.
184233
"""
185-
control_points = [matrix @ point for point in self._nurbs_curve.ctrlpts]
234+
control_points = []
235+
for point in self._nurbs_curve.ctrlpts:
236+
# Transform the control point using the transformation matrix
237+
transformed_point = matrix @ np.array([*point, 1])
238+
control_points.append(Point3D(transformed_point[:3]))
239+
186240
return NURBSCurve.from_control_points(
187241
control_points,
188242
self._nurbs_curve.degree,

src/ansys/geometry/core/shapes/curves/trimmed_curve.py

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@
2727
from ansys.api.geometry.v0.commands_pb2_grpc import CommandsStub
2828
from ansys.geometry.core.connection.client import GrpcClient
2929
from ansys.geometry.core.connection.conversions import trimmed_curve_to_grpc_trimmed_curve
30+
from ansys.geometry.core.math.matrix import Matrix44
3031
from ansys.geometry.core.math.point import Point3D
31-
from ansys.geometry.core.misc.measurements import DEFAULT_UNITS
32+
from ansys.geometry.core.math.vector import UnitVector3D, Vector3D
33+
from ansys.geometry.core.misc.measurements import DEFAULT_UNITS, Angle, Distance
3234
from ansys.geometry.core.shapes.curves.curve import Curve
3335
from ansys.geometry.core.shapes.curves.curve_evaluation import CurveEvaluation
3436
from ansys.geometry.core.shapes.parameterization import Interval
@@ -151,6 +153,90 @@ def intersect_curve(self, other: "TrimmedCurve") -> list[Point3D]:
151153
for point in res.points
152154
]
153155

156+
def transformed_copy(self, matrix: Matrix44) -> "TrimmedCurve":
157+
"""Return a copy of the trimmed curve transformed by the given matrix.
158+
159+
Parameters
160+
----------
161+
matrix : Matrix44
162+
Transformation matrix to apply to the curve.
163+
164+
Returns
165+
-------
166+
TrimmedCurve
167+
A new trimmed curve with the transformation applied.
168+
"""
169+
transformed_geometry = self.geometry.transformed_copy(matrix)
170+
transformed_start = self.start.transform(matrix)
171+
transformed_end = self.end.transform(matrix)
172+
173+
return TrimmedCurve(
174+
transformed_geometry,
175+
transformed_start,
176+
transformed_end,
177+
self.interval,
178+
self.length,
179+
)
180+
181+
def translate(self, direction: UnitVector3D, distance: Real | Quantity | Distance) -> None:
182+
"""Translate the trimmed curve by a given vector and distance.
183+
184+
Parameters
185+
----------
186+
direction : UnitVector3D
187+
Direction of translation.
188+
distance : Real | Quantity | Distance
189+
Distance to translate.
190+
"""
191+
distance = distance if isinstance(distance, Distance) else Distance(distance)
192+
translation_matrix = Matrix44.create_translation(direction * distance.value.m)
193+
194+
translated_copy = self.transformed_copy(translation_matrix)
195+
196+
# Update the current instance with the translated copy
197+
self._geometry = translated_copy.geometry
198+
self._start = translated_copy.start
199+
self._end = translated_copy.end
200+
self._length = translated_copy.length
201+
self._interval = translated_copy.interval
202+
203+
def rotate(self, origin: Point3D, axis: UnitVector3D, angle: Real | Quantity | Angle) -> None:
204+
"""Rotate the trimmed curve around a given axis centered at a given point.
205+
206+
Parameters
207+
----------
208+
origin : Point3D
209+
Origin point of the rotation.
210+
axis : UnitVector3D
211+
Axis of rotation.
212+
angle : Real | Quantity | Angle
213+
Angle to rotate in radians.
214+
"""
215+
angle = angle if isinstance(angle, Angle) else Angle(angle)
216+
217+
# Translate the curve to the origin
218+
translate_to_origin_matrix = Matrix44.create_translation(
219+
Vector3D([-origin.x.m, -origin.y.m, -origin.z.m])
220+
)
221+
translated_copy = self.transformed_copy(translate_to_origin_matrix)
222+
223+
# Rotate the curve around the axis
224+
rotation_matrix = Matrix44.create_matrix_from_rotation_about_axis(axis, angle.value.m)
225+
rotated_copy = translated_copy.transformed_copy(rotation_matrix)
226+
227+
# Translate the curve back to its original position
228+
translate_back_matrix = Matrix44.create_translation(
229+
Vector3D([origin.x.m, origin.y.m, origin.z.m])
230+
)
231+
translated_back_copy = rotated_copy.transformed_copy(translate_back_matrix)
232+
233+
# Update the current instance with the rotated copy
234+
self._geometry = translated_back_copy.geometry
235+
self._start = translated_back_copy.start
236+
self._end = translated_back_copy.end
237+
self._length = translated_back_copy.length
238+
self._interval = translated_back_copy.interval
239+
154240
def __repr__(self) -> str:
155241
"""Represent the trimmed curve as a string."""
156242
return (

tests/integration/test_trimmed_geometry.py

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,16 @@
4141
)
4242
from ansys.geometry.core.sketch.sketch import Sketch
4343

44-
"""A helper function to create a sketch line given two points and a design."""
45-
4644

4745
def create_sketch_line(design: Design, p1: Point3D, p2: Point3D):
46+
"""A helper function to create a sketch line given two points and a design."""
4847
point1 = point3d_to_grpc_point(p1)
4948
point2 = point3d_to_grpc_point(p2)
5049
design._commands_stub.CreateSketchLine(CreateSketchLineRequest(point1=point1, point2=point2))
5150

5251

53-
"""A helper function that creates the Hedgehog model."""
54-
55-
5652
def create_hedgehog(modeler: Modeler):
53+
"""A helper function that creates the Hedgehog model."""
5754
design = modeler.create_design("Hedgehog")
5855
sketch = Sketch().arc_from_three_points(
5956
Point2D([0.01, 0.01]), Point2D([0, -0.005]), Point2D([-0.01, 0.01])
@@ -97,19 +94,15 @@ def create_hedgehog(modeler: Modeler):
9794
return design
9895

9996

100-
"""A fixture of the hedgehog design to test the surface and curve properties individually."""
101-
102-
10397
@pytest.fixture
10498
def hedgehog_design(modeler: Modeler):
99+
"""A fixture of the hedgehog design to test the surface and curve properties individually."""
105100
h = create_hedgehog(modeler)
106101
yield h
107102

108103

109-
"""Tests the surface properties for the hedgehog design."""
110-
111-
112104
def test_trimmed_surface_properties(hedgehog_design):
105+
"""Tests the surface properties for the hedgehog design."""
113106
hedgehog_body = hedgehog_design.bodies[0]
114107
faces = hedgehog_body.faces
115108

@@ -161,10 +154,8 @@ def test_trimmed_surface_properties(hedgehog_design):
161154
assert faces[i].shape.box_uv.interval_v == Interval(start=interval_v[0], end=interval_v[1])
162155

163156

164-
"""Tests the normal vectors for the hedgehog design using the BoxUV coordinates."""
165-
166-
167157
def test_trimmed_surface_normals(hedgehog_design):
158+
"""Tests the normal vectors for the hedgehog design using the BoxUV coordinates."""
168159
hedgehog_body = hedgehog_design.bodies[0]
169160
faces = hedgehog_body.faces
170161
# corners to consider
@@ -219,10 +210,8 @@ def test_trimmed_surface_normals(hedgehog_design):
219210
assert np.allclose(faces[i].shape.normal(corner_param.u, corner_param.v), bottom_right)
220211

221212

222-
"""Tests the curve properties for the hedgehog design."""
223-
224-
225213
def test_trimmed_curve_properties(hedgehog_design):
214+
"""Tests the curve properties for the hedgehog design."""
226215
hedgehog_body = hedgehog_design.bodies[0]
227216
edges = hedgehog_body.edges
228217

@@ -245,3 +234,73 @@ def test_trimmed_curve_properties(hedgehog_design):
245234
assert isinstance(edges[i].shape.geometry, geometry_type)
246235
assert np.allclose(edges[i].shape.start, Point3D(start))
247236
assert np.allclose(edges[i].shape.end, Point3D(end))
237+
238+
239+
def test_trimmed_curve_line_translate(hedgehog_design):
240+
"""Tests the translation of a trimmed curve with line geometry."""
241+
hedgehog_body = hedgehog_design.bodies[0]
242+
edges = hedgehog_body.edges
243+
edge = edges[1]
244+
trimmed_curve = edge.shape
245+
246+
assert isinstance(trimmed_curve, TrimmedCurve)
247+
assert trimmed_curve.start == Point3D([0.01, 0.01, 0.0])
248+
assert trimmed_curve.end == Point3D([0.01, 0.01, 0.02])
249+
250+
trimmed_curve.translate(UnitVector3D([1, 0, 0]), 0.01)
251+
252+
assert trimmed_curve.start == Point3D([0.02, 0.01, 0.0])
253+
assert trimmed_curve.end == Point3D([0.02, 0.01, 0.02])
254+
255+
256+
def test_trimmed_curve_line_rotate(hedgehog_design):
257+
"""Tests the rotation of a trimmed curve with line geometry."""
258+
hedgehog_body = hedgehog_design.bodies[0]
259+
edges = hedgehog_body.edges
260+
edge = edges[1]
261+
trimmed_curve = edge.shape
262+
263+
assert isinstance(trimmed_curve, TrimmedCurve)
264+
assert trimmed_curve.start == Point3D([0.01, 0.01, 0.0])
265+
assert trimmed_curve.end == Point3D([0.01, 0.01, 0.02])
266+
267+
# Rotate the curve in the x-direction by 90 degrees about the point (0.01, 0.01, 0.0)
268+
trimmed_curve.rotate(Point3D([0.01, 0.01, 0.0]), UnitVector3D([1, 0, 0]), np.pi / 2)
269+
270+
assert np.allclose(trimmed_curve.start, Point3D([0.01, 0.01, 0.0]))
271+
assert np.allclose(trimmed_curve.end, Point3D([0.01, -0.01, 0.0]))
272+
273+
274+
def test_trimmed_curve_circle_translate(hedgehog_design):
275+
"""Tests the rotation of a trimmed curve with circle geometry."""
276+
hedgehog_body = hedgehog_design.bodies[0]
277+
edges = hedgehog_body.edges
278+
edge = edges[0]
279+
trimmed_curve = edge.shape
280+
281+
assert isinstance(trimmed_curve, TrimmedCurve)
282+
assert np.allclose(trimmed_curve.start, Point3D([0.01, 0.01, 0.02]))
283+
assert np.allclose(trimmed_curve.end, Point3D([-0.01, 0.01, 0.02]))
284+
285+
trimmed_curve.translate(UnitVector3D([1, 0, 0]), 0.01)
286+
287+
assert np.allclose(trimmed_curve.start, Point3D([0.02, 0.01, 0.02]))
288+
assert np.allclose(trimmed_curve.end, Point3D([0.0, 0.01, 0.02]))
289+
290+
291+
def test_trimmed_curve_circle_rotate(hedgehog_design):
292+
"""Tests the rotation of a trimmed curve with circle geometry."""
293+
hedgehog_body = hedgehog_design.bodies[0]
294+
edges = hedgehog_body.edges
295+
edge = edges[0]
296+
trimmed_curve = edge.shape
297+
298+
assert isinstance(trimmed_curve, TrimmedCurve)
299+
assert np.allclose(trimmed_curve.start, Point3D([0.01, 0.01, 0.02]))
300+
assert np.allclose(trimmed_curve.end, Point3D([-0.01, 0.01, 0.02]))
301+
302+
# Rotate the curve in the x-direction by 90 degrees about the point (0.01, 0.01, 0.02)
303+
trimmed_curve.rotate(Point3D([0.01, 0.01, 0.02]), UnitVector3D([0, 1, 0]), np.pi / 2)
304+
305+
assert np.allclose(trimmed_curve.start, Point3D([0.01, 0.01, 0.02]))
306+
assert np.allclose(trimmed_curve.end, Point3D([0.01, 0.01, 0.04]))

tests/test_primitives.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,33 @@ def test_nurbs_curve_from_control_points():
961961
assert nurbs_curve != nurbs_curve_weights
962962

963963

964+
def test_nurbs_curve_fitting():
965+
"""Test ``NURBSCurve`` fitting."""
966+
points = [
967+
Point3D([0, 0, 0]),
968+
Point3D([1, 1, 0]),
969+
Point3D([2, 0, 0]),
970+
Point3D([5, 2, 0]),
971+
]
972+
degree = 3
973+
nurbs_curve = NURBSCurve.fit_curve_from_points(points=points, degree=degree)
974+
975+
# Verify degree, knots, and control points
976+
assert nurbs_curve.degree == degree
977+
978+
assert len(nurbs_curve.knots) == 8
979+
980+
assert len(nurbs_curve.control_points) == 4
981+
assert np.allclose(nurbs_curve.control_points[0], Point3D([0, 0, 0]))
982+
assert np.allclose(
983+
nurbs_curve.control_points[1], Point3D([1.54969033497753, 4.03483016710592, 0])
984+
)
985+
assert np.allclose(
986+
nurbs_curve.control_points[2], Point3D([2.87290323505786, -5.66639579939497, 0])
987+
)
988+
assert np.allclose(nurbs_curve.control_points[3], Point3D([5, 2, 0]))
989+
990+
964991
def test_nurbs_curve_evaluation():
965992
"""Test ``NURBSCurve`` evaluation."""
966993
control_points = [

0 commit comments

Comments
 (0)