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,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,5 @@
1
+ __all__ = [
2
+ "Part",
3
+ ]
4
+
5
+ from .part import Part
@@ -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,5 @@
1
+ __all__ = [
2
+ "blend_part_colors",
3
+ ]
4
+
5
+ from .blend_part_colors import blend_part_colors
@@ -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,2 @@
1
+ from .shape import * # noqa: F403
2
+ from .utils import * # noqa: F403
@@ -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
+ )