Skip to content

Commit

Permalink
Merge pull request #16 from cemsbv/15-add-method-to-project-point3d-o…
Browse files Browse the repository at this point in the history
…nto-polygon3d

Add method to project point3d onto polygon3d.
  • Loading branch information
PabloVasconez authored Nov 10, 2023
2 parents cc07101 + 58bb840 commit 648d8eb
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 16 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ install_types = true
module = [
"plxscripting.*",
"skspatial.*",
"numpy.*",
"matplotlib.*",
"requests.*",
"nuclei.*",
Expand Down
18 changes: 9 additions & 9 deletions src/plxcontroller/geometry_3d/bounding_box_3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ def __init__(
TypeError
if any parameter is not of the expected type.
ValueError
if x_max is not > x_min
if y_max is not > y_max
if z_max is not > z_min
if x_max is not >= x_min
if y_max is not >= y_max
if z_max is not >= z_min
"""
# Validate types
if not isinstance(x_min, (float, int)):
Expand Down Expand Up @@ -71,14 +71,14 @@ def __init__(
)

# Validate values
if not x_max > x_min:
raise ValueError("x_max must be > x_min")
if not x_max >= x_min:
raise ValueError("x_max must be >= x_min")

if not y_max > y_min:
raise ValueError("y_max must be > y_min")
if not y_max >= y_min:
raise ValueError("y_max must be >= y_min")

if not z_max > z_min:
raise ValueError("z_max must be > z_min")
if not z_max >= z_min:
raise ValueError("z_max must be >= z_min")

self._x_min = x_min
self._y_min = y_min
Expand Down
67 changes: 67 additions & 0 deletions src/plxcontroller/geometry_3d/operations_3d.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from __future__ import annotations

import numpy as np
from skspatial.objects import Plane as ScikitSpatialPlane

from plxcontroller.geometry_3d.point_3d import Point3D
from plxcontroller.geometry_3d.polygon_3d import Polygon3D


def project_vertically_point_onto_polygon_3d(
point: Point3D, polygon: Polygon3D
) -> Point3D | None:
"""Returns the vertical projection of the Point3D onto Polygon3D.
If the point is not within the boundaries of the projection of the polygon
in the xy plane, then None is returned.
Parameters
----------
point : Point3D
the point to project.
polygon : Polygon3D
the polygon to project onto.
Raises
------
TypeError
if any parameter is not of the expected type.
Returns
-------
Point3D | None
the projected point onto the polygon.
"""
# Validate input
if not isinstance(point, Point3D):
raise TypeError(
f"Unexpected type for point. Expected Point3D, but got {type(point)}."
)

if not isinstance(polygon, Polygon3D):
raise TypeError(
f"Unexpected type for polygon. Expected Polygon3D, but got {type(point)}."
)

# Check whether the point is inside the projection of the polygon in the xy plane
if not polygon.shapely_polygon_xy_plane.contains(point.shapely_point_xy_plane):
return None

# Make plane using first three points of the polygon.
# Note that all points in Polygon3D are coplanar, so it does not matter which
# poitns are taken.
plane = ScikitSpatialPlane.from_points(
*[coord for coord in polygon.coordinates[:3]]
)

# Calculate the z-coordinate using the equation of the plane with the
# point x and y coordinates input
a, b, c, d = plane.cartesian()
if not np.isclose(c, 0.0):
z = (d - a * point.x - b * point.y) / c
else:
# if the coefficient c is close to zero, it means that
# the plane is parallel to the z-axis, so the z-coordinate
# is the same as the point itself.
z = point.z

return Point3D(x=point.x, y=point.y, z=z)
7 changes: 7 additions & 0 deletions src/plxcontroller/geometry_3d/point_3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from typing import Tuple

import shapely


class Point3D:
"""
Expand Down Expand Up @@ -62,3 +64,8 @@ def z(self) -> float:
def coordinates(self) -> Tuple[float, float, float]:
"""Returns a tuple with the (x,y,z) coordinates of the point."""
return (self.x, self.y, self.z)

@property
def shapely_point_xy_plane(self) -> shapely.Point:
"""Returns the projection of the point in the xy plane as a shapely.Point object."""
return shapely.Point((self.x, self.y))
25 changes: 22 additions & 3 deletions src/plxcontroller/geometry_3d/polygon_3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
from copy import copy
from typing import List, Tuple

import shapely
from skspatial.objects import Points as ScikitSpatialPoints

from plxcontroller.geometry_3d.bounding_box_3d import BoundingBox3D
from plxcontroller.geometry_3d.point_3d import Point3D


Expand Down Expand Up @@ -69,15 +71,32 @@ def __init__(self, coordinates: List[Tuple[float, float, float]]):

@property
def coordinates(self) -> List[Tuple[float, float, float]]:
"""Return the coordinates of the polygon a list of (x,y,z) tuples."""
"""Returns the coordinates of the polygon a list of (x,y,z) tuples."""
return copy(self._coordinates)

@property
def points(self) -> List[Point3D]:
"""Return the coordinates of the polygon a list of Point3D."""
"""Returns the coordinates of the polygon a list of Point3D."""
return [Point3D(*a) for a in self._coordinates]

@property
def scikit_spatial_points(self) -> ScikitSpatialPoints:
"""Return the coordinates of the polygon a skspatial.objects.Points object."""
"""Returns the coordinates of the polygon a skspatial.objects.Points object."""
return ScikitSpatialPoints(self._coordinates)

@property
def shapely_polygon_xy_plane(self) -> shapely.Polygon:
"""Returns the projection in the xy plane as a shapely polygon"""
return shapely.Polygon([(point.x, point.y) for point in self.points])

@property
def bounding_box(self) -> BoundingBox3D:
"""Returns the bounding box of the polygon."""
return BoundingBox3D(
x_min=min([point.x for point in self.points]),
y_min=min([point.y for point in self.points]),
z_min=min([point.z for point in self.points]),
x_max=max([point.x for point in self.points]),
y_max=max([point.y for point in self.points]),
z_max=max([point.z for point in self.points]),
)
7 changes: 3 additions & 4 deletions tests/test_geometry_3d/test_bounding_box_3d.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import pytest

from plxcontroller.geometry_3d.bounding_box_3d import BoundingBox3D
from plxcontroller.geometry_3d.point_3d import Point3D


def test_bounding_box_3d() -> None:
Expand Down Expand Up @@ -41,13 +40,13 @@ def test_bounding_box_3d() -> None:
)

# Assert invalid input (values)
with pytest.raises(ValueError, match="x_max must be > x_min"):
with pytest.raises(ValueError, match="x_max must be >= x_min"):
BoundingBox3D(x_min=3.0, y_min=2.0, z_min=3.0, x_max=2.0, y_max=3.0, z_max=4.0)

with pytest.raises(ValueError, match="y_max must be > y_min"):
with pytest.raises(ValueError, match="y_max must be >= y_min"):
BoundingBox3D(x_min=1.0, y_min=4.0, z_min=3.0, x_max=2.0, y_max=3.0, z_max=4.0)

with pytest.raises(ValueError, match="z_max must be > z_min"):
with pytest.raises(ValueError, match="z_max must be >= z_min"):
BoundingBox3D(x_min=1.0, y_min=2.0, z_min=5.0, x_max=2.0, y_max=3.0, z_max=4.0)

# Assert correct instance with valid input
Expand Down
47 changes: 47 additions & 0 deletions tests/test_geometry_3d/test_operations_3d.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import pytest

from plxcontroller.geometry_3d.operations_3d import (
project_vertically_point_onto_polygon_3d,
)
from plxcontroller.geometry_3d.point_3d import Point3D
from plxcontroller.geometry_3d.polygon_3d import Polygon3D


def test_project_vertically_point_onto_polygon_3d() -> None:
"""
Tests the method project_vertically_point_onto_polygon_3d.
"""

# Assert invalid input (types)
with pytest.raises(TypeError, match="Expected Point3D"):
project_vertically_point_onto_polygon_3d(
point="invalid input",
polygon=Polygon3D(coordinates=[(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)]),
)

with pytest.raises(TypeError, match="Expected Polygon3D"):
project_vertically_point_onto_polygon_3d(
point=Point3D(x=0.5, y=0.5, z=5.0),
polygon="invalid input",
)

# Assert it returns None if projection falls out the boundaries of the polygon
assert (
project_vertically_point_onto_polygon_3d(
point=Point3D(x=-0.5, y=-0.5, z=5.0),
polygon=Polygon3D(coordinates=[(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)]),
)
is None
)

# Assert it returns the expected point (polygon3d parallel to xy plane)
assert project_vertically_point_onto_polygon_3d(
point=Point3D(x=0.5, y=0.5, z=5.0),
polygon=Polygon3D(coordinates=[(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)]),
).coordinates == (0.5, 0.5, 0)

# Assert it returns the expected point (inclined polygon3d)
assert project_vertically_point_onto_polygon_3d(
point=Point3D(x=0.5, y=0.5, z=5.0),
polygon=Polygon3D(coordinates=[(0, 0, 0), (1, 0, 1), (1, 1, 1), (0, 1, 0)]),
).coordinates == (0.5, 0.5, 0.5)

0 comments on commit 648d8eb

Please sign in to comment.