ScadPy 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.
- scadpy/__init__.py +5 -0
- scadpy/color/__init__.py +3 -0
- scadpy/color/constants/BEIGE.py +3 -0
- scadpy/color/constants/BLACK.py +3 -0
- scadpy/color/constants/BLUE.py +3 -0
- scadpy/color/constants/BROWN.py +3 -0
- scadpy/color/constants/DARK_GRAY.py +3 -0
- scadpy/color/constants/DEFAULT_COLOR.py +3 -0
- scadpy/color/constants/DEFAULT_OPACITY.py +1 -0
- scadpy/color/constants/GRAY.py +3 -0
- scadpy/color/constants/GREEN.py +3 -0
- scadpy/color/constants/ORANGE.py +3 -0
- scadpy/color/constants/RED.py +3 -0
- scadpy/color/constants/WHITE.py +3 -0
- scadpy/color/constants/YELLOW.py +3 -0
- scadpy/color/constants/__init__.py +29 -0
- scadpy/color/type/__init__.py +3 -0
- scadpy/color/type/color.py +3 -0
- scadpy/color/utils/__init__.py +3 -0
- scadpy/color/utils/get_random_color.py +36 -0
- scadpy/core/__init__.py +3 -0
- scadpy/core/assembly/__init__.py +5 -0
- scadpy/core/assembly/combinations/__init__.py +13 -0
- scadpy/core/assembly/combinations/concat_assemblies.py +50 -0
- scadpy/core/assembly/combinations/exclude_assemblies.py +135 -0
- scadpy/core/assembly/combinations/intersect_assemblies.py +128 -0
- scadpy/core/assembly/combinations/subtract_assemblies.py +151 -0
- scadpy/core/assembly/combinations/unify_assemblies.py +59 -0
- scadpy/core/assembly/topologies/__init__.py +41 -0
- scadpy/core/assembly/topologies/directed_edge/__init__.py +9 -0
- scadpy/core/assembly/topologies/directed_edge/get_assembly_directed_edge_directions.py +70 -0
- scadpy/core/assembly/topologies/directed_edge/get_assembly_directed_edge_to_edge.py +49 -0
- scadpy/core/assembly/topologies/directed_edge/get_assembly_directed_edge_to_vertex.py +54 -0
- scadpy/core/assembly/topologies/edge/__init__.py +9 -0
- scadpy/core/assembly/topologies/edge/get_assembly_edge_lengths.py +46 -0
- scadpy/core/assembly/topologies/edge/get_assembly_edge_midpoints.py +51 -0
- scadpy/core/assembly/topologies/edge/get_assembly_edge_normals.py +67 -0
- scadpy/core/assembly/topologies/face_corner/__init__.py +13 -0
- scadpy/core/assembly/topologies/face_corner/get_assembly_face_corner_angles.py +72 -0
- scadpy/core/assembly/topologies/face_corner/get_assembly_face_corner_normals.py +103 -0
- scadpy/core/assembly/topologies/face_corner/get_assembly_face_corner_to_incoming_directed_edge.py +65 -0
- scadpy/core/assembly/topologies/face_corner/get_assembly_face_corner_to_outgoing_directed_edge.py +65 -0
- scadpy/core/assembly/topologies/face_corner/get_assembly_face_directed_edge_to_corner.py +79 -0
- scadpy/core/assembly/topologies/part/__init__.py +5 -0
- scadpy/core/assembly/topologies/part/get_assembly_part_colors.py +55 -0
- scadpy/core/assembly/topologies/vertex/__init__.py +7 -0
- scadpy/core/assembly/topologies/vertex/get_assembly_vertex_coordinates.py +70 -0
- scadpy/core/assembly/topologies/vertex/get_assembly_vertex_to_part.py +62 -0
- scadpy/core/assembly/transformations/__init__.py +19 -0
- scadpy/core/assembly/transformations/color_assembly.py +24 -0
- scadpy/core/assembly/transformations/mirror_vertex_coordinates.py +68 -0
- scadpy/core/assembly/transformations/pull_vertex_coordinates.py +64 -0
- scadpy/core/assembly/transformations/push_vertex_coordinates.py +64 -0
- scadpy/core/assembly/transformations/resize_vertex_coordinates.py +121 -0
- scadpy/core/assembly/transformations/rotate_vertex_coordinates.py +73 -0
- scadpy/core/assembly/transformations/scale_vertex_coordinates.py +76 -0
- scadpy/core/assembly/transformations/translate_vertex_coordinates.py +70 -0
- scadpy/core/assembly/types/__init__.py +7 -0
- scadpy/core/assembly/types/assembly.py +14 -0
- scadpy/core/assembly/types/topology_filter.py +6 -0
- scadpy/core/assembly/utils/__init__.py +9 -0
- scadpy/core/assembly/utils/lookup_pairs.py +56 -0
- scadpy/core/assembly/utils/resolve_topology_filter.py +84 -0
- scadpy/core/assembly/utils/transform_filtered_parts.py +55 -0
- scadpy/core/component/__init__.py +3 -0
- scadpy/core/component/exporters/__init__.py +7 -0
- scadpy/core/component/exporters/map_component_to_html_file.py +47 -0
- scadpy/core/component/exporters/map_component_to_screen.py +63 -0
- scadpy/core/component/features/__init__.py +5 -0
- scadpy/core/component/features/get_component_bounds.py +38 -0
- scadpy/core/component/utils/__init__.py +9 -0
- scadpy/core/component/utils/blend_component_colors.py +77 -0
- scadpy/core/component/utils/get_intersecting_component_index_groups.py +108 -0
- scadpy/core/part/__init__.py +3 -0
- scadpy/core/part/combinations/__init__.py +11 -0
- scadpy/core/part/combinations/concat_parts.py +48 -0
- scadpy/core/part/combinations/intersect_parts.py +147 -0
- scadpy/core/part/combinations/subtract_parts.py +94 -0
- scadpy/core/part/combinations/unify_parts.py +143 -0
- scadpy/core/part/types/__init__.py +5 -0
- scadpy/core/part/types/part.py +34 -0
- scadpy/core/part/utils/__init__.py +5 -0
- scadpy/core/part/utils/blend_part_colors.py +32 -0
- scadpy/d2/__init__.py +2 -0
- scadpy/d2/shape/__init__.py +9 -0
- scadpy/d2/shape/combinations/__init__.py +21 -0
- scadpy/d2/shape/combinations/are_shape_parts_intersecting.py +49 -0
- scadpy/d2/shape/combinations/concat_shape.py +48 -0
- scadpy/d2/shape/combinations/exclude_shape.py +71 -0
- scadpy/d2/shape/combinations/intersect_shape.py +64 -0
- scadpy/d2/shape/combinations/intersect_shape_parts.py +71 -0
- scadpy/d2/shape/combinations/subtract_shape.py +72 -0
- scadpy/d2/shape/combinations/subtract_shape_parts.py +66 -0
- scadpy/d2/shape/combinations/unify_shape.py +51 -0
- scadpy/d2/shape/combinations/unify_shape_parts.py +74 -0
- scadpy/d2/shape/exporters/__init__.py +17 -0
- scadpy/d2/shape/exporters/map_shape_to_dxf.py +43 -0
- scadpy/d2/shape/exporters/map_shape_to_dxf_file.py +38 -0
- scadpy/d2/shape/exporters/map_shape_to_html.py +117 -0
- scadpy/d2/shape/exporters/map_shape_to_html_file.py +58 -0
- scadpy/d2/shape/exporters/map_shape_to_screen.py +51 -0
- scadpy/d2/shape/exporters/map_shape_to_svg.py +40 -0
- scadpy/d2/shape/exporters/map_shape_to_svg_file.py +38 -0
- scadpy/d2/shape/features/__init__.py +9 -0
- scadpy/d2/shape/features/get_shape_bounds.py +40 -0
- scadpy/d2/shape/features/get_shape_part_bounds.py +37 -0
- scadpy/d2/shape/features/is_shape_empty.py +38 -0
- scadpy/d2/shape/importers/__init__.py +13 -0
- scadpy/d2/shape/importers/map_dxf_to_shape.py +55 -0
- scadpy/d2/shape/importers/map_geometries_to_shape.py +45 -0
- scadpy/d2/shape/importers/map_geometry_to_shape.py +43 -0
- scadpy/d2/shape/importers/map_parts_to_shape.py +62 -0
- scadpy/d2/shape/importers/map_svg_to_shape.py +55 -0
- scadpy/d2/shape/primitives/__init__.py +6 -0
- scadpy/d2/shape/primitives/circle.py +68 -0
- scadpy/d2/shape/primitives/polygon.py +86 -0
- scadpy/d2/shape/primitives/rectangle.py +85 -0
- scadpy/d2/shape/primitives/square.py +57 -0
- scadpy/d2/shape/topologies/__init__.py +53 -0
- scadpy/d2/shape/topologies/corner/__init__.py +15 -0
- scadpy/d2/shape/topologies/corner/are_shape_corners_convex.py +75 -0
- scadpy/d2/shape/topologies/corner/get_shape_corner_angles.py +58 -0
- scadpy/d2/shape/topologies/corner/get_shape_corner_normals.py +82 -0
- scadpy/d2/shape/topologies/corner/get_shape_corner_to_incoming_directed_edge.py +39 -0
- scadpy/d2/shape/topologies/corner/get_shape_corner_to_outgoing_directed_edge.py +39 -0
- scadpy/d2/shape/topologies/corner/get_shape_corner_to_vertex.py +65 -0
- scadpy/d2/shape/topologies/directed_edge/__init__.py +11 -0
- scadpy/d2/shape/topologies/directed_edge/get_shape_directed_edge_directions.py +44 -0
- scadpy/d2/shape/topologies/directed_edge/get_shape_directed_edge_to_corner.py +41 -0
- scadpy/d2/shape/topologies/directed_edge/get_shape_directed_edge_to_edge.py +51 -0
- scadpy/d2/shape/topologies/directed_edge/get_shape_directed_edge_to_vertex.py +63 -0
- scadpy/d2/shape/topologies/edge/__init__.py +11 -0
- scadpy/d2/shape/topologies/edge/get_shape_edge_lengths.py +43 -0
- scadpy/d2/shape/topologies/edge/get_shape_edge_midpoints.py +46 -0
- scadpy/d2/shape/topologies/edge/get_shape_edge_normals.py +40 -0
- scadpy/d2/shape/topologies/edge/get_shape_edge_to_vertex.py +71 -0
- scadpy/d2/shape/topologies/ring/__init__.py +7 -0
- scadpy/d2/shape/topologies/ring/get_shape_ring_to_part.py +46 -0
- scadpy/d2/shape/topologies/ring/get_shape_ring_types.py +46 -0
- scadpy/d2/shape/topologies/vertex/__init__.py +11 -0
- scadpy/d2/shape/topologies/vertex/get_shape_part_vertex_coordinates.py +62 -0
- scadpy/d2/shape/topologies/vertex/get_shape_vertex_coordinates.py +44 -0
- scadpy/d2/shape/topologies/vertex/get_shape_vertex_to_part.py +42 -0
- scadpy/d2/shape/topologies/vertex/get_shape_vertex_to_ring.py +63 -0
- scadpy/d2/shape/transformations/__init__.py +43 -0
- scadpy/d2/shape/transformations/chamfer_shape.py +259 -0
- scadpy/d2/shape/transformations/color_shape.py +46 -0
- scadpy/d2/shape/transformations/convexify_shape.py +79 -0
- scadpy/d2/shape/transformations/fill_shape.py +68 -0
- scadpy/d2/shape/transformations/fillet_shape.py +289 -0
- scadpy/d2/shape/transformations/grow_shape.py +82 -0
- scadpy/d2/shape/transformations/linear_cut_shape.py +116 -0
- scadpy/d2/shape/transformations/linear_extrude_shape.py +60 -0
- scadpy/d2/shape/transformations/linear_slice_shape.py +144 -0
- scadpy/d2/shape/transformations/mirror_shape.py +53 -0
- scadpy/d2/shape/transformations/pull_shape.py +67 -0
- scadpy/d2/shape/transformations/push_shape.py +67 -0
- scadpy/d2/shape/transformations/radial_extrude_shape.py +285 -0
- scadpy/d2/shape/transformations/radial_slice_shape.py +132 -0
- scadpy/d2/shape/transformations/recoordinate_shape.py +82 -0
- scadpy/d2/shape/transformations/resize_shape.py +91 -0
- scadpy/d2/shape/transformations/rotate_shape.py +63 -0
- scadpy/d2/shape/transformations/scale_shape.py +58 -0
- scadpy/d2/shape/transformations/shrink_shape.py +69 -0
- scadpy/d2/shape/transformations/translate_shape.py +54 -0
- scadpy/d2/shape/types/__init__.py +3 -0
- scadpy/d2/shape/types/shape.py +792 -0
- scadpy/d2/shape/types/utils/__init__.py +5 -0
- scadpy/d2/shape/types/utils/shapely_base_geometry_to_shapely_polygons.py +25 -0
- scadpy/d2/shape/utils/__init__.py +5 -0
- scadpy/d2/shape/utils/shapely_base_geometry_to_shapely_polygons.py +55 -0
- scadpy/d2/utils/__init__.py +3 -0
- scadpy/d2/utils/resolve_vector_2d.py +50 -0
- scadpy/d3/__init__.py +2 -0
- scadpy/d3/solid/__init__.py +8 -0
- scadpy/d3/solid/combinations/__init__.py +21 -0
- scadpy/d3/solid/combinations/are_solid_parts_intersecting.py +51 -0
- scadpy/d3/solid/combinations/concat_solid.py +48 -0
- scadpy/d3/solid/combinations/exclude_solid.py +71 -0
- scadpy/d3/solid/combinations/intersect_solid.py +64 -0
- scadpy/d3/solid/combinations/intersect_solid_parts.py +73 -0
- scadpy/d3/solid/combinations/subtract_solid.py +72 -0
- scadpy/d3/solid/combinations/subtract_solid_parts.py +68 -0
- scadpy/d3/solid/combinations/unify_solid.py +51 -0
- scadpy/d3/solid/combinations/unify_solid_parts.py +73 -0
- scadpy/d3/solid/exporters/__init__.py +11 -0
- scadpy/d3/solid/exporters/map_solid_to_html.py +318 -0
- scadpy/d3/solid/exporters/map_solid_to_html_file.py +58 -0
- scadpy/d3/solid/exporters/map_solid_to_screen.py +51 -0
- scadpy/d3/solid/exporters/map_solid_to_stl_file.py +48 -0
- scadpy/d3/solid/features/__init__.py +11 -0
- scadpy/d3/solid/features/get_solid_bounds.py +37 -0
- scadpy/d3/solid/features/get_solid_part_bounds.py +37 -0
- scadpy/d3/solid/features/get_solid_part_colors.py +39 -0
- scadpy/d3/solid/features/is_solid_empty.py +36 -0
- scadpy/d3/solid/importers/__init__.py +11 -0
- scadpy/d3/solid/importers/map_geometries_to_solid.py +42 -0
- scadpy/d3/solid/importers/map_geometry_to_solid.py +42 -0
- scadpy/d3/solid/importers/map_parts_to_solid.py +66 -0
- scadpy/d3/solid/importers/map_stl_to_solid.py +37 -0
- scadpy/d3/solid/primitives/__init__.py +7 -0
- scadpy/d3/solid/primitives/cone.py +70 -0
- scadpy/d3/solid/primitives/cuboid.py +75 -0
- scadpy/d3/solid/primitives/cylinder.py +73 -0
- scadpy/d3/solid/primitives/polyhedron.py +60 -0
- scadpy/d3/solid/primitives/sphere.py +58 -0
- scadpy/d3/solid/topologies/__init__.py +8 -0
- scadpy/d3/solid/topologies/triangle/__init__.py +5 -0
- scadpy/d3/solid/topologies/triangle/get_solid_triangle_to_vertex.py +49 -0
- scadpy/d3/solid/topologies/vertex/__init__.py +7 -0
- scadpy/d3/solid/topologies/vertex/get_solid_vertex_coordinates.py +39 -0
- scadpy/d3/solid/topologies/vertex/get_solid_vertex_to_part.py +37 -0
- scadpy/d3/solid/transformations/__init__.py +23 -0
- scadpy/d3/solid/transformations/color_solid.py +46 -0
- scadpy/d3/solid/transformations/convexify_solid.py +64 -0
- scadpy/d3/solid/transformations/mirror_solid.py +53 -0
- scadpy/d3/solid/transformations/pull_solid.py +67 -0
- scadpy/d3/solid/transformations/push_solid.py +67 -0
- scadpy/d3/solid/transformations/recoordinate_solid.py +68 -0
- scadpy/d3/solid/transformations/resize_solid.py +92 -0
- scadpy/d3/solid/transformations/rotate_solid.py +93 -0
- scadpy/d3/solid/transformations/scale_solid.py +58 -0
- scadpy/d3/solid/transformations/translate_solid.py +54 -0
- scadpy/d3/solid/types/__init__.py +3 -0
- scadpy/d3/solid/types/solid.py +448 -0
- scadpy/d3/utils/__init__.py +3 -0
- scadpy/d3/utils/resolve_vector_3d.py +50 -0
- scadpy/utils/__init__.py +6 -0
- scadpy/utils/resolve_vector.py +64 -0
- scadpy/utils/x.py +38 -0
- scadpy/utils/y.py +38 -0
- scadpy/utils/z.py +38 -0
- scadpy-0.1.0.dist-info/METADATA +282 -0
- scadpy-0.1.0.dist-info/RECORD +236 -0
- scadpy-0.1.0.dist-info/WHEEL +4 -0
- scadpy-0.1.0.dist-info/licenses/LICENSE.md +43 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from shapely.geometry import Polygon
|
|
7
|
+
from typeguard import typechecked
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from scadpy import Shape, TopologyFilter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@typechecked
|
|
14
|
+
def chamfer_shape(
|
|
15
|
+
shape: Shape,
|
|
16
|
+
size: float | np.ndarray,
|
|
17
|
+
corner_filter: TopologyFilter[Shape] | None = None,
|
|
18
|
+
epsilon: float = 1e-8,
|
|
19
|
+
) -> Shape:
|
|
20
|
+
"""
|
|
21
|
+
Apply a chamfer (straight cut/fill) to every corner of a shape.
|
|
22
|
+
|
|
23
|
+
Convex corners are cut; concave corners are filled with a straight triangle.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
shape : Shape
|
|
28
|
+
The input shape to chamfer.
|
|
29
|
+
size : float or ndarray
|
|
30
|
+
Distance from each corner vertex to the cut/fill points along the edges.
|
|
31
|
+
Can be:
|
|
32
|
+
|
|
33
|
+
- ``float``: same size on both sides of every corner.
|
|
34
|
+
- ``(n_active,)``: per-active-corner size, same on both sides.
|
|
35
|
+
- ``(n_active, 2)``: per-active-corner, per-side size. Column 0 is
|
|
36
|
+
the incoming side, column 1 is the outgoing side.
|
|
37
|
+
|
|
38
|
+
``n_active`` is the number of corners selected by ``corner_filter``
|
|
39
|
+
(or all corners if no filter). In all cases, each value is
|
|
40
|
+
automatically clamped to half the length of the corresponding edge
|
|
41
|
+
to avoid overlapping tangent points.
|
|
42
|
+
corner_filter : TopologyFilter[Shape] | None, optional
|
|
43
|
+
Boolean mask or callable ``(shape) -> NDArray[bool]`` of length ``n_corners``
|
|
44
|
+
selecting which corners to chamfer. If None, all corners are chamfered.
|
|
45
|
+
epsilon : float, optional
|
|
46
|
+
Small offset used to avoid coincident edges in boolean operations.
|
|
47
|
+
Defaults to ``1e-8``.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
Shape
|
|
52
|
+
A new shape with chamfered corners.
|
|
53
|
+
|
|
54
|
+
Examples
|
|
55
|
+
--------
|
|
56
|
+
>>> from scadpy import square, polygon, chamfer_shape
|
|
57
|
+
|
|
58
|
+
>>> sq = square(4)
|
|
59
|
+
>>> l_shape = polygon(
|
|
60
|
+
... [(0, 0), (4, 0), (4, 2), (2, 2), (2, 4), (0, 4)]
|
|
61
|
+
... )
|
|
62
|
+
>>> arrow = polygon(
|
|
63
|
+
... [(0, 1), (3, 0), (5, 2), (3, 4),
|
|
64
|
+
... (0, 3), (1, 2.5), (1, 1.5)]
|
|
65
|
+
... )
|
|
66
|
+
|
|
67
|
+
>>> # all corners
|
|
68
|
+
>>> chamfer_shape(sq, size=1.0) # doctest: +SKIP
|
|
69
|
+
|
|
70
|
+
.. render-example::
|
|
71
|
+
:name: chamfer_shape_all
|
|
72
|
+
:example: chamfer_shape(sq, size=1.0)
|
|
73
|
+
:ghost: sq
|
|
74
|
+
|
|
75
|
+
>>> # convex corners only (the 5 outer corners of the L-shape)
|
|
76
|
+
>>> chamfer_shape( # doctest: +SKIP
|
|
77
|
+
... l_shape, size=0.5,
|
|
78
|
+
... corner_filter=lambda s: s.are_corners_convex,
|
|
79
|
+
... )
|
|
80
|
+
|
|
81
|
+
.. render-example::
|
|
82
|
+
:name: chamfer_shape_convex_only
|
|
83
|
+
:example: chamfer_shape(l_shape, size=0.5, corner_filter=lambda s: s.are_corners_convex)
|
|
84
|
+
:ghost: l_shape
|
|
85
|
+
|
|
86
|
+
>>> import numpy as np
|
|
87
|
+
|
|
88
|
+
>>> # asymmetric: one side fills the full edge (length 2),
|
|
89
|
+
>>> # the other stays at 1.0
|
|
90
|
+
>>> chamfer_shape( # doctest: +SKIP
|
|
91
|
+
... l_shape, size=np.array([[2.0, 1.0]]),
|
|
92
|
+
... corner_filter=lambda s: ~s.are_corners_convex,
|
|
93
|
+
... )
|
|
94
|
+
|
|
95
|
+
.. render-example::
|
|
96
|
+
:name: chamfer_shape_concave_asymmetric
|
|
97
|
+
:example: chamfer_shape(l_shape, size=np.array([[2.0, 1.0]]), corner_filter=lambda s: ~s.are_corners_convex)
|
|
98
|
+
:ghost: l_shape
|
|
99
|
+
|
|
100
|
+
>>> # only sharp convex corners (angle > 100°):
|
|
101
|
+
>>> # the two 135° corners of the arrow tail
|
|
102
|
+
>>> chamfer_shape( # doctest: +SKIP
|
|
103
|
+
... arrow, size=0.4,
|
|
104
|
+
... corner_filter=lambda s: (
|
|
105
|
+
... s.are_corners_convex & (s.corner_angles > 100)
|
|
106
|
+
... ),
|
|
107
|
+
... )
|
|
108
|
+
|
|
109
|
+
.. render-example::
|
|
110
|
+
:name: chamfer_shape_sharp_convex
|
|
111
|
+
:example: chamfer_shape(arrow, size=0.4, corner_filter=lambda s: s.are_corners_convex & (s.corner_angles > 100))
|
|
112
|
+
:ghost: arrow
|
|
113
|
+
|
|
114
|
+
>>> # oversized: the 2 concave corners share an edge of length ~1;
|
|
115
|
+
>>> # size=10 is clamped proportionally so their contributions
|
|
116
|
+
>>> # sum to the edge length (0.5 + 0.5 each)
|
|
117
|
+
>>> chamfer_shape( # doctest: +SKIP
|
|
118
|
+
... arrow, size=10,
|
|
119
|
+
... corner_filter=lambda s: ~s.are_corners_convex,
|
|
120
|
+
... )
|
|
121
|
+
|
|
122
|
+
.. render-example::
|
|
123
|
+
:name: chamfer_shape_clamp_proportional
|
|
124
|
+
:example: chamfer_shape(arrow, size=10, corner_filter=lambda s: ~s.are_corners_convex)
|
|
125
|
+
:ghost: arrow
|
|
126
|
+
|
|
127
|
+
>>> # wrong size length raises ValueError
|
|
128
|
+
>>> chamfer_shape(
|
|
129
|
+
... sq, size=np.array([0.5, 0.5, 0.5])
|
|
130
|
+
... ) # doctest: +ELLIPSIS
|
|
131
|
+
Traceback (most recent call last):
|
|
132
|
+
...
|
|
133
|
+
ValueError: size array shape (3, 2) does not match...
|
|
134
|
+
"""
|
|
135
|
+
from scadpy import resolve_topology_filter, Shape
|
|
136
|
+
|
|
137
|
+
corner_to_vertex = shape.corner_to_vertex
|
|
138
|
+
if len(corner_to_vertex) == 0:
|
|
139
|
+
return shape
|
|
140
|
+
|
|
141
|
+
vertex_coordinates = shape.vertex_coordinates
|
|
142
|
+
is_corner_convex = shape.are_corners_convex
|
|
143
|
+
corner_normals = shape.corner_normals
|
|
144
|
+
|
|
145
|
+
active_mask = resolve_topology_filter(shape, len(corner_to_vertex), corner_filter)
|
|
146
|
+
if active_mask is not None and not np.any(active_mask):
|
|
147
|
+
return shape
|
|
148
|
+
|
|
149
|
+
# Filter all per-corner data to active corners only
|
|
150
|
+
active_indices = (
|
|
151
|
+
np.where(active_mask)[0]
|
|
152
|
+
if active_mask is not None
|
|
153
|
+
else np.arange(len(corner_to_vertex))
|
|
154
|
+
)
|
|
155
|
+
active_is_corner_convex = is_corner_convex[active_indices]
|
|
156
|
+
active_corner_normals = corner_normals[active_indices]
|
|
157
|
+
|
|
158
|
+
current_vertices = vertex_coordinates[corner_to_vertex[active_indices, 1]]
|
|
159
|
+
|
|
160
|
+
incoming_de = shape.corner_to_incoming_directed_edge[active_indices]
|
|
161
|
+
outgoing_de = shape.corner_to_outgoing_directed_edge[active_indices]
|
|
162
|
+
incoming_edge = shape.directed_edge_to_edge[incoming_de]
|
|
163
|
+
outgoing_edge = shape.directed_edge_to_edge[outgoing_de]
|
|
164
|
+
|
|
165
|
+
incoming_directions_normalized = shape.directed_edge_directions[incoming_de]
|
|
166
|
+
outgoing_directions_normalized = shape.directed_edge_directions[outgoing_de]
|
|
167
|
+
edge_lengths_incoming = shape.edge_lengths[incoming_edge]
|
|
168
|
+
edge_lengths_outgoing = shape.edge_lengths[outgoing_edge]
|
|
169
|
+
outward_normals_incoming = shape.edge_normals[incoming_edge]
|
|
170
|
+
outward_normals_outgoing = shape.edge_normals[outgoing_edge]
|
|
171
|
+
|
|
172
|
+
# Resolve size to (n_active, 2): column 0 = incoming side, column 1 = outgoing side
|
|
173
|
+
n_active = len(current_vertices)
|
|
174
|
+
sizes = np.asarray(size, dtype=np.float64)
|
|
175
|
+
if sizes.ndim == 0:
|
|
176
|
+
sizes = np.full((n_active, 2), sizes)
|
|
177
|
+
elif sizes.ndim == 1:
|
|
178
|
+
sizes = np.stack([sizes, sizes], axis=1)
|
|
179
|
+
if sizes.shape != (n_active, 2):
|
|
180
|
+
raise ValueError(
|
|
181
|
+
f"size array shape {sizes.shape} does not match "
|
|
182
|
+
f"expected ({n_active}, 2) for {n_active} active corners"
|
|
183
|
+
)
|
|
184
|
+
# Clamp sizes proportionally so adjacent corners don't overlap on a shared edge.
|
|
185
|
+
# For each active corner, find the adjacent corner on its outgoing/incoming edge.
|
|
186
|
+
# If both are active: scale both contributions so they sum to at most edge_length.
|
|
187
|
+
# If only one is active: it can use the full edge length.
|
|
188
|
+
active_index_of = np.full(len(shape.corner_to_vertex), -1, dtype=np.int64)
|
|
189
|
+
active_index_of[active_indices] = np.arange(n_active, dtype=np.int64)
|
|
190
|
+
de_to_corner = shape.directed_edge_to_corner
|
|
191
|
+
sizes_orig = sizes.copy()
|
|
192
|
+
|
|
193
|
+
adj_target_out = de_to_corner[outgoing_de, 1]
|
|
194
|
+
adj_idx_out = active_index_of[adj_target_out]
|
|
195
|
+
adj_size_out = np.where(adj_idx_out >= 0, sizes_orig[adj_idx_out.clip(0), 0], 0.0)
|
|
196
|
+
total_out = sizes_orig[:, 1] + adj_size_out
|
|
197
|
+
scale_out = np.where(
|
|
198
|
+
total_out > edge_lengths_outgoing, edge_lengths_outgoing / total_out, 1.0
|
|
199
|
+
)
|
|
200
|
+
sizes[:, 1] *= scale_out
|
|
201
|
+
|
|
202
|
+
adj_source_in = de_to_corner[incoming_de, 0]
|
|
203
|
+
adj_idx_in = active_index_of[adj_source_in]
|
|
204
|
+
adj_size_in = np.where(adj_idx_in >= 0, sizes_orig[adj_idx_in.clip(0), 1], 0.0)
|
|
205
|
+
total_in = sizes_orig[:, 0] + adj_size_in
|
|
206
|
+
scale_in = np.where(
|
|
207
|
+
total_in > edge_lengths_incoming, edge_lengths_incoming / total_in, 1.0
|
|
208
|
+
)
|
|
209
|
+
sizes[:, 0] *= scale_in
|
|
210
|
+
|
|
211
|
+
signs = np.where(active_is_corner_convex, 1.0, -1.0)
|
|
212
|
+
|
|
213
|
+
# Tangent points: slightly beyond size to avoid coincident inner points
|
|
214
|
+
tangent_points_incoming = current_vertices - incoming_directions_normalized * (
|
|
215
|
+
sizes[:, 0:1] + epsilon
|
|
216
|
+
)
|
|
217
|
+
tangent_points_outgoing = current_vertices + outgoing_directions_normalized * (
|
|
218
|
+
sizes[:, 1:2] + epsilon
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Outer offsets: perpendicular to edge to avoid coincident outer points
|
|
222
|
+
tangent_points_incoming_outer = (
|
|
223
|
+
tangent_points_incoming
|
|
224
|
+
+ signs[:, np.newaxis] * outward_normals_incoming * epsilon
|
|
225
|
+
)
|
|
226
|
+
tangent_points_outgoing_outer = (
|
|
227
|
+
tangent_points_outgoing
|
|
228
|
+
+ signs[:, np.newaxis] * outward_normals_outgoing * epsilon
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Extended corner vertex: push outward along the bisector (corner_normals already signed)
|
|
232
|
+
current_vertices_extended = current_vertices + active_corner_normals * epsilon
|
|
233
|
+
|
|
234
|
+
cutters: list[Polygon] = []
|
|
235
|
+
fillers: list[Polygon] = []
|
|
236
|
+
for i in range(n_active):
|
|
237
|
+
polygon = Polygon(
|
|
238
|
+
[
|
|
239
|
+
current_vertices_extended[i],
|
|
240
|
+
tangent_points_incoming_outer[i],
|
|
241
|
+
tangent_points_incoming[i],
|
|
242
|
+
tangent_points_outgoing[i],
|
|
243
|
+
tangent_points_outgoing_outer[i],
|
|
244
|
+
]
|
|
245
|
+
)
|
|
246
|
+
if polygon.is_empty or not polygon.is_valid:
|
|
247
|
+
continue
|
|
248
|
+
if active_is_corner_convex[i]:
|
|
249
|
+
cutters.append(polygon)
|
|
250
|
+
else:
|
|
251
|
+
fillers.append(polygon)
|
|
252
|
+
|
|
253
|
+
result = shape
|
|
254
|
+
if cutters:
|
|
255
|
+
result = result - Shape.from_geometries(cutters)
|
|
256
|
+
if fillers:
|
|
257
|
+
result = result | Shape.from_geometries(fillers)
|
|
258
|
+
|
|
259
|
+
return result
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from typeguard import typechecked
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from scadpy import Color, Shape
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@typechecked
|
|
12
|
+
def color_shape(shape: Shape, color: Color) -> Shape:
|
|
13
|
+
"""Set the color of all parts in a shape.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
shape : Shape
|
|
18
|
+
The shape whose parts will be recolored.
|
|
19
|
+
color : Color
|
|
20
|
+
The RGBA color to apply to all parts.
|
|
21
|
+
|
|
22
|
+
Returns
|
|
23
|
+
-------
|
|
24
|
+
Shape
|
|
25
|
+
A new shape with all parts set to the given color.
|
|
26
|
+
|
|
27
|
+
Examples
|
|
28
|
+
--------
|
|
29
|
+
>>> from scadpy import square, color_shape
|
|
30
|
+
>>> from scadpy.color.constants import RED
|
|
31
|
+
|
|
32
|
+
>>> color_shape(shape=square(4), color=RED) # doctest: +SKIP
|
|
33
|
+
|
|
34
|
+
.. render-example::
|
|
35
|
+
:name: color_shape
|
|
36
|
+
:example: color_shape(shape=square(4), color=RED)
|
|
37
|
+
:keep-color:
|
|
38
|
+
"""
|
|
39
|
+
from scadpy import Shape, color_assembly
|
|
40
|
+
|
|
41
|
+
return color_assembly(
|
|
42
|
+
assembly=shape,
|
|
43
|
+
color=color,
|
|
44
|
+
get_assembly_parts=lambda assembly: assembly._parts,
|
|
45
|
+
concat_parts=Shape.from_parts,
|
|
46
|
+
)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, cast
|
|
4
|
+
|
|
5
|
+
from shapely.geometry import MultiPolygon, Polygon
|
|
6
|
+
from typeguard import typechecked
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from scadpy import Shape, TopologyFilter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@typechecked
|
|
13
|
+
def convexify_shape(
|
|
14
|
+
shape: Shape, part_filter: TopologyFilter[Shape] | None = None
|
|
15
|
+
) -> Shape:
|
|
16
|
+
"""
|
|
17
|
+
Create a new shape whose single part is the convex hull of all parts in the input shape.
|
|
18
|
+
|
|
19
|
+
This function computes the convex hull that encloses all parts of the input shape,
|
|
20
|
+
resulting in a single convex polygon. The color of the resulting part is determined
|
|
21
|
+
by blending the colors of the original parts, weighted by their area.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
shape : Shape
|
|
26
|
+
The input shape whose parts will be merged and convexified.
|
|
27
|
+
part_filter : TopologyFilter[Shape] | None, optional
|
|
28
|
+
A boolean mask selecting which parts to convexify. Parts not selected are left
|
|
29
|
+
unchanged. If None, all parts are convexified.
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
Shape
|
|
34
|
+
A new shape consisting of the convex hull of the selected parts, plus the
|
|
35
|
+
unselected parts unchanged.
|
|
36
|
+
|
|
37
|
+
Examples
|
|
38
|
+
--------
|
|
39
|
+
>>> from scadpy import convexify_shape, square
|
|
40
|
+
>>> import numpy as np
|
|
41
|
+
|
|
42
|
+
>>> a = square(5)
|
|
43
|
+
>>> b = square(2).translate(10)
|
|
44
|
+
>>> c = square(3).translate([4, 8])
|
|
45
|
+
>>> convexify_shape(a + b + c) # doctest: +SKIP
|
|
46
|
+
|
|
47
|
+
.. render-example::
|
|
48
|
+
:name: convexify_shape
|
|
49
|
+
:example: convexify_shape(a + b + c)
|
|
50
|
+
:ghost: a + b + c
|
|
51
|
+
|
|
52
|
+
>>> # partial convexification
|
|
53
|
+
>>> convexify_shape(
|
|
54
|
+
... a + b + c,
|
|
55
|
+
... part_filter=np.array([True, True, False])
|
|
56
|
+
... ) # doctest: +SKIP
|
|
57
|
+
|
|
58
|
+
.. render-example::
|
|
59
|
+
:name: convexify_shape_partial
|
|
60
|
+
:example: convexify_shape(a + b + c, part_filter=np.array([True, True, False]))
|
|
61
|
+
:ghost: a + b + c
|
|
62
|
+
"""
|
|
63
|
+
from scadpy import Part, Shape, blend_part_colors, transform_filtered_parts
|
|
64
|
+
|
|
65
|
+
return transform_filtered_parts(
|
|
66
|
+
assembly=shape,
|
|
67
|
+
parts=shape._parts,
|
|
68
|
+
part_filter=part_filter,
|
|
69
|
+
transform=lambda parts: [
|
|
70
|
+
Part[Polygon].from_geometry(
|
|
71
|
+
cast(Polygon, MultiPolygon([p.geometry for p in parts]).convex_hull),
|
|
72
|
+
blend_part_colors(
|
|
73
|
+
parts=parts,
|
|
74
|
+
get_part_magnitude=lambda p: p.geometry.area,
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
],
|
|
78
|
+
concat_parts=Shape.from_parts,
|
|
79
|
+
)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from shapely.geometry import Polygon
|
|
6
|
+
from typeguard import typechecked
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from scadpy import Shape, TopologyFilter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@typechecked
|
|
13
|
+
def fill_shape(shape: Shape, part_filter: TopologyFilter[Shape] | None = None) -> Shape:
|
|
14
|
+
"""
|
|
15
|
+
Fill the interior holes of each selected part, keeping only the exterior ring.
|
|
16
|
+
|
|
17
|
+
Each selected part's interior rings (holes) are removed, producing a solid
|
|
18
|
+
polygon from its exterior boundary. Unselected parts are left unchanged.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
shape : Shape
|
|
23
|
+
The input shape whose parts will be filled.
|
|
24
|
+
part_filter : TopologyFilter[Shape] | None, optional
|
|
25
|
+
A boolean mask selecting which parts to fill. If None, all parts are filled.
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
Shape
|
|
30
|
+
A new shape with the selected parts filled and the unselected parts unchanged.
|
|
31
|
+
|
|
32
|
+
Examples
|
|
33
|
+
--------
|
|
34
|
+
>>> from scadpy import fill_shape, square, circle
|
|
35
|
+
>>> import numpy as np
|
|
36
|
+
|
|
37
|
+
>>> shape = square(10) - circle(3)
|
|
38
|
+
>>> fill_shape(shape) # doctest: +SKIP
|
|
39
|
+
|
|
40
|
+
.. render-example::
|
|
41
|
+
:name: fill_shape
|
|
42
|
+
:example: fill_shape(shape)
|
|
43
|
+
:ghost: shape
|
|
44
|
+
|
|
45
|
+
>>> # partial fill
|
|
46
|
+
>>> a = square(5) - circle(1)
|
|
47
|
+
>>> b = (square(3) - circle(0.5)).translate(10)
|
|
48
|
+
>>> fill_shape( # doctest: +SKIP
|
|
49
|
+
... a + b, part_filter=np.array([True, False])
|
|
50
|
+
... )
|
|
51
|
+
|
|
52
|
+
.. render-example::
|
|
53
|
+
:name: fill_shape_partial
|
|
54
|
+
:example: fill_shape(a + b, part_filter=np.array([True, False]))
|
|
55
|
+
:ghost: a + b
|
|
56
|
+
"""
|
|
57
|
+
from scadpy import Part, Shape, transform_filtered_parts
|
|
58
|
+
|
|
59
|
+
return transform_filtered_parts(
|
|
60
|
+
assembly=shape,
|
|
61
|
+
parts=shape._parts,
|
|
62
|
+
part_filter=part_filter,
|
|
63
|
+
transform=lambda parts: [
|
|
64
|
+
Part[Polygon].from_geometry(Polygon(p.geometry.exterior), p.color)
|
|
65
|
+
for p in parts
|
|
66
|
+
],
|
|
67
|
+
concat_parts=Shape.from_parts,
|
|
68
|
+
)
|