def shear_matrix(angle: float, direction: np.ndarray, point: np.ndarray, normal: np.ndarray):
    """Return matrix to shear by angle along direction vector on shear plane.

    The shear plane is defined by a point and normal vector. The direction
    vector must be orthogonal to the plane's normal vector.

    A point P is transformed by the shear matrix into P" such that
    the vector P-P" is parallel to the direction vector and its extent is
    given by the angle of P-P'-P", where P' is the orthogonal projection
    of P onto the shear plane.

    >>> angle = (np.random.random() - 0.5) * 4*pi
    >>> direct = np.random.random(3) - 0.5
    >>> point = np.random.random(3) - 0.5
    >>> normal = np.cross(direct, np.random.random(3))
    >>> S = shear_matrix(angle, direct, point, normal)
    >>> np.allclose(1, np.linalg.det(S))
    True

    """
    normal = unit_vector(normal[:3])
    direction = unit_vector(direction[:3])
    if abs(np.dot(normal, direction)) > 1e-6:
        raise ValueError("direction and normal vectors are not orthogonal")
    angle = tan(angle)
    M = np.identity(4)
    M[:3, :3] += angle * np.outer(direction, normal)
    M[:3, 3] = -angle * np.dot(point[:3], normal) * direction
    return M
def scale_matrix(factor: float, origin: np.ndarray = _origin, direction: np.ndarray = _origin):
    """Return matrix to scale by factor around origin in direction.

    Use factor -1 for point symmetry.

    >>> v = (np.random.rand(4, 5) - 0.5) * 20
    >>> v[3] = 1
    >>> S = scale_matrix(-1.234)
    >>> np.allclose(np.dot(S, v)[:3], -1.234*v[:3])
    True
    >>> factor = np.random.random() * 10 - 5
    >>> origin = np.random.random(3) - 0.5
    >>> direct = np.random.random(3) - 0.5
    >>> S = scale_matrix(factor, origin)
    >>> S = scale_matrix(factor, origin, direct)

    """
    if (direction == _origin).all():
        # uniform scaling
        M = np.diag(np.array((factor, factor, factor, 1.0), dtype=np.float64))
        if np.any(origin != _origin):
            M[:3, 3] = origin[:3]
            M[:3, 3] *= 1.0 - factor
    else:
        # nonuniform scaling
        direction = unit_vector(direction[:3])
        factor = 1.0 - factor
        M = np.identity(4)
        M[:3, :3] -= factor * np.outer(direction, direction)
        if (origin == _origin).all():
            M[:3, 3] = (factor * np.dot(origin[:3], direction)) * direction
    return M
def arcball_constrain_to_axis(point, axis):
    """Return sphere point perpendicular to axis."""
    v = np.array(point, dtype=np.float64, copy=True)
    a = np.array(axis, dtype=np.float64, copy=True)
    v -= a * np.dot(a, v)  # on plane
    n = norm(v)
    if n > EPS:
        if v[2] < 0.0:
            np.negative(v, v)
        v /= n
        return v
    if a[2] == 1.0:
        return vector(1.0, 0.0, 0.0)
    return unit_vector(vector(-a[1], a[0], 0.0))
def rotation_matrix(angle: float, direction: np.ndarray,
                    point: np.ndarray = _origin) -> np.ndarray:
    """Return matrix to rotate about axis defined by point and direction.
        :param angle: the angle for rotation
        :param direction: the direction vector of the rotation
        :param point: the reference point of rotation. By default origin is used.
        :returns: the resulting rotation matrix (4x4)
    
        >>> R = rotation_matrix(pi/2, vector(0, 0, 1), vector(1, 0, 0))
        >>> np.allclose(np.dot(R, [0, 0, 0, 1]), [1, -1, 0, 1])
        True
        >>> angle = (np.random.random() - 0.5) * (2*pi)
        >>> direc = np.random.random(3) - 0.5
        >>> point = np.random.random(3) - 0.5
        >>> R0 = rotation_matrix(angle, direc, point)
        >>> R1 = rotation_matrix(angle-2*pi, direc, point)
        >>> is_same_transform(R0, R1)
        True
        >>> R0 = rotation_matrix(angle, direc, point)
        >>> R1 = rotation_matrix(-angle, -direc, point)
        >>> is_same_transform(R0, R1)
        True
        >>> I = np.identity(4, np.float64)
        >>> np.allclose(I, rotation_matrix(pi*2, direc))
        True
        >>> np.allclose(2, np.trace(rotation_matrix(pi/2,
        ...                                               direc, point)))
        True
    
        """
    sina: float = sin(angle)
    cosa: float = cos(angle)
    direction = unit_vector(direction[:3])
    # rotation matrix around unit vector
    R = np.diag(np.array((cosa, cosa, cosa), dtype=double))
    R += np.outer(direction, direction) * (1.0 - cosa)
    direction *= sina
    R += np.array([[0.0, -direction[2], direction[1]],
                   [direction[2], 0.0, -direction[0]],
                   [-direction[1], direction[0], 0.0]])
    M = np.identity(4)
    M[:3, :3] = R
    if np.any(point != _origin):
        # rotation not around origin
        M[:3, 3] = point - np.dot(R, point)
    return M
