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,289 @@
|
|
|
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 fillet_shape(
|
|
15
|
+
shape: Shape,
|
|
16
|
+
size: float | np.ndarray,
|
|
17
|
+
corner_filter: TopologyFilter[Shape] | None = None,
|
|
18
|
+
segment_count: int = 32,
|
|
19
|
+
epsilon: float = 1e-8,
|
|
20
|
+
) -> Shape:
|
|
21
|
+
"""
|
|
22
|
+
Apply a fillet (circular arc) to every corner of a shape.
|
|
23
|
+
|
|
24
|
+
Convex corners are rounded; concave corners are filled with a circular arc.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
shape : Shape
|
|
29
|
+
The input shape to fillet.
|
|
30
|
+
size : float or ndarray
|
|
31
|
+
Fillet size: distance from the corner vertex to each tangent point along
|
|
32
|
+
the edges. Can be:
|
|
33
|
+
|
|
34
|
+
- ``float``: same size on both sides of every corner.
|
|
35
|
+
- ``(n_active,)``: per-active-corner size, same on both sides.
|
|
36
|
+
- ``(n_active, 2)``: per-active-corner, per-side size. Column 0 is
|
|
37
|
+
the incoming side, column 1 is the outgoing side.
|
|
38
|
+
|
|
39
|
+
``n_active`` is the number of corners selected by ``corner_filter``
|
|
40
|
+
(or all corners if no filter). In all cases, each value is
|
|
41
|
+
automatically clamped to half the length of the corresponding edge
|
|
42
|
+
to avoid overlapping tangent points.
|
|
43
|
+
corner_filter : TopologyFilter[Shape] | None, optional
|
|
44
|
+
Boolean mask or callable ``(shape) -> NDArray[bool]`` of length ``n_corners``
|
|
45
|
+
selecting which corners to fillet. If None, all corners are filleted.
|
|
46
|
+
segment_count : int, optional
|
|
47
|
+
Number of arc segments per corner. Defaults to 32.
|
|
48
|
+
epsilon : float, optional
|
|
49
|
+
Small offset used to avoid coincident edges in boolean operations.
|
|
50
|
+
Defaults to ``1e-8``.
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
Shape
|
|
55
|
+
A new shape with filleted corners.
|
|
56
|
+
|
|
57
|
+
Examples
|
|
58
|
+
--------
|
|
59
|
+
>>> from scadpy import square, polygon, fillet_shape
|
|
60
|
+
|
|
61
|
+
>>> sq = square(4)
|
|
62
|
+
>>> l_shape = polygon(
|
|
63
|
+
... [(0, 0), (4, 0), (4, 2), (2, 2), (2, 4), (0, 4)]
|
|
64
|
+
... )
|
|
65
|
+
>>> arrow = polygon(
|
|
66
|
+
... [(0, 1), (3, 0), (5, 2), (3, 4),
|
|
67
|
+
... (0, 3), (1, 2.5), (1, 1.5)]
|
|
68
|
+
... )
|
|
69
|
+
|
|
70
|
+
>>> # all corners
|
|
71
|
+
>>> fillet_shape(sq, size=1.0) # doctest: +SKIP
|
|
72
|
+
|
|
73
|
+
.. render-example::
|
|
74
|
+
:name: fillet_shape_all
|
|
75
|
+
:example: fillet_shape(sq, size=1.0)
|
|
76
|
+
:ghost: sq
|
|
77
|
+
|
|
78
|
+
>>> # convex corners only (the 5 outer corners of the L-shape)
|
|
79
|
+
>>> fillet_shape( # doctest: +SKIP
|
|
80
|
+
... l_shape, size=0.5,
|
|
81
|
+
... corner_filter=lambda s: s.are_corners_convex,
|
|
82
|
+
... )
|
|
83
|
+
|
|
84
|
+
.. render-example::
|
|
85
|
+
:name: fillet_shape_convex_only
|
|
86
|
+
:example: fillet_shape(l_shape, size=0.5, corner_filter=lambda s: s.are_corners_convex)
|
|
87
|
+
:ghost: l_shape
|
|
88
|
+
|
|
89
|
+
>>> import numpy as np
|
|
90
|
+
|
|
91
|
+
>>> # asymmetric: one side fills the full edge (length 2),
|
|
92
|
+
>>> # the other stays at 1.0
|
|
93
|
+
>>> fillet_shape( # doctest: +SKIP
|
|
94
|
+
... l_shape, size=np.array([[2.0, 1.0]]),
|
|
95
|
+
... corner_filter=lambda s: ~s.are_corners_convex,
|
|
96
|
+
... )
|
|
97
|
+
|
|
98
|
+
.. render-example::
|
|
99
|
+
:name: fillet_shape_concave_asymmetric
|
|
100
|
+
:example: fillet_shape(l_shape, size=np.array([[2.0, 1.0]]), corner_filter=lambda s: ~s.are_corners_convex)
|
|
101
|
+
:ghost: l_shape
|
|
102
|
+
|
|
103
|
+
>>> # only sharp convex corners (angle > 100°):
|
|
104
|
+
>>> # the two 135° corners of the arrow tail
|
|
105
|
+
>>> fillet_shape( # doctest: +SKIP
|
|
106
|
+
... arrow, size=0.4,
|
|
107
|
+
... corner_filter=lambda s: (
|
|
108
|
+
... s.are_corners_convex & (s.corner_angles > 100)
|
|
109
|
+
... ),
|
|
110
|
+
... )
|
|
111
|
+
|
|
112
|
+
.. render-example::
|
|
113
|
+
:name: fillet_shape_sharp_convex
|
|
114
|
+
:example: fillet_shape(arrow, size=0.4, corner_filter=lambda s: s.are_corners_convex & (s.corner_angles > 100))
|
|
115
|
+
:ghost: arrow
|
|
116
|
+
|
|
117
|
+
>>> # oversized: the 2 concave corners share an edge of length ~1;
|
|
118
|
+
>>> # size=10 is clamped proportionally so their contributions
|
|
119
|
+
>>> # sum to the edge length (0.5 + 0.5 each)
|
|
120
|
+
>>> fillet_shape( # doctest: +SKIP
|
|
121
|
+
... arrow, size=10,
|
|
122
|
+
... corner_filter=lambda s: ~s.are_corners_convex,
|
|
123
|
+
... )
|
|
124
|
+
|
|
125
|
+
.. render-example::
|
|
126
|
+
:name: fillet_shape_clamp_proportional
|
|
127
|
+
:example: fillet_shape(arrow, size=10, corner_filter=lambda s: ~s.are_corners_convex)
|
|
128
|
+
:ghost: arrow
|
|
129
|
+
|
|
130
|
+
>>> # wrong size length raises ValueError
|
|
131
|
+
>>> fillet_shape(
|
|
132
|
+
... sq, size=np.array([0.5, 0.5, 0.5])
|
|
133
|
+
... ) # doctest: +ELLIPSIS
|
|
134
|
+
Traceback (most recent call last):
|
|
135
|
+
...
|
|
136
|
+
ValueError: size array shape (3, 2) does not match...
|
|
137
|
+
"""
|
|
138
|
+
from scadpy import resolve_topology_filter, Shape
|
|
139
|
+
|
|
140
|
+
corner_to_vertex = shape.corner_to_vertex
|
|
141
|
+
if len(corner_to_vertex) == 0:
|
|
142
|
+
return shape
|
|
143
|
+
|
|
144
|
+
vertex_coordinates = shape.vertex_coordinates
|
|
145
|
+
is_corner_convex = shape.are_corners_convex
|
|
146
|
+
|
|
147
|
+
active_mask = resolve_topology_filter(shape, len(corner_to_vertex), corner_filter)
|
|
148
|
+
if active_mask is not None and not np.any(active_mask):
|
|
149
|
+
return shape
|
|
150
|
+
|
|
151
|
+
# Filter all per-corner data to active corners only
|
|
152
|
+
active_indices = (
|
|
153
|
+
np.where(active_mask)[0]
|
|
154
|
+
if active_mask is not None
|
|
155
|
+
else np.arange(len(corner_to_vertex))
|
|
156
|
+
)
|
|
157
|
+
active_is_corner_convex = is_corner_convex[active_indices]
|
|
158
|
+
|
|
159
|
+
current_vertices = vertex_coordinates[corner_to_vertex[active_indices, 1]]
|
|
160
|
+
|
|
161
|
+
incoming_de = shape.corner_to_incoming_directed_edge[active_indices]
|
|
162
|
+
outgoing_de = shape.corner_to_outgoing_directed_edge[active_indices]
|
|
163
|
+
incoming_edge = shape.directed_edge_to_edge[incoming_de]
|
|
164
|
+
outgoing_edge = shape.directed_edge_to_edge[outgoing_de]
|
|
165
|
+
|
|
166
|
+
incoming_directions_normalized = shape.directed_edge_directions[incoming_de]
|
|
167
|
+
outgoing_directions_normalized = shape.directed_edge_directions[outgoing_de]
|
|
168
|
+
edge_lengths_incoming = shape.edge_lengths[incoming_edge]
|
|
169
|
+
edge_lengths_outgoing = shape.edge_lengths[outgoing_edge]
|
|
170
|
+
outward_normals_incoming = shape.edge_normals[incoming_edge]
|
|
171
|
+
outward_normals_outgoing = shape.edge_normals[outgoing_edge]
|
|
172
|
+
|
|
173
|
+
# Resolve size to (n_active, 2): column 0 = incoming side, column 1 = outgoing side
|
|
174
|
+
n_active = len(current_vertices)
|
|
175
|
+
sizes = np.asarray(size, dtype=np.float64)
|
|
176
|
+
if sizes.ndim == 0:
|
|
177
|
+
sizes = np.full((n_active, 2), sizes)
|
|
178
|
+
elif sizes.ndim == 1:
|
|
179
|
+
sizes = np.stack([sizes, sizes], axis=1)
|
|
180
|
+
if sizes.shape != (n_active, 2):
|
|
181
|
+
raise ValueError(
|
|
182
|
+
f"size array shape {sizes.shape} does not match "
|
|
183
|
+
f"expected ({n_active}, 2) for {n_active} active corners"
|
|
184
|
+
)
|
|
185
|
+
# Clamp sizes proportionally so adjacent corners don't overlap on a shared edge.
|
|
186
|
+
# For each active corner, find the adjacent corner on its outgoing/incoming edge.
|
|
187
|
+
# If both are active: scale both contributions so they sum to at most edge_length.
|
|
188
|
+
# If only one is active: it can use the full edge length.
|
|
189
|
+
active_index_of = np.full(len(shape.corner_to_vertex), -1, dtype=np.int64)
|
|
190
|
+
active_index_of[active_indices] = np.arange(n_active, dtype=np.int64)
|
|
191
|
+
de_to_corner = shape.directed_edge_to_corner
|
|
192
|
+
sizes_orig = sizes.copy()
|
|
193
|
+
|
|
194
|
+
adj_target_out = de_to_corner[outgoing_de, 1]
|
|
195
|
+
adj_idx_out = active_index_of[adj_target_out]
|
|
196
|
+
adj_size_out = np.where(adj_idx_out >= 0, sizes_orig[adj_idx_out.clip(0), 0], 0.0)
|
|
197
|
+
total_out = sizes_orig[:, 1] + adj_size_out
|
|
198
|
+
scale_out = np.where(
|
|
199
|
+
total_out > edge_lengths_outgoing, edge_lengths_outgoing / total_out, 1.0
|
|
200
|
+
)
|
|
201
|
+
sizes[:, 1] *= scale_out
|
|
202
|
+
|
|
203
|
+
adj_source_in = de_to_corner[incoming_de, 0]
|
|
204
|
+
adj_idx_in = active_index_of[adj_source_in]
|
|
205
|
+
adj_size_in = np.where(adj_idx_in >= 0, sizes_orig[adj_idx_in.clip(0), 1], 0.0)
|
|
206
|
+
total_in = sizes_orig[:, 0] + adj_size_in
|
|
207
|
+
scale_in = np.where(
|
|
208
|
+
total_in > edge_lengths_incoming, edge_lengths_incoming / total_in, 1.0
|
|
209
|
+
)
|
|
210
|
+
sizes[:, 0] *= scale_in
|
|
211
|
+
|
|
212
|
+
signs = np.where(active_is_corner_convex, 1.0, -1.0)
|
|
213
|
+
|
|
214
|
+
# Tangent points: slightly beyond size to avoid coincident inner points
|
|
215
|
+
tangent_points_incoming = current_vertices - incoming_directions_normalized * (
|
|
216
|
+
sizes[:, 0:1] + epsilon
|
|
217
|
+
)
|
|
218
|
+
tangent_points_outgoing = current_vertices + outgoing_directions_normalized * (
|
|
219
|
+
sizes[:, 1:2] + epsilon
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Outer offsets: perpendicular to edge to avoid coincident outer points
|
|
223
|
+
tangent_points_incoming_outer = (
|
|
224
|
+
tangent_points_incoming
|
|
225
|
+
+ signs[:, np.newaxis] * outward_normals_incoming * epsilon
|
|
226
|
+
)
|
|
227
|
+
tangent_points_outgoing_outer = (
|
|
228
|
+
tangent_points_outgoing
|
|
229
|
+
+ signs[:, np.newaxis] * outward_normals_outgoing * epsilon
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Extended corner vertex: push outward along the bisector
|
|
233
|
+
corner_normals = shape.corner_normals[active_indices]
|
|
234
|
+
current_vertices_extended = current_vertices + corner_normals * epsilon
|
|
235
|
+
|
|
236
|
+
# Elliptic arc centered on the corner vertex:
|
|
237
|
+
# P(t) = corner - a*cos(t)*incoming_dir + b*sin(t)*outgoing_dir, t ∈ [0, π/2]
|
|
238
|
+
# At t=0: corner - a*incoming_dir = tp_in
|
|
239
|
+
# At t=π/2: corner + b*outgoing_dir = tp_out
|
|
240
|
+
# The arc bulges away from the corner (toward the shape interior for convex corners).
|
|
241
|
+
t_values = np.linspace(0.0, np.pi / 2, segment_count) # (segment_count,)
|
|
242
|
+
cos_t = np.cos(t_values) # (segment_count,)
|
|
243
|
+
sin_t = np.sin(t_values) # (segment_count,)
|
|
244
|
+
|
|
245
|
+
cutters: list[Polygon] = []
|
|
246
|
+
fillers: list[Polygon] = []
|
|
247
|
+
for i in range(n_active):
|
|
248
|
+
a = sizes[i, 0] + epsilon
|
|
249
|
+
b = sizes[i, 1] + epsilon
|
|
250
|
+
corner_ext = current_vertices_extended[i]
|
|
251
|
+
inc = incoming_directions_normalized[i]
|
|
252
|
+
out = outgoing_directions_normalized[i]
|
|
253
|
+
|
|
254
|
+
# Elliptic arc points using epsilon-extended sizes: (segment_count, 2)
|
|
255
|
+
arc_points = (
|
|
256
|
+
current_vertices[i]
|
|
257
|
+
- a * cos_t[:, np.newaxis] * inc
|
|
258
|
+
+ b * sin_t[:, np.newaxis] * out
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Double mirror = 180° rotation around midpoint of p0→p1
|
|
262
|
+
mid = (arc_points[0] + arc_points[-1]) / 2
|
|
263
|
+
arc_points = 2 * mid - arc_points
|
|
264
|
+
|
|
265
|
+
# Polygon: extended corner → outer tp_in → arc → outer tp_out
|
|
266
|
+
# For convex: corner_ext is outside the arc → polygon is the wedge to cut
|
|
267
|
+
# For concave: corner_ext is inside the arc → polygon is the notch to fill
|
|
268
|
+
polygon = Polygon(
|
|
269
|
+
[
|
|
270
|
+
corner_ext,
|
|
271
|
+
tangent_points_outgoing_outer[i],
|
|
272
|
+
*arc_points,
|
|
273
|
+
tangent_points_incoming_outer[i],
|
|
274
|
+
]
|
|
275
|
+
)
|
|
276
|
+
if polygon.is_empty or not polygon.is_valid:
|
|
277
|
+
continue
|
|
278
|
+
if active_is_corner_convex[i]:
|
|
279
|
+
cutters.append(polygon)
|
|
280
|
+
else:
|
|
281
|
+
fillers.append(polygon)
|
|
282
|
+
|
|
283
|
+
result = shape
|
|
284
|
+
if cutters:
|
|
285
|
+
result = result - Shape.from_geometries(cutters)
|
|
286
|
+
if fillers:
|
|
287
|
+
result = result | Shape.from_geometries(fillers)
|
|
288
|
+
|
|
289
|
+
return result
|
|
@@ -0,0 +1,82 @@
|
|
|
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 grow_shape(
|
|
14
|
+
shape: Shape, distance: float, part_filter: TopologyFilter[Shape] | None = None
|
|
15
|
+
) -> Shape:
|
|
16
|
+
"""
|
|
17
|
+
Grow or shrink each selected part by offsetting its boundary by a given distance.
|
|
18
|
+
|
|
19
|
+
A positive distance expands the shape outward, a negative distance shrinks it
|
|
20
|
+
inward. The offset uses mitre joins to preserve sharp corners.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
shape : Shape
|
|
25
|
+
The input shape whose parts will be grown.
|
|
26
|
+
distance : float
|
|
27
|
+
The offset distance. Positive values expand, negative values shrink.
|
|
28
|
+
part_filter : TopologyFilter[Shape] | None, optional
|
|
29
|
+
A boolean mask selecting which parts to grow. If None, all parts are grown.
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
Shape
|
|
34
|
+
A new shape with the selected parts grown and the unselected parts unchanged.
|
|
35
|
+
|
|
36
|
+
Examples
|
|
37
|
+
--------
|
|
38
|
+
>>> from scadpy import grow_shape, square
|
|
39
|
+
>>> import numpy as np
|
|
40
|
+
|
|
41
|
+
>>> shape = square(10)
|
|
42
|
+
>>> grow_shape(shape, 2) # doctest: +SKIP
|
|
43
|
+
|
|
44
|
+
.. render-example::
|
|
45
|
+
:name: grow_shape
|
|
46
|
+
:example: grow_shape(shape, 2)
|
|
47
|
+
:ghost: shape
|
|
48
|
+
|
|
49
|
+
>>> # shrink with negative distance
|
|
50
|
+
>>> grow_shape(shape, -2) # doctest: +SKIP
|
|
51
|
+
|
|
52
|
+
.. render-example::
|
|
53
|
+
:name: grow_shape_shrink
|
|
54
|
+
:example: grow_shape(shape, -2)
|
|
55
|
+
:ghost: shape
|
|
56
|
+
|
|
57
|
+
>>> # partial grow
|
|
58
|
+
>>> a = square(4)
|
|
59
|
+
>>> b = square(2).translate(10)
|
|
60
|
+
>>> grow_shape( # doctest: +SKIP
|
|
61
|
+
... a + b, 1, part_filter=np.array([True, False])
|
|
62
|
+
... )
|
|
63
|
+
|
|
64
|
+
.. render-example::
|
|
65
|
+
:name: grow_shape_partial
|
|
66
|
+
:example: grow_shape(a + b, 1, part_filter=np.array([True, False]))
|
|
67
|
+
:ghost: a + b
|
|
68
|
+
"""
|
|
69
|
+
from scadpy import Part, Shape, transform_filtered_parts
|
|
70
|
+
|
|
71
|
+
return transform_filtered_parts(
|
|
72
|
+
assembly=shape,
|
|
73
|
+
parts=shape._parts,
|
|
74
|
+
part_filter=part_filter,
|
|
75
|
+
transform=lambda parts: [
|
|
76
|
+
Part[Polygon].from_geometry(
|
|
77
|
+
p.geometry.buffer(distance, join_style="mitre"), p.color
|
|
78
|
+
)
|
|
79
|
+
for p in parts
|
|
80
|
+
],
|
|
81
|
+
concat_parts=Shape.from_parts,
|
|
82
|
+
)
|
|
@@ -0,0 +1,116 @@
|
|
|
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 typeguard import typechecked
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from scadpy.d2.shape import Shape
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _make_half_plane(
|
|
14
|
+
size: float,
|
|
15
|
+
axis_x: float,
|
|
16
|
+
axis_y: float,
|
|
17
|
+
pivot_x: float,
|
|
18
|
+
pivot_y: float,
|
|
19
|
+
positive: bool,
|
|
20
|
+
) -> Shape:
|
|
21
|
+
"""Large polygon covering one half-plane relative to an axis line."""
|
|
22
|
+
from scadpy.d2.shape.primitives.polygon import polygon
|
|
23
|
+
|
|
24
|
+
radial_x, radial_y = (axis_y, -axis_x) if positive else (-axis_y, axis_x)
|
|
25
|
+
return polygon(
|
|
26
|
+
[
|
|
27
|
+
[pivot_x - size * axis_x, pivot_y - size * axis_y],
|
|
28
|
+
[pivot_x + size * axis_x, pivot_y + size * axis_y],
|
|
29
|
+
[
|
|
30
|
+
pivot_x + size * axis_x + size * radial_x,
|
|
31
|
+
pivot_y + size * axis_y + size * radial_y,
|
|
32
|
+
],
|
|
33
|
+
[
|
|
34
|
+
pivot_x - size * axis_x + size * radial_x,
|
|
35
|
+
pivot_y - size * axis_y + size * radial_y,
|
|
36
|
+
],
|
|
37
|
+
]
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@typechecked
|
|
42
|
+
def linear_cut_shape(
|
|
43
|
+
shape: Shape,
|
|
44
|
+
axis: float | Iterable[float],
|
|
45
|
+
pivot: float | Iterable[float] = 0,
|
|
46
|
+
) -> Shape:
|
|
47
|
+
"""Cut a shape along an axis line through a pivot point.
|
|
48
|
+
|
|
49
|
+
The axis line divides the plane into two half-planes and the shape
|
|
50
|
+
is split accordingly. Both halves are returned as a single
|
|
51
|
+
:class:`Shape` whose parts are the individual cut pieces.
|
|
52
|
+
|
|
53
|
+
The *positive* side is 90° clockwise from the axis direction. For
|
|
54
|
+
``axis=[0, 1]`` (Y-axis) this means points with ``x >= 0``; for
|
|
55
|
+
``axis=[1, 0]`` (X-axis) this means points with ``y <= 0``.
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
shape : Shape
|
|
60
|
+
The shape to cut.
|
|
61
|
+
axis : float or Iterable[float]
|
|
62
|
+
Direction of the cut line (2D vector).
|
|
63
|
+
pivot : float or Iterable[float], optional
|
|
64
|
+
A point on the cut line. Default is the origin ``0``.
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
Shape
|
|
69
|
+
A new shape whose parts are the two halves of the original shape
|
|
70
|
+
concatenated together.
|
|
71
|
+
|
|
72
|
+
Examples
|
|
73
|
+
--------
|
|
74
|
+
>>> from scadpy import linear_cut_shape, square, circle
|
|
75
|
+
|
|
76
|
+
>>> shape = square(6) - circle(2)
|
|
77
|
+
|
|
78
|
+
>>> # vertical cut along the Y-axis
|
|
79
|
+
>>> linear_cut_shape(shape, axis=[0, 1], pivot=[-1, 0]) # doctest: +SKIP
|
|
80
|
+
|
|
81
|
+
.. render-example::
|
|
82
|
+
:name: linear_cut_shape
|
|
83
|
+
:example: linear_cut_shape(shape, axis=[0, 1], pivot=[-1, 0])
|
|
84
|
+
:ghost: shape
|
|
85
|
+
|
|
86
|
+
>>> # diagonal cut: axis=[1, 1]
|
|
87
|
+
>>> linear_cut_shape(shape, axis=[1, 1]) # doctest: +SKIP
|
|
88
|
+
|
|
89
|
+
.. render-example::
|
|
90
|
+
:name: linear_cut_shape_diagonal
|
|
91
|
+
:example: linear_cut_shape(shape, axis=[1, 1])
|
|
92
|
+
:ghost: shape
|
|
93
|
+
"""
|
|
94
|
+
from scadpy import resolve_vector_2d
|
|
95
|
+
|
|
96
|
+
axis = resolve_vector_2d(axis, 0)
|
|
97
|
+
axis = axis / np.linalg.norm(axis)
|
|
98
|
+
pivot = resolve_vector_2d(pivot, 0)
|
|
99
|
+
|
|
100
|
+
bounds = shape.bounds
|
|
101
|
+
size = float(np.max(np.abs(bounds))) * 10 + 100
|
|
102
|
+
|
|
103
|
+
axis_x, axis_y = float(axis[0]), float(axis[1])
|
|
104
|
+
pivot_x, pivot_y = float(pivot[0]), float(pivot[1])
|
|
105
|
+
|
|
106
|
+
positive_half_plane = _make_half_plane(
|
|
107
|
+
size, axis_x, axis_y, pivot_x, pivot_y, positive=True
|
|
108
|
+
)
|
|
109
|
+
negative_half_plane = _make_half_plane(
|
|
110
|
+
size, axis_x, axis_y, pivot_x, pivot_y, positive=False
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
positive_side = shape - negative_half_plane
|
|
114
|
+
negative_side = shape - positive_half_plane
|
|
115
|
+
|
|
116
|
+
return positive_side + negative_side
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from trimesh import Trimesh
|
|
6
|
+
from trimesh.creation import extrude_polygon
|
|
7
|
+
from typeguard import typechecked
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from scadpy.d2.shape import Shape
|
|
11
|
+
from scadpy.d3.solid import Solid
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@typechecked
|
|
15
|
+
def linear_extrude_shape(shape: Shape, height: float) -> Solid:
|
|
16
|
+
"""
|
|
17
|
+
Extrude a 2D shape along the Z axis into a 3D solid.
|
|
18
|
+
|
|
19
|
+
Each part of the shape is extruded vertically by the given height,
|
|
20
|
+
producing a solid with the same cross-section throughout.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
shape : Shape
|
|
25
|
+
The 2D shape to extrude.
|
|
26
|
+
height : float
|
|
27
|
+
The extrusion height along the Z axis.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
Solid
|
|
32
|
+
A 3D solid created by extruding the shape.
|
|
33
|
+
|
|
34
|
+
Examples
|
|
35
|
+
--------
|
|
36
|
+
>>> from scadpy import linear_extrude_shape, square, circle
|
|
37
|
+
|
|
38
|
+
>>> # simple box
|
|
39
|
+
>>> linear_extrude_shape(square(10), height=5) # doctest: +SKIP
|
|
40
|
+
|
|
41
|
+
.. render-example::
|
|
42
|
+
:name: linear_extrude_shape_square
|
|
43
|
+
:example: linear_extrude_shape(square(10), height=5)
|
|
44
|
+
|
|
45
|
+
>>> # tube from a hollow circle
|
|
46
|
+
>>> linear_extrude_shape( # doctest: +SKIP
|
|
47
|
+
... circle(5) - circle(3), height=10
|
|
48
|
+
... )
|
|
49
|
+
|
|
50
|
+
.. render-example::
|
|
51
|
+
:name: linear_extrude_shape_tube
|
|
52
|
+
:example: linear_extrude_shape(circle(5) - circle(3), height=10)
|
|
53
|
+
"""
|
|
54
|
+
from scadpy import Part, Solid
|
|
55
|
+
|
|
56
|
+
solid_parts = [
|
|
57
|
+
Part[Trimesh].from_geometry(extrude_polygon(p.geometry, height), p.color)
|
|
58
|
+
for p in shape._parts
|
|
59
|
+
]
|
|
60
|
+
return Solid.from_parts(solid_parts)
|
|
@@ -0,0 +1,144 @@
|
|
|
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 shapely.geometry.polygon import Polygon
|
|
8
|
+
from typeguard import typechecked
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from scadpy import Shape, TopologyFilter
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@typechecked
|
|
15
|
+
def linear_slice_shape(
|
|
16
|
+
shape: Shape,
|
|
17
|
+
thickness: float,
|
|
18
|
+
direction: float | Iterable[float],
|
|
19
|
+
pivot: float | Iterable[float] = 0,
|
|
20
|
+
part_filter: TopologyFilter[Shape] | None = None,
|
|
21
|
+
) -> Shape:
|
|
22
|
+
"""
|
|
23
|
+
Slice a shape along a directed line, keeping only a strip of given thickness.
|
|
24
|
+
|
|
25
|
+
Constructs an oriented rectangle along the specified direction, centered on the
|
|
26
|
+
pivot point, and intersects it with the selected parts. The rectangle is wide
|
|
27
|
+
enough to cover the entire shape, so only the strip of the given thickness remains.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
shape : Shape
|
|
32
|
+
The input shape to slice.
|
|
33
|
+
thickness : float
|
|
34
|
+
The width of the slice strip.
|
|
35
|
+
direction : float | Iterable[float]
|
|
36
|
+
The direction vector along which the slice is oriented.
|
|
37
|
+
pivot : float | Iterable[float], optional
|
|
38
|
+
The center point of the slice strip. Defaults to the origin.
|
|
39
|
+
part_filter : TopologyFilter[Shape] | None, optional
|
|
40
|
+
A boolean mask selecting which parts to slice. If None, all parts are sliced.
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
Shape
|
|
45
|
+
A new shape containing only the sliced strip of the selected parts,
|
|
46
|
+
plus the unselected parts unchanged.
|
|
47
|
+
|
|
48
|
+
Examples
|
|
49
|
+
--------
|
|
50
|
+
>>> from scadpy import linear_slice_shape, square, circle
|
|
51
|
+
>>> import numpy as np
|
|
52
|
+
|
|
53
|
+
>>> shape = square(10) - circle(3)
|
|
54
|
+
|
|
55
|
+
>>> # horizontal slice through center
|
|
56
|
+
>>> linear_slice_shape( # doctest: +SKIP
|
|
57
|
+
... shape, thickness=3, direction=[1, 0]
|
|
58
|
+
... )
|
|
59
|
+
|
|
60
|
+
.. render-example::
|
|
61
|
+
:name: linear_slice_shape_horizontal
|
|
62
|
+
:example: linear_slice_shape(shape, thickness=3, direction=[1, 0])
|
|
63
|
+
:ghost: shape
|
|
64
|
+
|
|
65
|
+
>>> # diagonal slice
|
|
66
|
+
>>> linear_slice_shape( # doctest: +SKIP
|
|
67
|
+
... shape, thickness=2, direction=[1, 1]
|
|
68
|
+
... )
|
|
69
|
+
|
|
70
|
+
.. render-example::
|
|
71
|
+
:name: linear_slice_shape_diagonal
|
|
72
|
+
:example: linear_slice_shape(shape, thickness=2, direction=[1, 1])
|
|
73
|
+
:ghost: shape
|
|
74
|
+
|
|
75
|
+
>>> # off-center slice with pivot
|
|
76
|
+
>>> linear_slice_shape( # doctest: +SKIP
|
|
77
|
+
... shape, thickness=2, direction=[0, 1], pivot=[3, 0]
|
|
78
|
+
... )
|
|
79
|
+
|
|
80
|
+
.. render-example::
|
|
81
|
+
:name: linear_slice_shape_pivot
|
|
82
|
+
:example: linear_slice_shape(shape, thickness=2, direction=[0, 1], pivot=[3, 0])
|
|
83
|
+
:ghost: shape
|
|
84
|
+
|
|
85
|
+
>>> # partial slice on a composite shape
|
|
86
|
+
>>> a = square(6)
|
|
87
|
+
>>> b = circle(3).translate(10)
|
|
88
|
+
|
|
89
|
+
>>> linear_slice_shape( # doctest: +SKIP
|
|
90
|
+
... a + b, thickness=2, direction=[1, 0],
|
|
91
|
+
... part_filter=np.array([True, False]),
|
|
92
|
+
... )
|
|
93
|
+
|
|
94
|
+
.. render-example::
|
|
95
|
+
:name: linear_slice_shape_partial
|
|
96
|
+
:example: linear_slice_shape(a + b, thickness=2, direction=[1, 0], part_filter=np.array([True, False]))
|
|
97
|
+
:ghost: a + b
|
|
98
|
+
"""
|
|
99
|
+
from scadpy import resolve_vector_2d, transform_filtered_parts, Shape
|
|
100
|
+
|
|
101
|
+
direction = resolve_vector_2d(direction, 0)
|
|
102
|
+
pivot = resolve_vector_2d(pivot, 0)
|
|
103
|
+
|
|
104
|
+
bounds = shape.bounds
|
|
105
|
+
size = max(bounds[2] - bounds[0], bounds[3] - bounds[1]) * 2
|
|
106
|
+
|
|
107
|
+
direction = direction / np.linalg.norm(direction)
|
|
108
|
+
direction_x, direction_y = direction[0], direction[1]
|
|
109
|
+
|
|
110
|
+
normal_x = -direction_y
|
|
111
|
+
normal_y = direction_x
|
|
112
|
+
|
|
113
|
+
half_thickness = thickness / 2
|
|
114
|
+
px, py = pivot[0], pivot[1]
|
|
115
|
+
|
|
116
|
+
half_size = size / 2
|
|
117
|
+
points = [
|
|
118
|
+
(
|
|
119
|
+
px - direction_x * half_size - normal_x * half_thickness,
|
|
120
|
+
py - direction_y * half_size - normal_y * half_thickness,
|
|
121
|
+
),
|
|
122
|
+
(
|
|
123
|
+
px + direction_x * half_size - normal_x * half_thickness,
|
|
124
|
+
py + direction_y * half_size - normal_y * half_thickness,
|
|
125
|
+
),
|
|
126
|
+
(
|
|
127
|
+
px + direction_x * half_size + normal_x * half_thickness,
|
|
128
|
+
py + direction_y * half_size + normal_y * half_thickness,
|
|
129
|
+
),
|
|
130
|
+
(
|
|
131
|
+
px - direction_x * half_size + normal_x * half_thickness,
|
|
132
|
+
py - direction_y * half_size + normal_y * half_thickness,
|
|
133
|
+
),
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
slice_mask = Shape.from_geometries([Polygon(points)])
|
|
137
|
+
|
|
138
|
+
return transform_filtered_parts(
|
|
139
|
+
assembly=shape,
|
|
140
|
+
parts=shape._parts,
|
|
141
|
+
part_filter=part_filter,
|
|
142
|
+
transform=lambda parts: (Shape.from_parts(parts) & slice_mask)._parts,
|
|
143
|
+
concat_parts=Shape.from_parts,
|
|
144
|
+
)
|