Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Offset plugin #1165

Draft
wants to merge 8 commits into
base: geometry-refactor
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Added `compas.datastructures.Graph.node_index` and `compas.datastructures.Graph.index_node`.
* Added `compas.datastructures.Graph.edge_index` and `compas.datastructures.Graph.index_edge`.
* Added `compas.datastructures.Halfedge.vertex_index` and `compas.datastructures.Halfedge.index_vertex`.
* xxxxx (to see if the PR passes...)
* Added support for offset operations using a shapely plugin implementation (for polyline and polygon)
* Added `compas.geometry.trimesh_descent_numpy`.
* Added `compas.geometry.trimesh_gradient_numpy`.

Expand Down
2 changes: 2 additions & 0 deletions src/compas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@

__all_plugins__ = [
"compas.geometry.booleans.booleans_shapely",
"compas.geometry.offset.offset",
"compas.geometry.offset.offset_shapely",
]


Expand Down
2 changes: 1 addition & 1 deletion src/compas/geometry/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@
intersection_sphere_line,
intersection_sphere_sphere,
)
from .offset.offset import offset_line, offset_polyline, offset_polygon
from .offset import offset_line, offset_polyline, offset_polygon
from .quadmesh.planarization import quadmesh_planarize
from .triangulation import conforming_delaunay_triangulation, constrained_delaunay_triangulation, delaunay_triangulation
from .triangulation.delaunay import delaunay_from_points
Expand Down
30 changes: 30 additions & 0 deletions src/compas/geometry/curves/line.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,3 +368,33 @@ def closest_point(self, point, return_parameter=False):
if return_parameter:
return c, t
return c

def offset(self, distance, **kwargs):
"""Offset a line by a distance.

Parameters
----------
line : :class:`~compas.geometry.Line`
A line defined by two points.
distance : float
The offset distance as float.

Returns
-------
list[point]
The two points of the offseted line.

Notes
-----
The offset direction is chosen such that if the line were along the positve
X axis and the normal of the offset plane is along the positive Z axis, the
offset line is in the direction of the postive Y axis.

Depending of the backend used, additional parameters can be added as keyword arguments. (point somewhere in api, or list
accepted arguments)

"""
from compas.geometry import offset_line

points = offset_line(self, distance, **kwargs)
return Line(*points)
30 changes: 30 additions & 0 deletions src/compas/geometry/curves/polyline.py
Original file line number Diff line number Diff line change
Expand Up @@ -640,3 +640,33 @@ def shortened(self, length):
crv = self.copy()
crv.shorten(length)
return crv

def offset(self, distance, **kwargs):
"""Offset a polyline by a distance.

Parameters
----------
polyline : :class:`~compas.geometry.Polyline`
A polyline defined by a sequence of points.
distance : float
The offset distance as float.

Returns
-------
list[point]
The points of the offseted polyline.

Notes
-----
The offset direction is chosen such that if the line were along the positve
X axis and the normal of the offset plane is along the positive Z axis, the
offset line is in the direction of the postive Y axis.

Depending of the backend used, additional parameters can be added as keyword arguments. (point somewhere in api, or list
accepted arguments)

"""
from compas.geometry import offset_polyline

points = offset_polyline(self, distance, **kwargs)
return Polyline(points)
92 changes: 92 additions & 0 deletions src/compas/geometry/offset/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from compas.plugins import pluggable


@pluggable(category="offset")
def offset_line(line, distance, **kwargs):
"""Offset a line by a distance.

Parameters
----------
line : :class:`~compas.geometry.Line`
A line defined by two points.
distance : float
The offset distance as float.

Returns
-------
list[point]
The two points of the offseted line.

Notes
-----
The offset direction is chosen such that if the line were along the positve
X axis and the normal of the offset plane is along the positive Z axis, the
offset line is in the direction of the postive Y axis.

Depending of the backend used, additional parameters can be added as keyword arguments. (point somewhere in api, or list
accepted arguments)

"""
raise NotImplementedError


