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,121 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+
5
+ import numpy as np
6
+ from numpy.typing import NDArray
7
+ from typeguard import typechecked
8
+
9
+
10
+ @typechecked
11
+ def resize_vertex_coordinates(
12
+ vertex_coordinates: NDArray[np.float64],
13
+ size: Iterable[float | None],
14
+ n_dims: int,
15
+ auto: bool = False,
16
+ pivot: float | Iterable[float] | None = None,
17
+ vertex_filter: NDArray[np.bool_] | None = None,
18
+ ) -> NDArray[np.float64]:
19
+ """
20
+ Resize vertex coordinates to fit target dimensions.
21
+
22
+ Computes per-axis scale factors from the bounding box of
23
+ ``vertex_coordinates`` and ``size``, then delegates to
24
+ :func:`scale_vertex_coordinates`.
25
+
26
+ ``None`` entries in ``size`` mark axes to leave unchanged. When
27
+ ``auto=True``, those axes are scaled proportionally to the average
28
+ ratio of the defined axes instead.
29
+
30
+ Parameters
31
+ ----------
32
+ vertex_coordinates : NDArray[np.float64]
33
+ 2D array of shape ``(n_vertices, n_dims)``.
34
+ size : Iterable[float | None]
35
+ Target dimensions. ``None`` for an axis means "leave unchanged"
36
+ (or "scale proportionally" when ``auto=True``). Broadcast rules
37
+ from :func:`resolve_vector` apply: a shorter iterable is padded
38
+ with ``None`` (no resize on missing axes).
39
+ n_dims : int
40
+ Number of coordinate dimensions. Used to broadcast ``size`` and
41
+ ``pivot`` to the correct length via :func:`resolve_vector`.
42
+ auto : bool, default=False
43
+ If ``True``, axes with ``None`` are scaled proportionally to the
44
+ average ratio of the defined axes.
45
+ pivot : float | Iterable[float] | None, default=None
46
+ The point relative to which scaling is applied. Defaults to the
47
+ center of the bounding box of ``vertex_coordinates``.
48
+ vertex_filter : NDArray[np.bool_] | None, default=None
49
+ Boolean array selecting which vertices are resized. If ``None``,
50
+ all vertices are resized.
51
+
52
+ Returns
53
+ -------
54
+ NDArray[np.float64]
55
+ Array of shape ``(n_vertices, n_dims)``, one row per vertex.
56
+
57
+ Examples
58
+ --------
59
+ >>> import numpy as np
60
+ >>> from scadpy import resize_vertex_coordinates
61
+
62
+ Resize a 4×2 rectangle to 6×6:
63
+
64
+ >>> coordinates = np.array(
65
+ ... [[0., 0.], [4., 0.], [4., 2.], [0., 2.]], dtype=np.float64
66
+ ... )
67
+ >>> result = resize_vertex_coordinates(coordinates, size=[6, 6], n_dims=2)
68
+ >>> (result.max(axis=0) - result.min(axis=0)).tolist()
69
+ [6.0, 6.0]
70
+
71
+ Freeze the Y axis (``None``):
72
+
73
+ >>> result = resize_vertex_coordinates(coordinates, size=[6, None], n_dims=2)
74
+ >>> (result.max(axis=0) - result.min(axis=0)).tolist()
75
+ [6.0, 2.0]
76
+
77
+ Scale the Y axis proportionally with ``auto=True``:
78
+
79
+ >>> result = resize_vertex_coordinates(
80
+ ... coordinates, size=[6, None], n_dims=2, auto=True
81
+ ... )
82
+ >>> (result.max(axis=0) - result.min(axis=0)).tolist()
83
+ [6.0, 3.0]
84
+ """
85
+ from scadpy import resolve_vector, scale_vertex_coordinates
86
+
87
+ # Convert None → nan so resolve_vector can broadcast to n_dims.
88
+ # resolve_vector replaces nan with its default_value; using nan as
89
+ # default_value preserves the sentinel for frozen axes.
90
+ size_as_floats = [float("nan") if s is None else float(s) for s in size]
91
+ size_array = resolve_vector(size_as_floats, float("nan"), n_dims)
92
+
93
+ if len(vertex_coordinates) == 0:
94
+ return vertex_coordinates
95
+
96
+ bounds_min = vertex_coordinates.min(axis=0)
97
+ bounds_max = vertex_coordinates.max(axis=0)
98
+ current_size = bounds_max - bounds_min
99
+
100
+ if pivot is None:
101
+ effective_pivot = (bounds_min + bounds_max) / 2
102
+ else:
103
+ effective_pivot = np.array(resolve_vector(pivot, 0, n_dims))
104
+
105
+ defined_scales = [
106
+ s / c
107
+ for s, c in zip(size_array, current_size)
108
+ if not np.isnan(s) and c != 0
109
+ ]
110
+ auto_scale = sum(defined_scales) / len(defined_scales) if defined_scales else 1.0
111
+
112
+ scale_array = np.ones(n_dims, dtype=np.float64)
113
+ for i, (s, c) in enumerate(zip(size_array, current_size)):
114
+ if not np.isnan(s):
115
+ scale_array[i] = s / c if c != 0 else 1.0
116
+ elif auto:
117
+ scale_array[i] = auto_scale
118
+
119
+ return scale_vertex_coordinates(
120
+ vertex_coordinates, scale_array, effective_pivot, vertex_filter
121
+ )
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+
5
+ import numpy as np
6
+ from numpy.typing import NDArray
7
+ from typeguard import typechecked
8
+
9
+
10
+ @typechecked
11
+ def rotate_vertex_coordinates(
12
+ vertex_coordinates: NDArray[np.float64],
13
+ rotation_matrix: NDArray[np.float64],
14
+ pivot: float | Iterable[float] = 0,
15
+ vertex_filter: NDArray[np.bool_] | None = None,
16
+ ) -> NDArray[np.float64]:
17
+ """
18
+ Rotate vertex coordinates using a precomputed rotation matrix and a pivot point.
19
+
20
+ Parameters
21
+ ----------
22
+ vertex_coordinates : NDArray[np.float64]
23
+ 2D array of shape (n_vertices, dimensions).
24
+ rotation_matrix : NDArray[np.float64]
25
+ Square rotation matrix of shape (dimensions, dimensions).
26
+ pivot : float | Iterable[float], default=0
27
+ The point around which rotation is applied. If a single float is provided,
28
+ it will be broadcast to all coordinate dimensions. Defaults to the origin.
29
+ vertex_filter : NDArray[np.bool_] | None, default=None
30
+ Boolean array selecting which vertices are rotated. If ``None``, all vertices
31
+ are rotated.
32
+
33
+ Returns
34
+ -------
35
+ NDArray[np.float64]
36
+ Array of shape (n_vertices, dimensions), one row per vertex.
37
+
38
+ Examples
39
+ --------
40
+ >>> import numpy as np
41
+ >>> from shapely.geometry import Polygon
42
+ >>> from scadpy import rotate_vertex_coordinates, Shape
43
+
44
+ >>> polygon = Polygon([(0, 0), (2, 0), (2, 2), (0, 2)])
45
+ >>> shape = Shape.from_geometries([polygon])
46
+ >>> angle = np.deg2rad(90)
47
+ >>> R = np.array([
48
+ ... [np.cos(angle), -np.sin(angle)],
49
+ ... [np.sin(angle), np.cos(angle)],
50
+ ... ])
51
+ >>> rotate_vertex_coordinates(
52
+ ... shape.vertex_coordinates,
53
+ ... R,
54
+ ... pivot=[1, 1],
55
+ ... ).round(10) # doctest: +NORMALIZE_WHITESPACE
56
+ array([[2., 0.],
57
+ [2., 2.],
58
+ [0., 2.],
59
+ [0., 0.]])
60
+ """
61
+ from scadpy import resolve_vector
62
+
63
+ dimensions: int = vertex_coordinates.shape[1]
64
+ pivot_array = np.array(resolve_vector(pivot, 0, dimensions))
65
+
66
+ if vertex_filter is None:
67
+ return (vertex_coordinates - pivot_array) @ rotation_matrix.T + pivot_array
68
+
69
+ result = np.array(vertex_coordinates)
70
+ result[vertex_filter] = (
71
+ (vertex_coordinates[vertex_filter] - pivot_array) @ rotation_matrix.T + pivot_array
72
+ )
73
+ return result
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+
5
+ import numpy as np
6
+ from numpy.typing import NDArray
7
+ from typeguard import typechecked
8
+
9
+
10
+ @typechecked
11
+ def scale_vertex_coordinates(
12
+ vertex_coordinates: NDArray[np.float64],
13
+ scale: float | Iterable[float],
14
+ pivot: float | Iterable[float] = 0,
15
+ vertex_filter: NDArray[np.bool_] | None = None,
16
+ ) -> NDArray[np.float64]:
17
+ """
18
+ Scale vertex coordinates by a given factor or vector, relative to a pivot.
19
+
20
+ Parameters
21
+ ----------
22
+ vertex_coordinates : NDArray[np.float64]
23
+ 2D array of shape (n_vertices, dimensions).
24
+ scale : float | Iterable[float]
25
+ The scaling factor(s) to apply. Its length should match the number of coordinate dimensions.
26
+ If a single float is provided, it will be broadcast to all coordinate dimensions.
27
+ pivot : float | Iterable[float], default=0
28
+ The point relative to which scaling is performed. If a single float is provided, it will be broadcast to all coordinate dimensions.
29
+ Defaults to 0 (the origin).
30
+ vertex_filter : NDArray[np.bool_] | None, default=None
31
+ Boolean array selecting which vertices are scaled. If ``None``, all vertices are scaled.
32
+
33
+ Returns
34
+ -------
35
+ NDArray[np.float64]
36
+ Array of shape (n_vertices, dimensions), one row per vertex.
37
+
38
+ Examples
39
+ --------
40
+ >>> from shapely.geometry import Polygon
41
+ >>> from scadpy import scale_vertex_coordinates, Shape
42
+
43
+ >>> polygon1 = Polygon(
44
+ ... shell=[(0, 0), (2, 0), (2, 2), (0, 2)],
45
+ ... holes=[[(0.5, 0.5), (1.5, 0.5), (1.5, 1.5), (0.5, 1.5)]]
46
+ ... )
47
+ >>> polygon2 = Polygon(
48
+ ... shell=[(10, 10), (12, 10), (12, 12), (10, 12)]
49
+ ... )
50
+ >>> shape = Shape.from_geometries([polygon1, polygon2])
51
+ >>> scale_vertex_coordinates(
52
+ ... shape.vertex_coordinates,
53
+ ... scale=[10, 0.5],
54
+ ... pivot=[1, 2]
55
+ ... ) # doctest: +NORMALIZE_WHITESPACE
56
+ array([[ -9. , 1. ],
57
+ [ 11. , 1. ],
58
+ [ 11. , 2. ],
59
+ ...
60
+ [111. , 6. ],
61
+ [111. , 7. ],
62
+ [ 91. , 7. ]])
63
+ """
64
+ from scadpy import resolve_vector
65
+
66
+ dimensions: int = vertex_coordinates.shape[1]
67
+
68
+ pivot_array = np.array(resolve_vector(pivot, 0, dimensions))
69
+ scale_array = np.array(resolve_vector(scale, 1, dimensions))
70
+
71
+ if vertex_filter is None:
72
+ return pivot_array + (vertex_coordinates - pivot_array) * scale_array
73
+
74
+ result = np.array(vertex_coordinates)
75
+ result[vertex_filter] = pivot_array + (vertex_coordinates[vertex_filter] - pivot_array) * scale_array
76
+ return result
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+
5
+ import numpy as np
6
+ from numpy.typing import NDArray
7
+ from typeguard import typechecked
8
+
9
+
10
+ @typechecked
11
+ def translate_vertex_coordinates(
12
+ vertex_coordinates: NDArray[np.float64],
13
+ translation: float | Iterable[float],
14
+ vertex_filter: NDArray[np.bool_] | None = None,
15
+ ) -> NDArray[np.float64]:
16
+ """
17
+ Translate vertex coordinates by a given vector.
18
+
19
+ Parameters
20
+ ----------
21
+ vertex_coordinates : NDArray[np.float64]
22
+ 2D array of shape (n_vertices, dimensions).
23
+ translation : float | Iterable[float]
24
+ The translation vector to apply. Its length should match the number of coordinate dimensions.
25
+ If a single float is provided, it will be broadcast to all coordinate dimensions.
26
+ vertex_filter : NDArray[np.bool_] | None, default=None
27
+ Boolean array selecting which vertices are translated. If ``None``, all vertices
28
+ are translated.
29
+
30
+ Returns
31
+ -------
32
+ NDArray[np.float64]
33
+ Array of shape (n_vertices, dimensions), one row per vertex.
34
+
35
+ Examples
36
+ --------
37
+ >>> from shapely.geometry import Polygon
38
+ >>> from scadpy import translate_vertex_coordinates, Shape
39
+
40
+ >>> polygon1 = Polygon(
41
+ ... shell=[(0, 0), (2, 0), (2, 2), (0, 2)],
42
+ ... holes=[[(0.5, 0.5), (1.5, 0.5), (1.5, 1.5), (0.5, 1.5)]]
43
+ ... )
44
+ >>> polygon2 = Polygon(
45
+ ... shell=[(10, 10), (12, 10), (12, 12), (10, 12)]
46
+ ... )
47
+ >>> shape = Shape.from_geometries([polygon1, polygon2])
48
+ >>> translate_vertex_coordinates(
49
+ ... shape.vertex_coordinates,
50
+ ... translation=[10, 20]
51
+ ... ) # doctest: +NORMALIZE_WHITESPACE
52
+ array([[10. , 20. ],
53
+ [12. , 20. ],
54
+ [12. , 22. ],
55
+ ...
56
+ [22. , 30. ],
57
+ [22. , 32. ],
58
+ [20. , 32. ]])
59
+ """
60
+ from scadpy import resolve_vector
61
+
62
+ dimensions: int = vertex_coordinates.shape[1]
63
+ translation_array = np.array(resolve_vector(translation, 0, dimensions))
64
+
65
+ if vertex_filter is None:
66
+ return vertex_coordinates + translation_array
67
+
68
+ result = np.array(vertex_coordinates)
69
+ result[vertex_filter] += translation_array
70
+ return result
@@ -0,0 +1,7 @@
1
+ __all__ = [
2
+ "Assembly",
3
+ "TopologyFilter",
4
+ ]
5
+
6
+ from .assembly import Assembly
7
+ from .topology_filter import TopologyFilter
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC
4
+ from collections.abc import Sequence
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ if TYPE_CHECKING:
8
+ from scadpy.core.part import Part
9
+
10
+
11
+ class Assembly[G](ABC):
12
+ def __init__(self, *args: Any, **kwargs: Any): # pyright: ignore[reportExplicitAny, reportAny]
13
+ super().__init__(*args, **kwargs)
14
+ self._parts: Sequence[Part[G]] = []
@@ -0,0 +1,6 @@
1
+ from collections.abc import Callable
2
+
3
+ import numpy as np
4
+ from numpy.typing import NDArray
5
+
6
+ type TopologyFilter[A] = NDArray[np.bool_] | Callable[[A], NDArray[np.bool_]]
@@ -0,0 +1,9 @@
1
+ __all__ = [
2
+ "lookup_pairs",
3
+ "resolve_topology_filter",
4
+ "transform_filtered_parts",
5
+ ]
6
+
7
+ from .lookup_pairs import lookup_pairs
8
+ from .resolve_topology_filter import resolve_topology_filter
9
+ from .transform_filtered_parts import transform_filtered_parts
@@ -0,0 +1,56 @@
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 lookup_pairs(
10
+ queries: NDArray[np.int64],
11
+ haystack: NDArray[np.int64],
12
+ ) -> NDArray[np.int64]:
13
+ """
14
+ For each queried pair, return its index in the haystack.
15
+
16
+ Each pair ``(a, b)`` is encoded as ``a * n + b`` where ``n`` is
17
+ ``max(haystack) + 1``, then looked up via binary search. This is
18
+ fully vectorized with no Python loops.
19
+
20
+ Parameters
21
+ ----------
22
+ queries : NDArray[np.int64]
23
+ 2D array of shape ``(n_queries, 2)``. Pairs to look up.
24
+ haystack : NDArray[np.int64]
25
+ 2D array of shape ``(n_items, 2)``. The reference pairs.
26
+ Each pair must appear exactly once.
27
+
28
+ Returns
29
+ -------
30
+ NDArray[np.int64]
31
+ 1D array of shape ``(n_queries,)``. Each entry is the index
32
+ in ``haystack`` of the corresponding query pair.
33
+
34
+ Examples
35
+ --------
36
+ >>> import numpy as np
37
+ >>> from scadpy import lookup_pairs
38
+
39
+ >>> haystack = np.array(
40
+ ... [[0, 1], [1, 0], [1, 2], [2, 1], [2, 0], [0, 2]],
41
+ ... dtype=np.int64,
42
+ ... )
43
+ >>> queries = np.array([[2, 0], [0, 1], [1, 2]], dtype=np.int64)
44
+ >>> lookup_pairs(queries, haystack)
45
+ array([4, 0, 2])
46
+ """
47
+ if len(queries) == 0:
48
+ return np.empty(0, dtype=np.int64)
49
+
50
+ n = int(haystack.max()) + 1
51
+ haystack_keys = haystack[:, 0] * n + haystack[:, 1]
52
+ sort_order = np.argsort(haystack_keys)
53
+ haystack_keys_sorted = haystack_keys[sort_order]
54
+
55
+ query_keys = queries[:, 0] * n + queries[:, 1]
56
+ return sort_order[np.searchsorted(haystack_keys_sorted, query_keys)]
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import numpy as np
6
+ from numpy.typing import NDArray
7
+ from typeguard import typechecked
8
+
9
+ if TYPE_CHECKING:
10
+ from scadpy import TopologyFilter
11
+
12
+
13
+ @typechecked
14
+ def resolve_topology_filter[A](
15
+ assembly: A,
16
+ count: int,
17
+ topology_filter: TopologyFilter[A] | None,
18
+ ) -> NDArray[np.bool_] | None:
19
+ """
20
+ Resolve a topology filter into a boolean mask.
21
+
22
+ A topology filter can be either a precomputed boolean mask or a callable
23
+ that derives one from the assembly. This function normalizes both forms
24
+ into a concrete mask and validates its length against the expected
25
+ topology count (vertices, edges, parts, etc.).
26
+
27
+ Parameters
28
+ ----------
29
+ assembly : A
30
+ The assembly to pass to the filter if it is a callable.
31
+ topology_filter : TopologyFilter[A] | None
32
+ A boolean mask, a callable that produces one from the assembly,
33
+ or None. If None, the function returns None immediately.
34
+ count : int
35
+ The expected length of the resulting mask. Should match the number
36
+ of topological elements being filtered (e.g. vertex_count, edge_count,
37
+ part count).
38
+
39
+ Returns
40
+ -------
41
+ NDArray[np.bool_] | None
42
+ The resolved boolean mask, or None if topology_filter is None.
43
+
44
+ Raises
45
+ ------
46
+ ValueError
47
+ If the resolved mask length does not match the expected count.
48
+
49
+ Examples
50
+ --------
51
+ >>> import numpy as np
52
+ >>> from scadpy import resolve_topology_filter
53
+
54
+ >>> # none passthrough
55
+ >>> resolve_topology_filter("any", 4, None) is None
56
+ True
57
+
58
+ >>> # direct mask
59
+ >>> mask = np.array([True, False, True, False])
60
+ >>> resolve_topology_filter("any", 4, mask)
61
+ array([ True, False, True, False])
62
+
63
+ >>> # callable filter
64
+ >>> resolve_topology_filter(
65
+ ... "hello", 1, lambda s: np.array([len(s) > 3])
66
+ ... )
67
+ array([ True])
68
+
69
+ >>> # mismatched count
70
+ >>> resolve_topology_filter(
71
+ ... "any", 5, np.array([True, False])
72
+ ... ) # doctest: +ELLIPSIS
73
+ Traceback (most recent call last):
74
+ ...
75
+ ValueError: Topology filter length (2) does not match...
76
+ """
77
+ if topology_filter is None:
78
+ return None
79
+ mask = topology_filter(assembly) if callable(topology_filter) else topology_filter
80
+ if len(mask) != count:
81
+ raise ValueError(
82
+ f"Topology filter length ({len(mask)}) does not match expected count ({count})."
83
+ )
84
+ return mask
@@ -0,0 +1,55 @@
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
+ from typeguard import typechecked
8
+
9
+ if TYPE_CHECKING:
10
+ from scadpy import Part, TopologyFilter
11
+
12
+
13
+ @typechecked
14
+ def transform_filtered_parts[A, G](
15
+ assembly: A,
16
+ parts: Sequence[Part[G]],
17
+ part_filter: TopologyFilter[A] | None,
18
+ transform: Callable[[Sequence[Part[G]]], Sequence[Part[G]]],
19
+ concat_parts: Callable[[Sequence[Part[G]]], A],
20
+ ) -> A:
21
+ """Apply a transformation to a filtered subset of parts, keeping the rest unchanged.
22
+
23
+ Parameters
24
+ ----------
25
+ assembly : A
26
+ The assembly used to evaluate *part_filter*.
27
+ parts : Sequence[Part[G]]
28
+ All parts of the assembly.
29
+ part_filter : TopologyFilter[A] | None
30
+ Optional filter selecting which parts to transform. If ``None``, all
31
+ parts are transformed.
32
+ transform : Callable[[Sequence[Part[G]]], Sequence[Part[G]]]
33
+ Function applied to the selected parts.
34
+ concat_parts : Callable[[Sequence[Part[G]]], A]
35
+ Function that assembles transformed and untouched parts into a new
36
+ assembly of type *A*.
37
+
38
+ Returns
39
+ -------
40
+ A
41
+ New assembly with the filtered parts transformed and the rest unchanged.
42
+ """
43
+ from scadpy import resolve_topology_filter
44
+
45
+ mask = resolve_topology_filter(
46
+ assembly=assembly, count=len(parts), topology_filter=part_filter
47
+ )
48
+ if mask is None:
49
+ mask = np.ones(len(parts), dtype=bool)
50
+
51
+ selected = [p for p, m in zip(parts, mask) if m]
52
+ unselected = [p for p, m in zip(parts, mask) if not m]
53
+
54
+ transformed = transform(selected)
55
+ return concat_parts([*transformed, *unselected])
@@ -0,0 +1,3 @@
1
+ from .exporters import * # noqa: F403
2
+ from .features import * # noqa: F403
3
+ from .utils import * # noqa: F403
@@ -0,0 +1,7 @@
1
+ __all__ = [
2
+ "map_component_to_html_file",
3
+ "map_component_to_screen",
4
+ ]
5
+
6
+ from .map_component_to_html_file import map_component_to_html_file
7
+ from .map_component_to_screen import map_component_to_screen
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import cast
5
+
6
+ from IPython.core.display import HTML
7
+ from typeguard import typechecked
8
+
9
+
10
+ @typechecked
11
+ def map_component_to_html_file[Component](
12
+ component: Component, path: str, to_html: Callable[[Component], HTML]
13
+ ) -> int:
14
+ """
15
+ Export a component to an HTML file.
16
+
17
+ This function uses dependency injection for the HTML conversion, making it
18
+ suitable for any component type that can be represented as HTML. The HTML
19
+ is written to the specified file path.
20
+
21
+ Parameters
22
+ ----------
23
+ component : Component
24
+ The component to export.
25
+ path : str
26
+ The file path where the HTML will be written.
27
+ to_html : Callable[[Component], HTML]
28
+ Function that converts the component to an IPython HTML object.
29
+
30
+ Returns
31
+ -------
32
+ int
33
+ The number of characters written to the file.
34
+
35
+ Examples
36
+ --------
37
+ >>> from IPython.core.display import HTML
38
+ >>> from scadpy import map_component_to_html_file
39
+ ...
40
+ >>> map_component_to_html_file(
41
+ ... "Hello, World!",
42
+ ... "test.html",
43
+ ... to_html=lambda c: HTML(f"<h1>{c}</h1>")
44
+ ... ) > 0 # doctest: +SKIP
45
+ True
46
+ """
47
+ return open(path, "w", encoding="utf-8").write(cast(str, to_html(component).data))