Skip to content

Commit

Permalink
Merge pull request #379 from specklesystems/dogukan/additional_geomet…
Browse files Browse the repository at this point in the history
…ry_classes

feat(specklepy): additional geometry classes
  • Loading branch information
dogukankaratas authored Feb 10, 2025
2 parents 06e2115 + 1ba6983 commit 1b53410
Show file tree
Hide file tree
Showing 29 changed files with 1,671 additions and 267 deletions.
2 changes: 2 additions & 0 deletions src/speckle_automate/automation_context.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# ignoring "line too long" check from linter
# ruff: noqa: E501
"""This module provides an abstraction layer above the Speckle Automate runtime."""

import time
Expand Down
40 changes: 32 additions & 8 deletions src/specklepy/objects/geometry/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,34 @@
from specklepy.objects.geometry.arc import Arc
from specklepy.objects.geometry.line import Line
from specklepy.objects.geometry.mesh import Mesh
from specklepy.objects.geometry.plane import Plane
from specklepy.objects.geometry.point import Point
from specklepy.objects.geometry.polyline import Polyline
from specklepy.objects.geometry.vector import Vector
from .arc import Arc
from .box import Box
from .circle import Circle
from .control_point import ControlPoint
from .ellipse import Ellipse
from .line import Line
from .mesh import Mesh
from .plane import Plane
from .point import Point
from .point_cloud import PointCloud
from .polycurve import Polycurve
from .polyline import Polyline
from .spiral import Spiral
from .surface import Surface
from .vector import Vector

# re-export them at the geometry package level
__all__ = ["Arc", "Line", "Mesh", "Plane", "Point", "Polyline", "Vector"]
__all__ = [
"Arc",
"Line",
"Mesh",
"Plane",
"Point",
"Polyline",
"Vector",
"Box",
"Circle",
"ControlPoint",
"Ellipse",
"PointCloud",
"Polycurve",
"Spiral",
"Surface",
]
40 changes: 40 additions & 0 deletions src/specklepy/objects/geometry/box.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from dataclasses import dataclass

from specklepy.objects.base import Base
from specklepy.objects.geometry.plane import Plane
from specklepy.objects.interfaces import IHasArea, IHasUnits, IHasVolume
from specklepy.objects.primitive import Interval


@dataclass(kw_only=True)
class Box(Base, IHasUnits, IHasArea, IHasVolume, speckle_type="Objects.Geometry.Box"):
"""
a 3-dimensional box oriented on a plane
"""

basePlane: Plane
xSize: Interval
ySize: Interval
zSize: Interval

def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"basePlane: {self.basePlane}, "
f"xSize: {self.xSize}, "
f"ySize: {self.ySize}, "
f"zSize: {self.zSize}, "
f"units: {self.units})"
)

@property
def area(self) -> float:
return 2 * (
self.xSize.length * self.ySize.length
+ self.xSize.length * self.zSize.length
+ self.ySize.length * self.zSize.length
)

@property
def volume(self) -> float:
return self.xSize.length * self.ySize.length * self.zSize.length
35 changes: 35 additions & 0 deletions src/specklepy/objects/geometry/circle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import math
from dataclasses import dataclass

from specklepy.objects.base import Base
from specklepy.objects.geometry.plane import Plane
from specklepy.objects.geometry.point import Point
from specklepy.objects.interfaces import ICurve, IHasArea, IHasUnits


@dataclass(kw_only=True)
class Circle(Base, IHasUnits, ICurve, IHasArea, speckle_type="Objects.Geometry.Circle"):
"""
a circular curve based on a plane
"""

plane: Plane
center: Point
radius: float

def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"plane: {self.plane}, "
f"center: {self.center}, "
f"radius: {self.radius}, "
f"units: {self.units})"
)

@property
def length(self) -> float:
return 2 * math.pi * self.radius

@property
def area(self) -> float:
return math.pi * self.radius**2
22 changes: 22 additions & 0 deletions src/specklepy/objects/geometry/control_point.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from dataclasses import dataclass

from specklepy.objects.geometry.point import Point


@dataclass(kw_only=True)
class ControlPoint(Point, speckle_type="Objects.Geometry.ControlPoint"):
"""
a single 3-dimensional point with weight
"""

weight: float

def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"x: {self.x}, "
f"y: {self.y}, "
f"z: {self.z}, "
f"weight: {self.weight}, "
f"units: {self.units})"
)
34 changes: 34 additions & 0 deletions src/specklepy/objects/geometry/ellipse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from dataclasses import dataclass

from specklepy.objects.base import Base
from specklepy.objects.geometry.plane import Plane
from specklepy.objects.interfaces import ICurve, IHasArea, IHasUnits


@dataclass(kw_only=True)
class Ellipse(
Base, IHasUnits, ICurve, IHasArea, speckle_type="Objects.Geometry.Ellipse"
):
"""
an ellipse
"""

plane: Plane
first_radius: float
second_radius: float

