Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a4e285f
nurbs sketch wip
jacobrkerstetter Jul 2, 2025
79cd02f
sketch nurbs working with visualization
jacobrkerstetter Jul 3, 2025
f11312f
doc edits
jacobrkerstetter Jul 3, 2025
9636f22
added degree check for fitting from < 4 points
jacobrkerstetter Jul 3, 2025
8f8cc96
Merge branch 'main' of https://github.com/ansys/pyansys-geometry into…
jacobrkerstetter Jul 7, 2025
38b92b0
adding contains_point for nurbs edge
jacobrkerstetter Jul 8, 2025
a1ffa39
added test to create a surface from a combination of nurbs and segmen…
jacobrkerstetter Jul 9, 2025
e591136
add fill command
jacobrkerstetter Jul 10, 2025
8a3bab5
Merge branch 'main' of https://github.com/ansys/pyansys-geometry into…
jacobrkerstetter Jul 11, 2025
2ee3921
added test for creating surface from nurbs sketch + overhanging edges
jacobrkerstetter Jul 11, 2025
6d25316
chore: auto fixes from pre-commit hooks
pre-commit-ci[bot] Jul 11, 2025
2bca7ab
chore: adding changelog file 2104.added.md [dependabot-skip]
pyansys-ci-bot Jul 11, 2025
a7dc888
code cleanup
jacobrkerstetter Jul 11, 2025
6e600ea
Merge branch 'feat/nurbs_sketching' of https://github.com/ansys/pyans…
jacobrkerstetter Jul 11, 2025
9601df4
chore: auto fixes from pre-commit hooks
pre-commit-ci[bot] Jul 11, 2025
1b5f78e
more cleanup
jacobrkerstetter Jul 11, 2025
d00f558
Merge branch 'feat/nurbs_sketching' of https://github.com/ansys/pyans…
jacobrkerstetter Jul 11, 2025
c931225
Merge branch 'main' into feat/nurbs_sketching
jacobrkerstetter Jul 15, 2025
a8f91e6
bump protos version
jacobrkerstetter Jul 15, 2025
9f65d13
remove plot statement used for testing
jacobrkerstetter Jul 15, 2025
0846adb
Merge branch 'main' into feat/nurbs_sketching
jacobrkerstetter Jul 15, 2025
cdf6400
resolving PR comments
jacobrkerstetter Jul 16, 2025
946a530
chore: auto fixes from pre-commit hooks
pre-commit-ci[bot] Jul 16, 2025
12e9b6b
Merge branch 'main' into feat/nurbs_sketching
jacobrkerstetter Jul 16, 2025
61d4883
Merge branch 'main' into feat/nurbs_sketching
jacobrkerstetter Jul 16, 2025
768158a
remove double import
jacobrkerstetter Jul 16, 2025
4704a4a
Merge branch 'feat/nurbs_sketching' of https://github.com/ansys/pyans…
jacobrkerstetter Jul 16, 2025
16fe900
remove double import
jacobrkerstetter Jul 16, 2025
d982367
chore: auto fixes from pre-commit hooks
pre-commit-ci[bot] Jul 16, 2025
21958aa
add warning for nurbs sketching being unavailable before 26r1
jacobrkerstetter Jul 16, 2025
fbf8705
Merge branch 'feat/nurbs_sketching' of https://github.com/ansys/pyans…
jacobrkerstetter Jul 16, 2025
3506be9
fixing docs order
jacobrkerstetter Jul 16, 2025
80e7c0e
chore: auto fixes from pre-commit hooks
pre-commit-ci[bot] Jul 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changelog.d/2104.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Nurbs sketching and surface support
58 changes: 54 additions & 4 deletions src/ansys/geometry/core/_grpc/_services/v0/conversions.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
from ansys.geometry.core.sketch.edge import SketchEdge
from ansys.geometry.core.sketch.ellipse import SketchEllipse
from ansys.geometry.core.sketch.face import SketchFace
from ansys.geometry.core.sketch.nurbs import SketchNurbs
from ansys.geometry.core.sketch.polygon import Polygon
from ansys.geometry.core.sketch.segment import SketchSegment

