From f7f9f73e7bcfdf8cc1b6411d6b4da3bb0ab305c8 Mon Sep 17 00:00:00 2001 From: Dogukan Karatas <61163577+dogukankaratas@users.noreply.github.com> Date: Fri, 11 Apr 2025 14:09:39 +0200 Subject: [PATCH] feat(specklepy): curve object class (#400) * adds curve class --- src/specklepy/objects/geometry/__init__.py | 2 + src/specklepy/objects/geometry/curve.py | 58 ++++++++ src/specklepy/objects/tests/test_curve.py | 158 +++++++++++++++++++++ 3 files changed, 218 insertions(+) create mode 100644 src/specklepy/objects/geometry/curve.py create mode 100644 src/specklepy/objects/tests/test_curve.py diff --git a/src/specklepy/objects/geometry/__init__.py b/src/specklepy/objects/geometry/__init__.py index 8d85c66..c8a4944 100644 --- a/src/specklepy/objects/geometry/__init__.py +++ b/src/specklepy/objects/geometry/__init__.py @@ -2,6 +2,7 @@ from .arc import Arc from .box import Box from .circle import Circle from .control_point import ControlPoint +from .curve import Curve from .ellipse import Ellipse from .line import Line from .mesh import Mesh @@ -33,4 +34,5 @@ __all__ = [ "Polycurve", "Spiral", "Surface", + "Curve", ] diff --git a/src/specklepy/objects/geometry/curve.py b/src/specklepy/objects/geometry/curve.py new file mode 100644 index 0000000..7b1d03d --- /dev/null +++ b/src/specklepy/objects/geometry/curve.py @@ -0,0 +1,58 @@ +from dataclasses import dataclass +from typing import List, Optional + +from specklepy.objects.base import Base +from specklepy.objects.geometry.box import Box +from specklepy.objects.geometry.polyline import Polyline +from specklepy.objects.interfaces import ICurve, IHasArea, IHasUnits + + +@dataclass(kw_only=True) +class Curve( + Base, + ICurve, + IHasArea, + IHasUnits, + speckle_type="Objects.Geometry.Curve", + detachable={"points", "weights", "knots", "displayValue"}, + chunkable={"points": 31250, "weights": 31250, "knots": 31250}, +): + """ + a NURBS curve + """ + + degree: int + periodic: bool + rational: bool + points: List[float] + weights: List[float] + knots: List[float] + closed: bool + displayValue: Polyline + bbox: Optional[Box] = None + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(" + f"degree: {self.degree}, " + f"periodic: {self.periodic}, " + f"rational: {self.rational}, " + f"closed: {self.closed}, " + f"units: {self.units})" + ) + + @property + def length(self) -> float: + return self.__dict__.get("_length", 0.0) + + @length.setter + def length(self, value: float) -> None: + self.__dict__["_length"] = value + + @property + def area(self) -> float: + return self.__dict__.get("_area", 0.0) + + @area.setter + def area(self, value: float) -> None: + self.__dict__["_area"] = value diff --git a/src/specklepy/objects/tests/test_curve.py b/src/specklepy/objects/tests/test_curve.py new file mode 100644 index 0000000..86b37c3 --- /dev/null +++ b/src/specklepy/objects/tests/test_curve.py @@ -0,0 +1,158 @@ +import pytest + +from specklepy.core.api.operations import deserialize, serialize +from specklepy.objects.geometry import Curve, Plane, Point, Polyline, Vector +from specklepy.objects.models.units import Units + + +@pytest.fixture +def sample_polyline(): + """ + sample polyline + """ + return Polyline(value=[0, 0, 0, 1, 0, 0, 1, 1, 0], units=Units.m) + + +@pytest.fixture +def sample_plane(): + """ + sample plane for bbox creation + """ + origin = Point(x=0, y=0, z=0, units=Units.m) + normal = Vector(x=0, y=0, z=1, units=Units.m) + xdir = Vector(x=1, y=0, z=0, units=Units.m) + ydir = Vector(x=0, y=1, z=0, units=Units.m) + return Plane(origin=origin, normal=normal, xdir=xdir, ydir=ydir, units=Units.m) + + +@pytest.fixture +def sample_curve(sample_polyline): + """ + sample curve for testing + """ + return Curve( + degree=3, + periodic=False, + rational=False, + points=[0, 0, 0, 1, 1, 0, 2, 0, 0, 3, 1, 0], + weights=[1, 1, 1, 1], + knots=[0, 0, 0, 0, 1, 1, 1, 1], + closed=False, + displayValue=sample_polyline, + units=Units.m, + ) + + +def test_curve_creation(sample_polyline): + """ + test curve initialization + """ + curve = Curve( + degree=3, + periodic=False, + rational=False, + points=[0, 0, 0, 1, 1, 0, 2, 0, 0, 3, 1, 0], + weights=[1, 1, 1, 1], + knots=[0, 0, 0, 0, 1, 1, 1, 1], + closed=False, + displayValue=sample_polyline, + units=Units.m, + ) + + assert curve.degree == 3 + assert curve.periodic is False + assert curve.rational is False + assert curve.points == [0, 0, 0, 1, 1, 0, 2, 0, 0, 3, 1, 0] + assert curve.weights == [1, 1, 1, 1] + assert curve.knots == [0, 0, 0, 0, 1, 1, 1, 1] + assert curve.closed is False + assert curve.units == Units.m.value + assert curve.displayValue == sample_polyline + + +def test_length_property(sample_polyline): + """ + test the length property setter and getter + """ + curve = Curve( + degree=1, + periodic=False, + rational=False, + points=[0, 0, 0, 1, 0, 0], + weights=[1, 1], + knots=[0, 0, 1, 1], + closed=False, + displayValue=sample_polyline, + units=Units.m, + ) + + assert curve.length == 0.0 + + curve.length = 1.5 + assert curve.length == 1.5 + + +def test_area_property(sample_polyline): + """ + test the area property setter and getter + """ + polyline = Polyline( + value=[0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0], units=Units.m + ) + + curve = Curve( + degree=1, + periodic=False, + rational=False, + points=[0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0], + weights=[1, 1, 1, 1, 1], + knots=[0, 0, 1, 2, 3, 4, 4], + closed=True, + displayValue=polyline, + units=Units.m, + ) + + assert curve.area == 0.0 + + curve.area = 1.0 + assert curve.area == 1.0 + + +def test_curve_serialization(sample_curve): + """ + test serialization and deserialization of the curve + """ + serialized = serialize(sample_curve) + deserialized = deserialize(serialized) + + assert deserialized.degree == sample_curve.degree + assert deserialized.periodic == sample_curve.periodic + assert deserialized.rational == sample_curve.rational + assert deserialized.points == sample_curve.points + assert deserialized.weights == sample_curve.weights + assert deserialized.knots == sample_curve.knots + assert deserialized.closed == sample_curve.closed + assert deserialized.units == sample_curve.units + + +@pytest.mark.parametrize("new_units", ["mm", "cm", "in"]) +def test_curve_units(sample_polyline, new_units): + """ + test changing units of a curve + """ + curve = Curve( + degree=3, + periodic=False, + rational=False, + points=[0, 0, 0, 1, 1, 0, 2, 0, 0, 3, 1, 0], + weights=[1, 1, 1, 1], + knots=[0, 0, 0, 0, 1, 1, 1, 1], + closed=False, + displayValue=sample_polyline, + units=Units.m, + ) + + assert curve.units == Units.m.value + + curve.units = new_units + assert curve.units == new_units