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,53 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
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 mirror_shape(
14
+ shape: Shape,
15
+ normal: float | Iterable[float],
16
+ pivot: float | Iterable[float] = 0,
17
+ ) -> Shape:
18
+ """Mirror a shape across a line defined by a normal vector and a pivot point.
19
+
20
+ Parameters
21
+ ----------
22
+ shape : Shape
23
+ The shape to mirror.
24
+ normal : float | Iterable[float]
25
+ The normal vector of the mirror line. Does not need to be normalized.
26
+ If a single float is provided, it is broadcast to all coordinate dimensions.
27
+ pivot : float | Iterable[float], default=0
28
+ The point through which the mirror line passes. If a single float is
29
+ provided, it is broadcast to all coordinate dimensions. Defaults to the origin.
30
+
31
+ Returns
32
+ -------
33
+ Shape
34
+ A new shape with all vertices mirrored across the specified line.
35
+
36
+ Examples
37
+ --------
38
+ >>> from scadpy import square, mirror_shape
39
+
40
+ >>> mirror_shape( # doctest: +SKIP
41
+ ... shape=square(4), normal=[1, 0], pivot=[2, 0]
42
+ ... )
43
+
44
+ .. render-example::
45
+ :name: mirror_shape
46
+ :example: mirror_shape(shape=square(4), normal=[1, 0], pivot=[2, 0])
47
+ :ghost: square(4)
48
+ """
49
+ from scadpy import mirror_vertex_coordinates
50
+
51
+ return shape.recoordinate(
52
+ mirror_vertex_coordinates(shape.vertex_coordinates, normal, pivot)
53
+ )
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+ from typing import TYPE_CHECKING
5
+
6
+ from typeguard import typechecked
7
+
8
+
9
+ if TYPE_CHECKING:
10
+ from scadpy import Shape, TopologyFilter
11
+
12
+
13
+ @typechecked
14
+ def pull_shape(
15
+ shape: Shape,
16
+ distance: float,
17
+ pivot: float | Iterable[float] = 0,
18
+ vertex_filter: TopologyFilter[Shape] | None = None,
19
+ ) -> Shape:
20
+ """Move a subset of shape vertices toward a pivot point by a given distance.
21
+
22
+ Each selected vertex is translated toward the pivot by at most ``distance`` units.
23
+ Vertices already closer than ``distance`` to the pivot are moved to the pivot.
24
+
25
+ Parameters
26
+ ----------
27
+ shape : Shape
28
+ The shape whose vertices will be pulled.
29
+ distance : float
30
+ The maximum distance each vertex is moved toward the pivot.
31
+ pivot : float | Iterable[float], default=0
32
+ The attraction point. If a single float is provided, it is broadcast
33
+ to all coordinate dimensions. Defaults to the origin.
34
+ vertex_filter : TopologyFilter[Shape] | None, default=None
35
+ A boolean array or callable selecting which vertices are affected. If ``None``,
36
+ all vertices are moved.
37
+
38
+ Returns
39
+ -------
40
+ Shape
41
+ A new shape with the selected vertices moved toward the pivot.
42
+
43
+ See Also
44
+ --------
45
+ push_shape : Move shape vertices away from a pivot point.
46
+
47
+ Examples
48
+ --------
49
+ >>> from scadpy import square, pull_shape
50
+
51
+ >>> shape = square(4)
52
+ >>> pull_shape( # doctest: +SKIP
53
+ ... shape=shape, distance=1.0, pivot=[2, 2],
54
+ ... vertex_filter=shape.vertex_coordinates[:, 0] < 1,
55
+ ... )
56
+
57
+ .. render-example::
58
+ :name: pull_shape
59
+ :example: pull_shape(shape=square(4), distance=1.0, pivot=[2, 2], vertex_filter=square(4).vertex_coordinates[:, 0] < 1)
60
+ :ghost: square(4)
61
+ """
62
+ from scadpy import resolve_topology_filter, pull_vertex_coordinates
63
+
64
+ resolved_vertex_filter = resolve_topology_filter(shape, len(shape.vertex_coordinates), vertex_filter)
65
+ return shape.recoordinate(
66
+ pull_vertex_coordinates(shape.vertex_coordinates, distance, pivot, resolved_vertex_filter)
67
+ )
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+ from typing import TYPE_CHECKING
5
+
6
+ from typeguard import typechecked
7
+
8
+
9
+ if TYPE_CHECKING:
10
+ from scadpy import Shape, TopologyFilter
11
+
12
+
13
+ @typechecked
14
+ def push_shape(
15
+ shape: Shape,
16
+ distance: float,
17
+ pivot: float | Iterable[float] = 0,
18
+ vertex_filter: TopologyFilter[Shape] | None = None,
19
+ ) -> Shape:
20
+ """Move a subset of shape vertices away from a pivot point by a given distance.
21
+
22
+ Each selected vertex is translated away from the pivot by exactly ``distance`` units.
23
+ Vertices located exactly at the pivot are not moved (undefined direction).
24
+
25
+ Parameters
26
+ ----------
27
+ shape : Shape
28
+ The shape whose vertices will be pushed.
29
+ distance : float
30
+ The distance each vertex is moved away from the pivot.
31
+ pivot : float | Iterable[float], default=0
32
+ The repulsion point. If a single float is provided, it is broadcast
33
+ to all coordinate dimensions. Defaults to the origin.
34
+ vertex_filter : TopologyFilter[Shape] | None, default=None
35
+ A boolean array or callable selecting which vertices are affected. If ``None``,
36
+ all vertices are moved.
37
+
38
+ Returns
39
+ -------
40
+ Shape
41
+ A new shape with the selected vertices moved away from the pivot.
42
+
43
+ See Also
44
+ --------
45
+ pull_shape : Move shape vertices toward a pivot point.
46
+
47
+ Examples
48
+ --------
49
+ >>> from scadpy import square, push_shape
50
+
51
+ >>> shape = square(4)
52
+ >>> push_shape( # doctest: +SKIP
53
+ ... shape=shape, distance=1.0, pivot=[2, 2],
54
+ ... vertex_filter=shape.vertex_coordinates[:, 0] < 1,
55
+ ... )
56
+
57
+ .. render-example::
58
+ :name: push_shape
59
+ :example: push_shape(shape=square(4), distance=1.0, pivot=[2, 2], vertex_filter=square(4).vertex_coordinates[:, 0] < 1)
60
+ :ghost: square(4)
61
+ """
62
+ from scadpy import resolve_topology_filter, push_vertex_coordinates
63
+
64
+ resolved_vertex_filter = resolve_topology_filter(shape, len(shape.vertex_coordinates), vertex_filter)
65
+ return shape.recoordinate(
66
+ push_vertex_coordinates(shape.vertex_coordinates, distance, pivot, resolved_vertex_filter)
67
+ )
@@ -0,0 +1,285 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+ from typing import TYPE_CHECKING
5
+
6
+ import numpy as np
7
+ from numpy.typing import NDArray
8
+ from shapely.geometry.polygon import Polygon
9
+ from trimesh import Trimesh
10
+ from trimesh.creation import triangulate_polygon
11
+ from trimesh.geometry import faces_to_edges
12
+ from trimesh.grouping import group_rows
13
+ from typeguard import typechecked
14
+
15
+ if TYPE_CHECKING:
16
+ from scadpy.d2.shape import Shape
17
+ from scadpy.d3.solid import Solid
18
+
19
+
20
+ def _rotate_3d(points, axis, angle, pivot):
21
+ points = points - pivot
22
+
23
+ axis = axis / np.linalg.norm(axis)
24
+ cos_a = np.cos(angle)
25
+ sin_a = np.sin(angle)
26
+ one_minus_cos = 1 - cos_a
27
+ x, y, z = axis
28
+
29
+ R = np.array(
30
+ [
31
+ [
32
+ cos_a + x * x * one_minus_cos,
33
+ x * y * one_minus_cos - z * sin_a,
34
+ x * z * one_minus_cos + y * sin_a,
35
+ ],
36
+ [
37
+ y * x * one_minus_cos + z * sin_a,
38
+ cos_a + y * y * one_minus_cos,
39
+ y * z * one_minus_cos - x * sin_a,
40
+ ],
41
+ [
42
+ z * x * one_minus_cos - y * sin_a,
43
+ z * y * one_minus_cos + x * sin_a,
44
+ cos_a + z * z * one_minus_cos,
45
+ ],
46
+ ]
47
+ )
48
+
49
+ rotated = points @ R.T
50
+
51
+ return rotated + pivot
52
+
53
+
54
+
55
+ def _radial_extrude_shapely_polygon_into_trimesh(
56
+ polygon: Polygon,
57
+ axis: float | Iterable[float],
58
+ start: float = 0,
59
+ end: float = 360,
60
+ pivot: float | Iterable[float] = 0,
61
+ segment_count: int = 64,
62
+ ) -> Trimesh:
63
+ from scadpy import resolve_vector_2d, resolve_vector_3d
64
+
65
+ slice_count = segment_count + 1
66
+
67
+ start = start % 360
68
+ end = end % 360
69
+ is_ring = False
70
+ if start == end:
71
+ slice_count -= 1
72
+ is_ring = True
73
+ if end <= start:
74
+ end += 360
75
+
76
+ angles = np.linspace(np.radians(start), np.radians(end), segment_count + 1)
77
+ axis = resolve_vector_2d(axis, 0)
78
+ pivot = resolve_vector_2d(pivot, 0)
79
+
80
+ triangulated_vertex_coordinates, triangulated_face_to_vertex = triangulate_polygon(
81
+ polygon, engine="triangle"
82
+ )
83
+ triangulated_edge_to_vertex = faces_to_edges(triangulated_face_to_vertex)
84
+
85
+ slice_border_vertex_coordinates = _create_slice_border_vertex_coordinates(
86
+ triangulated_vertex_coordinates, slice_count
87
+ )
88
+
89
+ triangulated_border_vertex_count = len(triangulated_vertex_coordinates)
90
+ triangulated_edge_to_vertex_sorted = np.sort(triangulated_edge_to_vertex, axis=1)
91
+ triangulated_edge_unique = group_rows(
92
+ triangulated_edge_to_vertex_sorted, require_count=1
93
+ )
94
+ slice_border_faces = _create_slice_border_faces(
95
+ triangulated_edge_to_vertex[triangulated_edge_unique],
96
+ triangulated_border_vertex_count,
97
+ slice_count - 1,
98
+ is_ring,
99
+ )
100
+
101
+ # make slices
102
+ final_vertex_coordinate_slices = [
103
+ slice_border_vertex_coordinates[
104
+ i * triangulated_border_vertex_count : i * triangulated_border_vertex_count
105
+ + triangulated_border_vertex_count
106
+ ]
107
+ for i in range(0, slice_count)
108
+ ]
109
+ for i, slice in enumerate(final_vertex_coordinate_slices):
110
+ slice[:] = _rotate_3d(
111
+ slice, resolve_vector_3d(axis, 0), angles[i], resolve_vector_3d(pivot, 0)
112
+ )
113
+
114
+ centroid_x = polygon.centroid.x - float(pivot[0])
115
+ centroid_y = polygon.centroid.y - float(pivot[1])
116
+ chirality = float(axis[0]) * centroid_y - float(axis[1]) * centroid_x
117
+
118
+ if chirality < 0:
119
+ slice_border_faces = slice_border_faces[:, ::-1]
120
+
121
+ last_slice_border_faces = (
122
+ triangulated_face_to_vertex + triangulated_border_vertex_count * (slice_count - 1)
123
+ )
124
+ if not is_ring:
125
+ start_cap_faces = triangulated_face_to_vertex
126
+ end_cap_faces = last_slice_border_faces
127
+ if chirality < 0:
128
+ end_cap_faces = end_cap_faces[:, ::-1]
129
+ else:
130
+ start_cap_faces = start_cap_faces[:, ::-1]
131
+
132
+ slice_border_faces = np.concatenate(
133
+ [start_cap_faces, slice_border_faces, end_cap_faces]
134
+ )
135
+
136
+ mesh = Trimesh(vertices=slice_border_vertex_coordinates, faces=slice_border_faces)
137
+ return mesh
138
+
139
+
140
+ def _create_slice_border_vertex_coordinates(
141
+ triangulated_border_vertex_coordinates: NDArray[np.float64],
142
+ triangulated_slice_count: int,
143
+ ) -> NDArray[np.float64]:
144
+ border_vertex_count = len(triangulated_border_vertex_coordinates)
145
+ vertex_coordinates = np.zeros(
146
+ (
147
+ border_vertex_count * triangulated_slice_count,
148
+ 3,
149
+ ),
150
+ dtype=np.float64,
151
+ )
152
+ vertex_coordinates[:, :2] = np.tile(
153
+ triangulated_border_vertex_coordinates, (triangulated_slice_count, 1)
154
+ )
155
+ return vertex_coordinates
156
+
157
+
158
+ def _create_slice_border_faces(
159
+ triangulated_border_edge_to_vertex: NDArray[np.int64],
160
+ triangulated_border_vertex_count: int,
161
+ slice_count: int,
162
+ is_ring: bool,
163
+ ) -> NDArray[np.int64]:
164
+ all_edges = np.tile(triangulated_border_edge_to_vertex, (slice_count, 1))
165
+ layer_offsets = (
166
+ np.repeat(np.arange(slice_count), len(triangulated_border_edge_to_vertex))
167
+ * triangulated_border_vertex_count
168
+ )
169
+ all_edges += layer_offsets.reshape(-1, 1)
170
+
171
+ v0 = all_edges[:, 0]
172
+ v1 = all_edges[:, 1]
173
+ v2 = v0 + triangulated_border_vertex_count
174
+ v3 = v1 + triangulated_border_vertex_count
175
+
176
+ triangle1 = np.column_stack([v0, v1, v2])
177
+ triangle2 = np.column_stack([v1, v3, v2])
178
+
179
+ faces = np.vstack([triangle1, triangle2])
180
+
181
+ if is_ring:
182
+ last_edges = (
183
+ triangulated_border_edge_to_vertex
184
+ + triangulated_border_vertex_count * slice_count
185
+ )
186
+ first_edges = triangulated_border_edge_to_vertex
187
+ v0 = last_edges[:, 0]
188
+ v1 = last_edges[:, 1]
189
+ v2 = first_edges[:, 0]
190
+ v3 = first_edges[:, 1]
191
+
192
+ triangle1 = np.column_stack([v0, v1, v2])
193
+ triangle2 = np.column_stack([v1, v3, v2])
194
+
195
+ ring_faces = np.vstack([triangle1, triangle2])
196
+ faces = np.vstack([faces, ring_faces])
197
+
198
+ return faces
199
+
200
+
201
+ @typechecked
202
+ def radial_extrude_shape(
203
+ shape: Shape,
204
+ axis: float | Iterable[float],
205
+ start: float = 0,
206
+ end: float = 360,
207
+ pivot: float | Iterable[float] = 0,
208
+ segment_count: int = 64,
209
+ ) -> Solid:
210
+ """Revolve a 2D shape around an axis to produce a 3D solid.
211
+
212
+ Each part of the shape is independently revolved around the given axis.
213
+ The axis is defined in 2D (the Z component is always 0). The revolution
214
+ spans from ``start`` to ``end`` degrees. A full 360° revolution produces
215
+ a closed solid; a partial revolution leaves the ends open. If ``start``
216
+ equals ``end``, an empty solid is returned.
217
+
218
+ Parameters
219
+ ----------
220
+ shape : Shape
221
+ The 2D shape to revolve.
222
+ axis : float or Iterable[float]
223
+ The 2D axis of revolution (e.g. ``[0, 1]`` for the Y-axis).
224
+ start : float, optional
225
+ Starting angle in degrees. Default is ``0``.
226
+ end : float, optional
227
+ Ending angle in degrees. Default is ``360`` (full revolution). If
228
+ equal to ``start``, an empty solid is returned.
229
+ pivot : float or Iterable[float], optional
230
+ 2D pivot point for the revolution. Default is ``0`` (origin).
231
+ segment_count : int, optional
232
+ Number of angular segments used to approximate the revolution.
233
+ Higher values produce smoother results. Default is ``64``.
234
+
235
+ Returns
236
+ -------
237
+ Solid
238
+ The resulting 3D solid.
239
+
240
+ Examples
241
+ --------
242
+ >>> from scadpy import radial_extrude_shape, circle
243
+
244
+ >>> # torus: circle profile offset from the Y-axis, revolved 360°
245
+ >>> radial_extrude_shape( # doctest: +SKIP
246
+ ... shape=circle(radius=0.5).translate([2, 0]),
247
+ ... axis=[0, 1],
248
+ ... )
249
+
250
+ .. render-example::
251
+ :name: radial_extrude_shape
252
+ :example: radial_extrude_shape(shape=circle(radius=0.5).translate([2, 0]), axis=[0, 1])
253
+
254
+ >>> # partial torus (270°)
255
+ >>> radial_extrude_shape( # doctest: +SKIP
256
+ ... shape=circle(radius=0.5).translate([2, 0]),
257
+ ... axis=[0, 1], end=270,
258
+ ... )
259
+
260
+ .. render-example::
261
+ :name: radial_extrude_shape_partial
262
+ :example: radial_extrude_shape(shape=circle(radius=0.5).translate([2, 0]), axis=[0, 1], end=270)
263
+ """
264
+ from scadpy import Part, Solid, linear_cut_shape, unify_solid
265
+
266
+ if start == end:
267
+ return Solid.from_parts([])
268
+
269
+ cut_shape = linear_cut_shape(shape=shape, axis=axis, pivot=pivot)
270
+ parts = [
271
+ Part[Trimesh].from_geometry(
272
+ _radial_extrude_shapely_polygon_into_trimesh(
273
+ p.geometry, axis, start, end, pivot, segment_count
274
+ ),
275
+ p.color,
276
+ )
277
+ for p in cut_shape._parts
278
+ ]
279
+
280
+ if not parts:
281
+ return Solid.from_parts([])
282
+ solids = [Solid.from_parts([p]) for p in parts]
283
+ if len(solids) == 1:
284
+ return solids[0]
285
+ return unify_solid(solids=solids)
@@ -0,0 +1,132 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+ from math import cos, radians, sin
5
+ from typing import TYPE_CHECKING
6
+
7
+ import numpy as np
8
+ from shapely.geometry.polygon import Polygon
9
+ from typeguard import typechecked
10
+
11
+ if TYPE_CHECKING:
12
+ from scadpy import Shape, TopologyFilter
13
+
14
+
15
+ @typechecked
16
+ def radial_slice_shape(
17
+ shape: Shape,
18
+ start: float = 0,
19
+ end: float = 360,
20
+ pivot: float | Iterable[float] = 0,
21
+ part_filter: TopologyFilter[Shape] | None = None,
22
+ ) -> Shape:
23
+ """
24
+ Slice a shape by keeping only the angular sector between two angles.
25
+
26
+ Constructs a pie-shaped wedge from the pivot point spanning the angular range
27
+ from start to end, and intersects it with the selected parts. Only the portion
28
+ of the shape within the wedge remains.
29
+
30
+ Parameters
31
+ ----------
32
+ shape : Shape
33
+ The input shape to slice.
34
+ start : float, optional
35
+ The start angle of the sector in degrees. Defaults to 0.
36
+ end : float, optional
37
+ The end angle of the sector in degrees. Defaults to 360.
38
+ pivot : float | Iterable[float], optional
39
+ The center point of the angular sector. Defaults to the origin.
40
+ part_filter : TopologyFilter[Shape] | None, optional
41
+ A boolean mask selecting which parts to slice. If None, all parts are sliced.
42
+
43
+ Returns
44
+ -------
45
+ Shape
46
+ A new shape containing only the angular sector of the selected parts,
47
+ plus the unselected parts unchanged. If start equals end, the shape
48
+ is returned unchanged.
49
+
50
+ Examples
51
+ --------
52
+ >>> from scadpy import radial_slice_shape, square, circle
53
+ >>> import numpy as np
54
+
55
+ >>> shape = square(10) - circle(3)
56
+
57
+ >>> # quarter slice
58
+ >>> radial_slice_shape(shape, start=0, end=90) # doctest: +SKIP
59
+
60
+ .. render-example::
61
+ :name: radial_slice_shape_quarter
62
+ :example: radial_slice_shape(shape, start=0, end=90)
63
+ :ghost: shape
64
+
65
+ >>> # three quarter slice
66
+ >>> radial_slice_shape(shape, start=45, end=315) # doctest: +SKIP
67
+
68
+ .. render-example::
69
+ :name: radial_slice_shape_three_quarter
70
+ :example: radial_slice_shape(shape, start=45, end=315)
71
+ :ghost: shape
72
+
73
+ >>> # off-center pivot
74
+ >>> radial_slice_shape( # doctest: +SKIP
75
+ ... shape, start=0, end=180, pivot=[3, 3]
76
+ ... )
77
+
78
+ .. render-example::
79
+ :name: radial_slice_shape_pivot
80
+ :example: radial_slice_shape(shape, start=0, end=180, pivot=[3, 3])
81
+ :ghost: shape
82
+
83
+ >>> # partial slice on a composite shape
84
+ >>> a = square(6) - circle(2)
85
+ >>> b = circle(3).translate(10)
86
+
87
+ >>> radial_slice_shape( # doctest: +SKIP
88
+ ... a + b, start=0, end=120,
89
+ ... part_filter=np.array([True, False]),
90
+ ... )
91
+
92
+ .. render-example::
93
+ :name: radial_slice_shape_partial
94
+ :example: radial_slice_shape(a + b, start=0, end=120, part_filter=np.array([True, False]))
95
+ :ghost: a + b
96
+ """
97
+ from scadpy import resolve_vector_2d, transform_filtered_parts, Shape
98
+
99
+ pivot = resolve_vector_2d(pivot, 0)
100
+
101
+ start = start % 360
102
+ end = end % 360
103
+ if start == end:
104
+ return shape
105
+ if end <= start:
106
+ end += 360
107
+
108
+ bounds = shape.bounds
109
+ radius = max(bounds[2] - bounds[0], bounds[3] - bounds[1]) * 2
110
+
111
+ n = max(2, int((end - start) / 5))
112
+ if n == 2:
113
+ theta = [radians(start), radians(end)]
114
+ else:
115
+ theta = np.linspace(radians(start), radians(end), n + 1)
116
+
117
+ px, py = pivot
118
+ points = (
119
+ [(px, py)]
120
+ + [(px + radius * cos(t), py + radius * sin(t)) for t in theta]
121
+ + [(px, py)]
122
+ )
123
+
124
+ slice_mask = Shape.from_geometries([Polygon(points)])
125
+
126
+ return transform_filtered_parts(
127
+ assembly=shape,
128
+ parts=shape._parts,
129
+ part_filter=part_filter,
130
+ transform=lambda parts: (Shape.from_parts(parts) & slice_mask)._parts,
131
+ concat_parts=Shape.from_parts,
132
+ )
@@ -0,0 +1,82 @@
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 shapely.geometry import Polygon
8
+ from typeguard import typechecked
9
+
10
+ if TYPE_CHECKING:
11
+ from scadpy.d2.shape import Shape
12
+
13
+
14
+ @typechecked
15
+ def recoordinate_shape(
16
+ shape: Shape, vertex_coordinates: NDArray[np.float64]
17
+ ) -> Shape:
18
+ """Rebuild a shape with new vertex coordinates, preserving topology and colors.
19
+
20
+ Parameters
21
+ ----------
22
+ shape : Shape
23
+ The source shape providing topology (part/ring structure) and colors.
24
+ vertex_coordinates : NDArray[np.float64]
25
+ New vertex coordinates of shape ``(n_vertices, 2)``, in the same order
26
+ as :func:`get_shape_vertex_coordinates`.
27
+
28
+ Returns
29
+ -------
30
+ Shape
31
+ A new shape with the same topology as *shape* but at the new positions.
32
+
33
+ Examples
34
+ --------
35
+ >>> import numpy as np
36
+ >>> from shapely.geometry import Polygon
37
+ >>> from scadpy import recoordinate_shape, Shape, square
38
+
39
+ >>> recoordinate_shape( # doctest: +SKIP
40
+ ... shape=square(4),
41
+ ... vertex_coordinates=(
42
+ ... square(4).vertex_coordinates + [2.0, 1.0]
43
+ ... ),
44
+ ... )
45
+
46
+ .. render-example::
47
+ :name: recoordinate_shape
48
+ :example: recoordinate_shape(shape=square(4), vertex_coordinates=square(4).vertex_coordinates + [2.0, 1.0])
49
+ :ghost: square(4)
50
+ """
51
+ from scadpy.core.part import Part
52
+
53
+ vertex_to_part = shape.vertex_to_part
54
+ vertex_to_ring = shape.vertex_to_ring
55
+ part_colors = shape.part_colors
56
+ ring_types = shape.ring_types
57
+
58
+ parts = []
59
+ for part_index in np.unique(vertex_to_part):
60
+ part_mask = vertex_to_part == part_index
61
+ part_coords = vertex_coordinates[part_mask]
62
+ part_ring_indices = vertex_to_ring[part_mask]
63
+ color = list(part_colors[part_index])
64
+
65
+ exterior = None
66
+ interiors = []
67
+ for ring_index in np.unique(part_ring_indices):
68
+ ring_mask = part_ring_indices == ring_index
69
+ coordinates = part_coords[ring_mask]
70
+ if ring_types[ring_index] == "exterior":
71
+ exterior = coordinates
72
+ else:
73
+ interiors.append(coordinates)
74
+
75
+ parts.append(
76
+ Part[Polygon].from_geometry(
77
+ Polygon(shell=exterior, holes=interiors),
78
+ color,
79
+ )
80
+ )
81
+
82
+ return shape.from_parts(parts)