summaryrefslogtreecommitdiff
path: root/src/silx/gui/plot3d/scene/utils.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/silx/gui/plot3d/scene/utils.py')
-rw-r--r--src/silx/gui/plot3d/scene/utils.py662
1 files changed, 662 insertions, 0 deletions
diff --git a/src/silx/gui/plot3d/scene/utils.py b/src/silx/gui/plot3d/scene/utils.py
new file mode 100644
index 0000000..c6cd129
--- /dev/null
+++ b/src/silx/gui/plot3d/scene/utils.py
@@ -0,0 +1,662 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2015-2020 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""
+This module provides functions to generate indices, to check intersection
+and to handle planes.
+"""
+
+from __future__ import absolute_import, division, unicode_literals
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "25/07/2016"
+
+
+import logging
+import numpy
+
+from . import event
+
+
+_logger = logging.getLogger(__name__)
+
+
+# numpy #######################################################################
+
+def _uniqueAlongLastAxis(a):
+ """Numpy unique on the last axis of a 2D array
+
+ Implemented here as not in numpy as of writing.
+
+ See adding axis parameter to numpy.unique:
+ https://github.com/numpy/numpy/pull/3584/files#r6225452
+
+ :param array_like a: Input array.
+ :return: Unique elements along the last axis.
+ :rtype: numpy.ndarray
+ """
+ assert len(a.shape) == 2
+
+ # Construct a type over last array dimension to run unique on a 1D array
+ if a.dtype.char in numpy.typecodes['AllInteger']:
+ # Bit-wise comparison of the 2 indices of a line at once
+ # Expect a C contiguous array of shape N, 2
+ uniquedt = numpy.dtype((numpy.void, a.itemsize * a.shape[-1]))
+ elif a.dtype.char in numpy.typecodes['Float']:
+ uniquedt = [('f{i}'.format(i=i), a.dtype) for i in range(a.shape[-1])]
+ else:
+ raise TypeError("Unsupported type {dtype}".format(dtype=a.dtype))
+
+ uniquearray = numpy.unique(numpy.ascontiguousarray(a).view(uniquedt))
+ return uniquearray.view(a.dtype).reshape((-1, a.shape[-1]))
+
+
+# conversions #################################################################
+
+def triangleToLineIndices(triangleIndices, unicity=False):
+ """Generates lines indices from triangle indices.
+
+ This is generating lines indices for the edges of the triangles.
+
+ :param triangleIndices: The indices to draw a set of vertices as triangles.
+ :type triangleIndices: numpy.ndarray
+ :param bool unicity: If True remove duplicated lines,
+ else (the default) returns all lines.
+ :return: The indices to draw the edges of the triangles as lines.
+ :rtype: 1D numpy.ndarray of uint16 or uint32.
+ """
+ # Makes sure indices ar packed by triangle
+ triangleIndices = triangleIndices.reshape(-1, 3)
+
+ # Pack line indices by triangle and by edge
+ lineindices = numpy.empty((len(triangleIndices), 3, 2),
+ dtype=triangleIndices.dtype)
+ lineindices[:, 0] = triangleIndices[:, :2] # edge = t0, t1
+ lineindices[:, 1] = triangleIndices[:, 1:] # edge =t1, t2
+ lineindices[:, 2] = triangleIndices[:, ::2] # edge = t0, t2
+
+ if unicity:
+ lineindices = _uniqueAlongLastAxis(lineindices.reshape(-1, 2))
+
+ # Make sure it is 1D
+ lineindices.shape = -1
+
+ return lineindices
+
+
+def verticesNormalsToLines(vertices, normals, scale=1.):
+ """Return vertices of lines representing normals at given positions.
+
+ :param vertices: Positions of the points.
+ :type vertices: numpy.ndarray with shape: (nbPoints, 3)
+ :param normals: Corresponding normals at the points.
+ :type normals: numpy.ndarray with shape: (nbPoints, 3)
+ :param float scale: The scale factor to apply to normals.
+ :returns: Array of vertices to draw corresponding lines.
+ :rtype: numpy.ndarray with shape: (nbPoints * 2, 3)
+ """
+ linevertices = numpy.empty((len(vertices) * 2, 3), dtype=vertices.dtype)
+ linevertices[0::2] = vertices
+ linevertices[1::2] = vertices + scale * normals
+ return linevertices
+
+
+def unindexArrays(mode, indices, *arrays):
+ """Convert indexed GL primitives to unindexed ones.
+
+ Given indices in arrays and the OpenGL primitive they represent,
+ return the unindexed equivalent.
+
+ :param str mode:
+ Kind of primitive represented by indices.
+ In: points, lines, line_strip, loop, triangles, triangle_strip, fan.
+ :param indices: Indices in other arrays
+ :type indices: numpy.ndarray of dimension 1.
+ :param arrays: Remaining arguments are arrays to convert
+ :return: Converted arrays
+ :rtype: tuple of numpy.ndarray
+ """
+ indices = numpy.array(indices, copy=False)
+
+ assert mode in ('points',
+ 'lines', 'line_strip', 'loop',
+ 'triangles', 'triangle_strip', 'fan')
+
+ if mode in ('lines', 'line_strip', 'loop'):
+ assert len(indices) >= 2
+ elif mode in ('triangles', 'triangle_strip', 'fan'):
+ assert len(indices) >= 3
+
+ assert indices.min() >= 0
+ max_index = indices.max()
+ for data in arrays:
+ assert len(data) >= max_index
+
+ if mode == 'line_strip':
+ unpacked = numpy.empty((2 * (len(indices) - 1),), dtype=indices.dtype)
+ unpacked[0::2] = indices[:-1]
+ unpacked[1::2] = indices[1:]
+ indices = unpacked
+
+ elif mode == 'loop':
+ unpacked = numpy.empty((2 * len(indices),), dtype=indices.dtype)
+ unpacked[0::2] = indices
+ unpacked[1:-1:2] = indices[1:]
+ unpacked[-1] = indices[0]
+ indices = unpacked
+
+ elif mode == 'triangle_strip':
+ unpacked = numpy.empty((3 * (len(indices) - 2),), dtype=indices.dtype)
+ unpacked[0::3] = indices[:-2]
+ unpacked[1::3] = indices[1:-1]
+ unpacked[2::3] = indices[2:]
+ indices = unpacked
+
+ elif mode == 'fan':
+ unpacked = numpy.empty((3 * (len(indices) - 2),), dtype=indices.dtype)
+ unpacked[0::3] = indices[0]
+ unpacked[1::3] = indices[1:-1]
+ unpacked[2::3] = indices[2:]
+ indices = unpacked
+
+ return tuple(numpy.ascontiguousarray(data[indices]) for data in arrays)
+
+
+def triangleStripToTriangles(strip):
+ """Convert a triangle strip to a set of triangles.
+
+ The order of the corners is inverted for odd triangles.
+
+ :param numpy.ndarray strip:
+ Array of triangle corners of shape (N, 3).
+ N must be at least 3.
+ :return: Equivalent triangles corner as an array of shape (N, 3, 3)
+ :rtype: numpy.ndarray
+ """
+ strip = numpy.array(strip).reshape(-1, 3)
+ assert len(strip) >= 3
+
+ triangles = numpy.empty((len(strip) - 2, 3, 3), dtype=strip.dtype)
+ triangles[0::2, 0] = strip[0:-2:2]
+ triangles[0::2, 1] = strip[1:-1:2]
+ triangles[0::2, 2] = strip[2::2]
+
+ triangles[1::2, 0] = strip[3::2]
+ triangles[1::2, 1] = strip[2:-1:2]
+ triangles[1::2, 2] = strip[1:-2:2]
+
+ return triangles
+
+
+def trianglesNormal(positions):
+ """Return normal for each triangle.
+
+ :param positions: Serie of triangle's corners
+ :type positions: numpy.ndarray of shape (NbTriangles*3, 3)
+ :return: Normals corresponding to each position.
+ :rtype: numpy.ndarray of shape (NbTriangles, 3)
+ """
+ assert positions.ndim == 2
+ assert positions.shape[1] == 3
+
+ positions = numpy.array(positions, copy=False).reshape(-1, 3, 3)
+
+ normals = numpy.cross(positions[:, 1] - positions[:, 0],
+ positions[:, 2] - positions[:, 0])
+
+ # Normalize normals
+ norms = numpy.linalg.norm(normals, axis=1)
+ norms[norms == 0] = 1
+
+ return normals / norms.reshape(-1, 1)
+
+
+# grid ########################################################################
+
+def gridVertices(dim0Array, dim1Array, dtype):
+ """Generate an array of 2D positions from 2 arrays of 1D coordinates.
+
+ :param dim0Array: 1D array-like of coordinates along the first dimension.
+ :param dim1Array: 1D array-like of coordinates along the second dimension.
+ :param numpy.dtype dtype: Data type of the output array.
+ :return: Array of grid coordinates.
+ :rtype: numpy.ndarray with shape: (len(dim0Array), len(dim1Array), 2)
+ """
+ grid = numpy.empty((len(dim0Array), len(dim1Array), 2), dtype=dtype)
+ grid.T[0, :, :] = dim0Array
+ grid.T[1, :, :] = numpy.array(dim1Array, copy=False)[:, None]
+ return grid
+
+
+def triangleStripGridIndices(dim0, dim1):
+ """Generate indices to draw a grid of vertices as a triangle strip.
+
+ Vertices are expected to be stored as row-major (i.e., C contiguous).
+
+ :param int dim0: The number of rows of vertices.
+ :param int dim1: The number of columns of vertices.
+ :return: The vertex indices
+ :rtype: 1D numpy.ndarray of uint32
+ """
+ assert dim0 >= 2
+ assert dim1 >= 2
+
+ # Filling a row of squares +
+ # an index before and one after for degenerated triangles
+ indices = numpy.empty((dim0 - 1, 2 * (dim1 + 1)), dtype=numpy.uint32)
+
+ # Init indices with minimum indices for each row of squares
+ indices[:] = (dim1 * numpy.arange(dim0 - 1, dtype=numpy.uint32))[:, None]
+
+ # Update indices with offset per row of squares
+ offset = numpy.arange(dim1, dtype=numpy.uint32)
+ indices[:, 1:-1:2] += offset
+ offset += dim1
+ indices[:, 2::2] += offset
+ indices[:, -1] += offset[-1]
+
+ # Remove extra indices for degenerated triangles before returning
+ return indices.ravel()[1:-1]
+
+ # Alternative:
+ # indices = numpy.zeros(2 * dim1 * (dim0 - 1) + 2 * (dim0 - 2),
+ # dtype=numpy.uint32)
+ #
+ # offset = numpy.arange(dim1, dtype=numpy.uint32)
+ # for d0Index in range(dim0 - 1):
+ # start = 2 * d0Index * (dim1 + 1)
+ # end = start + 2 * dim1
+ # if d0Index != 0:
+ # indices[start - 2] = offset[-1]
+ # indices[start - 1] = offset[0]
+ # indices[start:end:2] = offset
+ # offset += dim1
+ # indices[start + 1:end:2] = offset
+ # return indices
+
+
+def linesGridIndices(dim0, dim1):
+ """Generate indices to draw a grid of vertices as lines.
+
+ Vertices are expected to be stored as row-major (i.e., C contiguous).
+
+ :param int dim0: The number of rows of vertices.
+ :param int dim1: The number of columns of vertices.
+ :return: The vertex indices.
+ :rtype: 1D numpy.ndarray of uint32
+ """
+ # Horizontal and vertical lines
+ nbsegmentalongdim1 = 2 * (dim1 - 1)
+ nbsegmentalongdim0 = 2 * (dim0 - 1)
+
+ indices = numpy.empty(nbsegmentalongdim1 * dim0 +
+ nbsegmentalongdim0 * dim1,
+ dtype=numpy.uint32)
+
+ # Line indices over dim0
+ onedim1line = (numpy.arange(nbsegmentalongdim1,
+ dtype=numpy.uint32) + 1) // 2
+ indices[:dim0 * nbsegmentalongdim1] = \
+ (dim1 * numpy.arange(dim0, dtype=numpy.uint32)[:, None] +
+ onedim1line[None, :]).ravel()
+
+ # Line indices over dim1
+ onedim0line = (numpy.arange(nbsegmentalongdim0,
+ dtype=numpy.uint32) + 1) // 2
+ indices[dim0 * nbsegmentalongdim1:] = \
+ (numpy.arange(dim1, dtype=numpy.uint32)[:, None] +
+ dim1 * onedim0line[None, :]).ravel()
+
+ return indices
+
+
+# intersection ################################################################
+
+def angleBetweenVectors(refVector, vectors, norm=None):
+ """Return the angle between 2 vectors.
+
+ :param refVector: Coordinates of the reference vector.
+ :type refVector: numpy.ndarray of shape: (NCoords,)
+ :param vectors: Coordinates of the vector(s) to get angle from reference.
+ :type vectors: numpy.ndarray of shape: (NCoords,) or (NbVector, NCoords)
+ :param norm: A direction vector giving an orientation to the angles
+ or None.
+ :returns: The angles in radians in [0, pi] if norm is None
+ else in [0, 2pi].
+ :rtype: float or numpy.ndarray of shape (NbVectors,)
+ """
+ singlevector = len(vectors.shape) == 1
+ if singlevector: # Make it a 2D array for the computation
+ vectors = vectors.reshape(1, -1)
+
+ assert len(refVector.shape) == 1
+ assert len(vectors.shape) == 2
+ assert len(refVector) == vectors.shape[1]
+
+ # Normalize vectors
+ refVector /= numpy.linalg.norm(refVector)
+ vectors = numpy.array([v / numpy.linalg.norm(v) for v in vectors])
+
+ dots = numpy.sum(refVector * vectors, axis=-1)
+ angles = numpy.arccos(numpy.clip(dots, -1., 1.))
+ if norm is not None:
+ signs = numpy.sum(norm * numpy.cross(refVector, vectors), axis=-1) < 0.
+ angles[signs] = numpy.pi * 2. - angles[signs]
+
+ return angles[0] if singlevector else angles
+
+
+def segmentPlaneIntersect(s0, s1, planeNorm, planePt):
+ """Compute the intersection of a segment with a plane.
+
+ :param s0: First end of the segment
+ :type s0: 1D numpy.ndarray-like of length 3
+ :param s1: Second end of the segment
+ :type s1: 1D numpy.ndarray-like of length 3
+ :param planeNorm: Normal vector of the plane.
+ :type planeNorm: numpy.ndarray of shape: (3,)
+ :param planePt: A point of the plane.
+ :type planePt: numpy.ndarray of shape: (3,)
+ :return: The intersection points. The number of points goes
+ from 0 (no intersection) to 2 (segment in the plane)
+ :rtype: list of numpy.ndarray
+ """
+ s0, s1 = numpy.asarray(s0), numpy.asarray(s1)
+
+ segdir = s1 - s0
+ dotnormseg = numpy.dot(planeNorm, segdir)
+ if dotnormseg == 0:
+ # line and plane are parallels
+ if numpy.dot(planeNorm, planePt - s0) == 0: # segment is in plane
+ return [s0, s1]
+ else: # No intersection
+ return []
+
+ alpha = - numpy.dot(planeNorm, s0 - planePt) / dotnormseg
+ if 0. <= alpha <= 1.: # Intersection with segment
+ return [s0 + alpha * segdir]
+ else: # intersection outside segment
+ return []
+
+
+def boxPlaneIntersect(boxVertices, boxLineIndices, planeNorm, planePt):
+ """Return intersection points between a box and a plane.
+
+ :param boxVertices: Position of the corners of the box.
+ :type boxVertices: numpy.ndarray with shape: (8, 3)
+ :param boxLineIndices: Indices of the box edges.
+ :type boxLineIndices: numpy.ndarray-like with shape: (12, 2)
+ :param planeNorm: Normal vector of the plane.
+ :type planeNorm: numpy.ndarray of shape: (3,)
+ :param planePt: A point of the plane.
+ :type planePt: numpy.ndarray of shape: (3,)
+ :return: The found intersection points
+ :rtype: numpy.ndarray with 2 dimensions
+ """
+ segments = numpy.take(boxVertices, boxLineIndices, axis=0)
+
+ points = set() # Gather unique intersection points
+ for seg in segments:
+ for point in segmentPlaneIntersect(seg[0], seg[1], planeNorm, planePt):
+ points.add(tuple(point))
+ points = numpy.array(list(points))
+
+ if len(points) <= 2:
+ return numpy.array(())
+ elif len(points) == 3:
+ return points
+ else: # len(points) > 3
+ # Order point to have a polyline lying on the unit cube's faces
+ vectors = points - numpy.mean(points, axis=0)
+ angles = angleBetweenVectors(vectors[0], vectors, planeNorm)
+ points = numpy.take(points, numpy.argsort(angles), axis=0)
+ return points
+
+
+def clipSegmentToBounds(segment, bounds):
+ """Clip segment to volume aligned with axes.
+
+ :param numpy.ndarray segment: (p0, p1)
+ :param numpy.ndarray bounds: (lower corner, upper corner)
+ :return: Either clipped (p0, p1) or None if outside volume
+ :rtype: Union[None,List[numpy.ndarray]]
+ """
+ segment = numpy.array(segment, copy=False)
+ bounds = numpy.array(bounds, copy=False)
+
+ p0, p1 = segment
+ # Get intersection points of ray with volume boundary planes
+ # Line equation: P = offset * delta + p0
+ delta = p1 - p0
+ deltaNotZero = numpy.array(delta, copy=True)
+ deltaNotZero[deltaNotZero == 0] = numpy.nan # Invalidated to avoid division by zero
+ offsets = ((bounds - p0) / deltaNotZero).reshape(-1)
+ points = offsets.reshape(-1, 1) * delta + p0
+
+ # Avoid precision errors by using bounds value
+ points.shape = 2, 3, 3 # Reshape 1 point per bound value
+ for dim in range(3):
+ points[:, dim, dim] = bounds[:, dim]
+ points.shape = -1, 3 # Set back to 2D array
+
+ # Find intersection points that are included in the volume
+ mask = numpy.logical_and(numpy.all(bounds[0] <= points, axis=1),
+ numpy.all(points <= bounds[1], axis=1))
+ intersections = numpy.unique(offsets[mask])
+ if len(intersections) != 2:
+ return None
+
+ intersections.sort()
+ # Do p1 first as p0 is need to compute it
+ if intersections[1] < 1: # clip p1
+ segment[1] = intersections[1] * delta + p0
+ if intersections[0] > 0: # clip p0
+ segment[0] = intersections[0] * delta + p0
+ return segment
+
+
+def segmentVolumeIntersect(segment, nbins):
+ """Get bin indices intersecting with segment
+
+ It should work with N dimensions.
+ Coordinate convention (z, y, x) or (x, y, z) should not matter
+ as long as segment and nbins are consistent.
+
+ :param numpy.ndarray segment:
+ Segment end points as a 2xN array of coordinates
+ :param numpy.ndarray nbins:
+ Shape of the volume with same coordinates order as segment
+ :return: List of bins indices as a 2D array or None if no bins
+ :rtype: Union[None,numpy.ndarray]
+ """
+ segment = numpy.asarray(segment)
+ nbins = numpy.asarray(nbins)
+
+ assert segment.ndim == 2
+ assert segment.shape[0] == 2
+ assert nbins.ndim == 1
+ assert segment.shape[1] == nbins.size
+
+ dim = len(nbins)
+
+ bounds = numpy.array((numpy.zeros_like(nbins), nbins))
+ segment = clipSegmentToBounds(segment, bounds)
+ if segment is None:
+ return None # Segment outside volume
+ p0, p1 = segment
+
+ # Get intersections
+
+ # Get coordinates of bin edges crossing the segment
+ clipped = numpy.ceil(numpy.clip(segment, 0, nbins))
+ start = numpy.min(clipped, axis=0)
+ stop = numpy.max(clipped, axis=0) # stop is NOT included
+ edgesByDim = [numpy.arange(start[i], stop[i]) for i in range(dim)]
+
+ # Line equation: P = t * delta + p0
+ delta = p1 - p0
+
+ # Get bin edge/line intersections as sorted points along the line
+ # Get corresponding line parameters
+ t = []
+ if numpy.all(0 <= p0) and numpy.all(p0 <= nbins):
+ t.append([0.]) # p0 within volume, add it
+ t += [(edgesByDim[i] - p0[i]) / delta[i] for i in range(dim) if delta[i] != 0]
+ if numpy.all(0 <= p1) and numpy.all(p1 <= nbins):
+ t.append([1.]) # p1 within volume, add it
+ t = numpy.concatenate(t)
+ t.sort(kind='mergesort')
+
+ # Remove duplicates
+ unique = numpy.ones((len(t),), dtype=bool)
+ numpy.not_equal(t[1:], t[:-1], out=unique[1:])
+ t = t[unique]
+
+ if len(t) < 2:
+ return None # Not enough intersection points
+
+ # bin edges/line intersection points
+ points = t.reshape(-1, 1) * delta + p0
+ centers = (points[:-1] + points[1:]) / 2.
+ bins = numpy.floor(centers).astype(numpy.int64)
+ return bins
+
+
+# Plane #######################################################################
+
+class Plane(event.Notifier):
+ """Object handling a plane and notifying plane changes.
+
+ :param point: A point on the plane.
+ :type point: 3-tuple of float.
+ :param normal: Normal of the plane.
+ :type normal: 3-tuple of float.
+ """
+
+ def __init__(self, point=(0., 0., 0.), normal=(0., 0., 1.)):
+ super(Plane, self).__init__()
+
+ assert len(point) == 3
+ self._point = numpy.array(point, copy=True, dtype=numpy.float32)
+ assert len(normal) == 3
+ self._normal = numpy.array(normal, copy=True, dtype=numpy.float32)
+ self.notify()
+
+ def setPlane(self, point=None, normal=None):
+ """Set plane point and normal and notify.
+
+ :param point: A point on the plane.
+ :type point: 3-tuple of float or None.
+ :param normal: Normal of the plane.
+ :type normal: 3-tuple of float or None.
+ """
+ planechanged = False
+
+ if point is not None:
+ assert len(point) == 3
+ point = numpy.array(point, copy=True, dtype=numpy.float32)
+ if not numpy.all(numpy.equal(self._point, point)):
+ self._point = point
+ planechanged = True
+
+ if normal is not None:
+ assert len(normal) == 3
+ normal = numpy.array(normal, copy=True, dtype=numpy.float32)
+
+ norm = numpy.linalg.norm(normal)
+ if norm != 0.:
+ normal /= norm
+
+ if not numpy.all(numpy.equal(self._normal, normal)):
+ self._normal = normal
+ planechanged = True
+
+ if planechanged:
+ _logger.debug('Plane updated:\n\tpoint: %s\n\tnormal: %s',
+ str(self._point), str(self._normal))
+ self.notify()
+
+ @property
+ def point(self):
+ """A point on the plane."""
+ return self._point.copy()
+
+ @point.setter
+ def point(self, point):
+ self.setPlane(point=point)
+
+ @property
+ def normal(self):
+ """The (normalized) normal of the plane."""
+ return self._normal.copy()
+
+ @normal.setter
+ def normal(self, normal):
+ self.setPlane(normal=normal)
+
+ @property
+ def parameters(self):
+ """Plane equation parameters: a*x + b*y + c*z + d = 0."""
+ return numpy.append(self._normal,
+ - numpy.dot(self._point, self._normal))
+
+ @parameters.setter
+ def parameters(self, parameters):
+ assert len(parameters) == 4
+ parameters = numpy.array(parameters, dtype=numpy.float32)
+
+ # Normalize normal
+ norm = numpy.linalg.norm(parameters[:3])
+ if norm != 0:
+ parameters /= norm
+
+ normal = parameters[:3]
+ point = - parameters[3] * normal
+ self.setPlane(point, normal)
+
+ @property
+ def isPlane(self):
+ """True if a plane is defined (i.e., ||normal|| != 0)."""
+ return numpy.any(self.normal != 0.)
+
+ def move(self, step):
+ """Move the plane of step along the normal."""
+ self.point += step * self.normal
+
+ def segmentIntersection(self, s0, s1):
+ """Compute the plane intersection with segment [s0, s1].
+
+ :param s0: First end of the segment
+ :type s0: 1D numpy.ndarray-like of length 3
+ :param s1: Second end of the segment
+ :type s1: 1D numpy.ndarray-like of length 3
+ :return: The intersection points. The number of points goes
+ from 0 (no intersection) to 2 (segment in the plane)
+ :rtype: list of 1D numpy.ndarray
+ """
+ if not self.isPlane:
+ return []
+ else:
+ return segmentPlaneIntersect(s0, s1, self.normal, self.point)