File: mesh_normals.py

package info (click to toggle)
python-vispy 0.14.3-1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 8,840 kB
  • sloc: python: 59,436; javascript: 6,800; makefile: 69; sh: 6
file content (159 lines) | stat: -rw-r--r-- 6,354 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# Copyright (c) Vispy Development Team. All Rights Reserved.
# Distributed under the (new) BSD License. See LICENSE.txt for more info.
# -----------------------------------------------------------------------------
"""A visual for displaying mesh normals as lines."""

import numpy as np

from . import LineVisual


class MeshNormalsVisual(LineVisual):
    """Display mesh normals as lines.

    Parameters
    ----------
    meshdata : instance of :class:`~vispy.geometry.meshdata.MeshData`
        The mesh data.
    primitive : {'face', 'vertex'}
        The primitive type on which to compute and display the normals.
    length : None or float or array-like, optional
        The length(s) of the normals. If None, the length is computed with
        `length_method`.
    length_method : {'median_edge', 'max_extent'}, default='median_edge'
        The method to compute the length of the normals (when `length=None`).
        Methods: 'median_edge', the median edge length; 'max_extent', the
        maximum side length of the bounding box of the mesh.
    length_scale : float, default=1.0
        A scale factor applied to the length computed with `length_method`.
    **kwargs : dict, optional
        Extra arguments to define the appearance of lines. Refer to
        :class:`~vispy.visuals.line.line.LineVisual`.

    Examples
    --------
    Create a :class:`~vispy.visuals.mesh.MeshVisual` on which to display
    the normals and get the :class:`~vispy.geometry.meshdata.MeshData`:

    >>> mesh = MeshVisual(vertices=vertices, faces=faces, ...)
    >>> meshdata = mesh.mesh_data

    Create a visual for the mesh normals:

    >>> normals = MeshNormalsVisual(meshdata)

    Display the face normals:

    >>> MeshNormalsVisual(..., primitive='face')
    >>> MeshNormalsVisual(...)  # equivalent (default values)

    Display the vertex normals:

    >>> MeshNormalsVisual(..., primitive='vertex')

    Fixed length for all normals:

    >>> MeshNormalsVisual(..., length=0.25)

    Individual length per normal:

    >>> lengths = np.array([0.5, 0.2, 0.7, ..., 0.7], dtype=float)
    >>> MeshNormalsVisual(..., length=lengths)
    >>> assert len(lengths) == len(faces)  # for face normals
    >>> assert len(lengths) == len(vertices)  # for vertex normals

    Normals at about the length of a triangle:

    >>> MeshNormalsVisual(..., length_method='median_edge', length_scale=1.0)
    >>> MeshNormalsVisual(...)  # equivalent (default values)

    Normals at about 10% the size of the mesh:

    >>> MeshNormalsVisual(..., length_method='max_extent', length_scale=0.1)
    """

    def __init__(self, meshdata=None, primitive='face', length=None,
                 length_method='median_edge', length_scale=1.0, **kwargs):
        self._previous_meshdata = None
        super().__init__(connect='segments')
        self.set_data(meshdata, primitive, length, length_method, length_scale, **kwargs)

    def set_data(self, meshdata=None, primitive='face', length=None,
                 length_method='median_edge', length_scale=1.0, **kwargs):
        """Set the data used to draw this visual

        Parameters
        ----------
        meshdata : instance of :class:`~vispy.geometry.meshdata.MeshData`
            The mesh data.
        primitive : {'face', 'vertex'}
            The primitive type on which to compute and display the normals.
        length : None or float or array-like, optional
            The length(s) of the normals. If None, the length is computed with
            `length_method`.
        length_method : {'median_edge', 'max_extent'}, default='median_edge'
            The method to compute the length of the normals (when `length=None`).
            Methods: 'median_edge', the median edge length; 'max_extent', the
            maximum side length of the bounding box of the mesh.
        length_scale : float, default=1.0
            A scale factor applied to the length computed with `length_method`.
        **kwargs : dict, optional
            Extra arguments to define the appearance of lines. Refer to
            :class:`~vispy.visuals.line.line.LineVisual`.
        """
        if meshdata is None:
            meshdata = self._previous_meshdata

        if meshdata is None or meshdata.is_empty():
            normals = None
        elif primitive == 'face':
            normals = meshdata.get_face_normals()
        elif primitive == 'vertex':
            normals = meshdata.get_vertex_normals()
        else:
            raise ValueError('primitive must be "face" or "vertex", got %s'
                             % primitive)

        # remove connect from kwargs to make sure we don't change it
        kwargs.pop('connect', None)

        if normals is None:
            super().set_data(pos=np.empty((0, 3), dtype=np.float32), connect='segments', **kwargs)
            return

        self._previous_meshdata = meshdata

        norms = np.sqrt((normals ** 2).sum(axis=-1, keepdims=True))
        unit_normals = normals / norms

        if length is None and length_method == 'median_edge':
            face_corners = meshdata.get_vertices(indexed='faces')
            edges = np.stack((
                face_corners[:, 1, :] - face_corners[:, 0, :],
                face_corners[:, 2, :] - face_corners[:, 1, :],
                face_corners[:, 0, :] - face_corners[:, 2, :],
            ))
            edge_lengths = np.sqrt((edges ** 2).sum(axis=-1))
            length = np.median(edge_lengths)
        elif length is None and length_method == 'max_extent':
            vertices = meshdata.get_vertices()
            max_extent = np.max(vertices.max(axis=0) - vertices.min(axis=0))
            length = max_extent
        length *= length_scale

        if primitive == 'face':
            origins = meshdata.get_vertices(indexed='faces')
            origins = origins.mean(axis=1)
        elif primitive == 'vertex':
            origins = meshdata.get_vertices()

        # Ensure the broadcasting if the input is an `(n,)` array.
        length = np.atleast_1d(length)
        length = length[:, None]

        ends = origins + length * unit_normals
        segments = np.hstack((origins, ends)).reshape(-1, 3)

        super().set_data(pos=segments, connect='segments', **kwargs)