@pluggable(category="offset")
def offset_polyline(polyline, distance, **kwargs):
"""Offset a polyline by a distance.

Parameters
----------
polyline : :class:`~compas.geometry.Polyline`
A polyline defined by a sequence of points.
distance : float
The offset distance as float.

Returns
-------
list[point]
The points of the offseted polyline.

Notes
-----
The offset direction is chosen such that if the polyline were along the positve
X axis and the normal of the offset plane is along the positive Z axis, the
offset polyline is in the direction of the postive Y axis.

Depending of the backend used, additional parameters can be added as keyword arguments. (point somewhere in api, or list
accepted arguments)

"""
raise NotImplementedError


@pluggable(category="offset")
def offset_polygon(polygon, distance, **kwargs):
"""Offset a polygon by a distance.

Parameters
----------
polygon : :class:`~compas.geometry.Polygon`
A polygon defined by a sequence of vertices.
distance : float
The offset distance as float.

Returns
-------
list[point]
The vertices of the offseted polygon.

Notes
-----
The offset direction is determined by the provided normal vector.
If the polyline is in the XY plane and the normal is along the positive Z axis,
positive offset distances will result in counterclockwise offsets,
and negative values in clockwise direction.
Depending of the backend used, additional parameters can be added as keyword arguments. (point somewhere in api, or list
accepted arguments)

"""
raise NotImplementedError
10 changes: 7 additions & 3 deletions src/compas/geometry/offset/offset.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from __future__ import absolute_import
from __future__ import division

from compas.plugins import plugin
from compas.geometry import scale_vector
from compas.geometry import normalize_vector
from compas.geometry import add_vectors
Expand Down Expand Up @@ -51,7 +52,8 @@ def offset_segments(point_list, distances, normal):
return segments


def offset_line(line, distance, normal=[0.0, 0.0, 1.0]):
@plugin(category="offset", trylast=True)
def offset_line(line, distance, normal=[0.0, 0.0, 1.0], **kwargs):
"""Offset a line by a distance.

Parameters
Expand Down Expand Up @@ -97,7 +99,8 @@ def offset_line(line, distance, normal=[0.0, 0.0, 1.0]):
return c, d


def offset_polygon(polygon, distance, tol=1e-6):
@plugin(category="offset", trylast=True)
def offset_polygon(polygon, distance, tol=1e-6, **kwargs):
"""Offset a polygon (closed) by a distance.

Parameters
Expand Down Expand Up @@ -153,7 +156,8 @@ def offset_polygon(polygon, distance, tol=1e-6):
return offset


def offset_polyline(polyline, distance, normal=[0.0, 0.0, 1.0], tol=1e-6):
@plugin(category="offset", trylast=True)
def offset_polyline(polyline, distance, normal=[0.0, 0.0, 1.0], tol=1e-6, **kwargs):
"""Offset a polyline by a distance.

Parameters
Expand Down
118 changes: 118 additions & 0 deletions src/compas/geometry/offset/offset_shapely.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
from compas.plugins import plugin
from shapely.geometry import LineString
from shapely.geometry import Polygon
from shapely import is_ccw


# @plugin(category="offset", requires=["shapely"])
# def offset_line(line, distance, **kwargs):
# """Offset a line by a distance.

# Parameters
# ----------
# line : :class:`~compas.geometry.Line`
# A line defined by two points.
# distance : float
# The offset distance as float.

# Returns
# -------
# list[point]
# The two points of the offseted line.

# Notes
# -----
# The side is determined by the sign of the distance parameter
# (negative for right side offset, positive for left side offset).

# """
# linestring = LineString((line.start, line.end))
# offset = linestring.offset_curve(distance)
# return list(offset.coords)