def reflection_matrix(point: np.ndarray, normal: np.ndarray) -> np.ndarray:
    """Return matrix to mirror at plane defined by point and normal vector.

    >>> v0 = np.random.random(4) - 0.5
    >>> v0[3] = 1.
    >>> v1 = np.random.random(3) - 0.5
    >>> R = reflection_matrix(v0, v1)
    >>> np.allclose(2, np.trace(R))
    True
    >>> np.allclose(v0, np.dot(R, v0))
    True
    >>> v2 = v0.copy()
    >>> v2[:3] += v1
    >>> v3 = v0.copy()
    >>> v2[:3] -= v1
    >>> np.allclose(v2, np.dot(R, v3))
    True

    """
    normal = unit_vector(normal[:3])
    M = np.identity(4)
    M[:3, :3] -= 2.0 * np.outer(normal, normal)
    M[:3, 3] = (2.0 * np.dot(point[:3], normal)) * normal
    return M
def projection_matrix(point: np.ndarray, normal: np.ndarray, direction: np.ndarray = None,
                      perspective: np.ndarray = None, pseudo: bool = False) -> np.ndarray:
    """Return matrix to project onto plane defined by point and normal.

    Using either perspective point, projection direction, or none of both.

    If pseudo is True, perspective projections will preserve relative depth
    such that Perspective = dot(Orthogonal, PseudoPerspective).

    :param point: the point defining center of projection plane
    :param normal: the normal vector defining projection plane. should be unit vector
    :param direction: ???
    :param perspective: if given, is the point of observation, i.e. where eye is
    :param pseudo: ???
    :return: projection matrix 4x4
    >>> P = projection_matrix(vector(0, 0, 0), vector(1, 0, 0))
    >>> np.allclose(P[1:, 1:], np.identity(4)[1:, 1:])
    True
    >>> point = np.random.random(3) - 0.5
    >>> normal = np.random.random(3) - 0.5
    >>> direct = np.random.random(3) - 0.5
    >>> persp = np.random.random(3) - 0.5
    >>> P0 = projection_matrix(point, normal)
    >>> P1 = projection_matrix(point, normal, direction=direct)
    >>> P2 = projection_matrix(point, normal, perspective=persp)
    >>> P3 = projection_matrix(point, normal, perspective=persp, pseudo=True)
    >>> is_same_transform(P2, np.dot(P0, P3))
    True
    >>> P = projection_matrix(vector(3, 0, 0), vector(1, 1, 0), vector(1, 0, 0))
    >>> v0 = (np.random.rand(4, 5) - 0.5) * 20
    >>> v0[3] = 1
    >>> v1 = np.dot(P, v0)
    >>> np.allclose(v1[1], v0[1])
    True
    >>> np.allclose(v1[0], 3-v1[1])
    True

    """
    M = np.identity(4)
    point = np.copy(point[:3])
    normal = unit_vector(normal[:3])
    if perspective is not None:
        # perspective projection
        perspective = np.copy(perspective[:3])
        M[0, 0] = M[1, 1] = M[2, 2] = np.dot(perspective - point, normal)
        M[:3, :3] -= np.outer(perspective, normal)
        if pseudo:
            # preserve relative depth
            M[:3, :3] -= np.outer(normal, normal)
            M[:3, 3] = np.dot(point, normal) * (perspective + normal)
        else:
            M[:3, 3] = np.dot(point, normal) * perspective
        M[3, :3] = -normal
        M[3, 3] = np.dot(perspective, normal)
    elif direction is not None:
        # parallel projection
        direction = np.copy(direction[:3])

        # noinspection PyTypeChecker
        scale: float = np.dot(direction, normal)
        M[:3, :3] -= np.outer(direction, normal) / scale
        M[:3, 3] = direction * (np.dot(point, normal) / scale)
    else:
        # orthogonal projection
        M[:3, :3] -= np.outer(normal, normal)
        M[:3, 3] = np.dot(point, normal) * normal
    return M
 def setaxes(self, *axes):
     """Set axes to constrain rotations."""
     if axes is None:
         self._axes = None
     else:
         self._axes = [unit_vector(axis) for axis in axes]