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,59 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable, Iterable, Sequence
4
+
5
+ from typeguard import typechecked
6
+
7
+
8
+ @typechecked
9
+ def unify_assemblies[A, P](
10
+ assemblies: Sequence[A],
11
+ get_assembly_parts: Callable[[A], Iterable[P]],
12
+ unify_parts: Callable[[Sequence[P]], A],
13
+ ) -> A:
14
+ """
15
+ Unite (union) multiple assemblies into a single assembly by unifying all their parts.
16
+
17
+ This function uses dependency injection to remain type-agnostic, allowing it
18
+ to work with any assembly/part domain model by providing appropriate accessor
19
+ and unification functions.
20
+
21
+ Parameters
22
+ ----------
23
+ assemblies : Sequence[A]
24
+ Sequence of assembly objects to be unified.
25
+ get_assembly_parts : Callable[[A], Iterable[P]]
26
+ Function that extracts parts from an assembly.
27
+ unify_parts : Callable[[Sequence[P]], Assembly]
28
+ Function that unifies a sequence of parts into a new assembly.
29
+
30
+ Returns
31
+ -------
32
+ A
33
+ The assembly object created by unifying all parts from all input assemblies.
34
+
35
+ Examples
36
+ --------
37
+ >>> from scadpy import unify_assemblies
38
+
39
+ >>> assemblies = [
40
+ ... [[0, 0, 2, 2], [1, 1, 3, 3]],
41
+ ... [[5, 5, 6, 6]]
42
+ ... ]
43
+ ...
44
+ >>> def unify_parts(parts):
45
+ ... minx = min(p[0] for p in parts)
46
+ ... miny = min(p[1] for p in parts)
47
+ ... maxx = max(p[2] for p in parts)
48
+ ... maxy = max(p[3] for p in parts)
49
+ ... return [[minx, miny, maxx, maxy]]
50
+ ...
51
+ >>> result = unify_assemblies(
52
+ ... assemblies,
53
+ ... get_assembly_parts=lambda a: a,
54
+ ... unify_parts=unify_parts
55
+ ... )
56
+ >>> result == [[0, 0, 6, 6]]
57
+ True
58
+ """
59
+ return unify_parts([p for a in assemblies for p in get_assembly_parts(a)])
@@ -0,0 +1,41 @@
1
+ __all__ = [
2
+ "get_assembly_directed_edge_directions",
3
+ "get_assembly_directed_edge_to_edge",
4
+ "get_assembly_directed_edge_to_vertex",
5
+ "get_assembly_edge_lengths",
6
+ "get_assembly_edge_midpoints",
7
+ "get_assembly_edge_normals",
8
+ "get_assembly_face_corner_angles",
9
+ "get_assembly_face_corner_normals",
10
+ "get_assembly_face_corner_to_incoming_directed_edge",
11
+ "get_assembly_face_corner_to_outgoing_directed_edge",
12
+ "get_assembly_face_directed_edge_to_corner",
13
+ "get_assembly_part_colors",
14
+ "get_assembly_vertex_coordinates",
15
+ "get_assembly_vertex_to_part",
16
+ ]
17
+
18
+ from .directed_edge import (
19
+ get_assembly_directed_edge_directions,
20
+ get_assembly_directed_edge_to_edge,
21
+ get_assembly_directed_edge_to_vertex,
22
+ )
23
+ from .edge import (
24
+ get_assembly_edge_lengths,
25
+ get_assembly_edge_midpoints,
26
+ get_assembly_edge_normals,
27
+ )
28
+ from .face_corner import (
29
+ get_assembly_face_corner_angles,
30
+ get_assembly_face_corner_normals,
31
+ get_assembly_face_corner_to_incoming_directed_edge,
32
+ get_assembly_face_corner_to_outgoing_directed_edge,
33
+ get_assembly_face_directed_edge_to_corner,
34
+ )
35
+ from .part import (
36
+ get_assembly_part_colors,
37
+ )
38
+ from .vertex import (
39
+ get_assembly_vertex_coordinates,
40
+ get_assembly_vertex_to_part,
41
+ )
@@ -0,0 +1,9 @@
1
+ __all__ = [
2
+ "get_assembly_directed_edge_directions",
3
+ "get_assembly_directed_edge_to_edge",
4
+ "get_assembly_directed_edge_to_vertex",
5
+ ]
6
+
7
+ from .get_assembly_directed_edge_directions import get_assembly_directed_edge_directions
8
+ from .get_assembly_directed_edge_to_edge import get_assembly_directed_edge_to_edge
9
+ from .get_assembly_directed_edge_to_vertex import get_assembly_directed_edge_to_vertex
@@ -0,0 +1,70 @@
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_assembly_directed_edge_directions(
10
+ directed_edge_to_vertex: NDArray[np.int64],
11
+ vertex_coordinates: NDArray[np.float64],
12
+ ) -> NDArray[np.float64]:
13
+ """
14
+ For each directed edge, return its unit direction vector.
15
+
16
+ The direction vector points from the start vertex to the end vertex,
17
+ normalized to unit length.
18
+
19
+ Parameters
20
+ ----------
21
+ directed_edge_to_vertex : NDArray[np.int64]
22
+ 2D array of shape ``(n_directed_edges, 2)`` mapping each directed edge
23
+ to its ``[start_vertex, end_vertex]`` indices.
24
+ vertex_coordinates : NDArray[np.float64]
25
+ 2D array of shape ``(n_vertices, 2)`` with vertex coordinates.
26
+
27
+ Returns
28
+ -------
29
+ NDArray[np.float64]
30
+ 2D array of shape ``(n_directed_edges, 2)``. Each row is a unit vector
31
+ ``[dx, dy]`` pointing from start to end vertex.
32
+
33
+ Examples
34
+ --------
35
+ >>> import numpy as np
36
+ >>> from scadpy import get_assembly_directed_edge_directions
37
+
38
+ >>> # square: forward edges go right, up, left, down;
39
+ >>> # backward edges are reversed
40
+ >>> directed_edge_to_vertex = np.array([
41
+ ... [0, 1], [1, 0],
42
+ ... [1, 2], [2, 1],
43
+ ... [2, 3], [3, 2],
44
+ ... [3, 0], [0, 3],
45
+ ... ], dtype=np.int64)
46
+ >>> vertex_coordinates = np.array(
47
+ ... [[0., 0.], [1., 0.], [1., 1.], [0., 1.]]
48
+ ... )
49
+ >>> get_assembly_directed_edge_directions(
50
+ ... directed_edge_to_vertex, vertex_coordinates
51
+ ... ).round(4)
52
+ array([[ 1., 0.],
53
+ [-1., 0.],
54
+ [ 0., 1.],
55
+ [ 0., -1.],
56
+ [-1., 0.],
57
+ [ 1., 0.],
58
+ [ 0., -1.],
59
+ [ 0., 1.]])
60
+ """
61
+ if len(directed_edge_to_vertex) == 0:
62
+ d = vertex_coordinates.shape[1] if vertex_coordinates.ndim == 2 else 0
63
+ return np.empty((0, d), dtype=np.float64)
64
+
65
+ starts = vertex_coordinates[directed_edge_to_vertex[:, 0]]
66
+ ends = vertex_coordinates[directed_edge_to_vertex[:, 1]]
67
+
68
+ directions = ends - starts
69
+ lengths = np.linalg.norm(directions, axis=1, keepdims=True)
70
+ return directions / lengths
@@ -0,0 +1,49 @@
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_assembly_directed_edge_to_edge(
10
+ n_edges: int,
11
+ ) -> NDArray[np.int64]:
12
+ """
13
+ For each directed edge, return the index of its parent undirected edge.
14
+
15
+ Since directed edges are interleaved (``directed_edge 2i`` and
16
+ ``directed_edge 2i+1`` both belong to ``edge i``), the mapping is:
17
+
18
+ .. code-block:: text
19
+
20
+ edge index = directed_edge index // 2
21
+
22
+ Parameters
23
+ ----------
24
+ n_edges : int
25
+ Number of undirected edges.
26
+
27
+ Returns
28
+ -------
29
+ NDArray[np.int64]
30
+ 1D array of shape ``(2 * n_edges,)``. Each entry is the index of
31
+ the parent undirected edge.
32
+
33
+ Examples
34
+ --------
35
+ >>> import numpy as np
36
+ >>> from scadpy import get_assembly_directed_edge_to_edge
37
+
38
+ >>> # triangle: 3 edges → 6 directed edges
39
+ >>> get_assembly_directed_edge_to_edge(3)
40
+ array([0, 0, 1, 1, 2, 2])
41
+
42
+ >>> # square: 4 edges → 8 directed edges
43
+ >>> get_assembly_directed_edge_to_edge(4)
44
+ array([0, 0, 1, 1, 2, 2, 3, 3])
45
+ """
46
+ if n_edges == 0:
47
+ return np.empty(0, dtype=np.int64)
48
+
49
+ return np.arange(n_edges, dtype=np.int64).repeat(2)
@@ -0,0 +1,54 @@
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_assembly_directed_edge_to_vertex(
10
+ edge_to_vertex: NDArray[np.int64],
11
+ ) -> NDArray[np.int64]:
12
+ """
13
+ For each directed edge, return the indices of its start and end vertices.
14
+
15
+ Each undirected edge ``i`` gives rise to two directed edges, interleaved:
16
+
17
+ - ``directed_edge 2i`` : forward → ``[start, end]``
18
+ - ``directed_edge 2i+1`` : backward → ``[end, start]``
19
+
20
+ Parameters
21
+ ----------
22
+ edge_to_vertex : NDArray[np.int64]
23
+ 2D array of shape ``(n_edges, 2)`` mapping each edge to its
24
+ ``[start_vertex, end_vertex]`` indices.
25
+
26
+ Returns
27
+ -------
28
+ NDArray[np.int64]
29
+ 2D array of shape ``(2 * n_edges, 2)``. Each row is
30
+ ``[start_vertex, end_vertex]`` for the directed edge.
31
+
32
+ Examples
33
+ --------
34
+ >>> import numpy as np
35
+ >>> from scadpy import get_assembly_directed_edge_to_vertex
36
+
37
+ >>> # triangle: 3 edges → 6 directed edges
38
+ >>> edge_to_vertex = np.array(
39
+ ... [[0, 1], [1, 2], [2, 0]], dtype=np.int64
40
+ ... )
41
+ >>> get_assembly_directed_edge_to_vertex(edge_to_vertex)
42
+ array([[0, 1],
43
+ [1, 0],
44
+ [1, 2],
45
+ [2, 1],
46
+ [2, 0],
47
+ [0, 2]])
48
+ """
49
+ if len(edge_to_vertex) == 0:
50
+ return np.empty((0, 2), dtype=np.int64)
51
+
52
+ forward = edge_to_vertex
53
+ backward = edge_to_vertex[:, ::-1]
54
+ return np.stack([forward, backward], axis=1).reshape(-1, 2)
@@ -0,0 +1,9 @@
1
+ __all__ = [
2
+ "get_assembly_edge_lengths",
3
+ "get_assembly_edge_midpoints",
4
+ "get_assembly_edge_normals",
5
+ ]
6
+
7
+ from .get_assembly_edge_lengths import get_assembly_edge_lengths
8
+ from .get_assembly_edge_midpoints import get_assembly_edge_midpoints
9
+ from .get_assembly_edge_normals import get_assembly_edge_normals
@@ -0,0 +1,46 @@
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_assembly_edge_lengths(
10
+ edge_to_vertex: NDArray[np.int64],
11
+ vertex_coordinates: NDArray[np.float64],
12
+ ) -> NDArray[np.float64]:
13
+ """
14
+ For each edge, return the Euclidean distance between its two vertices.
15
+
16
+ Parameters
17
+ ----------
18
+ edge_to_vertex : NDArray[np.int64]
19
+ 2D array of shape ``(n_edges, 2)`` mapping each edge to its
20
+ ``[start_vertex, end_vertex]`` indices.
21
+ vertex_coordinates : NDArray[np.float64]
22
+ 2D array of shape ``(n_vertices, d)`` with vertex coordinates.
23
+
24
+ Returns
25
+ -------
26
+ NDArray[np.float64]
27
+ 1D array of shape ``(n_edges,)``, one length per edge.
28
+
29
+ Examples
30
+ --------
31
+ >>> import numpy as np
32
+ >>> from scadpy.core.assembly import get_assembly_edge_lengths
33
+
34
+ >>> edge_to_vertex = np.array(
35
+ ... [[0, 1], [1, 2], [2, 0]], dtype=np.int64
36
+ ... )
37
+ >>> vertex_coordinates = np.array([[0., 0.], [3., 0.], [0., 4.]])
38
+ >>> get_assembly_edge_lengths(edge_to_vertex, vertex_coordinates)
39
+ array([3., 5., 4.])
40
+ """
41
+ if len(edge_to_vertex) == 0:
42
+ return np.empty(0, dtype=np.float64)
43
+
44
+ starts = vertex_coordinates[edge_to_vertex[:, 0]]
45
+ ends = vertex_coordinates[edge_to_vertex[:, 1]]
46
+ return np.linalg.norm(ends - starts, axis=1)
@@ -0,0 +1,51 @@
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_assembly_edge_midpoints(
10
+ edge_to_vertex: NDArray[np.int64],
11
+ vertex_coordinates: NDArray[np.float64],
12
+ ) -> NDArray[np.float64]:
13
+ """
14
+ For each edge, return the midpoint between its two vertices.
15
+
16
+ Parameters
17
+ ----------
18
+ edge_to_vertex : NDArray[np.int64]
19
+ 2D array of shape ``(n_edges, 2)`` mapping each edge to its
20
+ ``[start_vertex, end_vertex]`` indices.
21
+ vertex_coordinates : NDArray[np.float64]
22
+ 2D array of shape ``(n_vertices, d)`` with vertex coordinates.
23
+
24
+ Returns
25
+ -------
26
+ NDArray[np.float64]
27
+ 2D array of shape ``(n_edges, d)``, one midpoint per edge.
28
+
29
+ Examples
30
+ --------
31
+ >>> import numpy as np
32
+ >>> from scadpy.core.assembly import get_assembly_edge_midpoints
33
+
34
+ >>> edge_to_vertex = np.array(
35
+ ... [[0, 1], [1, 2], [2, 0]], dtype=np.int64
36
+ ... )
37
+ >>> vertex_coordinates = np.array([[0., 0.], [2., 0.], [1., 2.]])
38
+ >>> get_assembly_edge_midpoints(
39
+ ... edge_to_vertex, vertex_coordinates
40
+ ... )
41
+ array([[1. , 0. ],
42
+ [1.5, 1. ],
43
+ [0.5, 1. ]])
44
+ """
45
+ if len(edge_to_vertex) == 0:
46
+ d = vertex_coordinates.shape[1] if vertex_coordinates.ndim == 2 else 0
47
+ return np.empty((0, d), dtype=np.float64)
48
+
49
+ starts = vertex_coordinates[edge_to_vertex[:, 0]]
50
+ ends = vertex_coordinates[edge_to_vertex[:, 1]]
51
+ return (starts + ends) / 2
@@ -0,0 +1,67 @@
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_assembly_edge_normals(
10
+ edge_to_vertex: NDArray[np.int64],
11
+ vertex_coordinates: NDArray[np.float64],
12
+ ) -> NDArray[np.float64]:
13
+ """
14
+ For each edge, return its outward unit normal.
15
+
16
+ The outward normal is the 90° clockwise rotation of the edge direction
17
+ vector ``(dx, dy) → (dy, -dx)``. For exterior rings (CCW winding), this
18
+ points away from the filled area. For interior rings (CW winding, i.e.
19
+ holes), this also points away from the filled area (outward into the hole).
20
+
21
+ Parameters
22
+ ----------
23
+ edge_to_vertex : NDArray[np.int64]
24
+ 2D array of shape ``(n_edges, 2)`` mapping each edge to its
25
+ ``[start_vertex, end_vertex]`` indices.
26
+ vertex_coordinates : NDArray[np.float64]
27
+ 2D array of shape ``(n_vertices, 2)`` with vertex coordinates.
28
+
29
+ Returns
30
+ -------
31
+ NDArray[np.float64]
32
+ 2D array of shape ``(n_edges, 2)``. Each row is a unit vector
33
+ ``[nx, ny]`` perpendicular to the edge and pointing outward.
34
+
35
+ Examples
36
+ --------
37
+ >>> import numpy as np
38
+ >>> from scadpy import get_assembly_edge_normals
39
+
40
+ >>> # square centered at origin: 4 edges, normals point outward
41
+ >>> edge_to_vertex = np.array(
42
+ ... [[0, 1], [1, 2], [2, 3], [3, 0]], dtype=np.int64
43
+ ... )
44
+ >>> vertex_coordinates = np.array(
45
+ ... [[-1., -1.], [1., -1.], [1., 1.], [-1., 1.]]
46
+ ... )
47
+ >>> get_assembly_edge_normals(
48
+ ... edge_to_vertex, vertex_coordinates
49
+ ... ).round(4)
50
+ array([[ 0., -1.],
51
+ [ 1., -0.],
52
+ [ 0., 1.],
53
+ [-1., -0.]])
54
+ """
55
+ if len(edge_to_vertex) == 0:
56
+ d = vertex_coordinates.shape[1] if vertex_coordinates.ndim == 2 else 0
57
+ return np.empty((0, d), dtype=np.float64)
58
+
59
+ starts = vertex_coordinates[edge_to_vertex[:, 0]]
60
+ ends = vertex_coordinates[edge_to_vertex[:, 1]]
61
+
62
+ directions = ends - starts
63
+ lengths = np.linalg.norm(directions, axis=1, keepdims=True)
64
+ directions_normalized = directions / lengths
65
+
66
+ # 90° CW rotation: (dx, dy) → (dy, -dx)
67
+ return np.stack([directions_normalized[:, 1], -directions_normalized[:, 0]], axis=1)
@@ -0,0 +1,13 @@
1
+ __all__ = [
2
+ "get_assembly_face_corner_angles",
3
+ "get_assembly_face_corner_normals",
4
+ "get_assembly_face_corner_to_incoming_directed_edge",
5
+ "get_assembly_face_corner_to_outgoing_directed_edge",
6
+ "get_assembly_face_directed_edge_to_corner",
7
+ ]
8
+
9
+ from .get_assembly_face_corner_angles import get_assembly_face_corner_angles
10
+ from .get_assembly_face_corner_normals import get_assembly_face_corner_normals
11
+ from .get_assembly_face_corner_to_incoming_directed_edge import get_assembly_face_corner_to_incoming_directed_edge
12
+ from .get_assembly_face_corner_to_outgoing_directed_edge import get_assembly_face_corner_to_outgoing_directed_edge
13
+ from .get_assembly_face_directed_edge_to_corner import get_assembly_face_directed_edge_to_corner
@@ -0,0 +1,72 @@
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_assembly_face_corner_angles(
10
+ corner_to_vertex: NDArray[np.int64],
11
+ vertex_coordinates: NDArray[np.float64],
12
+ ) -> NDArray[np.float64]:
13
+ """
14
+ For each corner, return its interior angle in degrees.
15
+
16
+ The angle is always positive, in the range (0°, 180°). It represents
17
+ the turning angle at the corner, regardless of whether the corner is
18
+ convex or concave. Use convexity information separately to distinguish
19
+ the two cases.
20
+
21
+ The angle is computed as the absolute value of the signed angle from the
22
+ incoming edge to the outgoing edge at each corner, using the 2D cross
23
+ product to determine orientation.
24
+
25
+ Parameters
26
+ ----------
27
+ corner_to_vertex : NDArray[np.int64]
28
+ 2D array of shape ``(n_corners, 3)``. Each row is
29
+ ``[prev_vertex, curr_vertex, next_vertex]``.
30
+ vertex_coordinates : NDArray[np.float64]
31
+ 2D array of shape ``(n_vertices, 2)`` with vertex coordinates.
32
+
33
+ Returns
34
+ -------
35
+ NDArray[np.float64]
36
+ 1D array of shape ``(n_corners,)``, one angle per corner in degrees.
37
+ All values are in the range (0°, 180°).
38
+
39
+ Examples
40
+ --------
41
+ >>> import numpy as np
42
+ >>> from scadpy import get_assembly_face_corner_angles
43
+
44
+ >>> # square: 4 right-angle corners
45
+ >>> corner_to_vertex = np.array(
46
+ ... [[3, 0, 1], [0, 1, 2], [1, 2, 3], [2, 3, 0]],
47
+ ... dtype=np.int64
48
+ ... )
49
+ >>> vertex_coordinates = np.array(
50
+ ... [[-1., -1.], [1., -1.], [1., 1.], [-1., 1.]]
51
+ ... )
52
+ >>> get_assembly_face_corner_angles(
53
+ ... corner_to_vertex, vertex_coordinates
54
+ ... )
55
+ array([90., 90., 90., 90.])
56
+ """
57
+ if len(corner_to_vertex) == 0:
58
+ return np.empty(0, dtype=np.float64)
59
+
60
+ prev_coords = vertex_coordinates[corner_to_vertex[:, 0]]
61
+ curr_coords = vertex_coordinates[corner_to_vertex[:, 1]]
62
+ next_coords = vertex_coordinates[corner_to_vertex[:, 2]]
63
+
64
+ v_in = curr_coords - prev_coords
65
+ v_out = next_coords - curr_coords
66
+
67
+ cross = v_in[:, 0] * v_out[:, 1] - v_in[:, 1] * v_out[:, 0]
68
+ dot = v_in[:, 0] * v_out[:, 0] + v_in[:, 1] * v_out[:, 1]
69
+
70
+ angles_rad = np.arctan2(cross, dot)
71
+
72
+ return np.abs(np.degrees(angles_rad))
@@ -0,0 +1,103 @@
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_assembly_face_corner_normals(
10
+ corner_to_vertex: NDArray[np.int64],
11
+ vertex_coordinates: NDArray[np.float64],
12
+ are_corners_convex: NDArray[np.bool_],
13
+ epsilon: float = 1e-10,
14
+ ) -> NDArray[np.float64]:
15
+ """
16
+ For each corner, return its outward unit normal.
17
+
18
+ The normal is the bisector of the two adjacent edge normals (90° CW
19
+ rotation of each edge direction), oriented outward for convex corners
20
+ and inward for concave ones. For degenerate 180° corners where the
21
+ bisector vanishes, falls back to the incoming edge normal.
22
+
23
+ The term *face* distinguishes this from a purely topological corner:
24
+ the computation relies on face geometry (vertex coordinates) and face
25
+ orientation (convexity), making it applicable to both 2D shape rings
26
+ and 3D solid faces.
27
+
28
+ Parameters
29
+ ----------
30
+ corner_to_vertex : NDArray[np.int64]
31
+ 2D array of shape ``(n_corners, 3)``. Each row is
32
+ ``[prev_vertex, curr_vertex, next_vertex]``.
33
+ vertex_coordinates : NDArray[np.float64]
34
+ 2D array of shape ``(n_vertices, d)`` with vertex coordinates.
35
+ are_corners_convex : NDArray[np.bool_]
36
+ 1D boolean array of shape ``(n_corners,)``. True if the corner
37
+ is convex (normal points outward), False if concave (normal points
38
+ inward).
39
+ epsilon : float, optional
40
+ Threshold below which the bisector norm is considered degenerate
41
+ (straight 180° corner). Defaults to ``1e-10``.
42
+
43
+ Returns
44
+ -------
45
+ NDArray[np.float64]
46
+ 2D array of shape ``(n_corners, 2)``. Each row is a unit vector
47
+ ``[nx, ny]``.
48
+
49
+ Examples
50
+ --------
51
+ >>> import numpy as np
52
+ >>> from scadpy import get_assembly_face_corner_normals
53
+
54
+ >>> # square: 4 convex corners, normals point outward
55
+ >>> # (diagonal directions)
56
+ >>> corner_to_vertex = np.array(
57
+ ... [[3, 0, 1], [0, 1, 2], [1, 2, 3], [2, 3, 0]],
58
+ ... dtype=np.int64
59
+ ... )
60
+ >>> vertex_coordinates = np.array(
61
+ ... [[-1., -1.], [1., -1.], [1., 1.], [-1., 1.]]
62
+ ... )
63
+ >>> are_corners_convex = np.array([True, True, True, True])
64
+ >>> get_assembly_face_corner_normals(
65
+ ... corner_to_vertex,
66
+ ... vertex_coordinates,
67
+ ... are_corners_convex,
68
+ ... ).round(4)
69
+ array([[-0.7071, -0.7071],
70
+ [ 0.7071, -0.7071],
71
+ [ 0.7071, 0.7071],
72
+ [-0.7071, 0.7071]])
73
+ """
74
+ if len(corner_to_vertex) == 0:
75
+ d = vertex_coordinates.shape[1] if vertex_coordinates.ndim == 2 else 0
76
+ return np.empty((0, d), dtype=np.float64)
77
+
78
+ prev_coords = vertex_coordinates[corner_to_vertex[:, 0]]
79
+ curr_coords = vertex_coordinates[corner_to_vertex[:, 1]]
80
+ next_coords = vertex_coordinates[corner_to_vertex[:, 2]]
81
+
82
+ v_in = curr_coords - prev_coords
83
+ v_out = next_coords - curr_coords
84
+
85
+ v_in_norm = v_in / np.linalg.norm(v_in, axis=1, keepdims=True)
86
+ v_out_norm = v_out / np.linalg.norm(v_out, axis=1, keepdims=True)
87
+
88
+ # outward edge normals: 90° CW rotation (dx, dy) → (dy, -dx)
89
+ n_in = np.stack([v_in_norm[:, 1], -v_in_norm[:, 0]], axis=1)
90
+ n_out = np.stack([v_out_norm[:, 1], -v_out_norm[:, 0]], axis=1)
91
+
92
+ bisector = n_in + n_out
93
+
94
+ # degenerate case: 180° corner — bisector vanishes, fall back to incoming edge normal
95
+ zero_mask = np.linalg.norm(bisector, axis=1) < epsilon
96
+ if np.any(zero_mask):
97
+ bisector[zero_mask] = n_in[zero_mask]
98
+
99
+ bisector_norm = bisector / np.linalg.norm(bisector, axis=1, keepdims=True)
100
+
101
+ sign = np.where(are_corners_convex, 1.0, -1.0)[:, np.newaxis]
102
+
103
+ return bisector_norm * sign