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,143 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numpy.typing import NDArray
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from scadpy.color import Color
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# @typechecked
|
|
14
|
+
def unify_parts[A, P, G](
|
|
15
|
+
parts: Sequence[P],
|
|
16
|
+
get_part_color: Callable[[P], Color],
|
|
17
|
+
get_part_magnitude: Callable[[P], float],
|
|
18
|
+
get_part_bounds: Callable[[P], NDArray[np.float64]],
|
|
19
|
+
are_parts_intersecting: Callable[[P, P], bool],
|
|
20
|
+
get_part_geometry: Callable[[P], G],
|
|
21
|
+
unify_geometries: Callable[[list[G]], Iterable[G]],
|
|
22
|
+
make_part_from_geometry: Callable[[G, Color], P],
|
|
23
|
+
make_assembly_from_parts: Callable[[Sequence[P]], A],
|
|
24
|
+
) -> A:
|
|
25
|
+
"""
|
|
26
|
+
Unite (union) groups of intersecting parts, blend their colors (weighted by magnitude), and return a new assembly.
|
|
27
|
+
|
|
28
|
+
This function is fully generic and uses dependency injection for all domain-specific
|
|
29
|
+
operations, making it suitable for a wide range of applications (2D, 3D, CAD, etc.).
|
|
30
|
+
Colors are blended using a weighted average, where each part's color is weighted by its magnitude.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
parts : Sequence[Part]
|
|
35
|
+
Sequence of part objects to process.
|
|
36
|
+
get_part_color : Callable[[P], Color]
|
|
37
|
+
Function to extract the color from a part (as a list or tuple of 4 floats: RGBA).
|
|
38
|
+
get_part_magnitude : Callable[[P], float]
|
|
39
|
+
Function to extract a magnitude (e.g., area, volume) from a part for color blending.
|
|
40
|
+
get_part_bounds : Callable[[P], NDArray[np.float64]]
|
|
41
|
+
Function to extract the bounding box of a part.
|
|
42
|
+
are_parts_intersecting : Callable[[P, P], bool]
|
|
43
|
+
Function to determine if two parts intersect.
|
|
44
|
+
get_part_geometry : Callable[[P], G]
|
|
45
|
+
Function to extract the geometry from a part.
|
|
46
|
+
unify_geometries : Callable[[list[G]], Iterable[G]]
|
|
47
|
+
Function to compute the union of a list of geometries.
|
|
48
|
+
make_part_from_geometry : Callable[[G, Color], P]
|
|
49
|
+
Function to create a part from a geometry and a color.
|
|
50
|
+
make_assembly_from_parts : Callable[[Sequence[P]], A]
|
|
51
|
+
Function to create an assembly from a sequence of parts.
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
A
|
|
56
|
+
The assembly object containing all unified parts with blended colors.
|
|
57
|
+
|
|
58
|
+
Examples
|
|
59
|
+
--------
|
|
60
|
+
>>> from scadpy import unify_parts
|
|
61
|
+
|
|
62
|
+
>>> parts = [
|
|
63
|
+
... {
|
|
64
|
+
... 'bounds': [0, 0, 2, 2],
|
|
65
|
+
... 'color': [1, 0, 0, 1],
|
|
66
|
+
... 'magnitude': 1,
|
|
67
|
+
... },
|
|
68
|
+
... {
|
|
69
|
+
... 'bounds': [1, 1, 3, 3],
|
|
70
|
+
... 'color': [0, 1, 0, 1],
|
|
71
|
+
... 'magnitude': 2,
|
|
72
|
+
... },
|
|
73
|
+
... {
|
|
74
|
+
... 'bounds': [5, 5, 6, 6],
|
|
75
|
+
... 'color': [0, 0, 1, 1],
|
|
76
|
+
... 'magnitude': 1,
|
|
77
|
+
... },
|
|
78
|
+
... ]
|
|
79
|
+
...
|
|
80
|
+
>>> def are_intersecting(p1, p2):
|
|
81
|
+
... b1, b2 = p1['bounds'], p2['bounds']
|
|
82
|
+
... return not (b1[2] <= b2[0] or b2[2] <= b1[0] or
|
|
83
|
+
... b1[3] <= b2[1] or b2[3] <= b1[1])
|
|
84
|
+
...
|
|
85
|
+
>>> def unify_geometries(geometries):
|
|
86
|
+
... minx = min(g[0] for g in geometries)
|
|
87
|
+
... miny = min(g[1] for g in geometries)
|
|
88
|
+
... maxx = max(g[2] for g in geometries)
|
|
89
|
+
... maxy = max(g[3] for g in geometries)
|
|
90
|
+
... return [[minx, miny, maxx, maxy]]
|
|
91
|
+
...
|
|
92
|
+
>>> result = unify_parts(
|
|
93
|
+
... parts,
|
|
94
|
+
... get_part_color=lambda p: p['color'],
|
|
95
|
+
... get_part_magnitude=lambda p: p['magnitude'],
|
|
96
|
+
... get_part_bounds=lambda p: p['bounds'],
|
|
97
|
+
... are_parts_intersecting=are_intersecting,
|
|
98
|
+
... get_part_geometry=lambda p: p['bounds'],
|
|
99
|
+
... unify_geometries=unify_geometries,
|
|
100
|
+
... make_part_from_geometry=lambda geometry, color: {
|
|
101
|
+
... 'bounds': geometry,
|
|
102
|
+
... 'color': [round(float(v), 2) for v in color]
|
|
103
|
+
... },
|
|
104
|
+
... make_assembly_from_parts=lambda parts: parts
|
|
105
|
+
... )
|
|
106
|
+
...
|
|
107
|
+
>>> result == [
|
|
108
|
+
... {'bounds': [0, 0, 3, 3], 'color': [0.33, 0.67, 0.0, 1.0]},
|
|
109
|
+
... {'bounds': [5, 5, 6, 6], 'color': [0.0, 0.0, 1.0, 1.0]}
|
|
110
|
+
... ]
|
|
111
|
+
True
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
from scadpy.core.component.utils import (
|
|
115
|
+
blend_component_colors,
|
|
116
|
+
get_intersecting_component_index_groups,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
intersecting_part_index_groups: list[list[int]] = (
|
|
120
|
+
get_intersecting_component_index_groups(
|
|
121
|
+
parts,
|
|
122
|
+
get_component_bounds=get_part_bounds,
|
|
123
|
+
are_components_intersecting=are_parts_intersecting,
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
intersecting_part_groups: list[list[P]] = [
|
|
127
|
+
[parts[i] for i in group] for group in intersecting_part_index_groups
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
unified_parts: list[P] = []
|
|
131
|
+
for parts in intersecting_part_groups:
|
|
132
|
+
blended_color = blend_component_colors(
|
|
133
|
+
components=parts,
|
|
134
|
+
get_component_color=get_part_color,
|
|
135
|
+
get_component_magnitude=get_part_magnitude,
|
|
136
|
+
)
|
|
137
|
+
geometries = [get_part_geometry(p) for p in parts]
|
|
138
|
+
unified_geometries = unify_geometries(geometries)
|
|
139
|
+
unified_parts += [
|
|
140
|
+
make_part_from_geometry(u, blended_color) for u in unified_geometries
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
return make_assembly_from_parts(unified_parts)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TYPE_CHECKING, Self
|
|
6
|
+
|
|
7
|
+
from scadpy.color import DEFAULT_COLOR
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from scadpy.color import Color
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class Part[G]:
|
|
15
|
+
_geometry: G
|
|
16
|
+
_color: Color
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def color(self) -> Color:
|
|
20
|
+
return self._color
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def from_geometry_ref(
|
|
24
|
+
cls: type[Self], geometry: G, color: Color = DEFAULT_COLOR
|
|
25
|
+
) -> Self:
|
|
26
|
+
return cls(_geometry=geometry, _color=color)
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def from_geometry(cls: type[Self], geometry: G, color: Color = DEFAULT_COLOR) -> Self:
|
|
30
|
+
return cls(_geometry=deepcopy(geometry), _color=color)
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def geometry(self) -> G:
|
|
34
|
+
return self._geometry
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Sequence
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from scadpy import Color, Part
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# @typechecked
|
|
13
|
+
def blend_part_colors[G](
|
|
14
|
+
parts: Sequence[Part[G]],
|
|
15
|
+
get_part_magnitude: Callable[[Part[G]], float],
|
|
16
|
+
) -> Color:
|
|
17
|
+
from scadpy.color import DEFAULT_COLOR
|
|
18
|
+
|
|
19
|
+
total_magnitude = 0.0
|
|
20
|
+
weighted_color = np.zeros(4, dtype=np.float64)
|
|
21
|
+
|
|
22
|
+
for part in parts:
|
|
23
|
+
color = np.array(part.color)
|
|
24
|
+
magnitude = get_part_magnitude(part)
|
|
25
|
+
weighted_color += color * magnitude
|
|
26
|
+
total_magnitude += magnitude
|
|
27
|
+
|
|
28
|
+
if total_magnitude == 0:
|
|
29
|
+
return DEFAULT_COLOR
|
|
30
|
+
|
|
31
|
+
blended = weighted_color / total_magnitude
|
|
32
|
+
return [float(x) for x in blended] # pyright: ignore[reportAny]
|
scadpy/d2/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from .combinations import * # noqa: F403
|
|
2
|
+
from .exporters import * # noqa: F403
|
|
3
|
+
from .features import * # noqa: F403
|
|
4
|
+
from .importers import * # noqa: F403
|
|
5
|
+
from .primitives import * # noqa: F403
|
|
6
|
+
from .topologies import * # noqa: F403
|
|
7
|
+
from .transformations import * # noqa: F403
|
|
8
|
+
from .types import * # noqa: F403
|
|
9
|
+
from .utils import * # noqa: F403
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"are_shape_parts_intersecting",
|
|
3
|
+
"concat_shape",
|
|
4
|
+
"exclude_shape",
|
|
5
|
+
"intersect_shape",
|
|
6
|
+
"intersect_shape_parts",
|
|
7
|
+
"subtract_shape",
|
|
8
|
+
"subtract_shape_parts",
|
|
9
|
+
"unify_shape",
|
|
10
|
+
"unify_shape_parts",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
from .are_shape_parts_intersecting import are_shape_parts_intersecting
|
|
14
|
+
from .concat_shape import concat_shape
|
|
15
|
+
from .exclude_shape import exclude_shape
|
|
16
|
+
from .intersect_shape import intersect_shape
|
|
17
|
+
from .intersect_shape_parts import intersect_shape_parts
|
|
18
|
+
from .subtract_shape import subtract_shape
|
|
19
|
+
from .subtract_shape_parts import subtract_shape_parts
|
|
20
|
+
from .unify_shape import unify_shape
|
|
21
|
+
from .unify_shape_parts import unify_shape_parts
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from shapely.geometry.polygon import Polygon
|
|
6
|
+
from typeguard import typechecked
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from scadpy.core.part import Part
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@typechecked
|
|
13
|
+
def are_shape_parts_intersecting(
|
|
14
|
+
part1: Part[Polygon],
|
|
15
|
+
part2: Part[Polygon],
|
|
16
|
+
) -> bool:
|
|
17
|
+
"""Return whether two shape parts intersect geometrically.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
part1 : Part[Polygon]
|
|
22
|
+
The first shape part.
|
|
23
|
+
part2 : Part[Polygon]
|
|
24
|
+
The second shape part.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
bool
|
|
29
|
+
True if the two parts intersect, False otherwise.
|
|
30
|
+
|
|
31
|
+
Examples
|
|
32
|
+
--------
|
|
33
|
+
>>> from scadpy import (
|
|
34
|
+
... square, circle, are_shape_parts_intersecting
|
|
35
|
+
... )
|
|
36
|
+
>>> are_shape_parts_intersecting(
|
|
37
|
+
... part1=square(2)._parts[0],
|
|
38
|
+
... part2=square(2)._parts[0],
|
|
39
|
+
... )
|
|
40
|
+
True
|
|
41
|
+
>>> are_shape_parts_intersecting(
|
|
42
|
+
... part1=square(1)._parts[0],
|
|
43
|
+
... part2=square(1).rotate(
|
|
44
|
+
... angle=0
|
|
45
|
+
... ).grow(distance=5)._parts[0],
|
|
46
|
+
... )
|
|
47
|
+
True
|
|
48
|
+
"""
|
|
49
|
+
return part1.geometry.intersects(part2.geometry)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from typeguard import typechecked
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from scadpy import Shape
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@typechecked
|
|
13
|
+
def concat_shape(shapes: Sequence[Shape]) -> Shape:
|
|
14
|
+
"""Concatenate a sequence of shapes into a single shape without any boolean operation.
|
|
15
|
+
|
|
16
|
+
All parts from all input shapes are merged into a single shape. Parts that
|
|
17
|
+
overlap are not merged geometrically — use :func:`unify_shape` for that.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
shapes : Sequence[Shape]
|
|
22
|
+
The shapes to concatenate.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
Shape
|
|
27
|
+
A new shape containing all parts from all input shapes.
|
|
28
|
+
|
|
29
|
+
Examples
|
|
30
|
+
--------
|
|
31
|
+
>>> from scadpy import square, circle, concat_shape
|
|
32
|
+
|
|
33
|
+
>>> concat_shape( # doctest: +SKIP
|
|
34
|
+
... shapes=[square(4), circle(radius=2).translate([3, 2])]
|
|
35
|
+
... )
|
|
36
|
+
|
|
37
|
+
.. render-example::
|
|
38
|
+
:name: concat_shape
|
|
39
|
+
:example: concat_shape(shapes=[square(4), circle(radius=2).translate([3, 2])])
|
|
40
|
+
"""
|
|
41
|
+
from scadpy import Shape
|
|
42
|
+
from scadpy.core.assembly import concat_assemblies
|
|
43
|
+
|
|
44
|
+
return concat_assemblies(
|
|
45
|
+
assemblies=shapes,
|
|
46
|
+
get_assembly_parts=lambda a: a._parts,
|
|
47
|
+
concat_parts=Shape.from_parts,
|
|
48
|
+
)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from typeguard import typechecked
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from scadpy import Shape
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@typechecked
|
|
13
|
+
def exclude_shape(shapes: Sequence[Shape]) -> Shape:
|
|
14
|
+
"""Compute the symmetric difference (XOR) of a sequence of shapes.
|
|
15
|
+
|
|
16
|
+
Keeps only the regions that belong to exactly one of the input shapes.
|
|
17
|
+
Regions shared by two or more shapes are removed.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
shapes : Sequence[Shape]
|
|
22
|
+
The shapes to compute the symmetric difference of.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
Shape
|
|
27
|
+
A new shape containing only the non-overlapping regions of the input shapes.
|
|
28
|
+
|
|
29
|
+
Examples
|
|
30
|
+
--------
|
|
31
|
+
>>> from scadpy import square, circle, exclude_shape
|
|
32
|
+
|
|
33
|
+
>>> exclude_shape( # doctest: +SKIP
|
|
34
|
+
... shapes=[square(4), circle(radius=2).translate([1, 1])]
|
|
35
|
+
... )
|
|
36
|
+
|
|
37
|
+
.. render-example::
|
|
38
|
+
:name: exclude_shape
|
|
39
|
+
:example: exclude_shape(shapes=[square(4), circle(radius=2).translate([1, 1])])
|
|
40
|
+
:ghost: square(4)
|
|
41
|
+
"""
|
|
42
|
+
from scadpy import (
|
|
43
|
+
Shape,
|
|
44
|
+
are_shape_parts_intersecting,
|
|
45
|
+
get_shape_part_bounds,
|
|
46
|
+
intersect_shape_parts,
|
|
47
|
+
subtract_shape_parts,
|
|
48
|
+
unify_shape_parts,
|
|
49
|
+
)
|
|
50
|
+
from scadpy.core.assembly import exclude_assemblies
|
|
51
|
+
|
|
52
|
+
return exclude_assemblies(
|
|
53
|
+
assemblies=shapes,
|
|
54
|
+
get_assembly_parts=lambda assembly: assembly._parts,
|
|
55
|
+
get_part_bounds=get_shape_part_bounds,
|
|
56
|
+
are_parts_intersecting=are_shape_parts_intersecting,
|
|
57
|
+
subtract_parts=lambda part_base, part_cutter: subtract_shape_parts(
|
|
58
|
+
to_be_subtracted=part_base,
|
|
59
|
+
to_subtract=part_cutter,
|
|
60
|
+
make_assembly_from_parts=Shape.from_parts,
|
|
61
|
+
),
|
|
62
|
+
intersect_parts=lambda parts: intersect_shape_parts(
|
|
63
|
+
parts=parts,
|
|
64
|
+
make_assembly_from_parts=Shape.from_parts,
|
|
65
|
+
),
|
|
66
|
+
unify_parts=lambda parts: unify_shape_parts(
|
|
67
|
+
parts=parts,
|
|
68
|
+
make_assembly_from_parts=Shape.from_parts,
|
|
69
|
+
),
|
|
70
|
+
concat_parts=Shape.from_parts,
|
|
71
|
+
)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from typeguard import typechecked
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from scadpy import Shape
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@typechecked
|
|
13
|
+
def intersect_shape(shapes: Sequence[Shape]) -> Shape:
|
|
14
|
+
"""Compute the intersection of a sequence of shapes.
|
|
15
|
+
|
|
16
|
+
Only the regions shared by all input shapes are kept.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
shapes : Sequence[Shape]
|
|
21
|
+
The shapes to intersect.
|
|
22
|
+
|
|
23
|
+
Returns
|
|
24
|
+
-------
|
|
25
|
+
Shape
|
|
26
|
+
A new shape containing only the regions present in all input shapes.
|
|
27
|
+
|
|
28
|
+
Examples
|
|
29
|
+
--------
|
|
30
|
+
>>> from scadpy import square, circle, intersect_shape
|
|
31
|
+
|
|
32
|
+
>>> intersect_shape( # doctest: +SKIP
|
|
33
|
+
... shapes=[square(4), circle(radius=2).translate([1, 1])]
|
|
34
|
+
... )
|
|
35
|
+
|
|
36
|
+
.. render-example::
|
|
37
|
+
:name: intersect_shape
|
|
38
|
+
:example: intersect_shape(shapes=[square(4), circle(radius=2).translate([1, 1])])
|
|
39
|
+
:ghost: square(4)
|
|
40
|
+
"""
|
|
41
|
+
from scadpy import (
|
|
42
|
+
Shape,
|
|
43
|
+
are_shape_parts_intersecting,
|
|
44
|
+
get_shape_part_bounds,
|
|
45
|
+
intersect_shape_parts,
|
|
46
|
+
unify_shape_parts,
|
|
47
|
+
)
|
|
48
|
+
from scadpy.core.assembly import intersect_assemblies
|
|
49
|
+
|
|
50
|
+
return intersect_assemblies(
|
|
51
|
+
assemblies=shapes,
|
|
52
|
+
get_assembly_parts=lambda assembly: assembly._parts,
|
|
53
|
+
get_part_bounds=get_shape_part_bounds,
|
|
54
|
+
are_parts_intersecting=are_shape_parts_intersecting,
|
|
55
|
+
intersect_parts=lambda parts: intersect_shape_parts(
|
|
56
|
+
parts=parts,
|
|
57
|
+
make_assembly_from_parts=Shape.from_parts,
|
|
58
|
+
),
|
|
59
|
+
unify_parts=lambda parts: unify_shape_parts(
|
|
60
|
+
parts=parts,
|
|
61
|
+
make_assembly_from_parts=Shape.from_parts,
|
|
62
|
+
),
|
|
63
|
+
concat_parts=Shape.from_parts,
|
|
64
|
+
)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Sequence
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from shapely import intersection_all # pyright: ignore[reportUnknownVariableType]
|
|
7
|
+
from shapely.geometry.polygon import Polygon
|
|
8
|
+
from typeguard import typechecked
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from scadpy.core.part import Part
|
|
12
|
+
from scadpy.d2.shape import Shape
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@typechecked
|
|
16
|
+
def intersect_shape_parts(
|
|
17
|
+
parts: Sequence[Part[Polygon]],
|
|
18
|
+
make_assembly_from_parts: Callable[[Sequence[Part[Polygon]]], Shape],
|
|
19
|
+
) -> Shape:
|
|
20
|
+
"""Intersect a sequence of shape parts and return the resulting shape.
|
|
21
|
+
|
|
22
|
+
Shortcut for :func:`intersect_parts`.
|
|
23
|
+
See :func:`intersect_parts` for full documentation.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
parts : Sequence[Part[Polygon]]
|
|
28
|
+
The shape parts to intersect.
|
|
29
|
+
make_assembly_from_parts : Callable[[Sequence[Part[Polygon]]], Shape]
|
|
30
|
+
Factory function to build the resulting Shape from a sequence of parts.
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
Shape
|
|
35
|
+
A new shape containing the geometric intersection of the input parts.
|
|
36
|
+
|
|
37
|
+
Examples
|
|
38
|
+
--------
|
|
39
|
+
>>> from scadpy import (
|
|
40
|
+
... square, circle, intersect_shape_parts, Shape
|
|
41
|
+
... )
|
|
42
|
+
|
|
43
|
+
>>> intersect_shape_parts( # doctest: +SKIP
|
|
44
|
+
... parts=(
|
|
45
|
+
... list(square(3)._parts)
|
|
46
|
+
... + list(circle(radius=1.5).translate([2, 2])._parts)
|
|
47
|
+
... ),
|
|
48
|
+
... make_assembly_from_parts=Shape.from_parts,
|
|
49
|
+
... )
|
|
50
|
+
|
|
51
|
+
.. render-example::
|
|
52
|
+
:name: intersect_shape_parts_example
|
|
53
|
+
:example: intersect_shape_parts(parts=list(square(3)._parts) + list(circle(radius=1.5).translate([2, 2])._parts), make_assembly_from_parts=Shape.from_parts)
|
|
54
|
+
:ghost: concat_shape(shapes=[square(3), circle(radius=1.5).translate([2, 2])])
|
|
55
|
+
"""
|
|
56
|
+
from scadpy import Part, are_shape_parts_intersecting, get_shape_part_bounds
|
|
57
|
+
from scadpy import intersect_parts, shapely_base_geometry_to_shapely_polygons
|
|
58
|
+
|
|
59
|
+
return intersect_parts(
|
|
60
|
+
parts=parts,
|
|
61
|
+
get_part_color=lambda p: p.color,
|
|
62
|
+
get_part_magnitude=lambda p: p.geometry.area,
|
|
63
|
+
get_part_bounds=get_shape_part_bounds,
|
|
64
|
+
are_parts_intersecting=are_shape_parts_intersecting,
|
|
65
|
+
get_part_geometry=lambda p: p.geometry,
|
|
66
|
+
intersect_geometries=lambda g: shapely_base_geometry_to_shapely_polygons(
|
|
67
|
+
intersection_all(g)
|
|
68
|
+
),
|
|
69
|
+
make_part_from_geometry=Part[Polygon].from_geometry,
|
|
70
|
+
make_assembly_from_parts=make_assembly_from_parts,
|
|
71
|
+
)
|
|
@@ -0,0 +1,72 @@
|
|
|
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 Shape
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@typechecked
|
|
12
|
+
def subtract_shape(to_be_subtracted: Shape, to_subtract: Shape) -> Shape:
|
|
13
|
+
"""Subtract one shape from another using boolean difference.
|
|
14
|
+
|
|
15
|
+
The geometry of ``to_subtract`` is removed from ``to_be_subtracted``.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
to_be_subtracted : Shape
|
|
20
|
+
The shape to subtract from.
|
|
21
|
+
to_subtract : Shape
|
|
22
|
+
The shape to subtract.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
Shape
|
|
27
|
+
A new shape with the geometry of ``to_subtract`` removed from ``to_be_subtracted``.
|
|
28
|
+
|
|
29
|
+
Examples
|
|
30
|
+
--------
|
|
31
|
+
>>> from scadpy import square, circle, subtract_shape
|
|
32
|
+
|
|
33
|
+
>>> subtract_shape( # doctest: +SKIP
|
|
34
|
+
... to_be_subtracted=square(4), to_subtract=circle(radius=1)
|
|
35
|
+
... )
|
|
36
|
+
|
|
37
|
+
.. render-example::
|
|
38
|
+
:name: subtract_shape
|
|
39
|
+
:example: subtract_shape(to_be_subtracted=square(4), to_subtract=circle(radius=1))
|
|
40
|
+
:ghost: square(4)
|
|
41
|
+
"""
|
|
42
|
+
from scadpy import (
|
|
43
|
+
Shape,
|
|
44
|
+
are_shape_parts_intersecting,
|
|
45
|
+
get_shape_part_bounds,
|
|
46
|
+
intersect_shape_parts,
|
|
47
|
+
subtract_shape_parts,
|
|
48
|
+
unify_shape_parts,
|
|
49
|
+
)
|
|
50
|
+
from scadpy.core.assembly import subtract_assemblies
|
|
51
|
+
|
|
52
|
+
return subtract_assemblies(
|
|
53
|
+
to_be_subtracted=to_be_subtracted,
|
|
54
|
+
to_subtract=to_subtract,
|
|
55
|
+
get_assembly_parts=lambda assembly: assembly._parts,
|
|
56
|
+
get_part_bounds=get_shape_part_bounds,
|
|
57
|
+
are_parts_intersecting=are_shape_parts_intersecting,
|
|
58
|
+
subtract_parts=lambda part_base, part_cutter: subtract_shape_parts(
|
|
59
|
+
to_be_subtracted=part_base,
|
|
60
|
+
to_subtract=part_cutter,
|
|
61
|
+
make_assembly_from_parts=Shape.from_parts,
|
|
62
|
+
),
|
|
63
|
+
intersect_parts=lambda parts: intersect_shape_parts(
|
|
64
|
+
parts=parts,
|
|
65
|
+
make_assembly_from_parts=Shape.from_parts,
|
|
66
|
+
),
|
|
67
|
+
unify_parts=lambda parts: unify_shape_parts(
|
|
68
|
+
parts=parts,
|
|
69
|
+
make_assembly_from_parts=Shape.from_parts,
|
|
70
|
+
),
|
|
71
|
+
concat_parts=Shape.from_parts,
|
|
72
|
+
)
|