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,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import tempfile
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
from IPython.core.display import HTML
|
|
8
|
+
from PySide6.QtCore import QUrl
|
|
9
|
+
from PySide6.QtWebEngineCore import QWebEngineSettings
|
|
10
|
+
from PySide6.QtWebEngineWidgets import QWebEngineView
|
|
11
|
+
from PySide6.QtWidgets import QApplication
|
|
12
|
+
from typeguard import typechecked
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@typechecked
|
|
16
|
+
def map_component_to_screen[Component](
|
|
17
|
+
component: Component, to_html: Callable[[Component], HTML]
|
|
18
|
+
):
|
|
19
|
+
"""
|
|
20
|
+
Render a component as HTML and display it in a Qt-based window.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
component : Component
|
|
25
|
+
The component to export and display.
|
|
26
|
+
to_html : Callable[[Component], HTML]
|
|
27
|
+
Function that converts the component to an IPython HTML object.
|
|
28
|
+
|
|
29
|
+
Examples
|
|
30
|
+
--------
|
|
31
|
+
>>> from IPython.core.display import HTML
|
|
32
|
+
>>> from scadpy import map_component_to_screen
|
|
33
|
+
|
|
34
|
+
>>> component = "Hello, World!"
|
|
35
|
+
>>> map_component_to_screen(
|
|
36
|
+
... component,
|
|
37
|
+
... to_html=lambda c: HTML(f"<h1>{c}</h1>")
|
|
38
|
+
... ) # doctest: +SKIP
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
html = str(to_html(component).data)
|
|
42
|
+
|
|
43
|
+
app = QApplication.instance()
|
|
44
|
+
if app is None:
|
|
45
|
+
app = QApplication(sys.argv)
|
|
46
|
+
|
|
47
|
+
tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8")
|
|
48
|
+
tmp.write(html)
|
|
49
|
+
tmp.close()
|
|
50
|
+
|
|
51
|
+
view = QWebEngineView()
|
|
52
|
+
view.settings().setAttribute(
|
|
53
|
+
QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True
|
|
54
|
+
)
|
|
55
|
+
view.load(QUrl.fromLocalFile(tmp.name))
|
|
56
|
+
view.setWindowTitle("ScadPy")
|
|
57
|
+
view.resize(800, 800)
|
|
58
|
+
view.show()
|
|
59
|
+
|
|
60
|
+
view.raise_()
|
|
61
|
+
view.activateWindow()
|
|
62
|
+
|
|
63
|
+
sys.exit(app.exec())
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from numpy.typing import NDArray
|
|
5
|
+
from typeguard import typechecked
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@typechecked
|
|
9
|
+
def get_component_bounds(vertex_coordinates: NDArray[np.float64]) -> NDArray[np.float64]:
|
|
10
|
+
"""
|
|
11
|
+
Compute the axis-aligned bounding box from vertex coordinates.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
vertex_coordinates : NDArray[np.float64]
|
|
16
|
+
2D array of shape ``(n_vertices, n_dimensions)`` with vertex coordinates.
|
|
17
|
+
|
|
18
|
+
Returns
|
|
19
|
+
-------
|
|
20
|
+
NDArray[np.float64]
|
|
21
|
+
1D array ``[min_x, min_y, (min_z,) max_x, max_y, (max_z,)]``.
|
|
22
|
+
Returns zeros if there are no vertices.
|
|
23
|
+
|
|
24
|
+
Examples
|
|
25
|
+
--------
|
|
26
|
+
>>> import numpy as np
|
|
27
|
+
>>> from scadpy import get_component_bounds
|
|
28
|
+
|
|
29
|
+
>>> get_component_bounds(np.array([[0., 0., 0.], [1., 2., 3.]]))
|
|
30
|
+
array([0., 0., 0., 1., 2., 3.])
|
|
31
|
+
|
|
32
|
+
>>> get_component_bounds(np.empty((0, 2)))
|
|
33
|
+
array([0., 0., 0., 0.])
|
|
34
|
+
"""
|
|
35
|
+
if len(vertex_coordinates) == 0:
|
|
36
|
+
dimensions = vertex_coordinates.shape[1]
|
|
37
|
+
return np.zeros(2 * dimensions, dtype=np.float64)
|
|
38
|
+
return np.concatenate([vertex_coordinates.min(axis=0), vertex_coordinates.max(axis=0)])
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Sequence
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from typeguard import typechecked
|
|
7
|
+
|
|
8
|
+
from scadpy.color import Color
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@typechecked
|
|
12
|
+
def blend_component_colors[C](
|
|
13
|
+
components: Sequence[C],
|
|
14
|
+
get_component_color: Callable[[C], Color],
|
|
15
|
+
get_component_magnitude: Callable[[C], float],
|
|
16
|
+
) -> Color:
|
|
17
|
+
"""
|
|
18
|
+
Compute the weighted average (blend) of component colors, using each component's magnitude as the weight.
|
|
19
|
+
|
|
20
|
+
This function is fully generic and uses dependency injection for all domain-specific
|
|
21
|
+
operations, making it suitable for a wide range of applications (2D, 3D, CAD, etc.).
|
|
22
|
+
Colors are expected to be RGBA sequences (length 4). If the total magnitude is zero,
|
|
23
|
+
a default color is returned.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
components : Sequence[C]
|
|
28
|
+
List of components whose colors will be blended.
|
|
29
|
+
get_component_color : Callable[[C], Color]
|
|
30
|
+
Function to extract the color from a component (as a list or tuple of 4 floats: RGBA).
|
|
31
|
+
get_component_magnitude : Callable[[C], float]
|
|
32
|
+
Function to extract a magnitude (e.g., area, volume) from a component for weighting.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
Color
|
|
37
|
+
The blended color as a list of 4 floats (RGBA).
|
|
38
|
+
|
|
39
|
+
Examples
|
|
40
|
+
--------
|
|
41
|
+
>>> from scadpy import blend_component_colors, DEFAULT_COLOR
|
|
42
|
+
|
|
43
|
+
>>> components = [
|
|
44
|
+
... {'color': [1, 0, 0, 1], 'magnitude': 1},
|
|
45
|
+
... {'color': [0, 1, 0, 1], 'magnitude': 2}
|
|
46
|
+
... ]
|
|
47
|
+
...
|
|
48
|
+
>>> blend_component_colors(
|
|
49
|
+
... components,
|
|
50
|
+
... get_component_color=lambda c: c['color'],
|
|
51
|
+
... get_component_magnitude=lambda c: c['magnitude']
|
|
52
|
+
... )
|
|
53
|
+
[0.3333333333333333, 0.6666666666666666, 0.0, 1.0]
|
|
54
|
+
|
|
55
|
+
>>> blend_component_colors(
|
|
56
|
+
... [],
|
|
57
|
+
... get_component_color=lambda c: c['color'],
|
|
58
|
+
... get_component_magnitude=lambda c: c['magnitude']
|
|
59
|
+
... ) == DEFAULT_COLOR
|
|
60
|
+
True
|
|
61
|
+
"""
|
|
62
|
+
from scadpy.color import DEFAULT_COLOR
|
|
63
|
+
|
|
64
|
+
total_magnitude = 0.0
|
|
65
|
+
weighted_color = np.zeros(4, dtype=np.float64)
|
|
66
|
+
|
|
67
|
+
for component in components:
|
|
68
|
+
color = np.array(get_component_color(component))
|
|
69
|
+
magnitude = get_component_magnitude(component)
|
|
70
|
+
weighted_color += color * magnitude
|
|
71
|
+
total_magnitude += magnitude
|
|
72
|
+
|
|
73
|
+
if total_magnitude == 0:
|
|
74
|
+
return DEFAULT_COLOR
|
|
75
|
+
|
|
76
|
+
blended = weighted_color / total_magnitude
|
|
77
|
+
return [float(x) for x in blended] # pyright: ignore[reportAny]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Sequence
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from numpy.typing import NDArray
|
|
7
|
+
from rtree import index as rtree_index
|
|
8
|
+
from typeguard import typechecked
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@typechecked
|
|
12
|
+
def get_intersecting_component_index_groups[C](
|
|
13
|
+
components: Sequence[C],
|
|
14
|
+
get_component_bounds: Callable[[C], NDArray[np.float64]],
|
|
15
|
+
are_components_intersecting: Callable[[C, C], bool],
|
|
16
|
+
) -> list[list[int]]:
|
|
17
|
+
"""
|
|
18
|
+
Find groups of mutually intersecting components by their indices.
|
|
19
|
+
|
|
20
|
+
This function uses an R-tree for efficient spatial indexing and a graph traversal
|
|
21
|
+
to group components that are directly or indirectly intersecting. It is fully
|
|
22
|
+
generic and uses dependency injection for all domain-specific operations.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
components : Sequence[C]
|
|
27
|
+
Sequence of components to group.
|
|
28
|
+
get_component_bounds : Callable[[C], NDArray[np.float64]]
|
|
29
|
+
Function to extract the bounding box of a component (as [minx, miny, maxx, maxy]).
|
|
30
|
+
are_components_intersecting : Callable[[C, C], bool]
|
|
31
|
+
Function to determine if two components intersect.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
list[list[int]]
|
|
36
|
+
A list of groups, each group being a list of indices into the original components
|
|
37
|
+
sequence, where all components in a group are mutually intersecting (directly or indirectly).
|
|
38
|
+
|
|
39
|
+
Examples
|
|
40
|
+
--------
|
|
41
|
+
>>> from scadpy import get_intersecting_component_index_groups
|
|
42
|
+
|
|
43
|
+
>>> components = [
|
|
44
|
+
... {'bounds': [0, 0, 2, 2]},
|
|
45
|
+
... {'bounds': [1, 1, 3, 3]},
|
|
46
|
+
... {'bounds': [5, 5, 6, 6]}
|
|
47
|
+
... ]
|
|
48
|
+
...
|
|
49
|
+
>>> def are_intersecting(c1, c2):
|
|
50
|
+
... b1, b2 = c1['bounds'], c2['bounds']
|
|
51
|
+
... return not (b1[2] <= b2[0] or b2[2] <= b1[0] or
|
|
52
|
+
... b1[3] <= b2[1] or b2[3] <= b1[1])
|
|
53
|
+
...
|
|
54
|
+
>>> get_intersecting_component_index_groups(
|
|
55
|
+
... components,
|
|
56
|
+
... get_component_bounds=lambda c: c['bounds'],
|
|
57
|
+
... are_components_intersecting=are_intersecting
|
|
58
|
+
... )
|
|
59
|
+
[[0, 1], [2]]
|
|
60
|
+
|
|
61
|
+
>>> get_intersecting_component_index_groups(
|
|
62
|
+
... [],
|
|
63
|
+
... get_component_bounds=lambda c: c['bounds'],
|
|
64
|
+
... are_components_intersecting=are_intersecting
|
|
65
|
+
... )
|
|
66
|
+
[]
|
|
67
|
+
"""
|
|
68
|
+
if not components:
|
|
69
|
+
return []
|
|
70
|
+
|
|
71
|
+
first_bounding_box = len(get_component_bounds(components[0]))
|
|
72
|
+
|
|
73
|
+
properties = rtree_index.Property()
|
|
74
|
+
properties.dimension = int(first_bounding_box / 2)
|
|
75
|
+
rtree = rtree_index.Index(properties=properties)
|
|
76
|
+
for i, component in enumerate(components):
|
|
77
|
+
rtree.insert(i, get_component_bounds(component))
|
|
78
|
+
|
|
79
|
+
neighbors: list[set[int]] = [set() for _ in range(len(components))]
|
|
80
|
+
|
|
81
|
+
for i, component_i in enumerate(components):
|
|
82
|
+
component_i_bounds = get_component_bounds(component_i)
|
|
83
|
+
candidate_indices = list(rtree.intersection(component_i_bounds))
|
|
84
|
+
|
|
85
|
+
for j in candidate_indices:
|
|
86
|
+
if j <= i:
|
|
87
|
+
# avoid double counting
|
|
88
|
+
continue
|
|
89
|
+
component_j = components[j]
|
|
90
|
+
if are_components_intersecting(component_i, component_j):
|
|
91
|
+
neighbors[i].add(j)
|
|
92
|
+
neighbors[j].add(i)
|
|
93
|
+
|
|
94
|
+
groups: list[list[int]] = []
|
|
95
|
+
visited: set[int] = set()
|
|
96
|
+
for i in range(len(components)):
|
|
97
|
+
if i not in visited:
|
|
98
|
+
stack = [i]
|
|
99
|
+
group: list[int] = []
|
|
100
|
+
while stack:
|
|
101
|
+
idx = stack.pop()
|
|
102
|
+
if idx not in visited:
|
|
103
|
+
visited.add(idx)
|
|
104
|
+
group.append(idx)
|
|
105
|
+
stack.extend(neighbors[idx] - visited)
|
|
106
|
+
groups.append(group)
|
|
107
|
+
|
|
108
|
+
return groups
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"concat_parts",
|
|
3
|
+
"intersect_parts",
|
|
4
|
+
"subtract_parts",
|
|
5
|
+
"unify_parts",
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
from .concat_parts import concat_parts
|
|
9
|
+
from .intersect_parts import intersect_parts
|
|
10
|
+
from .subtract_parts import subtract_parts
|
|
11
|
+
from .unify_parts import unify_parts
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Sequence
|
|
4
|
+
|
|
5
|
+
from typeguard import typechecked
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@typechecked
|
|
9
|
+
def concat_parts[A, P](
|
|
10
|
+
parts: Sequence[P],
|
|
11
|
+
make_assembly_from_parts: Callable[[Sequence[P]], A],
|
|
12
|
+
) -> A:
|
|
13
|
+
"""
|
|
14
|
+
Combine multiple parts into a single assembly using a user-provided constructor.
|
|
15
|
+
|
|
16
|
+
This function uses dependency injection to remain type-agnostic, allowing it
|
|
17
|
+
to work with any part and assembly domain model by providing an appropriate
|
|
18
|
+
assembly constructor function. No validation or modification of the parts is performed.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
parts : Sequence[P]
|
|
23
|
+
Sequence of part objects to be combined.
|
|
24
|
+
make_assembly_from_parts : Callable[[Sequence[P]], A]
|
|
25
|
+
Function that takes a sequence of parts and returns an assembly object.
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
A
|
|
30
|
+
The assembly object created by combining the provided parts.
|
|
31
|
+
|
|
32
|
+
Examples
|
|
33
|
+
--------
|
|
34
|
+
>>> from scadpy import concat_parts
|
|
35
|
+
|
|
36
|
+
>>> parts = [1, 2, 3]
|
|
37
|
+
>>> concat_parts(
|
|
38
|
+
... parts, make_assembly_from_parts=lambda ps: sum(ps)
|
|
39
|
+
... )
|
|
40
|
+
6
|
|
41
|
+
|
|
42
|
+
>>> parts = ['a', 'b', 'c']
|
|
43
|
+
>>> concat_parts(
|
|
44
|
+
... parts, make_assembly_from_parts=lambda ps: ''.join(ps)
|
|
45
|
+
... )
|
|
46
|
+
'abc'
|
|
47
|
+
"""
|
|
48
|
+
return make_assembly_from_parts(parts)
|
|
@@ -0,0 +1,147 @@
|
|
|
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 intersect_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
|
+
intersect_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
|
+
Compute intersections of groups of 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[P]
|
|
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
|
+
intersect_geometries : Callable[[list[G]], Iterable[G]]
|
|
47
|
+
Function to compute the intersection 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 intersected parts with blended colors.
|
|
57
|
+
|
|
58
|
+
Examples
|
|
59
|
+
--------
|
|
60
|
+
>>> from scadpy import intersect_parts
|
|
61
|
+
|
|
62
|
+
>>> # example: two parts intersect, third does not
|
|
63
|
+
>>> parts = [
|
|
64
|
+
... {
|
|
65
|
+
... 'bounds': [0, 0, 2, 2],
|
|
66
|
+
... 'color': [1, 0, 0, 1],
|
|
67
|
+
... 'magnitude': 1,
|
|
68
|
+
... },
|
|
69
|
+
... {
|
|
70
|
+
... 'bounds': [1, 1, 3, 3],
|
|
71
|
+
... 'color': [0, 1, 0, 1],
|
|
72
|
+
... 'magnitude': 2,
|
|
73
|
+
... },
|
|
74
|
+
... {
|
|
75
|
+
... 'bounds': [5, 5, 6, 6],
|
|
76
|
+
... 'color': [0, 0, 1, 1],
|
|
77
|
+
... 'magnitude': 1,
|
|
78
|
+
... },
|
|
79
|
+
... ]
|
|
80
|
+
...
|
|
81
|
+
>>> def are_intersecting(p1, p2):
|
|
82
|
+
... b1, b2 = p1['bounds'], p2['bounds']
|
|
83
|
+
... return not (b1[2] <= b2[0] or b2[2] <= b1[0] or
|
|
84
|
+
... b1[3] <= b2[1] or b2[3] <= b1[1])
|
|
85
|
+
...
|
|
86
|
+
>>> def intersect_geometries(geometries):
|
|
87
|
+
... if len(geometries) == 1:
|
|
88
|
+
... return [geometries[0]]
|
|
89
|
+
... minx = max(g[0] for g in geometries)
|
|
90
|
+
... miny = max(g[1] for g in geometries)
|
|
91
|
+
... maxx = min(g[2] for g in geometries)
|
|
92
|
+
... maxy = min(g[3] for g in geometries)
|
|
93
|
+
... if minx < maxx and miny < maxy:
|
|
94
|
+
... return [[minx, miny, maxx, maxy]]
|
|
95
|
+
... return []
|
|
96
|
+
...
|
|
97
|
+
>>> result = intersect_parts(
|
|
98
|
+
... parts,
|
|
99
|
+
... get_part_color=lambda p: p['color'],
|
|
100
|
+
... get_part_magnitude=lambda p: p['magnitude'],
|
|
101
|
+
... get_part_bounds=lambda p: p['bounds'],
|
|
102
|
+
... are_parts_intersecting=are_intersecting,
|
|
103
|
+
... get_part_geometry=lambda p: p['bounds'],
|
|
104
|
+
... intersect_geometries=intersect_geometries,
|
|
105
|
+
... make_part_from_geometry=lambda geometry, color: {
|
|
106
|
+
... 'bounds': geometry,
|
|
107
|
+
... 'color': [round(float(v), 2) for v in color]
|
|
108
|
+
... },
|
|
109
|
+
... make_assembly_from_parts=lambda parts: parts
|
|
110
|
+
... )
|
|
111
|
+
...
|
|
112
|
+
>>> result == [
|
|
113
|
+
... {'bounds': [1, 1, 2, 2], 'color': [0.33, 0.67, 0.0, 1.0]},
|
|
114
|
+
... {'bounds': [5, 5, 6, 6], 'color': [0.0, 0.0, 1.0, 1.0]}
|
|
115
|
+
... ]
|
|
116
|
+
True
|
|
117
|
+
"""
|
|
118
|
+
from scadpy.core.component import (
|
|
119
|
+
blend_component_colors,
|
|
120
|
+
get_intersecting_component_index_groups,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
intersecting_part_index_groups: list[list[int]] = (
|
|
124
|
+
get_intersecting_component_index_groups(
|
|
125
|
+
parts,
|
|
126
|
+
get_component_bounds=get_part_bounds,
|
|
127
|
+
are_components_intersecting=are_parts_intersecting,
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
intersecting_part_groups: list[list[P]] = [
|
|
131
|
+
[parts[i] for i in group] for group in intersecting_part_index_groups
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
intersected_parts: list[P] = []
|
|
135
|
+
for parts in intersecting_part_groups:
|
|
136
|
+
blended_color = blend_component_colors(
|
|
137
|
+
components=parts,
|
|
138
|
+
get_component_color=get_part_color,
|
|
139
|
+
get_component_magnitude=get_part_magnitude,
|
|
140
|
+
)
|
|
141
|
+
geometries = [get_part_geometry(p) for p in parts]
|
|
142
|
+
intersected_geometries = intersect_geometries(geometries)
|
|
143
|
+
intersected_parts += [
|
|
144
|
+
make_part_from_geometry(u, blended_color) for u in intersected_geometries
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
return make_assembly_from_parts(intersected_parts)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from scadpy.color import Color
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# @typechecked
|
|
11
|
+
def subtract_parts[A, P, G](
|
|
12
|
+
to_be_subtracted: P,
|
|
13
|
+
to_subtract: P,
|
|
14
|
+
get_part_color: Callable[[P], Color],
|
|
15
|
+
get_part_geometry: Callable[[P], G],
|
|
16
|
+
subtract_geometries: Callable[[G, G], Iterable[G]],
|
|
17
|
+
make_part_from_geometry: Callable[[G, Color], P],
|
|
18
|
+
make_assembly_from_parts: Callable[[Sequence[P]], A],
|
|
19
|
+
) -> A:
|
|
20
|
+
"""
|
|
21
|
+
Subtract the geometry of one part from another and return a new assembly.
|
|
22
|
+
|
|
23
|
+
This function is fully generic and uses dependency injection for all domain-specific
|
|
24
|
+
operations, making it suitable for a wide range of applications (2D, 3D, CAD, etc.).
|
|
25
|
+
The color of the resulting part(s) is inherited from the part being subtracted from.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
to_be_subtracted : P
|
|
30
|
+
The part whose geometry will be subtracted from.
|
|
31
|
+
to_subtract : P
|
|
32
|
+
The part whose geometry will be subtracted.
|
|
33
|
+
get_part_color : Callable[[P], Color]
|
|
34
|
+
Function to extract the color from a part (as a list or tuple of 4 floats: RGBA).
|
|
35
|
+
get_part_geometry : Callable[[P], G]
|
|
36
|
+
Function to extract the geometry from a part.
|
|
37
|
+
subtract_geometries : Callable[[G, G], Iterable[G]]
|
|
38
|
+
Function to compute the subtraction of two geometries.
|
|
39
|
+
make_part_from_geometry : Callable[[G, Color], P]
|
|
40
|
+
Function to create a part from a geometry and a color.
|
|
41
|
+
make_assembly_from_parts : Callable[[Sequence[P]], A]
|
|
42
|
+
Function to create an assembly from a sequence of parts.
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
A
|
|
47
|
+
The assembly object containing all subtracted parts.
|
|
48
|
+
|
|
49
|
+
Examples
|
|
50
|
+
--------
|
|
51
|
+
>>> from scadpy import subtract_parts
|
|
52
|
+
|
|
53
|
+
>>> part1 = {'bounds': [0, 0, 3, 3], 'color': [1, 0, 0, 1]}
|
|
54
|
+
>>> part2 = {'bounds': [1, 1, 2, 2], 'color': [0, 1, 0, 1]}
|
|
55
|
+
...
|
|
56
|
+
>>> def subtract_geoms(g1, g2):
|
|
57
|
+
... if (g2[0] > g1[0] and g2[2] < g1[2]
|
|
58
|
+
... and g2[1] > g1[1] and g2[3] < g1[3]):
|
|
59
|
+
... return [
|
|
60
|
+
... [g1[0], g1[1], g2[0], g1[3]], # left
|
|
61
|
+
... [g2[2], g1[1], g1[2], g1[3]], # right
|
|
62
|
+
... [g2[0], g1[1], g2[2], g2[1]], # bottom
|
|
63
|
+
... [g2[0], g2[3], g2[2], g1[3]], # top
|
|
64
|
+
... ]
|
|
65
|
+
... return [g1]
|
|
66
|
+
...
|
|
67
|
+
>>> result = subtract_parts(
|
|
68
|
+
... part1, part2,
|
|
69
|
+
... get_part_color=lambda p: p['color'],
|
|
70
|
+
... get_part_geometry=lambda p: p['bounds'],
|
|
71
|
+
... subtract_geometries=subtract_geoms,
|
|
72
|
+
... make_part_from_geometry=lambda geometry, color: {
|
|
73
|
+
... 'bounds': geometry,
|
|
74
|
+
... 'color': [float(v) for v in color]
|
|
75
|
+
... },
|
|
76
|
+
... make_assembly_from_parts=lambda parts: parts
|
|
77
|
+
... )
|
|
78
|
+
...
|
|
79
|
+
>>> result == [
|
|
80
|
+
... {'bounds': [0, 0, 1, 3], 'color': [1.0, 0.0, 0.0, 1.0]},
|
|
81
|
+
... {'bounds': [2, 0, 3, 3], 'color': [1.0, 0.0, 0.0, 1.0]},
|
|
82
|
+
... {'bounds': [1, 0, 2, 1], 'color': [1.0, 0.0, 0.0, 1.0]},
|
|
83
|
+
... {'bounds': [1, 2, 2, 3], 'color': [1.0, 0.0, 0.0, 1.0]}
|
|
84
|
+
... ]
|
|
85
|
+
True
|
|
86
|
+
"""
|
|
87
|
+
subtracted_geometries = subtract_geometries(
|
|
88
|
+
get_part_geometry(to_be_subtracted), get_part_geometry(to_subtract)
|
|
89
|
+
)
|
|
90
|
+
subtracted_parts = [
|
|
91
|
+
make_part_from_geometry(s, get_part_color(to_be_subtracted))
|
|
92
|
+
for s in subtracted_geometries
|
|
93
|
+
]
|
|
94
|
+
return make_assembly_from_parts(subtracted_parts)
|