Expand Down Expand Up @@ -404,6 +405,7 @@ def from_sketch_shapes_to_grpc_geometries(
converted_sketch_edges = from_sketch_edges_to_grpc_geometries(edges, plane)
geometries.lines.extend(converted_sketch_edges[0])
geometries.arcs.extend(converted_sketch_edges[1])
geometries.nurbs_curves.extend(converted_sketch_edges[2])

for face in faces:
if isinstance(face, SketchCircle):
Expand All @@ -429,6 +431,8 @@ def from_sketch_shapes_to_grpc_geometries(
one_curve_geometry.ellipses.append(geometries.ellipses[0])
elif len(geometries.polygons) > 0:
one_curve_geometry.polygons.append(geometries.polygons[0])
elif len(geometries.nurbs_curves) > 0:
one_curve_geometry.nurbs_curves.append(geometries.nurbs_curves[0])
return one_curve_geometry

else:
Expand All @@ -438,7 +442,7 @@ def from_sketch_shapes_to_grpc_geometries(
def from_sketch_edges_to_grpc_geometries(
edges: list["SketchEdge"],
plane: "Plane",
) -> tuple[list[GRPCLine], list[GRPCArc]]:
) -> tuple[list[GRPCLine], list[GRPCArc], list[GRPCNurbsCurve]]:
"""Convert a list of ``SketchEdge`` to a gRPC message.

Parameters
Expand All @@ -450,21 +454,25 @@ def from_sketch_edges_to_grpc_geometries(

Returns
-------
tuple[list[GRPCLine], list[GRPCArc]]
Geometry service gRPC line and arc messages. The unit is meters.
tuple[list[GRPCLine], list[GRPCArc], list[GRPCNurbsCurve]]
Geometry service gRPC line, arc, and NURBS curve messages. The unit is meters.
"""
from ansys.geometry.core.sketch.arc import Arc
from ansys.geometry.core.sketch.nurbs import SketchNurbs
from ansys.geometry.core.sketch.segment import SketchSegment

arcs = []
segments = []
nurbs_curves = []
for edge in edges:
if isinstance(edge, SketchSegment):
segments.append(from_sketch_segment_to_grpc_line(edge, plane))
elif isinstance(edge, Arc):
arcs.append(from_sketch_arc_to_grpc_arc(edge, plane))
elif isinstance(edge, SketchNurbs):
nurbs_curves.append(from_sketch_nurbs_to_grpc_nurbs_curve(edge, plane))

return (segments, arcs)
return (segments, arcs, nurbs_curves)


def from_sketch_arc_to_grpc_arc(arc: "Arc", plane: "Plane") -> GRPCArc:
Expand Down Expand Up @@ -496,6 +504,48 @@ def from_sketch_arc_to_grpc_arc(arc: "Arc", plane: "Plane") -> GRPCArc:
)


def from_sketch_nurbs_to_grpc_nurbs_curve(curve: "SketchNurbs", plane: "Plane") -> GRPCNurbsCurve:
"""Convert a ``SketchNurbs`` class to a NURBS curve gRPC message.

Parameters
----------
nurbs : SketchNurbs
Source NURBS data.
plane : Plane
Plane for positioning the NURBS curve.

Returns
-------
GRPCNurbsCurve
Geometry service gRPC NURBS curve message. The unit is meters.
"""
from ansys.api.geometry.v0.models_pb2 import (
ControlPoint as GRPCControlPoint,
NurbsData as GRPCNurbsData,
)

# Convert control points
control_points = [
GRPCControlPoint(
position=from_point2d_to_grpc_point(plane, pt),
weight=curve.weights[i],
)
for i, pt in enumerate(curve.control_points)
]

# Convert nurbs data
nurbs_data = GRPCNurbsData(
degree=curve.degree,
knots=from_knots_to_grpc_knots(curve.knots),
order=curve.degree + 1,
)

return GRPCNurbsCurve(
control_points=control_points,
nurbs_data=nurbs_data,
)


def from_sketch_ellipse_to_grpc_ellipse(ellipse: "SketchEllipse", plane: "Plane") -> GRPCEllipse:
"""Convert a ``SketchEllipse`` class to an ellipse gRPC message.

Expand Down
5 changes: 1 addition & 4 deletions src/ansys/geometry/core/shapes/curves/nurbs.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,7 @@ def fit_curve_from_points(
from geomdl import fitting

# Convert points to a format suitable for the fitting function
converted_points = []
for pt in points:
pt_raw = [*pt]
converted_points.append(pt_raw)
converted_points = [[*pt] for pt in points]

# Fit the curve to the points
curve = fitting.interpolate_curve(converted_points, degree)
Expand Down
6 changes: 5 additions & 1 deletion src/ansys/geometry/core/sketch/edge.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ def length(self) -> Quantity:
"""Length of the edge."""
raise NotImplementedError("Each edge must provide the length definition.")

def contains_point(self, point: Point2D, tol: float = 1e-6) -> bool:
"""Check if the edge contains the given point within a tolerance."""
raise NotImplementedError("Each edge must provide the contains_point method.")

@property
def visualization_polydata(self) -> "pv.PolyData":
"""VTK polydata representation for PyVista visualization.
Expand All @@ -76,7 +80,7 @@ def plane_change(self, plane: "Plane") -> None:
Notes
-----
This implies that their 3D definition might suffer changes. By default, this
metho does nothing. It is required to be implemented in child ``SketchEdge``
method does nothing. It is required to be implemented in child ``SketchEdge``
classes.
"""
pass
214 changes: 214 additions & 0 deletions src/ansys/geometry/core/sketch/nurbs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
# Copyright (C) 2023 - 2025 ANSYS, Inc. and/or its affiliates.
# SPDX-License-Identifier: MIT
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Provides for creating and managing a nurbs sketch curve."""

from typing import TYPE_CHECKING

from beartype import beartype as check_input_types
import numpy as np

from ansys.geometry.core.math.point import Point2D
from ansys.geometry.core.misc.checks import graphics_required
from ansys.geometry.core.sketch.edge import SketchEdge
from ansys.geometry.core.typing import Real

if TYPE_CHECKING: # pragma: no cover
import geomdl.NURBS as geomdl_nurbs # noqa: N811
import pyvista as pv


class SketchNurbs(SketchEdge):
"""Represents a NURBS sketch curve.

Warnings
--------
NURBS sketching is only supported in 26R1 and later versions of Ansys.

Notes
-----
This class is a wrapper around the NURBS curve class from the `geomdl` library.
By leveraging the `geomdl` library, this class provides a high-level interface
to create and manipulate NURBS curves. The `geomdl` library is a powerful
library for working with NURBS curves and surfaces. For more information, see
https://pypi.org/project/geomdl/.
"""

def __init__(self):
"""Initialize the NURBS sketch curve."""
super().__init__()
try:
import geomdl.NURBS as geomdl_nurbs # noqa: N811
except ImportError as e: # pragma: no cover
raise ImportError(
"The `geomdl` library is required to use the NURBSCurve class. "
"Please install it using `pip install geomdl`."
) from e

self._nurbs_curve = geomdl_nurbs.Curve()

@property
def geomdl_nurbs_curve(self) -> "geomdl_nurbs.Curve":
"""Get the underlying NURBS curve.

Notes
-----
This property gives access to the full functionality of the NURBS curve
coming from the `geomdl` library. Use with caution.
"""
return self._nurbs_curve

@property
def control_points(self) -> list[Point2D]:
"""Get the control points of the curve."""
return [Point2D(point) for point in self._nurbs_curve.ctrlpts]

@property
def degree(self) -> int:
"""Get the degree of the curve."""
return self._nurbs_curve.degree

@property
def knots(self) -> list[Real]:
"""Get the knot vector of the curve."""
return self._nurbs_curve.knotvector

@property
def weights(self) -> list[Real]:
"""Get the weights of the control points."""
return self._nurbs_curve.weights

@property
def start(self) -> Point2D:
"""Get the start point of the curve."""
return Point2D(self._nurbs_curve.evaluate_single(0.0))

@property
def end(self) -> Point2D:
"""Get the end point of the curve."""
return Point2D(self._nurbs_curve.evaluate_single(1.0))

@property
@graphics_required
def visualization_polydata(self) -> "pv.PolyData":
"""Get the VTK polydata representation for PyVista visualization.

Returns
-------
pyvista.PolyData
VTK pyvista.Polydata configuration.

Notes
-----
The representation lies in the X/Y plane within
the standard global Cartesian coordinate system.
"""
import pyvista as pv

# Sample points along the curve
params = np.linspace(0, 1, 100)
points = [self._nurbs_curve.evaluate_single(u) for u in params] # For 2D: [x, y]

# Add a zero z-coordinate for PyVista (only supports 3D points)
points = [(*pt, 0.0) for pt in points]

# Create PolyData and add the line
polydata = pv.PolyData(points)
polydata.lines = [len(points)] + list(range(len(points)))

return polydata

def contains_point(self, point: Point2D, tolerance: Real = 1e-6) -> bool:
"""Check if the curve contains a given point within a specified tolerance.

Parameters
----------
point : Point2D
The point to check.
tolerance : Real, optional
The tolerance for the containment check, by default 1e-6.

Returns
-------
bool
True if the curve contains the point within the tolerance, False otherwise.
"""
# Sample points along the curve
params = np.linspace(0, 1, 200)
sampled = [self._nurbs_curve.evaluate_single(u) for u in params]

# Check if any sampled point is close to the target point
return any(np.linalg.norm(np.array(pt) - np.array(point)) < tolerance for pt in sampled)

@classmethod
@check_input_types
def fit_curve_from_points(
cls,
points: list[Point2D],
degree: int = 3,
) -> "SketchNurbs":
"""Fit a NURBS curve to a set of points.

Parameters
----------
points : list[Point2D]
The points to fit the curve to.
degree : int, optional
The degree of the NURBS curve, by default 3.

Returns
-------
SketchNurbs
A new instance of SketchNurbs fitted to the given points.
"""
from geomdl import fitting

# Check degree compared to number of points provided
if degree < 1:
raise ValueError("Degree must be at least 1.")
if len(points) == 2:
degree = 1 # Force linear interpolation for two points
if len(points) == 3:
degree = 2 # Force quadratic interpolation for three points
if degree >= len(points):
raise ValueError(
f"Degree {degree} is too high for the number of points provided: {len(points)}."
)

curve = fitting.interpolate_curve(
[[*pt] for pt in points], # Convert Point2D to list of coordinates
degree=degree,
)

# Construct the NURBSCurve object
nurbs_curve = cls()
nurbs_curve._nurbs_curve.degree = curve.degree
nurbs_curve._nurbs_curve.ctrlpts = [Point2D(entry) for entry in curve.ctrlpts]
nurbs_curve._nurbs_curve.knotvector = curve.knotvector
nurbs_curve._nurbs_curve.weights = curve.weights

# Verify the curve is valid
try:
nurbs_curve._nurbs_curve._check_variables()
except ValueError as e:
raise ValueError(f"Invalid NURBS curve: {e}")

return nurbs_curve
23 changes: 23 additions & 0 deletions src/ansys/geometry/core/sketch/sketch.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from ansys.geometry.core.sketch.ellipse import SketchEllipse
from ansys.geometry.core.sketch.face import SketchFace
from ansys.geometry.core.sketch.gears import DummyGear, SpurGear
from ansys.geometry.core.sketch.nurbs import SketchNurbs
from ansys.geometry.core.sketch.polygon import Polygon
from ansys.geometry.core.sketch.segment import SketchSegment
from ansys.geometry.core.sketch.slot import Slot
Expand Down Expand Up @@ -561,6 +562,28 @@ def arc_from_start_center_and_angle(
)
return self.edge(arc, tag)

def nurbs_from_2d_points(
self,
points: list[Point2D],
tag: str | None = None,
) -> "Sketch":
"""Add a NURBS curve from a list of 2D points.

Parameters
----------
points : list[Point2D]
List of 2D points to define the NURBS curve.
tag : str | None, default: None
User-defined label for identifying the curve.

Returns
-------
Sketch
Revised sketch state ready for further sketch actions.
"""
nurbs_curve = SketchNurbs.fit_curve_from_points(points)
return self.edge(nurbs_curve, tag)

def triangle(
self,
point1: Point2D,
Expand Down
Loading
Loading