@plugin(category="offset", requires=["shapely"])
def offset_polyline(polyline, distance, join_style="sharp", sharp_limit=5, **kwargs):
"""Offset a polyline by a distance.

Parameters
----------
polyline : :class:`~compas.geometry.Polyline`
A polyline defined by a sequence of points.
distance : float
The offset distance as float.
join_style : {"sharp", "round", "chamfer"}
Specifies the shape of offsetted line midpoints. "round" results in rounded shapes.
"chamfer" results in a chamfered edge that touches the original vertex. "sharp" results in a single vertex
that is chamfered depending on the sharp_limit parameter. Defaults to "sharp".
sharp_limit: float, optional
The sharp limit ratio is used for very sharp corners. The sharp ratio is the ratio of the distance
from the corner to the end of the chamfered offset corner. When two line segments meet at a sharp angle,
a sharp join will extend the original geometry. To prevent unreasonable geometry,
the sharp limit allows controlling the maximum length of the join corner.
Corners with a ratio which exceed the limit will be chamfered.

Returns
-------
list[point]
The points of the offseted polyline.

Notes
-----
The side is determined by the sign of the distance parameter
(negative for right side offset, positive for left side offset).

"""
join_styles = ("round", "sharp", "chamfer")
try:
join_style_int = join_styles.index(join_style.lower()) + 1
except ValueError:
print("Join styles supported are round, sharp and chamfer.")
linestring = LineString(polyline.points)
offset = linestring.offset_curve(distance, join_style=join_style_int, mitre_limit=sharp_limit)
return list(offset.coords)


@plugin(category="offset", requires=["shapely"])
def offset_polygon(polygon, distance, join_style="sharp", sharp_limit=5, **kwargs):
"""Offset a polygon by a distance.

Parameters
----------
polygon : :class:`~compas.geometry.Polygon`
A polygon defined by a sequence of vertices.
distance : float
The offset distance as float.
join_style : {"sharp", "round", "chamfer"}
Specifies the shape of offsetted line midpoints. "round" results in rounded shapes.
"chamfer" results in a chamfered edge that touches the original vertex. "sharp" results in a single vertex
that is chamfered depending on the sharp_limit parameter. Defaults to "sharp".
sharp_limit: float, optional
The sharp limit ratio is used for very sharp corners. The sharp ratio is the ratio of the distance
from the corner to the end of the chamfered offset corner. When two line segments meet at a sharp angle,
a sharp join will extend the original geometry. To prevent unreasonable geometry,
the sharp limit allows controlling the maximum length of the join corner.
Corners with a ratio which exceed the limit will be chamfered.

Returns
-------
list[point]
The vertices of the offseted polygon.

Notes
-----
The offset direction is determined by the provided normal vector.
If the polyline is in the XY plane and the normal is along the positive Z axis,
positive offset distances will result in counterclockwise offsets,
and negative values in clockwise direction.

"""
join_styles_dict = {"round": "round", "sharp": "mitre", "chamfer": "bevel"}
join_style = join_styles_dict.get(join_style)
if not join_style:
raise ValueError("Join styles supported are round, sharp and chamfer.")
pgon = Polygon(polygon.points)
offset = pgon.buffer(-distance, join_style=join_style, mitre_limit=sharp_limit, single_sided=True)
if is_ccw(pgon.exterior):
return list(offset.reverse().exterior.coords)
return list(offset.exterior.coords)
26 changes: 26 additions & 0 deletions src/compas/geometry/polygon.py
Original file line number Diff line number Diff line change
Expand Up @@ -492,3 +492,29 @@ def boolean_intersection(self, other):

coords = boolean_intersection_polygon_polygon(self, other)
return Polygon([[x, y, 0] for x, y in coords]) # type: ignore

def offset(self, distance, **kwargs):
"""Offset a polygon by a distance.

Parameters
----------
polygon : :class:`~compas.geometry.Polygon`
A polygon defined by a sequence of vertices.
distance : float
The offset distance as float.

Returns
-------
list[point]
The vertices of the offseted polygon.

Notes
-----
Depending of the backend used, additional parameters can be added as keyword arguments. (point somewhere in api, or list
accepted arguments)

"""
from compas.geometry import offset_polygon

vertices = offset_polygon(self, distance, **kwargs)
return Polygon(vertices)
Loading