offset-poly 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- offset_poly/__init__.py +43 -0
- offset_poly/offset.py +156 -0
- offset_poly/offset_corner.py +48 -0
- offset_poly/prepare_poly.py +59 -0
- offset_poly/py.typed +0 -0
- offset_poly/vec2_math.py +129 -0
- offset_poly-0.1.0.dist-info/METADATA +55 -0
- offset_poly-0.1.0.dist-info/RECORD +9 -0
- offset_poly-0.1.0.dist-info/WHEEL +4 -0
offset_poly/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Import fuctions into the package namespace.
|
|
2
|
+
|
|
3
|
+
:author: Shay Hill
|
|
4
|
+
:created: 2023-08-19
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from offset_poly.offset import (
|
|
8
|
+
offset_poly_per_edge,
|
|
9
|
+
offset_poly_per_vert,
|
|
10
|
+
offset_polygon,
|
|
11
|
+
offset_polyline,
|
|
12
|
+
)
|
|
13
|
+
from offset_poly.offset_corner import miter_corner
|
|
14
|
+
from offset_poly.vec2_math import (
|
|
15
|
+
det,
|
|
16
|
+
dot,
|
|
17
|
+
get_norm,
|
|
18
|
+
get_signed_angle,
|
|
19
|
+
move_along,
|
|
20
|
+
qrotate,
|
|
21
|
+
set_norm,
|
|
22
|
+
vadd,
|
|
23
|
+
vscale,
|
|
24
|
+
vsub,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"miter_corner",
|
|
29
|
+
"offset_polyline",
|
|
30
|
+
"offset_polygon",
|
|
31
|
+
"offset_poly_per_vert",
|
|
32
|
+
"offset_poly_per_edge",
|
|
33
|
+
"det",
|
|
34
|
+
"dot",
|
|
35
|
+
"get_signed_angle",
|
|
36
|
+
"get_norm",
|
|
37
|
+
"set_norm",
|
|
38
|
+
"move_along",
|
|
39
|
+
"qrotate",
|
|
40
|
+
"vscale",
|
|
41
|
+
"vadd",
|
|
42
|
+
"vsub",
|
|
43
|
+
]
|
offset_poly/offset.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Offset a polyline to the right.
|
|
2
|
+
|
|
3
|
+
:author: Shay Hill
|
|
4
|
+
:created: 2023-08-17
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import enum
|
|
10
|
+
import itertools as it
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from offset_poly.offset_corner import miter_corner
|
|
14
|
+
from offset_poly.prepare_poly import (
|
|
15
|
+
align_closing_points,
|
|
16
|
+
remove_coincident_adjacent_points,
|
|
17
|
+
)
|
|
18
|
+
from offset_poly.vec2_math import vadd, vsub
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from collections.abc import Iterable, Sequence
|
|
22
|
+
|
|
23
|
+
_Vec2 = tuple[float, float]
|
|
24
|
+
|
|
25
|
+
_MIN_PTS_FOR_POLYGON = 3
|
|
26
|
+
_MIN_PTS_FOR_POLYLINE = 2
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PolyType(enum.Enum):
|
|
30
|
+
"""Polyline types."""
|
|
31
|
+
|
|
32
|
+
POLYLINE = enum.auto()
|
|
33
|
+
POLYGON = enum.auto()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _wrap_polygon(polyline: Sequence[_Vec2]) -> list[_Vec2]:
|
|
37
|
+
"""Wrap a polyline around to the beginning if it is closed.
|
|
38
|
+
|
|
39
|
+
:param polyline: polyline
|
|
40
|
+
:return: polyline with last point repeated if it is closed
|
|
41
|
+
|
|
42
|
+
From A, B, C, A to C, A, B, C, A
|
|
43
|
+
"""
|
|
44
|
+
points = list(polyline)
|
|
45
|
+
if points[0] == points[-1]:
|
|
46
|
+
points = points[:-1]
|
|
47
|
+
return points[-1:] + points + points[:1]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _anchor_polyline(polyline: Sequence[_Vec2]) -> list[_Vec2]:
|
|
51
|
+
"""Add edges to endpoints of an open polyline.
|
|
52
|
+
|
|
53
|
+
:param polyline: polyline
|
|
54
|
+
:return: polyline with extended edges at the beginning and end
|
|
55
|
+
|
|
56
|
+
A --- B
|
|
57
|
+
|
|
|
58
|
+
C
|
|
59
|
+
|
|
60
|
+
becomes
|
|
61
|
+
|
|
62
|
+
A` --- A --- B
|
|
63
|
+
|
|
|
64
|
+
C
|
|
65
|
+
|
|
|
66
|
+
C`
|
|
67
|
+
|
|
68
|
+
From A, B, C to (A + (A - B)), A, B, C, (C - (B - C))
|
|
69
|
+
"""
|
|
70
|
+
pnt_beg = vadd(polyline[0], vsub(*polyline[:2]))
|
|
71
|
+
pnt_end = vsub(polyline[-1], vsub(*polyline[-2:]))
|
|
72
|
+
return [pnt_beg, *list(polyline), pnt_end]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def offset_poly_per_vert(
|
|
76
|
+
polyline: Sequence[_Vec2],
|
|
77
|
+
vert_offsets: Iterable[tuple[float, float]],
|
|
78
|
+
poly_type: PolyType,
|
|
79
|
+
) -> list[_Vec2]:
|
|
80
|
+
"""Offset each corner of a polyline or polygon.
|
|
81
|
+
|
|
82
|
+
:param polyline: polyline
|
|
83
|
+
:param vert_offsets: iterable of (gap_1, gap_2) tuples. One pair per corner.
|
|
84
|
+
:param poly_type: PolyType.POLYGON or PolyType.POLYLINE
|
|
85
|
+
:return: polyline offset by vert_offsets
|
|
86
|
+
:raise ValueError: if fewer than three points are given for a polygon
|
|
87
|
+
:raise ValueError: if fewer than two points are given for a polyline
|
|
88
|
+
|
|
89
|
+
This is the engine of the offset_polyline and offset_polygon
|
|
90
|
+
functions. You can use it directly, but it's going to be tricky if
|
|
91
|
+
you have zero-length segments or closed points. These points will
|
|
92
|
+
be removed before the gaps are applied, so you'll have to pass
|
|
93
|
+
exactly enough for gap pairs for the segments that are retained.
|
|
94
|
+
"""
|
|
95
|
+
points = remove_coincident_adjacent_points(polyline)
|
|
96
|
+
gaps = it.cycle(vert_offsets or [(0, 0)])
|
|
97
|
+
|
|
98
|
+
if poly_type == PolyType.POLYGON:
|
|
99
|
+
if len(points) < _MIN_PTS_FOR_POLYGON:
|
|
100
|
+
msg = "at least three unique points required for a polygon"
|
|
101
|
+
raise ValueError(msg)
|
|
102
|
+
points = _wrap_polygon(points)
|
|
103
|
+
elif poly_type == PolyType.POLYLINE:
|
|
104
|
+
if len(points) < _MIN_PTS_FOR_POLYLINE:
|
|
105
|
+
msg = "at least two unique points required for a polyline"
|
|
106
|
+
raise ValueError(msg)
|
|
107
|
+
points = _anchor_polyline(points)
|
|
108
|
+
else:
|
|
109
|
+
msg = "poly_type must be PolyType.POLYGON or PolyType.POLYLINE"
|
|
110
|
+
raise ValueError(msg)
|
|
111
|
+
|
|
112
|
+
offset_points: list[_Vec2] = []
|
|
113
|
+
for pnt_a, pnt_b, pnt_c in zip(points, points[1:], points[2:]):
|
|
114
|
+
gap_1, gap_2 = next(gaps)
|
|
115
|
+
offset_points.append(miter_corner(pnt_a, pnt_b, pnt_c, gap_1, gap_2))
|
|
116
|
+
|
|
117
|
+
return align_closing_points(polyline, offset_points)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def offset_poly_per_edge(
|
|
121
|
+
polyline: Sequence[_Vec2], edge_offsets: Iterable[float], poly_type: PolyType
|
|
122
|
+
) -> list[_Vec2]:
|
|
123
|
+
"""Offset each edge of a polyline or polygon.
|
|
124
|
+
|
|
125
|
+
:param polyline: polyline
|
|
126
|
+
:param edge_offsets: iterable of offsets. One per edge.
|
|
127
|
+
:param poly_type: PolyType.POLYGON or PolyType.POLYLINE
|
|
128
|
+
:return: polyline offset by edge_offsets
|
|
129
|
+
|
|
130
|
+
This function allows each edge of a polygon or polyline to be offset by
|
|
131
|
+
a different amount. You will end up with a ValueError in miter_corner
|
|
132
|
+
if you try to offset consecutive, parallel edges by different amounts.
|
|
133
|
+
"""
|
|
134
|
+
prev_edges, next_edges = it.tee(edge_offsets, 2)
|
|
135
|
+
next_edges = it.islice(it.cycle(next_edges), 1, None)
|
|
136
|
+
return offset_poly_per_vert(polyline, zip(prev_edges, next_edges), poly_type)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def offset_polyline(polyline: Sequence[_Vec2], offset: float) -> list[_Vec2]:
|
|
140
|
+
"""Offset polygon edges (to the left) by a constant amount.
|
|
141
|
+
|
|
142
|
+
:param polyline: polyline
|
|
143
|
+
:param offset: distance to offset from each edge
|
|
144
|
+
:return: polyline offset by offset
|
|
145
|
+
"""
|
|
146
|
+
return offset_poly_per_edge(polyline, it.cycle([offset]), PolyType.POLYLINE)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def offset_polygon(polyline: Sequence[_Vec2], offset: float) -> list[_Vec2]:
|
|
150
|
+
"""Offset polygon edges (to the left) by a constant amount.
|
|
151
|
+
|
|
152
|
+
:param polyline: polyline
|
|
153
|
+
:param offset: distance to offset from each edge
|
|
154
|
+
:return: polygon offset by offset
|
|
155
|
+
"""
|
|
156
|
+
return offset_poly_per_edge(polyline, it.cycle([offset]), PolyType.POLYGON)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Simple math helpers and offsetting one corner defined by three points.
|
|
2
|
+
|
|
3
|
+
:author: Shay Hill
|
|
4
|
+
:created: 2023-08-17
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
|
|
11
|
+
from offset_poly.vec2_math import _Vec2, get_signed_angle, move_along, qrotate, vsub
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def miter_corner(
|
|
15
|
+
pnt_a: _Vec2, pnt_b: _Vec2, pnt_c: _Vec2, gap_1: float, gap_2: float | None = None
|
|
16
|
+
) -> tuple[float, float]:
|
|
17
|
+
"""Offset a corner (to the right) defined by three points.
|
|
18
|
+
|
|
19
|
+
:param pnt_a: previous point
|
|
20
|
+
:param pnt_b: corner point
|
|
21
|
+
:param pnt_c: next point
|
|
22
|
+
:param gap_1: distance to offset from vec12
|
|
23
|
+
:param gap_2: optional distance to offset from vec23. If not given, gap_1 is used
|
|
24
|
+
:return: the unique point gap_1 distance from ab and gap_2 distance from bc
|
|
25
|
+
:raise ValueError: if angle abc is zero and gap_1 != gap_2
|
|
26
|
+
"""
|
|
27
|
+
if gap_2 is None:
|
|
28
|
+
gap_2 = gap_1
|
|
29
|
+
|
|
30
|
+
if gap_1 is 0 and gap_2 is 0:
|
|
31
|
+
return pnt_b
|
|
32
|
+
|
|
33
|
+
vec12 = vsub(pnt_b, pnt_a)
|
|
34
|
+
vec23 = vsub(pnt_c, pnt_b)
|
|
35
|
+
angle = get_signed_angle(vec12, vec23)
|
|
36
|
+
|
|
37
|
+
if math.isclose(angle, 0, abs_tol=1e-6):
|
|
38
|
+
if gap_1 != gap_2:
|
|
39
|
+
except_msg = "gap_1 and gap_2 must be equal when angle is zero"
|
|
40
|
+
raise ValueError(except_msg)
|
|
41
|
+
return move_along(pnt_b, qrotate(vec12, 1), gap_1)
|
|
42
|
+
|
|
43
|
+
sin_angle = math.sin(angle)
|
|
44
|
+
if math.isclose(sin_angle, 0, abs_tol=1e-6):
|
|
45
|
+
return math.nan, math.nan
|
|
46
|
+
|
|
47
|
+
intermediate = move_along(pnt_b, vec23, gap_1 / sin_angle)
|
|
48
|
+
return move_along(intermediate, vec12, -(gap_2 or 0) / sin_angle)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Remove points that would give indeterminate results.
|
|
2
|
+
|
|
3
|
+
:author: Shay Hill
|
|
4
|
+
:created: 2023-08-17
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import itertools as it
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Sequence
|
|
14
|
+
|
|
15
|
+
_Vec2 = tuple[float, float]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def remove_coincident_adjacent_points(polyline: Sequence[_Vec2]) -> list[_Vec2]:
|
|
19
|
+
"""Remove zero-length segments from a polyline.
|
|
20
|
+
|
|
21
|
+
:param polyline: polyline
|
|
22
|
+
:return: polyline with zero-length segments removed
|
|
23
|
+
"""
|
|
24
|
+
new_points: list[_Vec2] = []
|
|
25
|
+
for i, point in enumerate(polyline):
|
|
26
|
+
if i == 0 or point != polyline[i - 1]:
|
|
27
|
+
new_points.append(point)
|
|
28
|
+
return new_points
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def align_closing_points(
|
|
32
|
+
polyline_a: Sequence[_Vec2], polyline_b: list[_Vec2]
|
|
33
|
+
) -> list[_Vec2]:
|
|
34
|
+
"""Set b[0] to b[-1] if a[0] == a[-1].
|
|
35
|
+
|
|
36
|
+
:param polyline_a: polyline
|
|
37
|
+
:param polyline_b: polyline
|
|
38
|
+
:return: polyline_b with first point set to last point if polyline_a is closed
|
|
39
|
+
|
|
40
|
+
Match multiples of the same point.
|
|
41
|
+
polyline_a -> [A, B, B, C]
|
|
42
|
+
polyline_b -> [D, E, F]
|
|
43
|
+
polyline_c -> [D, E, E, F]
|
|
44
|
+
|
|
45
|
+
Wrap endpoints if polyline_a is closed.
|
|
46
|
+
polyline_a -> [A, B, C, A]
|
|
47
|
+
polyline_b -> [D, E, F]
|
|
48
|
+
polyline_c -> [D, E, F, D]
|
|
49
|
+
"""
|
|
50
|
+
polyline_c: list[_Vec2] = []
|
|
51
|
+
points_b = it.cycle(polyline_b)
|
|
52
|
+
for i, point_a in enumerate(polyline_a):
|
|
53
|
+
if i == 0:
|
|
54
|
+
polyline_c.append(next(points_b))
|
|
55
|
+
elif point_a == polyline_a[i - 1]:
|
|
56
|
+
polyline_c.append(polyline_c[-1])
|
|
57
|
+
else:
|
|
58
|
+
polyline_c.append(next(points_b))
|
|
59
|
+
return polyline_c
|
offset_poly/py.typed
ADDED
|
File without changes
|
offset_poly/vec2_math.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Simple operations on vectors. All return 2tuples.
|
|
2
|
+
|
|
3
|
+
:author: Shay Hill
|
|
4
|
+
:created: 2023-08-19
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
from typing import Union
|
|
12
|
+
|
|
13
|
+
_Vec2 = Union[tuple[float, float], Sequence[float]]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def det(vec_a: _Vec2, vec_b: _Vec2) -> float:
|
|
17
|
+
"""Return determinant of a 2x2 matrix.
|
|
18
|
+
|
|
19
|
+
:param vec_a: 2d vector
|
|
20
|
+
:param vec_b: 2d vector
|
|
21
|
+
:return: determinant of the 2x2 matrix where
|
|
22
|
+
vec_a is the first row and vec_b is the second
|
|
23
|
+
"""
|
|
24
|
+
return vec_a[0] * vec_b[1] - vec_a[1] * vec_b[0]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def dot(vec_a: _Vec2, vec_b: _Vec2) -> float:
|
|
28
|
+
"""Return dot product of two 2d vectors.
|
|
29
|
+
|
|
30
|
+
:param vec_a: 2d vector
|
|
31
|
+
:param vec_b: 2d vector
|
|
32
|
+
:return: dot product of the vectors
|
|
33
|
+
"""
|
|
34
|
+
return vec_a[0] * vec_b[0] + vec_a[1] * vec_b[1]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_signed_angle(vec_a: _Vec2, vec_b: _Vec2) -> float:
|
|
38
|
+
"""Calculate the signed angle at a corner defined by two vectors.
|
|
39
|
+
|
|
40
|
+
:param vec_a: 2d vector
|
|
41
|
+
:param vec_b: 2d vector
|
|
42
|
+
:return: signed angle between the vectors
|
|
43
|
+
|
|
44
|
+
Counterclockwise angles will be positive
|
|
45
|
+
Clockwise angles will be negative
|
|
46
|
+
"""
|
|
47
|
+
return math.atan2(det(vec_a, vec_b), dot(vec_a, vec_b))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_norm(vec: _Vec2) -> float:
|
|
51
|
+
"""Return Euclidean norm of a 2d vector.
|
|
52
|
+
|
|
53
|
+
:param vec: 2d vector
|
|
54
|
+
:return: Euclidean norm of the vector
|
|
55
|
+
"""
|
|
56
|
+
return math.sqrt(vec[0] ** 2 + vec[1] ** 2)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def set_norm(vec: _Vec2, norm: float = 1) -> tuple[float, float]:
|
|
60
|
+
"""Scale a 2d vector to a given magnitude.
|
|
61
|
+
|
|
62
|
+
:param vec: 2d vector
|
|
63
|
+
:param norm: desired magnitude of the output vector, default is 1
|
|
64
|
+
:return: normalized (then optionally scaled) 2d vector
|
|
65
|
+
:raise ValueError: if trying to scale a zero-length vector to a nonzero length
|
|
66
|
+
"""
|
|
67
|
+
input_norm = get_norm(vec)
|
|
68
|
+
if input_norm == 0 and norm != 0:
|
|
69
|
+
except_msg = "cannot scale a zero-length vector to a nonzero length"
|
|
70
|
+
raise ValueError(except_msg)
|
|
71
|
+
if norm == 0:
|
|
72
|
+
return 0, 0
|
|
73
|
+
scale = norm / input_norm
|
|
74
|
+
return vscale(vec, scale)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def vscale(vec: _Vec2, scale: float) -> tuple[float, float]:
|
|
78
|
+
"""Multiply a 2d vector by a scalar.
|
|
79
|
+
|
|
80
|
+
:param vec: 2d vector
|
|
81
|
+
:param scale: scalar for vec
|
|
82
|
+
:return: vec * scale
|
|
83
|
+
"""
|
|
84
|
+
return vec[0] * scale, vec[1] * scale
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def vadd(vec_a: _Vec2, vec_b: _Vec2) -> tuple[float, float]:
|
|
88
|
+
"""Add two 2d vectors.
|
|
89
|
+
|
|
90
|
+
:param vec_a: 2d vector
|
|
91
|
+
:param vec_b: 2d vector
|
|
92
|
+
:return: sum of the vectors
|
|
93
|
+
"""
|
|
94
|
+
return vec_a[0] + vec_b[0], vec_a[1] + vec_b[1]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def vsub(vec_a: _Vec2, vec_b: _Vec2) -> tuple[float, float]:
|
|
98
|
+
"""Subtract two 2d vectors.
|
|
99
|
+
|
|
100
|
+
:param vec_a: 2d vector
|
|
101
|
+
:param vec_b: 2d vector
|
|
102
|
+
:return: difference of the vectors
|
|
103
|
+
"""
|
|
104
|
+
return vadd(vec_a, vscale(vec_b, -1))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def move_along(pnt: _Vec2, vec: _Vec2, distance: float) -> tuple[float, float]:
|
|
108
|
+
"""Move a point along a vector.
|
|
109
|
+
|
|
110
|
+
:param pnt: 2d vector
|
|
111
|
+
:param vec: 2d vector
|
|
112
|
+
:param distance: distance to move v1 along v2
|
|
113
|
+
:return: new point
|
|
114
|
+
"""
|
|
115
|
+
vec12 = set_norm(vec, distance)
|
|
116
|
+
return vadd(pnt, vec12)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def qrotate(vec: _Vec2, quadrants: int) -> tuple[float, float]:
|
|
120
|
+
"""Rotate a 2d vector 90 degrees counterclockwise.
|
|
121
|
+
|
|
122
|
+
:param vec: 2d vector
|
|
123
|
+
:param quadrants: number of 90 degree clockwise rotations
|
|
124
|
+
:return: rotated vector
|
|
125
|
+
"""
|
|
126
|
+
quadrants = quadrants % 4
|
|
127
|
+
if quadrants == 0:
|
|
128
|
+
return vec[0], vec[1]
|
|
129
|
+
return qrotate((-vec[1], vec[0]), quadrants - 1)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: offset-poly
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary:
|
|
5
|
+
Author: Shay Hill
|
|
6
|
+
Author-email: shay_public@hotmail.com
|
|
7
|
+
Requires-Python: >=3.9,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# offset_poly
|
|
15
|
+
|
|
16
|
+
Move a polyline or polygon (left) by a given offset.
|
|
17
|
+
|
|
18
|
+
A counterclockwise polygon will be offset to the inside. I clockwise polygon will be offset to the outside.
|
|
19
|
+
|
|
20
|
+
## offset_polygon
|
|
21
|
+
|
|
22
|
+
~~~python
|
|
23
|
+
def offset_polygon(polyline: Sequence[_Vec2], offset: float) -> list[_Vec2]:
|
|
24
|
+
"""Offset polygon edges (to the left) by a constant amount.
|
|
25
|
+
|
|
26
|
+
:param polyline: polyline
|
|
27
|
+
:param offset: distance to offset from each edge
|
|
28
|
+
:return: polygon offset by offset
|
|
29
|
+
"""
|
|
30
|
+
~~~
|
|
31
|
+
|
|
32
|
+
## offset_polyline
|
|
33
|
+
|
|
34
|
+
~~~python
|
|
35
|
+
def offset_polyline(polyline: Sequence[_Vec2], offset: float) -> list[_Vec2]:
|
|
36
|
+
"""Offset polygon edges (to the left) by a constant amount.
|
|
37
|
+
|
|
38
|
+
:param polyline: polyline
|
|
39
|
+
:param offset: distance to offset from each edge
|
|
40
|
+
:return: polyline offset by offset
|
|
41
|
+
"""
|
|
42
|
+
~~~
|
|
43
|
+
|
|
44
|
+
The difference between the two is that `offset_polygon` will close the polygon if it is not already closed, whereas `offset_polyline` will leave the polyline open even if the first and last points are identical.
|
|
45
|
+
|
|
46
|
+
This package is the simplest version of polyline offsetting, it does not anticipate or account for self intersections that may come up when offsetting a polyline.
|
|
47
|
+
|
|
48
|
+
This is not nearly as sophisticated as curve offsetting, but you can use this for control polygon offsetting, which will be nearly as good in some instances.
|
|
49
|
+
|
|
50
|
+
* multiple points (knots in your control points) are preserved.
|
|
51
|
+
* if input[0] == input[-1], output[0] will equal output[-1]
|
|
52
|
+
|
|
53
|
+
## You may see (nan, nan) in the result.
|
|
54
|
+
|
|
55
|
+
If you pass two adjacent, opposite, parallel edges, you will get a (nan, nan) in the result. With points A -> B -> A, for instance, there is no point that would be any given distance (except 0) left of both A B and B A.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
offset_poly/__init__.py,sha256=l543Y1tZmvI0BAfItYWzXWLMfQNm5Sizv2JKnOBAOAQ,767
|
|
2
|
+
offset_poly/offset.py,sha256=7Rlw_hFfn6MYxkK5eVjzk9IoM3XsjWTPQWLSGK6gqxc,5186
|
|
3
|
+
offset_poly/offset_corner.py,sha256=QLvl2q--XY2Dr2rcVhNvI8hvp0vo3CfxyJC6Ykm6GN8,1583
|
|
4
|
+
offset_poly/prepare_poly.py,sha256=qvNS3rWsYvIdwUux9RQrz5kr_J4DliHqk6UlNfRtHLI,1669
|
|
5
|
+
offset_poly/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
offset_poly/vec2_math.py,sha256=IcGZID390t6rc9OeqjrrgcYba8adzZu6_Kt8eT7mEQU,3608
|
|
7
|
+
offset_poly-0.1.0.dist-info/METADATA,sha256=7PL4TEp8FC1Hgem3pGbAm_KTV9DfVxzEZ6oY-1mwVm4,2104
|
|
8
|
+
offset_poly-0.1.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
|
|
9
|
+
offset_poly-0.1.0.dist-info/RECORD,,
|