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,36 @@
|
|
|
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 Solid
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@typechecked
|
|
12
|
+
def is_solid_empty(solid: Solid) -> bool:
|
|
13
|
+
"""Return whether the solid has no vertices.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
solid : Solid
|
|
18
|
+
The solid to check.
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
bool
|
|
23
|
+
True if the solid has no vertices, False otherwise.
|
|
24
|
+
|
|
25
|
+
Examples
|
|
26
|
+
--------
|
|
27
|
+
>>> from scadpy import Solid, is_solid_empty
|
|
28
|
+
|
|
29
|
+
>>> is_solid_empty(Solid.from_parts([]))
|
|
30
|
+
True
|
|
31
|
+
|
|
32
|
+
>>> from scadpy import cuboid
|
|
33
|
+
>>> is_solid_empty(cuboid(2))
|
|
34
|
+
False
|
|
35
|
+
"""
|
|
36
|
+
return len(solid.vertex_coordinates) == 0
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"map_geometries_to_solid",
|
|
3
|
+
"map_geometry_to_solid",
|
|
4
|
+
"map_parts_to_solid",
|
|
5
|
+
"map_stl_to_solid",
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
from .map_geometries_to_solid import map_geometries_to_solid
|
|
9
|
+
from .map_geometry_to_solid import map_geometry_to_solid
|
|
10
|
+
from .map_parts_to_solid import map_parts_to_solid
|
|
11
|
+
from .map_stl_to_solid import map_stl_to_solid
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from trimesh import Trimesh
|
|
7
|
+
from typeguard import typechecked
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from scadpy.d3.solid import Solid
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@typechecked
|
|
14
|
+
def map_geometries_to_solid(geometries: Sequence[Trimesh]) -> Solid:
|
|
15
|
+
"""Map a sequence of Trimesh geometries to a solid.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
geometries : Sequence[Trimesh]
|
|
20
|
+
The geometries to map.
|
|
21
|
+
|
|
22
|
+
Returns
|
|
23
|
+
-------
|
|
24
|
+
Solid
|
|
25
|
+
A new solid containing all input geometries as parts.
|
|
26
|
+
|
|
27
|
+
Examples
|
|
28
|
+
--------
|
|
29
|
+
>>> from scadpy import cuboid, map_geometries_to_solid
|
|
30
|
+
|
|
31
|
+
>>> map_geometries_to_solid( # doctest: +SKIP
|
|
32
|
+
... [cuboid(4)._parts[0].geometry]
|
|
33
|
+
... )
|
|
34
|
+
|
|
35
|
+
.. render-example::
|
|
36
|
+
:name: map_geometries_to_solid
|
|
37
|
+
:example: map_geometries_to_solid([cuboid(4)._parts[0].geometry])
|
|
38
|
+
"""
|
|
39
|
+
from scadpy.core.part import Part
|
|
40
|
+
from scadpy.d3.solid import Solid
|
|
41
|
+
|
|
42
|
+
return Solid.from_parts([Part[Trimesh].from_geometry(g) for g in geometries])
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from trimesh import Trimesh
|
|
6
|
+
from typeguard import typechecked
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from scadpy.d3.solid import Solid
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@typechecked
|
|
13
|
+
def map_geometry_to_solid(geometry: Trimesh) -> Solid:
|
|
14
|
+
"""Map a single Trimesh geometry to a solid.
|
|
15
|
+
|
|
16
|
+
Shortcut for :func:`map_geometries_to_solid` with a single geometry.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
geometry : Trimesh
|
|
21
|
+
The geometry to map.
|
|
22
|
+
|
|
23
|
+
Returns
|
|
24
|
+
-------
|
|
25
|
+
Solid
|
|
26
|
+
A new solid containing the single geometry as a part.
|
|
27
|
+
|
|
28
|
+
Examples
|
|
29
|
+
--------
|
|
30
|
+
>>> from scadpy import cuboid, map_geometry_to_solid
|
|
31
|
+
|
|
32
|
+
>>> map_geometry_to_solid( # doctest: +SKIP
|
|
33
|
+
... cuboid(4)._parts[0].geometry
|
|
34
|
+
... )
|
|
35
|
+
|
|
36
|
+
.. render-example::
|
|
37
|
+
:name: map_geometry_to_solid
|
|
38
|
+
:example: map_geometry_to_solid(cuboid(4)._parts[0].geometry)
|
|
39
|
+
"""
|
|
40
|
+
from scadpy.d3.solid.importers import map_geometries_to_solid
|
|
41
|
+
|
|
42
|
+
return map_geometries_to_solid([geometry])
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from trimesh import Trimesh
|
|
7
|
+
from typeguard import typechecked
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from scadpy import Part, Solid
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@typechecked
|
|
14
|
+
def map_parts_to_solid(
|
|
15
|
+
parts: Sequence[Part[Trimesh]],
|
|
16
|
+
) -> Solid:
|
|
17
|
+
"""Map a sequence of parts to a solid, repairing geometry where needed.
|
|
18
|
+
|
|
19
|
+
For each part, two repairs are applied if needed:
|
|
20
|
+
|
|
21
|
+
- **Locally inconsistent winding**: if adjacent faces have inconsistent winding
|
|
22
|
+
order (e.g. after some boolean operations), :func:`trimesh.Trimesh.fix_normals`
|
|
23
|
+
is called to make them consistent.
|
|
24
|
+
- **Globally inverted normals**: if the mesh has negative volume (e.g. after
|
|
25
|
+
a mirror transform), the face winding order is reversed to restore
|
|
26
|
+
outward-pointing normals.
|
|
27
|
+
|
|
28
|
+
A mesh copy is only made when a repair is actually needed.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
parts : Sequence[Part[Trimesh]]
|
|
33
|
+
The parts to map. Each part holds a Trimesh mesh and a color.
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
Solid
|
|
38
|
+
A new solid containing all parts with corrected geometry.
|
|
39
|
+
|
|
40
|
+
Examples
|
|
41
|
+
--------
|
|
42
|
+
>>> from scadpy import cuboid, map_parts_to_solid
|
|
43
|
+
|
|
44
|
+
>>> map_parts_to_solid( # doctest: +SKIP
|
|
45
|
+
... cuboid(4)._parts
|
|
46
|
+
... )
|
|
47
|
+
|
|
48
|
+
.. render-example::
|
|
49
|
+
:name: map_parts_to_solid
|
|
50
|
+
:example: map_parts_to_solid(cuboid(4)._parts)
|
|
51
|
+
"""
|
|
52
|
+
from scadpy.core.part import Part
|
|
53
|
+
from scadpy.d3.solid.types.solid import Solid
|
|
54
|
+
|
|
55
|
+
fixed_parts: list[Part[Trimesh]] = []
|
|
56
|
+
for part in parts:
|
|
57
|
+
mesh = part.geometry
|
|
58
|
+
needs_fix = (not mesh.is_winding_consistent) or (mesh.volume < 0)
|
|
59
|
+
if needs_fix:
|
|
60
|
+
mesh = mesh.copy()
|
|
61
|
+
mesh.fix_normals()
|
|
62
|
+
fixed_parts.append(Part[Trimesh].from_geometry(mesh, part.color))
|
|
63
|
+
|
|
64
|
+
solid = Solid()
|
|
65
|
+
solid._parts = fixed_parts
|
|
66
|
+
return solid
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from trimesh import Trimesh, load
|
|
7
|
+
from typeguard import typechecked
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from scadpy.d3.solid import Solid
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@typechecked
|
|
14
|
+
def map_stl_to_solid(source: str | Path) -> Solid:
|
|
15
|
+
"""Load a solid from an STL file.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
source : str or Path
|
|
20
|
+
Path to the ``.stl`` file.
|
|
21
|
+
|
|
22
|
+
Returns
|
|
23
|
+
-------
|
|
24
|
+
Solid
|
|
25
|
+
A new solid loaded from the STL file.
|
|
26
|
+
|
|
27
|
+
Examples
|
|
28
|
+
--------
|
|
29
|
+
>>> from scadpy import map_stl_to_solid
|
|
30
|
+
|
|
31
|
+
>>> map_stl_to_solid("model.stl") # doctest: +SKIP
|
|
32
|
+
"""
|
|
33
|
+
from typing import cast
|
|
34
|
+
|
|
35
|
+
from scadpy.d3.solid.importers import map_geometry_to_solid
|
|
36
|
+
|
|
37
|
+
return map_geometry_to_solid(cast(Trimesh, load(source, force="mesh")))
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from typeguard import typechecked
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from scadpy.d3.solid import Solid
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@typechecked
|
|
13
|
+
def cone(radius: float, height: float, section_count: int = 32) -> Solid:
|
|
14
|
+
"""Create a cone centered at the origin, apex pointing along +z.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
radius : float
|
|
19
|
+
The radius of the base circle.
|
|
20
|
+
height : float
|
|
21
|
+
The total height of the cone.
|
|
22
|
+
section_count : int, optional
|
|
23
|
+
The number of sides of the polygonal base approximation. Default is 32.
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
Solid
|
|
28
|
+
A :class:`~scadpy.d3.solid.types.solid.Solid` object representing the
|
|
29
|
+
cone, centered at the origin.
|
|
30
|
+
|
|
31
|
+
Examples
|
|
32
|
+
--------
|
|
33
|
+
>>> from scadpy import cone
|
|
34
|
+
|
|
35
|
+
>>> cone(radius=2, height=4) # doctest: +SKIP
|
|
36
|
+
|
|
37
|
+
.. render-example::
|
|
38
|
+
:name: cone
|
|
39
|
+
:example: cone(radius=2, height=4)
|
|
40
|
+
|
|
41
|
+
>>> cone(radius=2, height=4, section_count=6) # doctest: +SKIP
|
|
42
|
+
|
|
43
|
+
.. render-example::
|
|
44
|
+
:name: hexagonal_cone
|
|
45
|
+
:example: cone(radius=2, height=4, section_count=6)
|
|
46
|
+
"""
|
|
47
|
+
from scadpy.d3.solid.primitives.polyhedron import polyhedron
|
|
48
|
+
|
|
49
|
+
n = section_count
|
|
50
|
+
angles = np.linspace(0, 2 * np.pi, n, endpoint=False)
|
|
51
|
+
cos_a = np.cos(angles)
|
|
52
|
+
sin_a = np.sin(angles)
|
|
53
|
+
|
|
54
|
+
# Vertex layout:
|
|
55
|
+
# 0 .. n-1 base circle at z = -height/2
|
|
56
|
+
# n apex at z = +height/2
|
|
57
|
+
# n+1 base center at z = -height/2
|
|
58
|
+
base = np.column_stack([radius * cos_a, radius * sin_a, np.full(n, -height / 2)])
|
|
59
|
+
apex = np.array([[0.0, 0.0, height / 2]])
|
|
60
|
+
base_center = np.array([[0.0, 0.0, -height / 2]])
|
|
61
|
+
vertices = np.vstack([base, apex, base_center]).astype(np.float64)
|
|
62
|
+
|
|
63
|
+
i = np.arange(n, dtype=np.int64)
|
|
64
|
+
j = (i + 1) % n
|
|
65
|
+
|
|
66
|
+
side = np.column_stack([i, j, np.full(n, n, dtype=np.int64)]) # normal outward+up
|
|
67
|
+
base_cap = np.column_stack([np.full(n, n + 1, dtype=np.int64), j, i]) # normal -z
|
|
68
|
+
|
|
69
|
+
faces = np.vstack([side, base_cap]).astype(np.int64)
|
|
70
|
+
return polyhedron(vertices=vertices, faces=faces)
|
|
@@ -0,0 +1,75 @@
|
|
|
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.d3.solid import Solid
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@typechecked
|
|
14
|
+
def cuboid(size: float | Iterable[float]) -> Solid:
|
|
15
|
+
"""Create a box (rectangular cuboid) centered at the origin.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
size : float | Iterable[float]
|
|
20
|
+
The dimensions of the cuboid as ``[width, depth, height]``.
|
|
21
|
+
If a single float is provided, it is broadcast to all three dimensions,
|
|
22
|
+
producing a cube. Missing values default to 0.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
Solid
|
|
27
|
+
A :class:`~scadpy.d3.solid.types.solid.Solid` object representing the cuboid,
|
|
28
|
+
centered at the origin.
|
|
29
|
+
|
|
30
|
+
Examples
|
|
31
|
+
--------
|
|
32
|
+
>>> from scadpy import cuboid
|
|
33
|
+
|
|
34
|
+
>>> cuboid(4) # doctest: +SKIP
|
|
35
|
+
|
|
36
|
+
.. render-example::
|
|
37
|
+
:name: cube
|
|
38
|
+
:example: cuboid(4)
|
|
39
|
+
|
|
40
|
+
>>> cuboid([4, 2, 1]) # doctest: +SKIP
|
|
41
|
+
|
|
42
|
+
.. render-example::
|
|
43
|
+
:name: cuboid
|
|
44
|
+
:example: cuboid([4, 2, 1])
|
|
45
|
+
"""
|
|
46
|
+
from scadpy.d3 import resolve_vector_3d
|
|
47
|
+
from scadpy.d3.solid.primitives.polyhedron import polyhedron
|
|
48
|
+
|
|
49
|
+
w, d, h = resolve_vector_3d(size, 0) / 2
|
|
50
|
+
|
|
51
|
+
vertices = np.array(
|
|
52
|
+
[
|
|
53
|
+
[-w, -d, -h], # 0
|
|
54
|
+
[+w, -d, -h], # 1
|
|
55
|
+
[+w, +d, -h], # 2
|
|
56
|
+
[-w, +d, -h], # 3
|
|
57
|
+
[-w, -d, +h], # 4
|
|
58
|
+
[+w, -d, +h], # 5
|
|
59
|
+
[+w, +d, +h], # 6
|
|
60
|
+
[-w, +d, +h], # 7
|
|
61
|
+
],
|
|
62
|
+
dtype=np.float64,
|
|
63
|
+
)
|
|
64
|
+
faces = np.array(
|
|
65
|
+
[
|
|
66
|
+
[0, 3, 2], [0, 2, 1], # bottom (normal -z)
|
|
67
|
+
[4, 5, 6], [4, 6, 7], # top (normal +z)
|
|
68
|
+
[0, 1, 5], [0, 5, 4], # front (normal -y)
|
|
69
|
+
[3, 7, 6], [3, 6, 2], # back (normal +y)
|
|
70
|
+
[0, 4, 7], [0, 7, 3], # left (normal -x)
|
|
71
|
+
[1, 2, 6], [1, 6, 5], # right (normal +x)
|
|
72
|
+
],
|
|
73
|
+
dtype=np.int64,
|
|
74
|
+
)
|
|
75
|
+
return polyhedron(vertices=vertices, faces=faces)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from typeguard import typechecked
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from scadpy.d3.solid import Solid
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@typechecked
|
|
13
|
+
def cylinder(radius: float, height: float, section_count: int = 32) -> Solid:
|
|
14
|
+
"""Create a cylinder centered at the origin, aligned along the z-axis.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
radius : float
|
|
19
|
+
The radius of the cylinder.
|
|
20
|
+
height : float
|
|
21
|
+
The total height of the cylinder.
|
|
22
|
+
section_count : int, optional
|
|
23
|
+
The number of sides of the polygonal approximation. Default is 32.
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
Solid
|
|
28
|
+
A :class:`~scadpy.d3.solid.types.solid.Solid` object representing the
|
|
29
|
+
cylinder, centered at the origin.
|
|
30
|
+
|
|
31
|
+
Examples
|
|
32
|
+
--------
|
|
33
|
+
>>> from scadpy import cylinder
|
|
34
|
+
|
|
35
|
+
>>> cylinder(radius=2, height=4) # doctest: +SKIP
|
|
36
|
+
|
|
37
|
+
.. render-example::
|
|
38
|
+
:name: cylinder
|
|
39
|
+
:example: cylinder(radius=2, height=4)
|
|
40
|
+
|
|
41
|
+
>>> cylinder(radius=2, height=4, section_count=6) # doctest: +SKIP
|
|
42
|
+
|
|
43
|
+
.. render-example::
|
|
44
|
+
:name: hexagonal_cylinder
|
|
45
|
+
:example: cylinder(radius=2, height=4, section_count=6)
|
|
46
|
+
"""
|
|
47
|
+
from scadpy.d3.solid.primitives.polyhedron import polyhedron
|
|
48
|
+
|
|
49
|
+
n = section_count
|
|
50
|
+
angles = np.linspace(0, 2 * np.pi, n, endpoint=False)
|
|
51
|
+
cos_a = np.cos(angles)
|
|
52
|
+
sin_a = np.sin(angles)
|
|
53
|
+
|
|
54
|
+
# Vertex layout:
|
|
55
|
+
# 0 .. n-1 bottom circle
|
|
56
|
+
# n .. 2n-1 top circle
|
|
57
|
+
# 2n bottom center
|
|
58
|
+
# 2n+1 top center
|
|
59
|
+
bottom = np.column_stack([radius * cos_a, radius * sin_a, np.full(n, -height / 2)])
|
|
60
|
+
top = np.column_stack([radius * cos_a, radius * sin_a, np.full(n, height / 2)])
|
|
61
|
+
centers = np.array([[0.0, 0.0, -height / 2], [0.0, 0.0, height / 2]])
|
|
62
|
+
vertices = np.vstack([bottom, top, centers]).astype(np.float64)
|
|
63
|
+
|
|
64
|
+
i = np.arange(n, dtype=np.int64)
|
|
65
|
+
j = (i + 1) % n
|
|
66
|
+
|
|
67
|
+
side1 = np.column_stack([i, j, n + j]) # side triangle A
|
|
68
|
+
side2 = np.column_stack([i, n + j, n + i]) # side triangle B
|
|
69
|
+
bot_cap = np.column_stack([np.full(n, 2 * n, dtype=np.int64), j, i]) # normal -z
|
|
70
|
+
top_cap = np.column_stack([np.full(n, 2 * n + 1, dtype=np.int64), n + i, n + j]) # normal +z
|
|
71
|
+
|
|
72
|
+
faces = np.vstack([side1, side2, bot_cap, top_cap]).astype(np.int64)
|
|
73
|
+
return polyhedron(vertices=vertices, faces=faces)
|
|
@@ -0,0 +1,60 @@
|
|
|
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 trimesh import Trimesh
|
|
8
|
+
from typeguard import typechecked
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from scadpy.d3.solid import Solid
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@typechecked
|
|
15
|
+
def polyhedron(
|
|
16
|
+
vertices: NDArray[np.float64],
|
|
17
|
+
faces: NDArray[np.int64],
|
|
18
|
+
) -> Solid:
|
|
19
|
+
"""Create a solid from raw vertex coordinates and triangular face indices.
|
|
20
|
+
|
|
21
|
+
This is the base primitive constructor. All other solid primitives ultimately
|
|
22
|
+
call this function with numpy-computed geometry.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
vertices : NDArray[np.float64]
|
|
27
|
+
Vertex coordinates of shape ``(n, 3)``.
|
|
28
|
+
faces : NDArray[np.int64]
|
|
29
|
+
Triangle face indices of shape ``(m, 3)``. Each row contains the indices
|
|
30
|
+
of three vertices forming a triangle. Winding order follows the
|
|
31
|
+
right-hand rule: outward-pointing normals require counter-clockwise
|
|
32
|
+
vertex ordering when viewed from outside.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
Solid
|
|
37
|
+
A new solid built from the given geometry.
|
|
38
|
+
|
|
39
|
+
Examples
|
|
40
|
+
--------
|
|
41
|
+
>>> import numpy as np
|
|
42
|
+
>>> from scadpy import polyhedron
|
|
43
|
+
|
|
44
|
+
>>> polyhedron( # doctest: +SKIP
|
|
45
|
+
... vertices=np.array([
|
|
46
|
+
... [ 1.0, 1.0, 1.0],
|
|
47
|
+
... [ 1.0, -1.0, -1.0],
|
|
48
|
+
... [-1.0, 1.0, -1.0],
|
|
49
|
+
... [-1.0, -1.0, 1.0],
|
|
50
|
+
... ]),
|
|
51
|
+
... faces=np.array([[0, 1, 2], [0, 2, 3], [0, 3, 1], [1, 3, 2]], dtype=np.int64),
|
|
52
|
+
... )
|
|
53
|
+
|
|
54
|
+
.. render-example::
|
|
55
|
+
:name: polyhedron
|
|
56
|
+
:example: polyhedron(vertices=np.array([[1.0,1.0,1.0],[1.0,-1.0,-1.0],[-1.0,1.0,-1.0],[-1.0,-1.0,1.0]]), faces=np.array([[0,1,2],[0,2,3],[0,3,1],[1,3,2]], dtype=np.int64))
|
|
57
|
+
"""
|
|
58
|
+
from scadpy.d3.solid import Solid
|
|
59
|
+
|
|
60
|
+
return Solid.from_geometry(Trimesh(vertices=vertices, faces=faces))
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from trimesh.creation import icosphere
|
|
7
|
+
from typeguard import typechecked
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from scadpy.d3.solid import Solid
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@typechecked
|
|
14
|
+
def sphere(radius: float, subdivision_count: int = 4) -> Solid:
|
|
15
|
+
"""Create a sphere approximated by an icosphere mesh.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
radius : float
|
|
20
|
+
The radius of the sphere. Must be strictly positive.
|
|
21
|
+
subdivision_count : int, optional
|
|
22
|
+
The number of subdivision iterations applied to the base icosahedron.
|
|
23
|
+
Higher values produce a smoother approximation. Default is 4.
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
Solid
|
|
28
|
+
A :class:`~scadpy.d3.solid.types.solid.Solid` object representing the
|
|
29
|
+
approximated sphere, centered at the origin.
|
|
30
|
+
|
|
31
|
+
Notes
|
|
32
|
+
-----
|
|
33
|
+
- The sphere is always centered at the origin.
|
|
34
|
+
- Subdivision count of 4 produces 2562 vertices.
|
|
35
|
+
|
|
36
|
+
Examples
|
|
37
|
+
--------
|
|
38
|
+
>>> from scadpy import sphere
|
|
39
|
+
|
|
40
|
+
>>> sphere(radius=2) # doctest: +SKIP
|
|
41
|
+
|
|
42
|
+
.. render-example::
|
|
43
|
+
:name: sphere
|
|
44
|
+
:example: sphere(radius=2)
|
|
45
|
+
|
|
46
|
+
>>> sphere(radius=2, subdivision_count=1) # doctest: +SKIP
|
|
47
|
+
|
|
48
|
+
.. render-example::
|
|
49
|
+
:name: low_resolution_sphere
|
|
50
|
+
:example: sphere(radius=2, subdivision_count=1)
|
|
51
|
+
"""
|
|
52
|
+
from scadpy.d3.solid.primitives.polyhedron import polyhedron
|
|
53
|
+
|
|
54
|
+
mesh = icosphere(radius=radius, subdivisions=subdivision_count)
|
|
55
|
+
return polyhedron(
|
|
56
|
+
vertices=mesh.vertices.astype(np.float64),
|
|
57
|
+
faces=mesh.faces.astype(np.int64),
|
|
58
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
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 typeguard import typechecked
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from scadpy.d3.solid import Solid
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@typechecked
|
|
14
|
+
def get_solid_triangle_to_vertex(
|
|
15
|
+
solid: Solid,
|
|
16
|
+
) -> NDArray[np.int64]:
|
|
17
|
+
"""For each triangle in the solid, return the indices of its three vertices.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
solid : Solid
|
|
22
|
+
The solid to extract triangle-to-vertex mapping from.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
NDArray[np.int64]
|
|
27
|
+
2D array of shape (n_triangles, 3), one row per triangle containing
|
|
28
|
+
the global vertex indices of its three corners.
|
|
29
|
+
|
|
30
|
+
Examples
|
|
31
|
+
--------
|
|
32
|
+
>>> from scadpy import cuboid, get_solid_triangle_to_vertex
|
|
33
|
+
|
|
34
|
+
>>> triangle_to_vertex = get_solid_triangle_to_vertex(cuboid(2))
|
|
35
|
+
>>> triangle_to_vertex.shape[1]
|
|
36
|
+
3
|
|
37
|
+
"""
|
|
38
|
+
if not solid._parts:
|
|
39
|
+
return np.empty((0, 3), dtype=np.int64)
|
|
40
|
+
|
|
41
|
+
part_vertex_counts = [len(part.geometry.vertices) for part in solid._parts]
|
|
42
|
+
offsets = np.concatenate([[0], np.cumsum(part_vertex_counts[:-1])])
|
|
43
|
+
|
|
44
|
+
return np.concatenate(
|
|
45
|
+
[
|
|
46
|
+
(part.geometry.faces + offset).astype(np.int64)
|
|
47
|
+
for part, offset in zip(solid._parts, offsets)
|
|
48
|
+
]
|
|
49
|
+
)
|