@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
3 changes: 2 additions & 1 deletion src/specklepy/objects/geometry/mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ class Mesh(
serialize_ignore={"vertices_count", "texture_coordinates_count"},
):
"""
a 3D mesh consisting of vertices and faces with optional colors and texture coordinates
a 3D mesh consisting of vertices and faces
with optional colors and texture coordinates
"""

vertices: List[float]
Expand Down
8 changes: 7 additions & 1 deletion src/specklepy/objects/geometry/point.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@ class Point(Base, IHasUnits, speckle_type="Objects.Geometry.Point"):
z: float

def __repr__(self) -> str:
return f"{self.__class__.__name__}(x: {self.x}, y: {self.y}, z: {self.z}, units: {self.units})"
return (
f"{self.__class__.__name__}("
f"x: {self.x}, "
f"y: {self.y}, "
f"z: {self.z}, "
f"units: {self.units})"
)

def distance_to(self, other: "Point") -> float:
"""
Expand Down
24 changes: 24 additions & 0 deletions src/specklepy/objects/geometry/point_cloud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from dataclasses import dataclass
from typing import List

from specklepy.objects.base import Base
from specklepy.objects.geometry.point import Point
from specklepy.objects.interfaces import IHasUnits


@dataclass(kw_only=True)
class PointCloud(Base, IHasUnits, speckle_type="Objects.Geometry.PointCloud"):
"""
a collection of 3-dimensional points
"""

points: List[Point]

def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"points: {len(self.points)}, "
f"units: {self.units})"
)

# sizes and colors could be added in the future
97 changes: 97 additions & 0 deletions src/specklepy/objects/geometry/polycurve.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from dataclasses import dataclass, field
from typing import List

from specklepy.objects.base import Base
from specklepy.objects.geometry.line import Line
from specklepy.objects.geometry.point import Point
from specklepy.objects.geometry.polyline import Polyline
from specklepy.objects.interfaces import ICurve, IHasArea, IHasUnits


@dataclass(kw_only=True)
class Polycurve(
Base, IHasUnits, ICurve, IHasArea, speckle_type="Objects.Geometry.Polycurve"
):
"""
a curve that is comprised of multiple curves connected
"""

segments: List[ICurve] = field(default_factory=list)

def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"segments: {len(self.segments)}, "
f"closed: {self.is_closed()}, "
f"units: {self.units})"
)

def is_closed(self, tolerance: float = 1e-6) -> bool:
"""
checks if the polycurve is closed
(comparing start of first segment to end of last segment)
"""
if len(self.segments) < 1:
return False

first_segment = self.segments[0]
last_segment = self.segments[-1]

if not (hasattr(first_segment, "start") and hasattr(last_segment, "end")):
return False

start_pt = first_segment.start
end_pt = last_segment.end

if not (isinstance(start_pt, Point) and isinstance(end_pt, Point)):
return False

return start_pt.distance_to(end_pt) <= tolerance

@property
def length(self) -> float:
return self.__dict__.get("_length", 0.0)

@length.setter
def length(self, value: float) -> None:
self.__dict__["_length"] = value

def calculate_length(self) -> float:
"""
calculate total length of all segments
"""
total_length = 0.0
for segment in self.segments:
if hasattr(segment, "length"):
total_length += segment.length
return total_length

@property
def area(self) -> float:
return self.__dict__.get("_area", 0.0)

@area.setter
def area(self, value: float) -> None:
self.__dict__["_area"] = value

@classmethod
def from_polyline(cls, polyline: Polyline) -> "Polycurve":
"""
constructs a new polycurve instance from an existing polyline curve
"""
polycurve = cls(units=polyline.units)
points = polyline.get_points()
for i in range(len(points) - 1):
line = Line(start=points[i], end=points[i + 1], units=polyline.units)
polycurve.segments.append(line)

if polyline.is_closed():
line = Line(start=points[-1], end=points[0], units=polyline.units)
polycurve.segments.append(line)

if hasattr(polyline, "_length"):
polycurve.length = polyline.length
if hasattr(polyline, "_area"):
polycurve.area = polyline.area

return polycurve
49 changes: 49 additions & 0 deletions src/specklepy/objects/geometry/spiral.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from dataclasses import dataclass

from specklepy.objects.base import Base
from specklepy.objects.geometry.plane import Plane
from specklepy.objects.geometry.point import Point
from specklepy.objects.geometry.vector import Vector
from specklepy.objects.interfaces import ICurve, IHasArea, IHasUnits


@dataclass(kw_only=True)
class Spiral(Base, IHasUnits, ICurve, IHasArea, speckle_type="Objects.Geometry.Spiral"):
"""
a spiral
"""

start_point: Point
end_point: Point
plane: Plane # plane with origin at spiral center
turns: float # total angle of spiral.
pitch: float
pitch_axis: Vector

def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"start_point: {self.start_point}, "
f"end_point: {self.end_point}, "
f"plane: {self.plane}, "
f"turns: {self.turns}, "
f"pitch: {self.pitch}, "
f"pitch_axis: {self.pitch_axis}, "
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
Loading

0 comments on commit 1b53410

Please sign in to comment.