[boolean] Add basic boolean functionality (boolean() and Polygon.boolean())
This commit is contained in:
parent
5d040061f4
commit
7ad59d6b89
8 changed files with 430 additions and 4 deletions
|
|
@ -55,6 +55,7 @@ from .pattern import (
|
||||||
map_targets as map_targets,
|
map_targets as map_targets,
|
||||||
chain_elements as chain_elements,
|
chain_elements as chain_elements,
|
||||||
)
|
)
|
||||||
|
from .utils.boolean import boolean as boolean
|
||||||
|
|
||||||
from .library import (
|
from .library import (
|
||||||
ILibraryView as ILibraryView,
|
ILibraryView as ILibraryView,
|
||||||
|
|
|
||||||
|
|
@ -502,6 +502,61 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
]
|
]
|
||||||
return polys
|
return polys
|
||||||
|
|
||||||
|
def layer_as_polygons(
|
||||||
|
self,
|
||||||
|
layer: layer_t,
|
||||||
|
flatten: bool = True,
|
||||||
|
library: Mapping[str, 'Pattern'] | None = None,
|
||||||
|
) -> list[Polygon]:
|
||||||
|
"""
|
||||||
|
Collect all geometry effectively on a given layer as a list of polygons.
|
||||||
|
|
||||||
|
If `flatten=True`, it recursively gathers shapes on `layer` from all `self.refs`.
|
||||||
|
`Repetition` objects are expanded, and non-polygon shapes are converted
|
||||||
|
to `Polygon` approximations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
layer: The layer to collect geometry from.
|
||||||
|
flatten: If `True`, include geometry from referenced patterns.
|
||||||
|
library: Required if `flatten=True` to resolve references.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of `Polygon` objects.
|
||||||
|
"""
|
||||||
|
if flatten and self.has_refs() and library is None:
|
||||||
|
raise PatternError("Must provide a library to layer_as_polygons() when flatten=True")
|
||||||
|
|
||||||
|
polys: list[Polygon] = []
|
||||||
|
|
||||||
|
# Local shapes
|
||||||
|
for shape in self.shapes.get(layer, []):
|
||||||
|
for p in shape.to_polygons():
|
||||||
|
# expand repetitions
|
||||||
|
if p.repetition is not None:
|
||||||
|
for offset in p.repetition.displacements:
|
||||||
|
polys.append(p.deepcopy().translate(offset).set_repetition(None))
|
||||||
|
else:
|
||||||
|
polys.append(p.deepcopy())
|
||||||
|
|
||||||
|
if flatten and self.has_refs():
|
||||||
|
assert library is not None
|
||||||
|
for target, refs in self.refs.items():
|
||||||
|
if target is None:
|
||||||
|
continue
|
||||||
|
target_pat = library[target]
|
||||||
|
for ref in refs:
|
||||||
|
# Get polygons from target pattern on the same layer
|
||||||
|
ref_polys = target_pat.layer_as_polygons(layer, flatten=True, library=library)
|
||||||
|
# Apply ref transformations
|
||||||
|
for p in ref_polys:
|
||||||
|
p_pat = ref.as_pattern(Pattern(shapes={layer: [p]}))
|
||||||
|
# as_pattern expands repetition of the ref itself
|
||||||
|
# but we need to pull the polygons back out
|
||||||
|
for p_transformed in p_pat.shapes[layer]:
|
||||||
|
polys.append(cast('Polygon', p_transformed))
|
||||||
|
|
||||||
|
return polys
|
||||||
|
|
||||||
def referenced_patterns(self) -> set[str | None]:
|
def referenced_patterns(self) -> set[str | None]:
|
||||||
"""
|
"""
|
||||||
Get all pattern namers referenced by this pattern. Non-recursive.
|
Get all pattern namers referenced by this pattern. Non-recursive.
|
||||||
|
|
|
||||||
|
|
@ -302,9 +302,7 @@ class PortList(metaclass=ABCMeta):
|
||||||
raise PortError(f'Unrenamed ports would be overwritten: {duplicates}')
|
raise PortError(f'Unrenamed ports would be overwritten: {duplicates}')
|
||||||
|
|
||||||
for kk, vv in mapping.items():
|
for kk, vv in mapping.items():
|
||||||
if vv is None:
|
if vv is None or vv != kk:
|
||||||
self._log_port_removal(kk)
|
|
||||||
elif vv != kk:
|
|
||||||
self._log_port_removal(kk)
|
self._log_port_removal(kk)
|
||||||
|
|
||||||
renamed = {vv: self.ports.pop(kk) for kk, vv in mapping.items()}
|
renamed = {vv: self.ports.pop(kk) for kk, vv in mapping.items()}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Any, cast, TYPE_CHECKING, Self
|
from typing import Any, cast, TYPE_CHECKING, Self, Literal
|
||||||
import copy
|
import copy
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
|
|
@ -462,3 +462,23 @@ class Polygon(Shape):
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
centroid = self.vertices.mean(axis=0)
|
centroid = self.vertices.mean(axis=0)
|
||||||
return f'<Polygon centroid {centroid} v{len(self.vertices)}>'
|
return f'<Polygon centroid {centroid} v{len(self.vertices)}>'
|
||||||
|
|
||||||
|
def boolean(
|
||||||
|
self,
|
||||||
|
other: Any,
|
||||||
|
operation: Literal['union', 'intersection', 'difference', 'xor'] = 'union',
|
||||||
|
scale: float = 1e6,
|
||||||
|
) -> list['Polygon']:
|
||||||
|
"""
|
||||||
|
Perform a boolean operation using this polygon as the subject.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
other: Polygon, Iterable[Polygon], or raw vertices acting as the CLIP.
|
||||||
|
operation: 'union', 'intersection', 'difference', 'xor'.
|
||||||
|
scale: Scaling factor for integer conversion.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of resulting Polygons.
|
||||||
|
"""
|
||||||
|
from ..utils.boolean import boolean
|
||||||
|
return boolean([self], other, operation=operation, scale=scale)
|
||||||
|
|
|
||||||
119
masque/test/test_boolean.py
Normal file
119
masque/test/test_boolean.py
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
import pytest
|
||||||
|
import numpy
|
||||||
|
from numpy.testing import assert_allclose
|
||||||
|
from masque.pattern import Pattern
|
||||||
|
from masque.shapes.polygon import Polygon
|
||||||
|
from masque.repetition import Grid
|
||||||
|
from masque.library import Library
|
||||||
|
|
||||||
|
def test_layer_as_polygons_basic() -> None:
|
||||||
|
pat = Pattern()
|
||||||
|
pat.polygon((1, 0), [[0, 0], [1, 0], [1, 1], [0, 1]])
|
||||||
|
|
||||||
|
polys = pat.layer_as_polygons((1, 0), flatten=False)
|
||||||
|
assert len(polys) == 1
|
||||||
|
assert isinstance(polys[0], Polygon)
|
||||||
|
assert_allclose(polys[0].vertices, [[0, 0], [1, 0], [1, 1], [0, 1]])
|
||||||
|
|
||||||
|
def test_layer_as_polygons_repetition() -> None:
|
||||||
|
pat = Pattern()
|
||||||
|
rep = Grid(a_vector=(2, 0), a_count=2)
|
||||||
|
pat.polygon((1, 0), [[0, 0], [1, 0], [1, 1], [0, 1]], repetition=rep)
|
||||||
|
|
||||||
|
polys = pat.layer_as_polygons((1, 0), flatten=False)
|
||||||
|
assert len(polys) == 2
|
||||||
|
# First polygon at (0,0)
|
||||||
|
assert_allclose(polys[0].vertices, [[0, 0], [1, 0], [1, 1], [0, 1]])
|
||||||
|
# Second polygon at (2,0)
|
||||||
|
assert_allclose(polys[1].vertices, [[2, 0], [3, 0], [3, 1], [2, 1]])
|
||||||
|
|
||||||
|
def test_layer_as_polygons_flatten() -> None:
|
||||||
|
lib = Library()
|
||||||
|
|
||||||
|
child = Pattern()
|
||||||
|
child.polygon((1, 0), [[0, 0], [1, 0], [1, 1]])
|
||||||
|
lib['child'] = child
|
||||||
|
|
||||||
|
parent = Pattern()
|
||||||
|
parent.ref('child', offset=(10, 10), rotation=numpy.pi/2)
|
||||||
|
|
||||||
|
polys = parent.layer_as_polygons((1, 0), flatten=True, library=lib)
|
||||||
|
assert len(polys) == 1
|
||||||
|
# Original child at (0,0) with rot pi/2 is still at (0,0) in its own space?
|
||||||
|
# No, ref.as_pattern(child) will apply the transform.
|
||||||
|
# Child (0,0), (1,0), (1,1) rotated pi/2 around (0,0) -> (0,0), (0,1), (-1,1)
|
||||||
|
# Then offset by (10,10) -> (10,10), (10,11), (9,11)
|
||||||
|
|
||||||
|
# Let's verify the vertices
|
||||||
|
expected = numpy.array([[10, 10], [10, 11], [9, 11]])
|
||||||
|
assert_allclose(polys[0].vertices, expected, atol=1e-10)
|
||||||
|
|
||||||
|
def test_boolean_import_error() -> None:
|
||||||
|
from masque import boolean
|
||||||
|
# If pyclipper is not installed, this should raise ImportError
|
||||||
|
try:
|
||||||
|
import pyclipper # noqa: F401
|
||||||
|
pytest.skip("pyclipper is installed, cannot test ImportError")
|
||||||
|
except ImportError:
|
||||||
|
with pytest.raises(ImportError, match="Boolean operations require 'pyclipper'"):
|
||||||
|
boolean([], [], operation='union')
|
||||||
|
|
||||||
|
def test_polygon_boolean_shortcut() -> None:
|
||||||
|
poly = Polygon([[0, 0], [1, 0], [1, 1]])
|
||||||
|
# This should also raise ImportError if pyclipper is missing
|
||||||
|
try:
|
||||||
|
import pyclipper # noqa: F401
|
||||||
|
pytest.skip("pyclipper is installed")
|
||||||
|
except ImportError:
|
||||||
|
with pytest.raises(ImportError, match="Boolean operations require 'pyclipper'"):
|
||||||
|
poly.boolean(poly)
|
||||||
|
|
||||||
|
def test_bridge_holes() -> None:
|
||||||
|
from masque.utils.boolean import _bridge_holes
|
||||||
|
|
||||||
|
# Outer: 10x10 square
|
||||||
|
outer = numpy.array([[0, 0], [10, 0], [10, 10], [0, 10]])
|
||||||
|
# Hole: 2x2 square in the middle
|
||||||
|
hole = numpy.array([[4, 4], [6, 4], [6, 6], [4, 6]])
|
||||||
|
|
||||||
|
bridged = _bridge_holes(outer, [hole])
|
||||||
|
|
||||||
|
# We expect more vertices than outer + hole
|
||||||
|
# Original outer has 4, hole has 4. Bridge adds 2 (to hole) and 2 (back to outer) + 1 to close hole loop?
|
||||||
|
# Our implementation:
|
||||||
|
# 1. outer up to bridge edge (best_edge_idx)
|
||||||
|
# 2. bridge point on outer
|
||||||
|
# 3. hole reordered starting at max X
|
||||||
|
# 4. close hole loop (repeat max X)
|
||||||
|
# 5. bridge point on outer again
|
||||||
|
# 6. rest of outer
|
||||||
|
|
||||||
|
# max X of hole is 6 at (6,4) or (6,6). argmax will pick first one.
|
||||||
|
# hole vertices: [4,4], [6,4], [6,6], [4,6]. argmax(x) is index 1: (6,4)
|
||||||
|
# roll hole to start at (6,4): [6,4], [6,6], [4,6], [4,4]
|
||||||
|
|
||||||
|
# intersection of ray from (6,4) to right:
|
||||||
|
# edges of outer: (0,0)-(10,0), (10,0)-(10,10), (10,10)-(0,10), (0,10)-(0,0)
|
||||||
|
# edge (10,0)-(10,10) spans y=4.
|
||||||
|
# intersection at (10,4). best_edge_idx = 1 (edge from index 1 to 2)
|
||||||
|
|
||||||
|
# vertices added:
|
||||||
|
# outer[0:2]: (0,0), (10,0)
|
||||||
|
# bridge pt: (10,4)
|
||||||
|
# hole: (6,4), (6,6), (4,6), (4,4)
|
||||||
|
# hole close: (6,4)
|
||||||
|
# bridge pt back: (10,4)
|
||||||
|
# outer[2:]: (10,10), (0,10)
|
||||||
|
|
||||||
|
expected_len = 11
|
||||||
|
assert len(bridged) == expected_len
|
||||||
|
|
||||||
|
# verify it wraps around the hole and back
|
||||||
|
# index 2 is bridge_pt
|
||||||
|
assert_allclose(bridged[2], [10, 4])
|
||||||
|
# index 3 is hole reordered max X
|
||||||
|
assert_allclose(bridged[3], [6, 4])
|
||||||
|
# index 7 is hole closed at max X
|
||||||
|
assert_allclose(bridged[7], [6, 4])
|
||||||
|
# index 8 is bridge_pt back
|
||||||
|
assert_allclose(bridged[8], [10, 4])
|
||||||
180
masque/utils/boolean.py
Normal file
180
masque/utils/boolean.py
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
from typing import Any, Literal
|
||||||
|
from collections.abc import Iterable
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
from numpy.typing import NDArray
|
||||||
|
|
||||||
|
from ..shapes.polygon import Polygon
|
||||||
|
from ..error import PatternError
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def _bridge_holes(outer_path: NDArray[numpy.float64], holes: list[NDArray[numpy.float64]]) -> NDArray[numpy.float64]:
|
||||||
|
"""
|
||||||
|
Bridge multiple holes into an outer boundary using zero-width slits.
|
||||||
|
"""
|
||||||
|
current_outer = outer_path
|
||||||
|
|
||||||
|
# Sort holes by max X to potentially minimize bridge lengths or complexity
|
||||||
|
# (though not strictly necessary for correctness)
|
||||||
|
holes = sorted(holes, key=lambda h: numpy.max(h[:, 0]), reverse=True)
|
||||||
|
|
||||||
|
for hole in holes:
|
||||||
|
# Find max X vertex of hole
|
||||||
|
max_idx = numpy.argmax(hole[:, 0])
|
||||||
|
m = hole[max_idx]
|
||||||
|
|
||||||
|
# Find intersection of ray (m.x, m.y) + (t, 0) with current_outer edges
|
||||||
|
best_t = numpy.inf
|
||||||
|
best_pt = None
|
||||||
|
best_edge_idx = -1
|
||||||
|
|
||||||
|
n = len(current_outer)
|
||||||
|
for i in range(n):
|
||||||
|
p1 = current_outer[i]
|
||||||
|
p2 = current_outer[(i + 1) % n]
|
||||||
|
|
||||||
|
# Check if edge (p1, p2) spans m.y
|
||||||
|
if (p1[1] <= m[1] < p2[1]) or (p2[1] <= m[1] < p1[1]):
|
||||||
|
# Intersection x:
|
||||||
|
# x = p1.x + (m.y - p1.y) * (p2.x - p1.x) / (p2.y - p1.y)
|
||||||
|
t = (p1[0] + (m[1] - p1[1]) * (p2[0] - p1[0]) / (p2[1] - p1[1])) - m[0]
|
||||||
|
if 0 <= t < best_t:
|
||||||
|
best_t = t
|
||||||
|
best_pt = numpy.array([m[0] + t, m[1]])
|
||||||
|
best_edge_idx = i
|
||||||
|
|
||||||
|
if best_edge_idx == -1:
|
||||||
|
# Fallback: find nearest vertex if ray fails (shouldn't happen for valid hole)
|
||||||
|
dists = numpy.linalg.norm(current_outer - m, axis=1)
|
||||||
|
best_edge_idx = int(numpy.argmin(dists))
|
||||||
|
best_pt = current_outer[best_edge_idx]
|
||||||
|
# Adjust best_edge_idx to insert AFTER this vertex
|
||||||
|
# (treating it as a degenerate edge)
|
||||||
|
|
||||||
|
assert best_pt is not None
|
||||||
|
|
||||||
|
# Reorder hole vertices to start at m
|
||||||
|
hole_reordered = numpy.roll(hole, -max_idx, axis=0)
|
||||||
|
|
||||||
|
# Construct new outer:
|
||||||
|
# 1. Start of outer up to best_edge_idx
|
||||||
|
# 2. Intersection point
|
||||||
|
# 3. Hole vertices (starting and ending at m)
|
||||||
|
# 4. Intersection point (to close slit)
|
||||||
|
# 5. Rest of outer
|
||||||
|
|
||||||
|
new_outer: list[NDArray[numpy.float64]] = []
|
||||||
|
new_outer.extend(current_outer[:best_edge_idx + 1])
|
||||||
|
new_outer.append(best_pt)
|
||||||
|
new_outer.extend(hole_reordered)
|
||||||
|
new_outer.append(hole_reordered[0]) # close hole loop at m
|
||||||
|
new_outer.append(best_pt) # back to outer
|
||||||
|
new_outer.extend(current_outer[best_edge_idx + 1:])
|
||||||
|
|
||||||
|
current_outer = numpy.array(new_outer)
|
||||||
|
|
||||||
|
return current_outer
|
||||||
|
|
||||||
|
def boolean(
|
||||||
|
subjects: Iterable[Any],
|
||||||
|
clips: Iterable[Any] | None = None,
|
||||||
|
operation: Literal['union', 'intersection', 'difference', 'xor'] = 'union',
|
||||||
|
scale: float = 1e6,
|
||||||
|
) -> list[Polygon]:
|
||||||
|
"""
|
||||||
|
Perform a boolean operation on two sets of polygons.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
subjects: List of subjects (Polygons or vertex arrays).
|
||||||
|
clips: List of clips (Polygons or vertex arrays).
|
||||||
|
operation: The boolean operation to perform.
|
||||||
|
scale: Scaling factor for integer conversion (pyclipper uses integers).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of result Polygons.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import pyclipper
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError(
|
||||||
|
"Boolean operations require 'pyclipper'. "
|
||||||
|
"Install it with 'pip install pyclipper' or 'pip install masque[boolean]'."
|
||||||
|
) from None
|
||||||
|
|
||||||
|
op_map = {
|
||||||
|
'union': pyclipper.PT_UNION,
|
||||||
|
'intersection': pyclipper.PT_INTERSECTION,
|
||||||
|
'difference': pyclipper.PT_DIFFERENCE,
|
||||||
|
'xor': pyclipper.PT_XOR,
|
||||||
|
}
|
||||||
|
|
||||||
|
def to_vertices(objs: Iterable[Any] | None) -> list[NDArray]:
|
||||||
|
if objs is None:
|
||||||
|
return []
|
||||||
|
verts = []
|
||||||
|
for obj in objs:
|
||||||
|
if hasattr(obj, 'to_polygons'):
|
||||||
|
for p in obj.to_polygons():
|
||||||
|
verts.append(p.vertices)
|
||||||
|
elif isinstance(obj, numpy.ndarray):
|
||||||
|
verts.append(obj)
|
||||||
|
elif isinstance(obj, Polygon):
|
||||||
|
verts.append(obj.vertices)
|
||||||
|
else:
|
||||||
|
# Try to iterate if it's an iterable of shapes
|
||||||
|
try:
|
||||||
|
for sub in obj:
|
||||||
|
if hasattr(sub, 'to_polygons'):
|
||||||
|
for p in sub.to_polygons():
|
||||||
|
verts.append(p.vertices)
|
||||||
|
elif isinstance(sub, Polygon):
|
||||||
|
verts.append(sub.vertices)
|
||||||
|
except TypeError:
|
||||||
|
raise PatternError(f"Unsupported type for boolean operation: {type(obj)}") from None
|
||||||
|
return verts
|
||||||
|
|
||||||
|
subject_verts = to_vertices(subjects)
|
||||||
|
clip_verts = to_vertices(clips)
|
||||||
|
|
||||||
|
pc = pyclipper.Pyclipper()
|
||||||
|
pc.AddPaths(pyclipper.scale_to_clipper(subject_verts, scale), pyclipper.PT_SUBJECT, True)
|
||||||
|
if clip_verts:
|
||||||
|
pc.AddPaths(pyclipper.scale_to_clipper(clip_verts, scale), pyclipper.PT_CLIP, True)
|
||||||
|
|
||||||
|
# Use GetPolyTree to distinguish between outers and holes
|
||||||
|
polytree = pc.Execute2(op_map[operation.lower()], pyclipper.PFT_NONZERO, pyclipper.PFT_NONZERO)
|
||||||
|
|
||||||
|
result_polygons = []
|
||||||
|
|
||||||
|
def process_node(node: Any) -> None:
|
||||||
|
if not node.IsHole:
|
||||||
|
# This is an outer boundary
|
||||||
|
outer_path = numpy.array(pyclipper.scale_from_clipper(node.Contour, scale))
|
||||||
|
|
||||||
|
# Find immediate holes
|
||||||
|
holes = []
|
||||||
|
for child in node.Childs:
|
||||||
|
if child.IsHole:
|
||||||
|
holes.append(numpy.array(pyclipper.scale_from_clipper(child.Contour, scale)))
|
||||||
|
|
||||||
|
if holes:
|
||||||
|
combined_vertices = _bridge_holes(outer_path, holes)
|
||||||
|
result_polygons.append(Polygon(combined_vertices))
|
||||||
|
else:
|
||||||
|
result_polygons.append(Polygon(outer_path))
|
||||||
|
|
||||||
|
# Recursively process children of holes (which are nested outers)
|
||||||
|
for child in node.Childs:
|
||||||
|
if child.IsHole:
|
||||||
|
for grandchild in child.Childs:
|
||||||
|
process_node(grandchild)
|
||||||
|
else:
|
||||||
|
# Holes are processed as children of outers
|
||||||
|
pass
|
||||||
|
|
||||||
|
for top_node in polytree.Childs:
|
||||||
|
process_node(top_node)
|
||||||
|
|
||||||
|
return result_polygons
|
||||||
|
|
@ -69,6 +69,7 @@ visualize = ["matplotlib"]
|
||||||
text = ["matplotlib", "freetype-py"]
|
text = ["matplotlib", "freetype-py"]
|
||||||
manhattanize = ["scikit-image"]
|
manhattanize = ["scikit-image"]
|
||||||
manhattanize_slow = ["float_raster"]
|
manhattanize_slow = ["float_raster"]
|
||||||
|
boolean = ["pyclipper"]
|
||||||
|
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
|
|
@ -106,3 +107,9 @@ lint.ignore = [
|
||||||
addopts = "-rsXx"
|
addopts = "-rsXx"
|
||||||
testpaths = ["masque"]
|
testpaths = ["masque"]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
mypy_path = "stubs"
|
||||||
|
python_version = "3.11"
|
||||||
|
strict = false
|
||||||
|
check_untyped_defs = true
|
||||||
|
|
||||||
|
|
|
||||||
46
stubs/pyclipper/__init__.pyi
Normal file
46
stubs/pyclipper/__init__.pyi
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
from typing import Any
|
||||||
|
from collections.abc import Iterable, Sequence
|
||||||
|
import numpy
|
||||||
|
from numpy.typing import NDArray
|
||||||
|
|
||||||
|
|
||||||
|
# Basic types for Clipper integer coordinates
|
||||||
|
Path = Sequence[tuple[int, int]]
|
||||||
|
Paths = Sequence[Path]
|
||||||
|
|
||||||
|
# Types for input/output floating point coordinates
|
||||||
|
FloatPoint = tuple[float, float] | NDArray[numpy.floating]
|
||||||
|
FloatPath = Sequence[FloatPoint] | NDArray[numpy.floating]
|
||||||
|
FloatPaths = Iterable[FloatPath]
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
PT_SUBJECT: int
|
||||||
|
PT_CLIP: int
|
||||||
|
|
||||||
|
PT_UNION: int
|
||||||
|
PT_INTERSECTION: int
|
||||||
|
PT_DIFFERENCE: int
|
||||||
|
PT_XOR: int
|
||||||
|
|
||||||
|
PFT_EVENODD: int
|
||||||
|
PFT_NONZERO: int
|
||||||
|
PFT_POSITIVE: int
|
||||||
|
PFT_NEGATIVE: int
|
||||||
|
|
||||||
|
# Scaling functions
|
||||||
|
def scale_to_clipper(paths: FloatPaths, scale: float = ...) -> Paths: ...
|
||||||
|
def scale_from_clipper(paths: Path | Paths, scale: float = ...) -> Any: ...
|
||||||
|
|
||||||
|
class PolyNode:
|
||||||
|
Contour: Path
|
||||||
|
Childs: list[PolyNode]
|
||||||
|
Parent: PolyNode
|
||||||
|
IsHole: bool
|
||||||
|
|
||||||
|
class Pyclipper:
|
||||||
|
def __init__(self) -> None: ...
|
||||||
|
def AddPath(self, path: Path, poly_type: int, closed: bool) -> None: ...
|
||||||
|
def AddPaths(self, paths: Paths, poly_type: int, closed: bool) -> None: ...
|
||||||
|
def Execute(self, clip_type: int, subj_fill_type: int = ..., clip_fill_type: int = ...) -> Paths: ...
|
||||||
|
def Execute2(self, clip_type: int, subj_fill_type: int = ..., clip_fill_type: int = ...) -> PolyNode: ...
|
||||||
|
def Clear(self) -> None: ...
|
||||||
Loading…
Add table
Add a link
Reference in a new issue