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,259 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import numpy as np
6
+ from shapely.geometry import Polygon
7
+ from typeguard import typechecked
8
+
9
+ if TYPE_CHECKING:
10
+ from scadpy import Shape, TopologyFilter
11
+
12
+
13
+ @typechecked
14
+ def chamfer_shape(
15
+ shape: Shape,
16
+ size: float | np.ndarray,
17
+ corner_filter: TopologyFilter[Shape] | None = None,
18
+ epsilon: float = 1e-8,
19
+ ) -> Shape:
20
+ """
21
+ Apply a chamfer (straight cut/fill) to every corner of a shape.
22
+
23
+ Convex corners are cut; concave corners are filled with a straight triangle.
24
+
25
+ Parameters
26
+ ----------
27
+ shape : Shape
28
+ The input shape to chamfer.
29
+ size : float or ndarray
30
+ Distance from each corner vertex to the cut/fill points along the edges.
31
+ Can be:
32
+
33
+ - ``float``: same size on both sides of every corner.
34
+ - ``(n_active,)``: per-active-corner size, same on both sides.
35
+ - ``(n_active, 2)``: per-active-corner, per-side size. Column 0 is
36
+ the incoming side, column 1 is the outgoing side.
37
+
38
+ ``n_active`` is the number of corners selected by ``corner_filter``
39
+ (or all corners if no filter). In all cases, each value is
40
+ automatically clamped to half the length of the corresponding edge
41
+ to avoid overlapping tangent points.
42
+ corner_filter : TopologyFilter[Shape] | None, optional
43
+ Boolean mask or callable ``(shape) -> NDArray[bool]`` of length ``n_corners``
44
+ selecting which corners to chamfer. If None, all corners are chamfered.
45
+ epsilon : float, optional
46
+ Small offset used to avoid coincident edges in boolean operations.
47
+ Defaults to ``1e-8``.
48
+
49
+ Returns
50
+ -------
51
+ Shape
52
+ A new shape with chamfered corners.
53
+
54
+ Examples
55
+ --------
56
+ >>> from scadpy import square, polygon, chamfer_shape
57
+
58
+ >>> sq = square(4)
59
+ >>> l_shape = polygon(
60
+ ... [(0, 0), (4, 0), (4, 2), (2, 2), (2, 4), (0, 4)]
61
+ ... )
62
+ >>> arrow = polygon(
63
+ ... [(0, 1), (3, 0), (5, 2), (3, 4),
64
+ ... (0, 3), (1, 2.5), (1, 1.5)]
65
+ ... )
66
+
67
+ >>> # all corners
68
+ >>> chamfer_shape(sq, size=1.0) # doctest: +SKIP
69
+
70
+ .. render-example::
71
+ :name: chamfer_shape_all
72
+ :example: chamfer_shape(sq, size=1.0)
73
+ :ghost: sq
74
+
75
+ >>> # convex corners only (the 5 outer corners of the L-shape)
76
+ >>> chamfer_shape( # doctest: +SKIP
77
+ ... l_shape, size=0.5,
78
+ ... corner_filter=lambda s: s.are_corners_convex,
79
+ ... )
80
+
81
+ .. render-example::
82
+ :name: chamfer_shape_convex_only
83
+ :example: chamfer_shape(l_shape, size=0.5, corner_filter=lambda s: s.are_corners_convex)
84
+ :ghost: l_shape
85
+
86
+ >>> import numpy as np
87
+
88
+ >>> # asymmetric: one side fills the full edge (length 2),
89
+ >>> # the other stays at 1.0
90
+ >>> chamfer_shape( # doctest: +SKIP
91
+ ... l_shape, size=np.array([[2.0, 1.0]]),
92
+ ... corner_filter=lambda s: ~s.are_corners_convex,
93
+ ... )
94
+
95
+ .. render-example::
96
+ :name: chamfer_shape_concave_asymmetric
97
+ :example: chamfer_shape(l_shape, size=np.array([[2.0, 1.0]]), corner_filter=lambda s: ~s.are_corners_convex)
98
+ :ghost: l_shape
99
+
100
+ >>> # only sharp convex corners (angle > 100°):
101
+ >>> # the two 135° corners of the arrow tail
102
+ >>> chamfer_shape( # doctest: +SKIP
103
+ ... arrow, size=0.4,
104
+ ... corner_filter=lambda s: (
105
+ ... s.are_corners_convex & (s.corner_angles > 100)
106
+ ... ),
107
+ ... )
108
+
109
+ .. render-example::
110
+ :name: chamfer_shape_sharp_convex
111
+ :example: chamfer_shape(arrow, size=0.4, corner_filter=lambda s: s.are_corners_convex & (s.corner_angles > 100))
112
+ :ghost: arrow
113
+
114
+ >>> # oversized: the 2 concave corners share an edge of length ~1;
115
+ >>> # size=10 is clamped proportionally so their contributions
116
+ >>> # sum to the edge length (0.5 + 0.5 each)
117
+ >>> chamfer_shape( # doctest: +SKIP
118
+ ... arrow, size=10,
119
+ ... corner_filter=lambda s: ~s.are_corners_convex,
120
+ ... )
121
+
122
+ .. render-example::
123
+ :name: chamfer_shape_clamp_proportional
124
+ :example: chamfer_shape(arrow, size=10, corner_filter=lambda s: ~s.are_corners_convex)
125
+ :ghost: arrow
126
+
127
+ >>> # wrong size length raises ValueError
128
+ >>> chamfer_shape(
129
+ ... sq, size=np.array([0.5, 0.5, 0.5])
130
+ ... ) # doctest: +ELLIPSIS
131
+ Traceback (most recent call last):
132
+ ...
133
+ ValueError: size array shape (3, 2) does not match...
134
+ """
135
+ from scadpy import resolve_topology_filter, Shape
136
+
137
+ corner_to_vertex = shape.corner_to_vertex
138
+ if len(corner_to_vertex) == 0:
139
+ return shape
140
+
141
+ vertex_coordinates = shape.vertex_coordinates
142
+ is_corner_convex = shape.are_corners_convex
143
+ corner_normals = shape.corner_normals
144
+
145
+ active_mask = resolve_topology_filter(shape, len(corner_to_vertex), corner_filter)
146
+ if active_mask is not None and not np.any(active_mask):
147
+ return shape
148
+
149
+ # Filter all per-corner data to active corners only
150
+ active_indices = (
151
+ np.where(active_mask)[0]
152
+ if active_mask is not None
153
+ else np.arange(len(corner_to_vertex))
154
+ )
155
+ active_is_corner_convex = is_corner_convex[active_indices]
156
+ active_corner_normals = corner_normals[active_indices]
157
+
158
+ current_vertices = vertex_coordinates[corner_to_vertex[active_indices, 1]]
159
+
160
+ incoming_de = shape.corner_to_incoming_directed_edge[active_indices]
161
+ outgoing_de = shape.corner_to_outgoing_directed_edge[active_indices]
162
+ incoming_edge = shape.directed_edge_to_edge[incoming_de]
163
+ outgoing_edge = shape.directed_edge_to_edge[outgoing_de]
164
+
165
+ incoming_directions_normalized = shape.directed_edge_directions[incoming_de]
166
+ outgoing_directions_normalized = shape.directed_edge_directions[outgoing_de]
167
+ edge_lengths_incoming = shape.edge_lengths[incoming_edge]
168
+ edge_lengths_outgoing = shape.edge_lengths[outgoing_edge]
169
+ outward_normals_incoming = shape.edge_normals[incoming_edge]
170
+ outward_normals_outgoing = shape.edge_normals[outgoing_edge]
171
+
172
+ # Resolve size to (n_active, 2): column 0 = incoming side, column 1 = outgoing side
173
+ n_active = len(current_vertices)
174
+ sizes = np.asarray(size, dtype=np.float64)
175
+ if sizes.ndim == 0:
176
+ sizes = np.full((n_active, 2), sizes)
177
+ elif sizes.ndim == 1:
178
+ sizes = np.stack([sizes, sizes], axis=1)
179
+ if sizes.shape != (n_active, 2):
180
+ raise ValueError(
181
+ f"size array shape {sizes.shape} does not match "
182
+ f"expected ({n_active}, 2) for {n_active} active corners"
183
+ )
184
+ # Clamp sizes proportionally so adjacent corners don't overlap on a shared edge.
185
+ # For each active corner, find the adjacent corner on its outgoing/incoming edge.
186
+ # If both are active: scale both contributions so they sum to at most edge_length.
187
+ # If only one is active: it can use the full edge length.
188
+ active_index_of = np.full(len(shape.corner_to_vertex), -1, dtype=np.int64)
189
+ active_index_of[active_indices] = np.arange(n_active, dtype=np.int64)
190
+ de_to_corner = shape.directed_edge_to_corner
191
+ sizes_orig = sizes.copy()
192
+
193
+ adj_target_out = de_to_corner[outgoing_de, 1]
194
+ adj_idx_out = active_index_of[adj_target_out]
195
+ adj_size_out = np.where(adj_idx_out >= 0, sizes_orig[adj_idx_out.clip(0), 0], 0.0)
196
+ total_out = sizes_orig[:, 1] + adj_size_out
197
+ scale_out = np.where(
198
+ total_out > edge_lengths_outgoing, edge_lengths_outgoing / total_out, 1.0
199
+ )
200
+ sizes[:, 1] *= scale_out
201
+
202
+ adj_source_in = de_to_corner[incoming_de, 0]
203
+ adj_idx_in = active_index_of[adj_source_in]
204
+ adj_size_in = np.where(adj_idx_in >= 0, sizes_orig[adj_idx_in.clip(0), 1], 0.0)
205
+ total_in = sizes_orig[:, 0] + adj_size_in
206
+ scale_in = np.where(
207
+ total_in > edge_lengths_incoming, edge_lengths_incoming / total_in, 1.0
208
+ )
209
+ sizes[:, 0] *= scale_in
210
+
211
+ signs = np.where(active_is_corner_convex, 1.0, -1.0)
212
+
213
+ # Tangent points: slightly beyond size to avoid coincident inner points
214
+ tangent_points_incoming = current_vertices - incoming_directions_normalized * (
215
+ sizes[:, 0:1] + epsilon
216
+ )
217
+ tangent_points_outgoing = current_vertices + outgoing_directions_normalized * (
218
+ sizes[:, 1:2] + epsilon
219
+ )
220
+
221
+ # Outer offsets: perpendicular to edge to avoid coincident outer points
222
+ tangent_points_incoming_outer = (
223
+ tangent_points_incoming
224
+ + signs[:, np.newaxis] * outward_normals_incoming * epsilon
225
+ )
226
+ tangent_points_outgoing_outer = (
227
+ tangent_points_outgoing
228
+ + signs[:, np.newaxis] * outward_normals_outgoing * epsilon
229
+ )
230
+
231
+ # Extended corner vertex: push outward along the bisector (corner_normals already signed)
232
+ current_vertices_extended = current_vertices + active_corner_normals * epsilon
233
+
234
+ cutters: list[Polygon] = []
235
+ fillers: list[Polygon] = []
236
+ for i in range(n_active):
237
+ polygon = Polygon(
238
+ [
239
+ current_vertices_extended[i],
240
+ tangent_points_incoming_outer[i],
241
+ tangent_points_incoming[i],
242
+ tangent_points_outgoing[i],
243
+ tangent_points_outgoing_outer[i],
244
+ ]
245
+ )
246
+ if polygon.is_empty or not polygon.is_valid:
247
+ continue
248
+ if active_is_corner_convex[i]:
249
+ cutters.append(polygon)
250
+ else:
251
+ fillers.append(polygon)
252
+
253
+ result = shape
254
+ if cutters:
255
+ result = result - Shape.from_geometries(cutters)
256
+ if fillers:
257
+ result = result | Shape.from_geometries(fillers)
258
+
259
+ return result
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from typeguard import typechecked
6
+
7
+ if TYPE_CHECKING:
8
+ from scadpy import Color, Shape
9
+
10
+
11
+ @typechecked
12
+ def color_shape(shape: Shape, color: Color) -> Shape:
13
+ """Set the color of all parts in a shape.
14
+
15
+ Parameters
16
+ ----------
17
+ shape : Shape
18
+ The shape whose parts will be recolored.
19
+ color : Color
20
+ The RGBA color to apply to all parts.
21
+
22
+ Returns
23
+ -------
24
+ Shape
25
+ A new shape with all parts set to the given color.
26
+
27
+ Examples
28
+ --------
29
+ >>> from scadpy import square, color_shape
30
+ >>> from scadpy.color.constants import RED
31
+
32
+ >>> color_shape(shape=square(4), color=RED) # doctest: +SKIP
33
+
34
+ .. render-example::
35
+ :name: color_shape
36
+ :example: color_shape(shape=square(4), color=RED)
37
+ :keep-color:
38
+ """
39
+ from scadpy import Shape, color_assembly
40
+
41
+ return color_assembly(
42
+ assembly=shape,
43
+ color=color,
44
+ get_assembly_parts=lambda assembly: assembly._parts,
45
+ concat_parts=Shape.from_parts,
46
+ )
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, cast
4
+
5
+ from shapely.geometry import MultiPolygon, Polygon
6
+ from typeguard import typechecked
7
+
8
+ if TYPE_CHECKING:
9
+ from scadpy import Shape, TopologyFilter
10
+
11
+
12
+ @typechecked
13
+ def convexify_shape(
14
+ shape: Shape, part_filter: TopologyFilter[Shape] | None = None
15
+ ) -> Shape:
16
+ """
17
+ Create a new shape whose single part is the convex hull of all parts in the input shape.
18
+
19
+ This function computes the convex hull that encloses all parts of the input shape,
20
+ resulting in a single convex polygon. The color of the resulting part is determined
21
+ by blending the colors of the original parts, weighted by their area.
22
+
23
+ Parameters
24
+ ----------
25
+ shape : Shape
26
+ The input shape whose parts will be merged and convexified.
27
+ part_filter : TopologyFilter[Shape] | None, optional
28
+ A boolean mask selecting which parts to convexify. Parts not selected are left
29
+ unchanged. If None, all parts are convexified.
30
+
31
+ Returns
32
+ -------
33
+ Shape
34
+ A new shape consisting of the convex hull of the selected parts, plus the
35
+ unselected parts unchanged.
36
+
37
+ Examples
38
+ --------
39
+ >>> from scadpy import convexify_shape, square
40
+ >>> import numpy as np
41
+
42
+ >>> a = square(5)
43
+ >>> b = square(2).translate(10)
44
+ >>> c = square(3).translate([4, 8])
45
+ >>> convexify_shape(a + b + c) # doctest: +SKIP
46
+
47
+ .. render-example::
48
+ :name: convexify_shape
49
+ :example: convexify_shape(a + b + c)
50
+ :ghost: a + b + c
51
+
52
+ >>> # partial convexification
53
+ >>> convexify_shape(
54
+ ... a + b + c,
55
+ ... part_filter=np.array([True, True, False])
56
+ ... ) # doctest: +SKIP
57
+
58
+ .. render-example::
59
+ :name: convexify_shape_partial
60
+ :example: convexify_shape(a + b + c, part_filter=np.array([True, True, False]))
61
+ :ghost: a + b + c
62
+ """
63
+ from scadpy import Part, Shape, blend_part_colors, transform_filtered_parts
64
+
65
+ return transform_filtered_parts(
66
+ assembly=shape,
67
+ parts=shape._parts,
68
+ part_filter=part_filter,
69
+ transform=lambda parts: [
70
+ Part[Polygon].from_geometry(
71
+ cast(Polygon, MultiPolygon([p.geometry for p in parts]).convex_hull),
72
+ blend_part_colors(
73
+ parts=parts,
74
+ get_part_magnitude=lambda p: p.geometry.area,
75
+ ),
76
+ )
77
+ ],
78
+ concat_parts=Shape.from_parts,
79
+ )
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from shapely.geometry import Polygon
6
+ from typeguard import typechecked
7
+
8
+ if TYPE_CHECKING:
9
+ from scadpy import Shape, TopologyFilter
10
+
11
+
12
+ @typechecked
13
+ def fill_shape(shape: Shape, part_filter: TopologyFilter[Shape] | None = None) -> Shape:
14
+ """
15
+ Fill the interior holes of each selected part, keeping only the exterior ring.
16
+
17
+ Each selected part's interior rings (holes) are removed, producing a solid
18
+ polygon from its exterior boundary. Unselected parts are left unchanged.
19
+
20
+ Parameters
21
+ ----------
22
+ shape : Shape
23
+ The input shape whose parts will be filled.
24
+ part_filter : TopologyFilter[Shape] | None, optional
25
+ A boolean mask selecting which parts to fill. If None, all parts are filled.
26
+
27
+ Returns
28
+ -------
29
+ Shape
30
+ A new shape with the selected parts filled and the unselected parts unchanged.
31
+
32
+ Examples
33
+ --------
34
+ >>> from scadpy import fill_shape, square, circle
35
+ >>> import numpy as np
36
+
37
+ >>> shape = square(10) - circle(3)
38
+ >>> fill_shape(shape) # doctest: +SKIP
39
+
40
+ .. render-example::
41
+ :name: fill_shape
42
+ :example: fill_shape(shape)
43
+ :ghost: shape
44
+
45
+ >>> # partial fill
46
+ >>> a = square(5) - circle(1)
47
+ >>> b = (square(3) - circle(0.5)).translate(10)
48
+ >>> fill_shape( # doctest: +SKIP
49
+ ... a + b, part_filter=np.array([True, False])
50
+ ... )
51
+
52
+ .. render-example::
53
+ :name: fill_shape_partial
54
+ :example: fill_shape(a + b, part_filter=np.array([True, False]))
55
+ :ghost: a + b
56
+ """
57
+ from scadpy import Part, Shape, transform_filtered_parts
58
+
59
+ return transform_filtered_parts(
60
+ assembly=shape,
61
+ parts=shape._parts,
62
+ part_filter=part_filter,
63
+ transform=lambda parts: [
64
+ Part[Polygon].from_geometry(Polygon(p.geometry.exterior), p.color)
65
+ for p in parts
66
+ ],
67
+ concat_parts=Shape.from_parts,
68
+ )