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.
Files changed (236) hide show
  1. scadpy/__init__.py +5 -0
  2. scadpy/color/__init__.py +3 -0
  3. scadpy/color/constants/BEIGE.py +3 -0
  4. scadpy/color/constants/BLACK.py +3 -0
  5. scadpy/color/constants/BLUE.py +3 -0
  6. scadpy/color/constants/BROWN.py +3 -0
  7. scadpy/color/constants/DARK_GRAY.py +3 -0
  8. scadpy/color/constants/DEFAULT_COLOR.py +3 -0
  9. scadpy/color/constants/DEFAULT_OPACITY.py +1 -0
  10. scadpy/color/constants/GRAY.py +3 -0
  11. scadpy/color/constants/GREEN.py +3 -0
  12. scadpy/color/constants/ORANGE.py +3 -0
  13. scadpy/color/constants/RED.py +3 -0
  14. scadpy/color/constants/WHITE.py +3 -0
  15. scadpy/color/constants/YELLOW.py +3 -0
  16. scadpy/color/constants/__init__.py +29 -0
  17. scadpy/color/type/__init__.py +3 -0
  18. scadpy/color/type/color.py +3 -0
  19. scadpy/color/utils/__init__.py +3 -0
  20. scadpy/color/utils/get_random_color.py +36 -0
  21. scadpy/core/__init__.py +3 -0
  22. scadpy/core/assembly/__init__.py +5 -0
  23. scadpy/core/assembly/combinations/__init__.py +13 -0
  24. scadpy/core/assembly/combinations/concat_assemblies.py +50 -0
  25. scadpy/core/assembly/combinations/exclude_assemblies.py +135 -0
  26. scadpy/core/assembly/combinations/intersect_assemblies.py +128 -0
  27. scadpy/core/assembly/combinations/subtract_assemblies.py +151 -0
  28. scadpy/core/assembly/combinations/unify_assemblies.py +59 -0
  29. scadpy/core/assembly/topologies/__init__.py +41 -0
  30. scadpy/core/assembly/topologies/directed_edge/__init__.py +9 -0
  31. scadpy/core/assembly/topologies/directed_edge/get_assembly_directed_edge_directions.py +70 -0
  32. scadpy/core/assembly/topologies/directed_edge/get_assembly_directed_edge_to_edge.py +49 -0
  33. scadpy/core/assembly/topologies/directed_edge/get_assembly_directed_edge_to_vertex.py +54 -0
  34. scadpy/core/assembly/topologies/edge/__init__.py +9 -0
  35. scadpy/core/assembly/topologies/edge/get_assembly_edge_lengths.py +46 -0
  36. scadpy/core/assembly/topologies/edge/get_assembly_edge_midpoints.py +51 -0
  37. scadpy/core/assembly/topologies/edge/get_assembly_edge_normals.py +67 -0
  38. scadpy/core/assembly/topologies/face_corner/__init__.py +13 -0
  39. scadpy/core/assembly/topologies/face_corner/get_assembly_face_corner_angles.py +72 -0
  40. scadpy/core/assembly/topologies/face_corner/get_assembly_face_corner_normals.py +103 -0
  41. scadpy/core/assembly/topologies/face_corner/get_assembly_face_corner_to_incoming_directed_edge.py +65 -0
  42. scadpy/core/assembly/topologies/face_corner/get_assembly_face_corner_to_outgoing_directed_edge.py +65 -0
  43. scadpy/core/assembly/topologies/face_corner/get_assembly_face_directed_edge_to_corner.py +79 -0
  44. scadpy/core/assembly/topologies/part/__init__.py +5 -0
  45. scadpy/core/assembly/topologies/part/get_assembly_part_colors.py +55 -0
  46. scadpy/core/assembly/topologies/vertex/__init__.py +7 -0
  47. scadpy/core/assembly/topologies/vertex/get_assembly_vertex_coordinates.py +70 -0
  48. scadpy/core/assembly/topologies/vertex/get_assembly_vertex_to_part.py +62 -0
  49. scadpy/core/assembly/transformations/__init__.py +19 -0
  50. scadpy/core/assembly/transformations/color_assembly.py +24 -0
  51. scadpy/core/assembly/transformations/mirror_vertex_coordinates.py +68 -0
  52. scadpy/core/assembly/transformations/pull_vertex_coordinates.py +64 -0
  53. scadpy/core/assembly/transformations/push_vertex_coordinates.py +64 -0
  54. scadpy/core/assembly/transformations/resize_vertex_coordinates.py +121 -0
  55. scadpy/core/assembly/transformations/rotate_vertex_coordinates.py +73 -0
  56. scadpy/core/assembly/transformations/scale_vertex_coordinates.py +76 -0
  57. scadpy/core/assembly/transformations/translate_vertex_coordinates.py +70 -0
  58. scadpy/core/assembly/types/__init__.py +7 -0
  59. scadpy/core/assembly/types/assembly.py +14 -0
  60. scadpy/core/assembly/types/topology_filter.py +6 -0
  61. scadpy/core/assembly/utils/__init__.py +9 -0
  62. scadpy/core/assembly/utils/lookup_pairs.py +56 -0
  63. scadpy/core/assembly/utils/resolve_topology_filter.py +84 -0
  64. scadpy/core/assembly/utils/transform_filtered_parts.py +55 -0
  65. scadpy/core/component/__init__.py +3 -0
  66. scadpy/core/component/exporters/__init__.py +7 -0
  67. scadpy/core/component/exporters/map_component_to_html_file.py +47 -0
  68. scadpy/core/component/exporters/map_component_to_screen.py +63 -0
  69. scadpy/core/component/features/__init__.py +5 -0
  70. scadpy/core/component/features/get_component_bounds.py +38 -0
  71. scadpy/core/component/utils/__init__.py +9 -0
  72. scadpy/core/component/utils/blend_component_colors.py +77 -0
  73. scadpy/core/component/utils/get_intersecting_component_index_groups.py +108 -0
  74. scadpy/core/part/__init__.py +3 -0
  75. scadpy/core/part/combinations/__init__.py +11 -0
  76. scadpy/core/part/combinations/concat_parts.py +48 -0
  77. scadpy/core/part/combinations/intersect_parts.py +147 -0
  78. scadpy/core/part/combinations/subtract_parts.py +94 -0
  79. scadpy/core/part/combinations/unify_parts.py +143 -0
  80. scadpy/core/part/types/__init__.py +5 -0
  81. scadpy/core/part/types/part.py +34 -0
  82. scadpy/core/part/utils/__init__.py +5 -0
  83. scadpy/core/part/utils/blend_part_colors.py +32 -0
  84. scadpy/d2/__init__.py +2 -0
  85. scadpy/d2/shape/__init__.py +9 -0
  86. scadpy/d2/shape/combinations/__init__.py +21 -0
  87. scadpy/d2/shape/combinations/are_shape_parts_intersecting.py +49 -0
  88. scadpy/d2/shape/combinations/concat_shape.py +48 -0
  89. scadpy/d2/shape/combinations/exclude_shape.py +71 -0
  90. scadpy/d2/shape/combinations/intersect_shape.py +64 -0
  91. scadpy/d2/shape/combinations/intersect_shape_parts.py +71 -0
  92. scadpy/d2/shape/combinations/subtract_shape.py +72 -0
  93. scadpy/d2/shape/combinations/subtract_shape_parts.py +66 -0
  94. scadpy/d2/shape/combinations/unify_shape.py +51 -0
  95. scadpy/d2/shape/combinations/unify_shape_parts.py +74 -0
  96. scadpy/d2/shape/exporters/__init__.py +17 -0
  97. scadpy/d2/shape/exporters/map_shape_to_dxf.py +43 -0
  98. scadpy/d2/shape/exporters/map_shape_to_dxf_file.py +38 -0
  99. scadpy/d2/shape/exporters/map_shape_to_html.py +117 -0
  100. scadpy/d2/shape/exporters/map_shape_to_html_file.py +58 -0
  101. scadpy/d2/shape/exporters/map_shape_to_screen.py +51 -0
  102. scadpy/d2/shape/exporters/map_shape_to_svg.py +40 -0
  103. scadpy/d2/shape/exporters/map_shape_to_svg_file.py +38 -0
  104. scadpy/d2/shape/features/__init__.py +9 -0
  105. scadpy/d2/shape/features/get_shape_bounds.py +40 -0
  106. scadpy/d2/shape/features/get_shape_part_bounds.py +37 -0
  107. scadpy/d2/shape/features/is_shape_empty.py +38 -0
  108. scadpy/d2/shape/importers/__init__.py +13 -0
  109. scadpy/d2/shape/importers/map_dxf_to_shape.py +55 -0
  110. scadpy/d2/shape/importers/map_geometries_to_shape.py +45 -0
  111. scadpy/d2/shape/importers/map_geometry_to_shape.py +43 -0
  112. scadpy/d2/shape/importers/map_parts_to_shape.py +62 -0
  113. scadpy/d2/shape/importers/map_svg_to_shape.py +55 -0
  114. scadpy/d2/shape/primitives/__init__.py +6 -0
  115. scadpy/d2/shape/primitives/circle.py +68 -0
  116. scadpy/d2/shape/primitives/polygon.py +86 -0
  117. scadpy/d2/shape/primitives/rectangle.py +85 -0
  118. scadpy/d2/shape/primitives/square.py +57 -0
  119. scadpy/d2/shape/topologies/__init__.py +53 -0
  120. scadpy/d2/shape/topologies/corner/__init__.py +15 -0
  121. scadpy/d2/shape/topologies/corner/are_shape_corners_convex.py +75 -0
  122. scadpy/d2/shape/topologies/corner/get_shape_corner_angles.py +58 -0
  123. scadpy/d2/shape/topologies/corner/get_shape_corner_normals.py +82 -0
  124. scadpy/d2/shape/topologies/corner/get_shape_corner_to_incoming_directed_edge.py +39 -0
  125. scadpy/d2/shape/topologies/corner/get_shape_corner_to_outgoing_directed_edge.py +39 -0
  126. scadpy/d2/shape/topologies/corner/get_shape_corner_to_vertex.py +65 -0
  127. scadpy/d2/shape/topologies/directed_edge/__init__.py +11 -0
  128. scadpy/d2/shape/topologies/directed_edge/get_shape_directed_edge_directions.py +44 -0
  129. scadpy/d2/shape/topologies/directed_edge/get_shape_directed_edge_to_corner.py +41 -0
  130. scadpy/d2/shape/topologies/directed_edge/get_shape_directed_edge_to_edge.py +51 -0
  131. scadpy/d2/shape/topologies/directed_edge/get_shape_directed_edge_to_vertex.py +63 -0
  132. scadpy/d2/shape/topologies/edge/__init__.py +11 -0
  133. scadpy/d2/shape/topologies/edge/get_shape_edge_lengths.py +43 -0
  134. scadpy/d2/shape/topologies/edge/get_shape_edge_midpoints.py +46 -0
  135. scadpy/d2/shape/topologies/edge/get_shape_edge_normals.py +40 -0
  136. scadpy/d2/shape/topologies/edge/get_shape_edge_to_vertex.py +71 -0
  137. scadpy/d2/shape/topologies/ring/__init__.py +7 -0
  138. scadpy/d2/shape/topologies/ring/get_shape_ring_to_part.py +46 -0
  139. scadpy/d2/shape/topologies/ring/get_shape_ring_types.py +46 -0
  140. scadpy/d2/shape/topologies/vertex/__init__.py +11 -0
  141. scadpy/d2/shape/topologies/vertex/get_shape_part_vertex_coordinates.py +62 -0
  142. scadpy/d2/shape/topologies/vertex/get_shape_vertex_coordinates.py +44 -0
  143. scadpy/d2/shape/topologies/vertex/get_shape_vertex_to_part.py +42 -0
  144. scadpy/d2/shape/topologies/vertex/get_shape_vertex_to_ring.py +63 -0
  145. scadpy/d2/shape/transformations/__init__.py +43 -0
  146. scadpy/d2/shape/transformations/chamfer_shape.py +259 -0
  147. scadpy/d2/shape/transformations/color_shape.py +46 -0
  148. scadpy/d2/shape/transformations/convexify_shape.py +79 -0
  149. scadpy/d2/shape/transformations/fill_shape.py +68 -0
  150. scadpy/d2/shape/transformations/fillet_shape.py +289 -0
  151. scadpy/d2/shape/transformations/grow_shape.py +82 -0
  152. scadpy/d2/shape/transformations/linear_cut_shape.py +116 -0
  153. scadpy/d2/shape/transformations/linear_extrude_shape.py +60 -0
  154. scadpy/d2/shape/transformations/linear_slice_shape.py +144 -0
  155. scadpy/d2/shape/transformations/mirror_shape.py +53 -0
  156. scadpy/d2/shape/transformations/pull_shape.py +67 -0
  157. scadpy/d2/shape/transformations/push_shape.py +67 -0
  158. scadpy/d2/shape/transformations/radial_extrude_shape.py +285 -0
  159. scadpy/d2/shape/transformations/radial_slice_shape.py +132 -0
  160. scadpy/d2/shape/transformations/recoordinate_shape.py +82 -0
  161. scadpy/d2/shape/transformations/resize_shape.py +91 -0
  162. scadpy/d2/shape/transformations/rotate_shape.py +63 -0
  163. scadpy/d2/shape/transformations/scale_shape.py +58 -0
  164. scadpy/d2/shape/transformations/shrink_shape.py +69 -0
  165. scadpy/d2/shape/transformations/translate_shape.py +54 -0
  166. scadpy/d2/shape/types/__init__.py +3 -0
  167. scadpy/d2/shape/types/shape.py +792 -0
  168. scadpy/d2/shape/types/utils/__init__.py +5 -0
  169. scadpy/d2/shape/types/utils/shapely_base_geometry_to_shapely_polygons.py +25 -0
  170. scadpy/d2/shape/utils/__init__.py +5 -0
  171. scadpy/d2/shape/utils/shapely_base_geometry_to_shapely_polygons.py +55 -0
  172. scadpy/d2/utils/__init__.py +3 -0
  173. scadpy/d2/utils/resolve_vector_2d.py +50 -0
  174. scadpy/d3/__init__.py +2 -0
  175. scadpy/d3/solid/__init__.py +8 -0
  176. scadpy/d3/solid/combinations/__init__.py +21 -0
  177. scadpy/d3/solid/combinations/are_solid_parts_intersecting.py +51 -0
  178. scadpy/d3/solid/combinations/concat_solid.py +48 -0
  179. scadpy/d3/solid/combinations/exclude_solid.py +71 -0
  180. scadpy/d3/solid/combinations/intersect_solid.py +64 -0
  181. scadpy/d3/solid/combinations/intersect_solid_parts.py +73 -0
  182. scadpy/d3/solid/combinations/subtract_solid.py +72 -0
  183. scadpy/d3/solid/combinations/subtract_solid_parts.py +68 -0
  184. scadpy/d3/solid/combinations/unify_solid.py +51 -0
  185. scadpy/d3/solid/combinations/unify_solid_parts.py +73 -0
  186. scadpy/d3/solid/exporters/__init__.py +11 -0
  187. scadpy/d3/solid/exporters/map_solid_to_html.py +318 -0
  188. scadpy/d3/solid/exporters/map_solid_to_html_file.py +58 -0
  189. scadpy/d3/solid/exporters/map_solid_to_screen.py +51 -0
  190. scadpy/d3/solid/exporters/map_solid_to_stl_file.py +48 -0
  191. scadpy/d3/solid/features/__init__.py +11 -0
  192. scadpy/d3/solid/features/get_solid_bounds.py +37 -0
  193. scadpy/d3/solid/features/get_solid_part_bounds.py +37 -0
  194. scadpy/d3/solid/features/get_solid_part_colors.py +39 -0
  195. scadpy/d3/solid/features/is_solid_empty.py +36 -0
  196. scadpy/d3/solid/importers/__init__.py +11 -0
  197. scadpy/d3/solid/importers/map_geometries_to_solid.py +42 -0
  198. scadpy/d3/solid/importers/map_geometry_to_solid.py +42 -0
  199. scadpy/d3/solid/importers/map_parts_to_solid.py +66 -0
  200. scadpy/d3/solid/importers/map_stl_to_solid.py +37 -0
  201. scadpy/d3/solid/primitives/__init__.py +7 -0
  202. scadpy/d3/solid/primitives/cone.py +70 -0
  203. scadpy/d3/solid/primitives/cuboid.py +75 -0
  204. scadpy/d3/solid/primitives/cylinder.py +73 -0
  205. scadpy/d3/solid/primitives/polyhedron.py +60 -0
  206. scadpy/d3/solid/primitives/sphere.py +58 -0
  207. scadpy/d3/solid/topologies/__init__.py +8 -0
  208. scadpy/d3/solid/topologies/triangle/__init__.py +5 -0
  209. scadpy/d3/solid/topologies/triangle/get_solid_triangle_to_vertex.py +49 -0
  210. scadpy/d3/solid/topologies/vertex/__init__.py +7 -0
  211. scadpy/d3/solid/topologies/vertex/get_solid_vertex_coordinates.py +39 -0
  212. scadpy/d3/solid/topologies/vertex/get_solid_vertex_to_part.py +37 -0
  213. scadpy/d3/solid/transformations/__init__.py +23 -0
  214. scadpy/d3/solid/transformations/color_solid.py +46 -0
  215. scadpy/d3/solid/transformations/convexify_solid.py +64 -0
  216. scadpy/d3/solid/transformations/mirror_solid.py +53 -0
  217. scadpy/d3/solid/transformations/pull_solid.py +67 -0
  218. scadpy/d3/solid/transformations/push_solid.py +67 -0
  219. scadpy/d3/solid/transformations/recoordinate_solid.py +68 -0
  220. scadpy/d3/solid/transformations/resize_solid.py +92 -0
  221. scadpy/d3/solid/transformations/rotate_solid.py +93 -0
  222. scadpy/d3/solid/transformations/scale_solid.py +58 -0
  223. scadpy/d3/solid/transformations/translate_solid.py +54 -0
  224. scadpy/d3/solid/types/__init__.py +3 -0
  225. scadpy/d3/solid/types/solid.py +448 -0
  226. scadpy/d3/utils/__init__.py +3 -0
  227. scadpy/d3/utils/resolve_vector_3d.py +50 -0
  228. scadpy/utils/__init__.py +6 -0
  229. scadpy/utils/resolve_vector.py +64 -0
  230. scadpy/utils/x.py +38 -0
  231. scadpy/utils/y.py +38 -0
  232. scadpy/utils/z.py +38 -0
  233. scadpy-0.1.0.dist-info/METADATA +282 -0
  234. scadpy-0.1.0.dist-info/RECORD +236 -0
  235. scadpy-0.1.0.dist-info/WHEEL +4 -0
  236. 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,5 @@
1
+ __all__ = [
2
+ "get_component_bounds",
3
+ ]
4
+
5
+ from .get_component_bounds import get_component_bounds
@@ -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,9 @@
1
+ __all__ = [
2
+ "blend_component_colors",
3
+ "get_intersecting_component_index_groups",
4
+ ]
5
+
6
+ from .blend_component_colors import blend_component_colors
7
+ from .get_intersecting_component_index_groups import (
8
+ get_intersecting_component_index_groups,
9
+ )
@@ -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,3 @@
1
+ from .combinations import * # noqa: F403
2
+ from .types import * # noqa: F403
3
+ from .utils import * # noqa: F403
@@ -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)