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,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
4
|
+
|
|
5
|
+
from typeguard import typechecked
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@typechecked
|
|
9
|
+
def unify_assemblies[A, P](
|
|
10
|
+
assemblies: Sequence[A],
|
|
11
|
+
get_assembly_parts: Callable[[A], Iterable[P]],
|
|
12
|
+
unify_parts: Callable[[Sequence[P]], A],
|
|
13
|
+
) -> A:
|
|
14
|
+
"""
|
|
15
|
+
Unite (union) multiple assemblies into a single assembly by unifying all their parts.
|
|
16
|
+
|
|
17
|
+
This function uses dependency injection to remain type-agnostic, allowing it
|
|
18
|
+
to work with any assembly/part domain model by providing appropriate accessor
|
|
19
|
+
and unification functions.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
assemblies : Sequence[A]
|
|
24
|
+
Sequence of assembly objects to be unified.
|
|
25
|
+
get_assembly_parts : Callable[[A], Iterable[P]]
|
|
26
|
+
Function that extracts parts from an assembly.
|
|
27
|
+
unify_parts : Callable[[Sequence[P]], Assembly]
|
|
28
|
+
Function that unifies a sequence of parts into a new assembly.
|
|
29
|
+
|
|
30
|
+
Returns
|
|
31
|
+
-------
|
|
32
|
+
A
|
|
33
|
+
The assembly object created by unifying all parts from all input assemblies.
|
|
34
|
+
|
|
35
|
+
Examples
|
|
36
|
+
--------
|
|
37
|
+
>>> from scadpy import unify_assemblies
|
|
38
|
+
|
|
39
|
+
>>> assemblies = [
|
|
40
|
+
... [[0, 0, 2, 2], [1, 1, 3, 3]],
|
|
41
|
+
... [[5, 5, 6, 6]]
|
|
42
|
+
... ]
|
|
43
|
+
...
|
|
44
|
+
>>> def unify_parts(parts):
|
|
45
|
+
... minx = min(p[0] for p in parts)
|
|
46
|
+
... miny = min(p[1] for p in parts)
|
|
47
|
+
... maxx = max(p[2] for p in parts)
|
|
48
|
+
... maxy = max(p[3] for p in parts)
|
|
49
|
+
... return [[minx, miny, maxx, maxy]]
|
|
50
|
+
...
|
|
51
|
+
>>> result = unify_assemblies(
|
|
52
|
+
... assemblies,
|
|
53
|
+
... get_assembly_parts=lambda a: a,
|
|
54
|
+
... unify_parts=unify_parts
|
|
55
|
+
... )
|
|
56
|
+
>>> result == [[0, 0, 6, 6]]
|
|
57
|
+
True
|
|
58
|
+
"""
|
|
59
|
+
return unify_parts([p for a in assemblies for p in get_assembly_parts(a)])
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"get_assembly_directed_edge_directions",
|
|
3
|
+
"get_assembly_directed_edge_to_edge",
|
|
4
|
+
"get_assembly_directed_edge_to_vertex",
|
|
5
|
+
"get_assembly_edge_lengths",
|
|
6
|
+
"get_assembly_edge_midpoints",
|
|
7
|
+
"get_assembly_edge_normals",
|
|
8
|
+
"get_assembly_face_corner_angles",
|
|
9
|
+
"get_assembly_face_corner_normals",
|
|
10
|
+
"get_assembly_face_corner_to_incoming_directed_edge",
|
|
11
|
+
"get_assembly_face_corner_to_outgoing_directed_edge",
|
|
12
|
+
"get_assembly_face_directed_edge_to_corner",
|
|
13
|
+
"get_assembly_part_colors",
|
|
14
|
+
"get_assembly_vertex_coordinates",
|
|
15
|
+
"get_assembly_vertex_to_part",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
from .directed_edge import (
|
|
19
|
+
get_assembly_directed_edge_directions,
|
|
20
|
+
get_assembly_directed_edge_to_edge,
|
|
21
|
+
get_assembly_directed_edge_to_vertex,
|
|
22
|
+
)
|
|
23
|
+
from .edge import (
|
|
24
|
+
get_assembly_edge_lengths,
|
|
25
|
+
get_assembly_edge_midpoints,
|
|
26
|
+
get_assembly_edge_normals,
|
|
27
|
+
)
|
|
28
|
+
from .face_corner import (
|
|
29
|
+
get_assembly_face_corner_angles,
|
|
30
|
+
get_assembly_face_corner_normals,
|
|
31
|
+
get_assembly_face_corner_to_incoming_directed_edge,
|
|
32
|
+
get_assembly_face_corner_to_outgoing_directed_edge,
|
|
33
|
+
get_assembly_face_directed_edge_to_corner,
|
|
34
|
+
)
|
|
35
|
+
from .part import (
|
|
36
|
+
get_assembly_part_colors,
|
|
37
|
+
)
|
|
38
|
+
from .vertex import (
|
|
39
|
+
get_assembly_vertex_coordinates,
|
|
40
|
+
get_assembly_vertex_to_part,
|
|
41
|
+
)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"get_assembly_directed_edge_directions",
|
|
3
|
+
"get_assembly_directed_edge_to_edge",
|
|
4
|
+
"get_assembly_directed_edge_to_vertex",
|
|
5
|
+
]
|
|
6
|
+
|
|
7
|
+
from .get_assembly_directed_edge_directions import get_assembly_directed_edge_directions
|
|
8
|
+
from .get_assembly_directed_edge_to_edge import get_assembly_directed_edge_to_edge
|
|
9
|
+
from .get_assembly_directed_edge_to_vertex import get_assembly_directed_edge_to_vertex
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from numpy.typing import NDArray
|
|
5
|
+
from typeguard import typechecked
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@typechecked
|
|
9
|
+
def get_assembly_directed_edge_directions(
|
|
10
|
+
directed_edge_to_vertex: NDArray[np.int64],
|
|
11
|
+
vertex_coordinates: NDArray[np.float64],
|
|
12
|
+
) -> NDArray[np.float64]:
|
|
13
|
+
"""
|
|
14
|
+
For each directed edge, return its unit direction vector.
|
|
15
|
+
|
|
16
|
+
The direction vector points from the start vertex to the end vertex,
|
|
17
|
+
normalized to unit length.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
directed_edge_to_vertex : NDArray[np.int64]
|
|
22
|
+
2D array of shape ``(n_directed_edges, 2)`` mapping each directed edge
|
|
23
|
+
to its ``[start_vertex, end_vertex]`` indices.
|
|
24
|
+
vertex_coordinates : NDArray[np.float64]
|
|
25
|
+
2D array of shape ``(n_vertices, 2)`` with vertex coordinates.
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
NDArray[np.float64]
|
|
30
|
+
2D array of shape ``(n_directed_edges, 2)``. Each row is a unit vector
|
|
31
|
+
``[dx, dy]`` pointing from start to end vertex.
|
|
32
|
+
|
|
33
|
+
Examples
|
|
34
|
+
--------
|
|
35
|
+
>>> import numpy as np
|
|
36
|
+
>>> from scadpy import get_assembly_directed_edge_directions
|
|
37
|
+
|
|
38
|
+
>>> # square: forward edges go right, up, left, down;
|
|
39
|
+
>>> # backward edges are reversed
|
|
40
|
+
>>> directed_edge_to_vertex = np.array([
|
|
41
|
+
... [0, 1], [1, 0],
|
|
42
|
+
... [1, 2], [2, 1],
|
|
43
|
+
... [2, 3], [3, 2],
|
|
44
|
+
... [3, 0], [0, 3],
|
|
45
|
+
... ], dtype=np.int64)
|
|
46
|
+
>>> vertex_coordinates = np.array(
|
|
47
|
+
... [[0., 0.], [1., 0.], [1., 1.], [0., 1.]]
|
|
48
|
+
... )
|
|
49
|
+
>>> get_assembly_directed_edge_directions(
|
|
50
|
+
... directed_edge_to_vertex, vertex_coordinates
|
|
51
|
+
... ).round(4)
|
|
52
|
+
array([[ 1., 0.],
|
|
53
|
+
[-1., 0.],
|
|
54
|
+
[ 0., 1.],
|
|
55
|
+
[ 0., -1.],
|
|
56
|
+
[-1., 0.],
|
|
57
|
+
[ 1., 0.],
|
|
58
|
+
[ 0., -1.],
|
|
59
|
+
[ 0., 1.]])
|
|
60
|
+
"""
|
|
61
|
+
if len(directed_edge_to_vertex) == 0:
|
|
62
|
+
d = vertex_coordinates.shape[1] if vertex_coordinates.ndim == 2 else 0
|
|
63
|
+
return np.empty((0, d), dtype=np.float64)
|
|
64
|
+
|
|
65
|
+
starts = vertex_coordinates[directed_edge_to_vertex[:, 0]]
|
|
66
|
+
ends = vertex_coordinates[directed_edge_to_vertex[:, 1]]
|
|
67
|
+
|
|
68
|
+
directions = ends - starts
|
|
69
|
+
lengths = np.linalg.norm(directions, axis=1, keepdims=True)
|
|
70
|
+
return directions / lengths
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from numpy.typing import NDArray
|
|
5
|
+
from typeguard import typechecked
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@typechecked
|
|
9
|
+
def get_assembly_directed_edge_to_edge(
|
|
10
|
+
n_edges: int,
|
|
11
|
+
) -> NDArray[np.int64]:
|
|
12
|
+
"""
|
|
13
|
+
For each directed edge, return the index of its parent undirected edge.
|
|
14
|
+
|
|
15
|
+
Since directed edges are interleaved (``directed_edge 2i`` and
|
|
16
|
+
``directed_edge 2i+1`` both belong to ``edge i``), the mapping is:
|
|
17
|
+
|
|
18
|
+
.. code-block:: text
|
|
19
|
+
|
|
20
|
+
edge index = directed_edge index // 2
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
n_edges : int
|
|
25
|
+
Number of undirected edges.
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
NDArray[np.int64]
|
|
30
|
+
1D array of shape ``(2 * n_edges,)``. Each entry is the index of
|
|
31
|
+
the parent undirected edge.
|
|
32
|
+
|
|
33
|
+
Examples
|
|
34
|
+
--------
|
|
35
|
+
>>> import numpy as np
|
|
36
|
+
>>> from scadpy import get_assembly_directed_edge_to_edge
|
|
37
|
+
|
|
38
|
+
>>> # triangle: 3 edges → 6 directed edges
|
|
39
|
+
>>> get_assembly_directed_edge_to_edge(3)
|
|
40
|
+
array([0, 0, 1, 1, 2, 2])
|
|
41
|
+
|
|
42
|
+
>>> # square: 4 edges → 8 directed edges
|
|
43
|
+
>>> get_assembly_directed_edge_to_edge(4)
|
|
44
|
+
array([0, 0, 1, 1, 2, 2, 3, 3])
|
|
45
|
+
"""
|
|
46
|
+
if n_edges == 0:
|
|
47
|
+
return np.empty(0, dtype=np.int64)
|
|
48
|
+
|
|
49
|
+
return np.arange(n_edges, dtype=np.int64).repeat(2)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from numpy.typing import NDArray
|
|
5
|
+
from typeguard import typechecked
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@typechecked
|
|
9
|
+
def get_assembly_directed_edge_to_vertex(
|
|
10
|
+
edge_to_vertex: NDArray[np.int64],
|
|
11
|
+
) -> NDArray[np.int64]:
|
|
12
|
+
"""
|
|
13
|
+
For each directed edge, return the indices of its start and end vertices.
|
|
14
|
+
|
|
15
|
+
Each undirected edge ``i`` gives rise to two directed edges, interleaved:
|
|
16
|
+
|
|
17
|
+
- ``directed_edge 2i`` : forward → ``[start, end]``
|
|
18
|
+
- ``directed_edge 2i+1`` : backward → ``[end, start]``
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
edge_to_vertex : NDArray[np.int64]
|
|
23
|
+
2D array of shape ``(n_edges, 2)`` mapping each edge to its
|
|
24
|
+
``[start_vertex, end_vertex]`` indices.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
NDArray[np.int64]
|
|
29
|
+
2D array of shape ``(2 * n_edges, 2)``. Each row is
|
|
30
|
+
``[start_vertex, end_vertex]`` for the directed edge.
|
|
31
|
+
|
|
32
|
+
Examples
|
|
33
|
+
--------
|
|
34
|
+
>>> import numpy as np
|
|
35
|
+
>>> from scadpy import get_assembly_directed_edge_to_vertex
|
|
36
|
+
|
|
37
|
+
>>> # triangle: 3 edges → 6 directed edges
|
|
38
|
+
>>> edge_to_vertex = np.array(
|
|
39
|
+
... [[0, 1], [1, 2], [2, 0]], dtype=np.int64
|
|
40
|
+
... )
|
|
41
|
+
>>> get_assembly_directed_edge_to_vertex(edge_to_vertex)
|
|
42
|
+
array([[0, 1],
|
|
43
|
+
[1, 0],
|
|
44
|
+
[1, 2],
|
|
45
|
+
[2, 1],
|
|
46
|
+
[2, 0],
|
|
47
|
+
[0, 2]])
|
|
48
|
+
"""
|
|
49
|
+
if len(edge_to_vertex) == 0:
|
|
50
|
+
return np.empty((0, 2), dtype=np.int64)
|
|
51
|
+
|
|
52
|
+
forward = edge_to_vertex
|
|
53
|
+
backward = edge_to_vertex[:, ::-1]
|
|
54
|
+
return np.stack([forward, backward], axis=1).reshape(-1, 2)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"get_assembly_edge_lengths",
|
|
3
|
+
"get_assembly_edge_midpoints",
|
|
4
|
+
"get_assembly_edge_normals",
|
|
5
|
+
]
|
|
6
|
+
|
|
7
|
+
from .get_assembly_edge_lengths import get_assembly_edge_lengths
|
|
8
|
+
from .get_assembly_edge_midpoints import get_assembly_edge_midpoints
|
|
9
|
+
from .get_assembly_edge_normals import get_assembly_edge_normals
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from numpy.typing import NDArray
|
|
5
|
+
from typeguard import typechecked
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@typechecked
|
|
9
|
+
def get_assembly_edge_lengths(
|
|
10
|
+
edge_to_vertex: NDArray[np.int64],
|
|
11
|
+
vertex_coordinates: NDArray[np.float64],
|
|
12
|
+
) -> NDArray[np.float64]:
|
|
13
|
+
"""
|
|
14
|
+
For each edge, return the Euclidean distance between its two vertices.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
edge_to_vertex : NDArray[np.int64]
|
|
19
|
+
2D array of shape ``(n_edges, 2)`` mapping each edge to its
|
|
20
|
+
``[start_vertex, end_vertex]`` indices.
|
|
21
|
+
vertex_coordinates : NDArray[np.float64]
|
|
22
|
+
2D array of shape ``(n_vertices, d)`` with vertex coordinates.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
NDArray[np.float64]
|
|
27
|
+
1D array of shape ``(n_edges,)``, one length per edge.
|
|
28
|
+
|
|
29
|
+
Examples
|
|
30
|
+
--------
|
|
31
|
+
>>> import numpy as np
|
|
32
|
+
>>> from scadpy.core.assembly import get_assembly_edge_lengths
|
|
33
|
+
|
|
34
|
+
>>> edge_to_vertex = np.array(
|
|
35
|
+
... [[0, 1], [1, 2], [2, 0]], dtype=np.int64
|
|
36
|
+
... )
|
|
37
|
+
>>> vertex_coordinates = np.array([[0., 0.], [3., 0.], [0., 4.]])
|
|
38
|
+
>>> get_assembly_edge_lengths(edge_to_vertex, vertex_coordinates)
|
|
39
|
+
array([3., 5., 4.])
|
|
40
|
+
"""
|
|
41
|
+
if len(edge_to_vertex) == 0:
|
|
42
|
+
return np.empty(0, dtype=np.float64)
|
|
43
|
+
|
|
44
|
+
starts = vertex_coordinates[edge_to_vertex[:, 0]]
|
|
45
|
+
ends = vertex_coordinates[edge_to_vertex[:, 1]]
|
|
46
|
+
return np.linalg.norm(ends - starts, axis=1)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from numpy.typing import NDArray
|
|
5
|
+
from typeguard import typechecked
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@typechecked
|
|
9
|
+
def get_assembly_edge_midpoints(
|
|
10
|
+
edge_to_vertex: NDArray[np.int64],
|
|
11
|
+
vertex_coordinates: NDArray[np.float64],
|
|
12
|
+
) -> NDArray[np.float64]:
|
|
13
|
+
"""
|
|
14
|
+
For each edge, return the midpoint between its two vertices.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
edge_to_vertex : NDArray[np.int64]
|
|
19
|
+
2D array of shape ``(n_edges, 2)`` mapping each edge to its
|
|
20
|
+
``[start_vertex, end_vertex]`` indices.
|
|
21
|
+
vertex_coordinates : NDArray[np.float64]
|
|
22
|
+
2D array of shape ``(n_vertices, d)`` with vertex coordinates.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
NDArray[np.float64]
|
|
27
|
+
2D array of shape ``(n_edges, d)``, one midpoint per edge.
|
|
28
|
+
|
|
29
|
+
Examples
|
|
30
|
+
--------
|
|
31
|
+
>>> import numpy as np
|
|
32
|
+
>>> from scadpy.core.assembly import get_assembly_edge_midpoints
|
|
33
|
+
|
|
34
|
+
>>> edge_to_vertex = np.array(
|
|
35
|
+
... [[0, 1], [1, 2], [2, 0]], dtype=np.int64
|
|
36
|
+
... )
|
|
37
|
+
>>> vertex_coordinates = np.array([[0., 0.], [2., 0.], [1., 2.]])
|
|
38
|
+
>>> get_assembly_edge_midpoints(
|
|
39
|
+
... edge_to_vertex, vertex_coordinates
|
|
40
|
+
... )
|
|
41
|
+
array([[1. , 0. ],
|
|
42
|
+
[1.5, 1. ],
|
|
43
|
+
[0.5, 1. ]])
|
|
44
|
+
"""
|
|
45
|
+
if len(edge_to_vertex) == 0:
|
|
46
|
+
d = vertex_coordinates.shape[1] if vertex_coordinates.ndim == 2 else 0
|
|
47
|
+
return np.empty((0, d), dtype=np.float64)
|
|
48
|
+
|
|
49
|
+
starts = vertex_coordinates[edge_to_vertex[:, 0]]
|
|
50
|
+
ends = vertex_coordinates[edge_to_vertex[:, 1]]
|
|
51
|
+
return (starts + ends) / 2
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from numpy.typing import NDArray
|
|
5
|
+
from typeguard import typechecked
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@typechecked
|
|
9
|
+
def get_assembly_edge_normals(
|
|
10
|
+
edge_to_vertex: NDArray[np.int64],
|
|
11
|
+
vertex_coordinates: NDArray[np.float64],
|
|
12
|
+
) -> NDArray[np.float64]:
|
|
13
|
+
"""
|
|
14
|
+
For each edge, return its outward unit normal.
|
|
15
|
+
|
|
16
|
+
The outward normal is the 90° clockwise rotation of the edge direction
|
|
17
|
+
vector ``(dx, dy) → (dy, -dx)``. For exterior rings (CCW winding), this
|
|
18
|
+
points away from the filled area. For interior rings (CW winding, i.e.
|
|
19
|
+
holes), this also points away from the filled area (outward into the hole).
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
edge_to_vertex : NDArray[np.int64]
|
|
24
|
+
2D array of shape ``(n_edges, 2)`` mapping each edge to its
|
|
25
|
+
``[start_vertex, end_vertex]`` indices.
|
|
26
|
+
vertex_coordinates : NDArray[np.float64]
|
|
27
|
+
2D array of shape ``(n_vertices, 2)`` with vertex coordinates.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
NDArray[np.float64]
|
|
32
|
+
2D array of shape ``(n_edges, 2)``. Each row is a unit vector
|
|
33
|
+
``[nx, ny]`` perpendicular to the edge and pointing outward.
|
|
34
|
+
|
|
35
|
+
Examples
|
|
36
|
+
--------
|
|
37
|
+
>>> import numpy as np
|
|
38
|
+
>>> from scadpy import get_assembly_edge_normals
|
|
39
|
+
|
|
40
|
+
>>> # square centered at origin: 4 edges, normals point outward
|
|
41
|
+
>>> edge_to_vertex = np.array(
|
|
42
|
+
... [[0, 1], [1, 2], [2, 3], [3, 0]], dtype=np.int64
|
|
43
|
+
... )
|
|
44
|
+
>>> vertex_coordinates = np.array(
|
|
45
|
+
... [[-1., -1.], [1., -1.], [1., 1.], [-1., 1.]]
|
|
46
|
+
... )
|
|
47
|
+
>>> get_assembly_edge_normals(
|
|
48
|
+
... edge_to_vertex, vertex_coordinates
|
|
49
|
+
... ).round(4)
|
|
50
|
+
array([[ 0., -1.],
|
|
51
|
+
[ 1., -0.],
|
|
52
|
+
[ 0., 1.],
|
|
53
|
+
[-1., -0.]])
|
|
54
|
+
"""
|
|
55
|
+
if len(edge_to_vertex) == 0:
|
|
56
|
+
d = vertex_coordinates.shape[1] if vertex_coordinates.ndim == 2 else 0
|
|
57
|
+
return np.empty((0, d), dtype=np.float64)
|
|
58
|
+
|
|
59
|
+
starts = vertex_coordinates[edge_to_vertex[:, 0]]
|
|
60
|
+
ends = vertex_coordinates[edge_to_vertex[:, 1]]
|
|
61
|
+
|
|
62
|
+
directions = ends - starts
|
|
63
|
+
lengths = np.linalg.norm(directions, axis=1, keepdims=True)
|
|
64
|
+
directions_normalized = directions / lengths
|
|
65
|
+
|
|
66
|
+
# 90° CW rotation: (dx, dy) → (dy, -dx)
|
|
67
|
+
return np.stack([directions_normalized[:, 1], -directions_normalized[:, 0]], axis=1)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"get_assembly_face_corner_angles",
|
|
3
|
+
"get_assembly_face_corner_normals",
|
|
4
|
+
"get_assembly_face_corner_to_incoming_directed_edge",
|
|
5
|
+
"get_assembly_face_corner_to_outgoing_directed_edge",
|
|
6
|
+
"get_assembly_face_directed_edge_to_corner",
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
from .get_assembly_face_corner_angles import get_assembly_face_corner_angles
|
|
10
|
+
from .get_assembly_face_corner_normals import get_assembly_face_corner_normals
|
|
11
|
+
from .get_assembly_face_corner_to_incoming_directed_edge import get_assembly_face_corner_to_incoming_directed_edge
|
|
12
|
+
from .get_assembly_face_corner_to_outgoing_directed_edge import get_assembly_face_corner_to_outgoing_directed_edge
|
|
13
|
+
from .get_assembly_face_directed_edge_to_corner import get_assembly_face_directed_edge_to_corner
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from numpy.typing import NDArray
|
|
5
|
+
from typeguard import typechecked
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@typechecked
|
|
9
|
+
def get_assembly_face_corner_angles(
|
|
10
|
+
corner_to_vertex: NDArray[np.int64],
|
|
11
|
+
vertex_coordinates: NDArray[np.float64],
|
|
12
|
+
) -> NDArray[np.float64]:
|
|
13
|
+
"""
|
|
14
|
+
For each corner, return its interior angle in degrees.
|
|
15
|
+
|
|
16
|
+
The angle is always positive, in the range (0°, 180°). It represents
|
|
17
|
+
the turning angle at the corner, regardless of whether the corner is
|
|
18
|
+
convex or concave. Use convexity information separately to distinguish
|
|
19
|
+
the two cases.
|
|
20
|
+
|
|
21
|
+
The angle is computed as the absolute value of the signed angle from the
|
|
22
|
+
incoming edge to the outgoing edge at each corner, using the 2D cross
|
|
23
|
+
product to determine orientation.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
corner_to_vertex : NDArray[np.int64]
|
|
28
|
+
2D array of shape ``(n_corners, 3)``. Each row is
|
|
29
|
+
``[prev_vertex, curr_vertex, next_vertex]``.
|
|
30
|
+
vertex_coordinates : NDArray[np.float64]
|
|
31
|
+
2D array of shape ``(n_vertices, 2)`` with vertex coordinates.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
NDArray[np.float64]
|
|
36
|
+
1D array of shape ``(n_corners,)``, one angle per corner in degrees.
|
|
37
|
+
All values are in the range (0°, 180°).
|
|
38
|
+
|
|
39
|
+
Examples
|
|
40
|
+
--------
|
|
41
|
+
>>> import numpy as np
|
|
42
|
+
>>> from scadpy import get_assembly_face_corner_angles
|
|
43
|
+
|
|
44
|
+
>>> # square: 4 right-angle corners
|
|
45
|
+
>>> corner_to_vertex = np.array(
|
|
46
|
+
... [[3, 0, 1], [0, 1, 2], [1, 2, 3], [2, 3, 0]],
|
|
47
|
+
... dtype=np.int64
|
|
48
|
+
... )
|
|
49
|
+
>>> vertex_coordinates = np.array(
|
|
50
|
+
... [[-1., -1.], [1., -1.], [1., 1.], [-1., 1.]]
|
|
51
|
+
... )
|
|
52
|
+
>>> get_assembly_face_corner_angles(
|
|
53
|
+
... corner_to_vertex, vertex_coordinates
|
|
54
|
+
... )
|
|
55
|
+
array([90., 90., 90., 90.])
|
|
56
|
+
"""
|
|
57
|
+
if len(corner_to_vertex) == 0:
|
|
58
|
+
return np.empty(0, dtype=np.float64)
|
|
59
|
+
|
|
60
|
+
prev_coords = vertex_coordinates[corner_to_vertex[:, 0]]
|
|
61
|
+
curr_coords = vertex_coordinates[corner_to_vertex[:, 1]]
|
|
62
|
+
next_coords = vertex_coordinates[corner_to_vertex[:, 2]]
|
|
63
|
+
|
|
64
|
+
v_in = curr_coords - prev_coords
|
|
65
|
+
v_out = next_coords - curr_coords
|
|
66
|
+
|
|
67
|
+
cross = v_in[:, 0] * v_out[:, 1] - v_in[:, 1] * v_out[:, 0]
|
|
68
|
+
dot = v_in[:, 0] * v_out[:, 0] + v_in[:, 1] * v_out[:, 1]
|
|
69
|
+
|
|
70
|
+
angles_rad = np.arctan2(cross, dot)
|
|
71
|
+
|
|
72
|
+
return np.abs(np.degrees(angles_rad))
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from numpy.typing import NDArray
|
|
5
|
+
from typeguard import typechecked
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@typechecked
|
|
9
|
+
def get_assembly_face_corner_normals(
|
|
10
|
+
corner_to_vertex: NDArray[np.int64],
|
|
11
|
+
vertex_coordinates: NDArray[np.float64],
|
|
12
|
+
are_corners_convex: NDArray[np.bool_],
|
|
13
|
+
epsilon: float = 1e-10,
|
|
14
|
+
) -> NDArray[np.float64]:
|
|
15
|
+
"""
|
|
16
|
+
For each corner, return its outward unit normal.
|
|
17
|
+
|
|
18
|
+
The normal is the bisector of the two adjacent edge normals (90° CW
|
|
19
|
+
rotation of each edge direction), oriented outward for convex corners
|
|
20
|
+
and inward for concave ones. For degenerate 180° corners where the
|
|
21
|
+
bisector vanishes, falls back to the incoming edge normal.
|
|
22
|
+
|
|
23
|
+
The term *face* distinguishes this from a purely topological corner:
|
|
24
|
+
the computation relies on face geometry (vertex coordinates) and face
|
|
25
|
+
orientation (convexity), making it applicable to both 2D shape rings
|
|
26
|
+
and 3D solid faces.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
corner_to_vertex : NDArray[np.int64]
|
|
31
|
+
2D array of shape ``(n_corners, 3)``. Each row is
|
|
32
|
+
``[prev_vertex, curr_vertex, next_vertex]``.
|
|
33
|
+
vertex_coordinates : NDArray[np.float64]
|
|
34
|
+
2D array of shape ``(n_vertices, d)`` with vertex coordinates.
|
|
35
|
+
are_corners_convex : NDArray[np.bool_]
|
|
36
|
+
1D boolean array of shape ``(n_corners,)``. True if the corner
|
|
37
|
+
is convex (normal points outward), False if concave (normal points
|
|
38
|
+
inward).
|
|
39
|
+
epsilon : float, optional
|
|
40
|
+
Threshold below which the bisector norm is considered degenerate
|
|
41
|
+
(straight 180° corner). Defaults to ``1e-10``.
|
|
42
|
+
|
|
43
|
+
Returns
|
|
44
|
+
-------
|
|
45
|
+
NDArray[np.float64]
|
|
46
|
+
2D array of shape ``(n_corners, 2)``. Each row is a unit vector
|
|
47
|
+
``[nx, ny]``.
|
|
48
|
+
|
|
49
|
+
Examples
|
|
50
|
+
--------
|
|
51
|
+
>>> import numpy as np
|
|
52
|
+
>>> from scadpy import get_assembly_face_corner_normals
|
|
53
|
+
|
|
54
|
+
>>> # square: 4 convex corners, normals point outward
|
|
55
|
+
>>> # (diagonal directions)
|
|
56
|
+
>>> corner_to_vertex = np.array(
|
|
57
|
+
... [[3, 0, 1], [0, 1, 2], [1, 2, 3], [2, 3, 0]],
|
|
58
|
+
... dtype=np.int64
|
|
59
|
+
... )
|
|
60
|
+
>>> vertex_coordinates = np.array(
|
|
61
|
+
... [[-1., -1.], [1., -1.], [1., 1.], [-1., 1.]]
|
|
62
|
+
... )
|
|
63
|
+
>>> are_corners_convex = np.array([True, True, True, True])
|
|
64
|
+
>>> get_assembly_face_corner_normals(
|
|
65
|
+
... corner_to_vertex,
|
|
66
|
+
... vertex_coordinates,
|
|
67
|
+
... are_corners_convex,
|
|
68
|
+
... ).round(4)
|
|
69
|
+
array([[-0.7071, -0.7071],
|
|
70
|
+
[ 0.7071, -0.7071],
|
|
71
|
+
[ 0.7071, 0.7071],
|
|
72
|
+
[-0.7071, 0.7071]])
|
|
73
|
+
"""
|
|
74
|
+
if len(corner_to_vertex) == 0:
|
|
75
|
+
d = vertex_coordinates.shape[1] if vertex_coordinates.ndim == 2 else 0
|
|
76
|
+
return np.empty((0, d), dtype=np.float64)
|
|
77
|
+
|
|
78
|
+
prev_coords = vertex_coordinates[corner_to_vertex[:, 0]]
|
|
79
|
+
curr_coords = vertex_coordinates[corner_to_vertex[:, 1]]
|
|
80
|
+
next_coords = vertex_coordinates[corner_to_vertex[:, 2]]
|
|
81
|
+
|
|
82
|
+
v_in = curr_coords - prev_coords
|
|
83
|
+
v_out = next_coords - curr_coords
|
|
84
|
+
|
|
85
|
+
v_in_norm = v_in / np.linalg.norm(v_in, axis=1, keepdims=True)
|
|
86
|
+
v_out_norm = v_out / np.linalg.norm(v_out, axis=1, keepdims=True)
|
|
87
|
+
|
|
88
|
+
# outward edge normals: 90° CW rotation (dx, dy) → (dy, -dx)
|
|
89
|
+
n_in = np.stack([v_in_norm[:, 1], -v_in_norm[:, 0]], axis=1)
|
|
90
|
+
n_out = np.stack([v_out_norm[:, 1], -v_out_norm[:, 0]], axis=1)
|
|
91
|
+
|
|
92
|
+
bisector = n_in + n_out
|
|
93
|
+
|
|
94
|
+
# degenerate case: 180° corner — bisector vanishes, fall back to incoming edge normal
|
|
95
|
+
zero_mask = np.linalg.norm(bisector, axis=1) < epsilon
|
|
96
|
+
if np.any(zero_mask):
|
|
97
|
+
bisector[zero_mask] = n_in[zero_mask]
|
|
98
|
+
|
|
99
|
+
bisector_norm = bisector / np.linalg.norm(bisector, axis=1, keepdims=True)
|
|
100
|
+
|
|
101
|
+
sign = np.where(are_corners_convex, 1.0, -1.0)[:, np.newaxis]
|
|
102
|
+
|
|
103
|
+
return bisector_norm * sign
|