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.
- scadpy/__init__.py +5 -0
- scadpy/color/__init__.py +3 -0
- scadpy/color/constants/BEIGE.py +3 -0
- scadpy/color/constants/BLACK.py +3 -0
- scadpy/color/constants/BLUE.py +3 -0
- scadpy/color/constants/BROWN.py +3 -0
- scadpy/color/constants/DARK_GRAY.py +3 -0
- scadpy/color/constants/DEFAULT_COLOR.py +3 -0
- scadpy/color/constants/DEFAULT_OPACITY.py +1 -0
- scadpy/color/constants/GRAY.py +3 -0
- scadpy/color/constants/GREEN.py +3 -0
- scadpy/color/constants/ORANGE.py +3 -0
- scadpy/color/constants/RED.py +3 -0
- scadpy/color/constants/WHITE.py +3 -0
- scadpy/color/constants/YELLOW.py +3 -0
- scadpy/color/constants/__init__.py +29 -0
- scadpy/color/type/__init__.py +3 -0
- scadpy/color/type/color.py +3 -0
- scadpy/color/utils/__init__.py +3 -0
- scadpy/color/utils/get_random_color.py +36 -0
- scadpy/core/__init__.py +3 -0
- scadpy/core/assembly/__init__.py +5 -0
- scadpy/core/assembly/combinations/__init__.py +13 -0
- scadpy/core/assembly/combinations/concat_assemblies.py +50 -0
- scadpy/core/assembly/combinations/exclude_assemblies.py +135 -0
- scadpy/core/assembly/combinations/intersect_assemblies.py +128 -0
- scadpy/core/assembly/combinations/subtract_assemblies.py +151 -0
- scadpy/core/assembly/combinations/unify_assemblies.py +59 -0
- scadpy/core/assembly/topologies/__init__.py +41 -0
- scadpy/core/assembly/topologies/directed_edge/__init__.py +9 -0
- scadpy/core/assembly/topologies/directed_edge/get_assembly_directed_edge_directions.py +70 -0
- scadpy/core/assembly/topologies/directed_edge/get_assembly_directed_edge_to_edge.py +49 -0
- scadpy/core/assembly/topologies/directed_edge/get_assembly_directed_edge_to_vertex.py +54 -0
- scadpy/core/assembly/topologies/edge/__init__.py +9 -0
- scadpy/core/assembly/topologies/edge/get_assembly_edge_lengths.py +46 -0
- scadpy/core/assembly/topologies/edge/get_assembly_edge_midpoints.py +51 -0
- scadpy/core/assembly/topologies/edge/get_assembly_edge_normals.py +67 -0
- scadpy/core/assembly/topologies/face_corner/__init__.py +13 -0
- scadpy/core/assembly/topologies/face_corner/get_assembly_face_corner_angles.py +72 -0
- scadpy/core/assembly/topologies/face_corner/get_assembly_face_corner_normals.py +103 -0
- scadpy/core/assembly/topologies/face_corner/get_assembly_face_corner_to_incoming_directed_edge.py +65 -0
- scadpy/core/assembly/topologies/face_corner/get_assembly_face_corner_to_outgoing_directed_edge.py +65 -0
- scadpy/core/assembly/topologies/face_corner/get_assembly_face_directed_edge_to_corner.py +79 -0
- scadpy/core/assembly/topologies/part/__init__.py +5 -0
- scadpy/core/assembly/topologies/part/get_assembly_part_colors.py +55 -0
- scadpy/core/assembly/topologies/vertex/__init__.py +7 -0
- scadpy/core/assembly/topologies/vertex/get_assembly_vertex_coordinates.py +70 -0
- scadpy/core/assembly/topologies/vertex/get_assembly_vertex_to_part.py +62 -0
- scadpy/core/assembly/transformations/__init__.py +19 -0
- scadpy/core/assembly/transformations/color_assembly.py +24 -0
- scadpy/core/assembly/transformations/mirror_vertex_coordinates.py +68 -0
- scadpy/core/assembly/transformations/pull_vertex_coordinates.py +64 -0
- scadpy/core/assembly/transformations/push_vertex_coordinates.py +64 -0
- scadpy/core/assembly/transformations/resize_vertex_coordinates.py +121 -0
- scadpy/core/assembly/transformations/rotate_vertex_coordinates.py +73 -0
- scadpy/core/assembly/transformations/scale_vertex_coordinates.py +76 -0
- scadpy/core/assembly/transformations/translate_vertex_coordinates.py +70 -0
- scadpy/core/assembly/types/__init__.py +7 -0
- scadpy/core/assembly/types/assembly.py +14 -0
- scadpy/core/assembly/types/topology_filter.py +6 -0
- scadpy/core/assembly/utils/__init__.py +9 -0
- scadpy/core/assembly/utils/lookup_pairs.py +56 -0
- scadpy/core/assembly/utils/resolve_topology_filter.py +84 -0
- scadpy/core/assembly/utils/transform_filtered_parts.py +55 -0
- scadpy/core/component/__init__.py +3 -0
- scadpy/core/component/exporters/__init__.py +7 -0
- scadpy/core/component/exporters/map_component_to_html_file.py +47 -0
- scadpy/core/component/exporters/map_component_to_screen.py +63 -0
- scadpy/core/component/features/__init__.py +5 -0
- scadpy/core/component/features/get_component_bounds.py +38 -0
- scadpy/core/component/utils/__init__.py +9 -0
- scadpy/core/component/utils/blend_component_colors.py +77 -0
- scadpy/core/component/utils/get_intersecting_component_index_groups.py +108 -0
- scadpy/core/part/__init__.py +3 -0
- scadpy/core/part/combinations/__init__.py +11 -0
- scadpy/core/part/combinations/concat_parts.py +48 -0
- scadpy/core/part/combinations/intersect_parts.py +147 -0
- scadpy/core/part/combinations/subtract_parts.py +94 -0
- scadpy/core/part/combinations/unify_parts.py +143 -0
- scadpy/core/part/types/__init__.py +5 -0
- scadpy/core/part/types/part.py +34 -0
- scadpy/core/part/utils/__init__.py +5 -0
- scadpy/core/part/utils/blend_part_colors.py +32 -0
- scadpy/d2/__init__.py +2 -0
- scadpy/d2/shape/__init__.py +9 -0
- scadpy/d2/shape/combinations/__init__.py +21 -0
- scadpy/d2/shape/combinations/are_shape_parts_intersecting.py +49 -0
- scadpy/d2/shape/combinations/concat_shape.py +48 -0
- scadpy/d2/shape/combinations/exclude_shape.py +71 -0
- scadpy/d2/shape/combinations/intersect_shape.py +64 -0
- scadpy/d2/shape/combinations/intersect_shape_parts.py +71 -0
- scadpy/d2/shape/combinations/subtract_shape.py +72 -0
- scadpy/d2/shape/combinations/subtract_shape_parts.py +66 -0
- scadpy/d2/shape/combinations/unify_shape.py +51 -0
- scadpy/d2/shape/combinations/unify_shape_parts.py +74 -0
- scadpy/d2/shape/exporters/__init__.py +17 -0
- scadpy/d2/shape/exporters/map_shape_to_dxf.py +43 -0
- scadpy/d2/shape/exporters/map_shape_to_dxf_file.py +38 -0
- scadpy/d2/shape/exporters/map_shape_to_html.py +117 -0
- scadpy/d2/shape/exporters/map_shape_to_html_file.py +58 -0
- scadpy/d2/shape/exporters/map_shape_to_screen.py +51 -0
- scadpy/d2/shape/exporters/map_shape_to_svg.py +40 -0
- scadpy/d2/shape/exporters/map_shape_to_svg_file.py +38 -0
- scadpy/d2/shape/features/__init__.py +9 -0
- scadpy/d2/shape/features/get_shape_bounds.py +40 -0
- scadpy/d2/shape/features/get_shape_part_bounds.py +37 -0
- scadpy/d2/shape/features/is_shape_empty.py +38 -0
- scadpy/d2/shape/importers/__init__.py +13 -0
- scadpy/d2/shape/importers/map_dxf_to_shape.py +55 -0
- scadpy/d2/shape/importers/map_geometries_to_shape.py +45 -0
- scadpy/d2/shape/importers/map_geometry_to_shape.py +43 -0
- scadpy/d2/shape/importers/map_parts_to_shape.py +62 -0
- scadpy/d2/shape/importers/map_svg_to_shape.py +55 -0
- scadpy/d2/shape/primitives/__init__.py +6 -0
- scadpy/d2/shape/primitives/circle.py +68 -0
- scadpy/d2/shape/primitives/polygon.py +86 -0
- scadpy/d2/shape/primitives/rectangle.py +85 -0
- scadpy/d2/shape/primitives/square.py +57 -0
- scadpy/d2/shape/topologies/__init__.py +53 -0
- scadpy/d2/shape/topologies/corner/__init__.py +15 -0
- scadpy/d2/shape/topologies/corner/are_shape_corners_convex.py +75 -0
- scadpy/d2/shape/topologies/corner/get_shape_corner_angles.py +58 -0
- scadpy/d2/shape/topologies/corner/get_shape_corner_normals.py +82 -0
- scadpy/d2/shape/topologies/corner/get_shape_corner_to_incoming_directed_edge.py +39 -0
- scadpy/d2/shape/topologies/corner/get_shape_corner_to_outgoing_directed_edge.py +39 -0
- scadpy/d2/shape/topologies/corner/get_shape_corner_to_vertex.py +65 -0
- scadpy/d2/shape/topologies/directed_edge/__init__.py +11 -0
- scadpy/d2/shape/topologies/directed_edge/get_shape_directed_edge_directions.py +44 -0
- scadpy/d2/shape/topologies/directed_edge/get_shape_directed_edge_to_corner.py +41 -0
- scadpy/d2/shape/topologies/directed_edge/get_shape_directed_edge_to_edge.py +51 -0
- scadpy/d2/shape/topologies/directed_edge/get_shape_directed_edge_to_vertex.py +63 -0
- scadpy/d2/shape/topologies/edge/__init__.py +11 -0
- scadpy/d2/shape/topologies/edge/get_shape_edge_lengths.py +43 -0
- scadpy/d2/shape/topologies/edge/get_shape_edge_midpoints.py +46 -0
- scadpy/d2/shape/topologies/edge/get_shape_edge_normals.py +40 -0
- scadpy/d2/shape/topologies/edge/get_shape_edge_to_vertex.py +71 -0
- scadpy/d2/shape/topologies/ring/__init__.py +7 -0
- scadpy/d2/shape/topologies/ring/get_shape_ring_to_part.py +46 -0
- scadpy/d2/shape/topologies/ring/get_shape_ring_types.py +46 -0
- scadpy/d2/shape/topologies/vertex/__init__.py +11 -0
- scadpy/d2/shape/topologies/vertex/get_shape_part_vertex_coordinates.py +62 -0
- scadpy/d2/shape/topologies/vertex/get_shape_vertex_coordinates.py +44 -0
- scadpy/d2/shape/topologies/vertex/get_shape_vertex_to_part.py +42 -0
- scadpy/d2/shape/topologies/vertex/get_shape_vertex_to_ring.py +63 -0
- scadpy/d2/shape/transformations/__init__.py +43 -0
- scadpy/d2/shape/transformations/chamfer_shape.py +259 -0
- scadpy/d2/shape/transformations/color_shape.py +46 -0
- scadpy/d2/shape/transformations/convexify_shape.py +79 -0
- scadpy/d2/shape/transformations/fill_shape.py +68 -0
- scadpy/d2/shape/transformations/fillet_shape.py +289 -0
- scadpy/d2/shape/transformations/grow_shape.py +82 -0
- scadpy/d2/shape/transformations/linear_cut_shape.py +116 -0
- scadpy/d2/shape/transformations/linear_extrude_shape.py +60 -0
- scadpy/d2/shape/transformations/linear_slice_shape.py +144 -0
- scadpy/d2/shape/transformations/mirror_shape.py +53 -0
- scadpy/d2/shape/transformations/pull_shape.py +67 -0
- scadpy/d2/shape/transformations/push_shape.py +67 -0
- scadpy/d2/shape/transformations/radial_extrude_shape.py +285 -0
- scadpy/d2/shape/transformations/radial_slice_shape.py +132 -0
- scadpy/d2/shape/transformations/recoordinate_shape.py +82 -0
- scadpy/d2/shape/transformations/resize_shape.py +91 -0
- scadpy/d2/shape/transformations/rotate_shape.py +63 -0
- scadpy/d2/shape/transformations/scale_shape.py +58 -0
- scadpy/d2/shape/transformations/shrink_shape.py +69 -0
- scadpy/d2/shape/transformations/translate_shape.py +54 -0
- scadpy/d2/shape/types/__init__.py +3 -0
- scadpy/d2/shape/types/shape.py +792 -0
- scadpy/d2/shape/types/utils/__init__.py +5 -0
- scadpy/d2/shape/types/utils/shapely_base_geometry_to_shapely_polygons.py +25 -0
- scadpy/d2/shape/utils/__init__.py +5 -0
- scadpy/d2/shape/utils/shapely_base_geometry_to_shapely_polygons.py +55 -0
- scadpy/d2/utils/__init__.py +3 -0
- scadpy/d2/utils/resolve_vector_2d.py +50 -0
- scadpy/d3/__init__.py +2 -0
- scadpy/d3/solid/__init__.py +8 -0
- scadpy/d3/solid/combinations/__init__.py +21 -0
- scadpy/d3/solid/combinations/are_solid_parts_intersecting.py +51 -0
- scadpy/d3/solid/combinations/concat_solid.py +48 -0
- scadpy/d3/solid/combinations/exclude_solid.py +71 -0
- scadpy/d3/solid/combinations/intersect_solid.py +64 -0
- scadpy/d3/solid/combinations/intersect_solid_parts.py +73 -0
- scadpy/d3/solid/combinations/subtract_solid.py +72 -0
- scadpy/d3/solid/combinations/subtract_solid_parts.py +68 -0
- scadpy/d3/solid/combinations/unify_solid.py +51 -0
- scadpy/d3/solid/combinations/unify_solid_parts.py +73 -0
- scadpy/d3/solid/exporters/__init__.py +11 -0
- scadpy/d3/solid/exporters/map_solid_to_html.py +318 -0
- scadpy/d3/solid/exporters/map_solid_to_html_file.py +58 -0
- scadpy/d3/solid/exporters/map_solid_to_screen.py +51 -0
- scadpy/d3/solid/exporters/map_solid_to_stl_file.py +48 -0
- scadpy/d3/solid/features/__init__.py +11 -0
- scadpy/d3/solid/features/get_solid_bounds.py +37 -0
- scadpy/d3/solid/features/get_solid_part_bounds.py +37 -0
- scadpy/d3/solid/features/get_solid_part_colors.py +39 -0
- scadpy/d3/solid/features/is_solid_empty.py +36 -0
- scadpy/d3/solid/importers/__init__.py +11 -0
- scadpy/d3/solid/importers/map_geometries_to_solid.py +42 -0
- scadpy/d3/solid/importers/map_geometry_to_solid.py +42 -0
- scadpy/d3/solid/importers/map_parts_to_solid.py +66 -0
- scadpy/d3/solid/importers/map_stl_to_solid.py +37 -0
- scadpy/d3/solid/primitives/__init__.py +7 -0
- scadpy/d3/solid/primitives/cone.py +70 -0
- scadpy/d3/solid/primitives/cuboid.py +75 -0
- scadpy/d3/solid/primitives/cylinder.py +73 -0
- scadpy/d3/solid/primitives/polyhedron.py +60 -0
- scadpy/d3/solid/primitives/sphere.py +58 -0
- scadpy/d3/solid/topologies/__init__.py +8 -0
- scadpy/d3/solid/topologies/triangle/__init__.py +5 -0
- scadpy/d3/solid/topologies/triangle/get_solid_triangle_to_vertex.py +49 -0
- scadpy/d3/solid/topologies/vertex/__init__.py +7 -0
- scadpy/d3/solid/topologies/vertex/get_solid_vertex_coordinates.py +39 -0
- scadpy/d3/solid/topologies/vertex/get_solid_vertex_to_part.py +37 -0
- scadpy/d3/solid/transformations/__init__.py +23 -0
- scadpy/d3/solid/transformations/color_solid.py +46 -0
- scadpy/d3/solid/transformations/convexify_solid.py +64 -0
- scadpy/d3/solid/transformations/mirror_solid.py +53 -0
- scadpy/d3/solid/transformations/pull_solid.py +67 -0
- scadpy/d3/solid/transformations/push_solid.py +67 -0
- scadpy/d3/solid/transformations/recoordinate_solid.py +68 -0
- scadpy/d3/solid/transformations/resize_solid.py +92 -0
- scadpy/d3/solid/transformations/rotate_solid.py +93 -0
- scadpy/d3/solid/transformations/scale_solid.py +58 -0
- scadpy/d3/solid/transformations/translate_solid.py +54 -0
- scadpy/d3/solid/types/__init__.py +3 -0
- scadpy/d3/solid/types/solid.py +448 -0
- scadpy/d3/utils/__init__.py +3 -0
- scadpy/d3/utils/resolve_vector_3d.py +50 -0
- scadpy/utils/__init__.py +6 -0
- scadpy/utils/resolve_vector.py +64 -0
- scadpy/utils/x.py +38 -0
- scadpy/utils/y.py +38 -0
- scadpy/utils/z.py +38 -0
- scadpy-0.1.0.dist-info/METADATA +282 -0
- scadpy-0.1.0.dist-info/RECORD +236 -0
- scadpy-0.1.0.dist-info/WHEEL +4 -0
- 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)
|