def percentile(points, axis, percentile): """ Given a cloud of points and an axis, find a point along that axis from the centroid at the given percentile. Args: points (np.arraylike): A `kx3` stack of points. axis (np.arraylike): A 3D vector specifying the direction of interest. percentile (float): The desired percentile. Returns: np.ndarray: A 3D point at the requested percentile. """ k = vg.shape.check(locals(), "points", (-1, 3)) if k < 1: raise ValueError("At least one point is needed") vg.shape.check(locals(), "axis", (3,)) if vg.almost_zero(axis): raise ValueError("Axis must be non-zero") axis = vg.normalize(axis) coords_on_axis = points.dot(axis) selected_coord_on_axis = np.percentile(coords_on_axis, percentile) centroid = np.average(points, axis=0) return vg.reject(centroid, axis) + selected_coord_on_axis * axis
def point_along_path(self, fraction_of_total): """ Selects a point the given fraction of the total length of the polyline. For example, to find the halfway point, pass `fraction_of_total=0.5`. Also works with stacked values, e.g. `fraction_of_total=np.linspace(0, 1, 11)`. Args: fraction_of_total (object): Fraction of the total length, from 0 to 1 Returns (object): A point on the polyline that is the given fraction of the total length from the starting point to the endpoint. For stacked fractions, return the points. """ from .._common.shape import columnize fraction_of_total, _, transform_result = columnize( fraction_of_total, (-1, ), name="fraction_of_total") if np.any(0 > fraction_of_total) or np.any(fraction_of_total > 1): raise ValueError( "fraction_of_total must be a value between 0 and 1") desired_length = self.total_length * fraction_of_total cumulative_length = np.cumsum([0, *self.segment_lengths]) index_of_segment = (np.argmax( cumulative_length.reshape(-1, 1) > desired_length, axis=0) - 1) return transform_result( self.v[index_of_segment] + (desired_length - cumulative_length[index_of_segment]).reshape(-1, 1) * vg.normalize(self.segment_vectors[index_of_segment]))
def test_project_point_to_line_stacked_both(): p1 = np.array([5.0, 5.0, 4.0]) p2 = np.array([10.0, 10.0, 6.0]) along_line = p2 - p1 common_kwargs = dict( reference_points_of_lines=np.array([p1, p1, p1]), vectors_along_lines=np.array([along_line, along_line, along_line]), ) other_point_on_line = np.array([0.0, 0.0, 2.0]) example_perpendicular_displacement = [ k * vg.perpendicular(vg.normalize(along_line), vg.basis.x) for k in [0.1, 0.5, -2.0] ] example_points = np.vstack([p1, p2, other_point_on_line]) expected_projected_points = np.vstack([p1, p2, other_point_on_line]) np.testing.assert_array_almost_equal( project_point_to_line(points=example_points, **common_kwargs), expected_projected_points, ) np.testing.assert_array_almost_equal( project_point_to_line(points=example_points + example_perpendicular_displacement, **common_kwargs), expected_projected_points, )
def test_project_point_to_line(): p1 = np.array([5.0, 5.0, 4.0]) p2 = np.array([10.0, 10.0, 6.0]) along_line = p2 - p1 common_kwargs = dict(reference_points_of_lines=p1, vectors_along_lines=along_line) np.testing.assert_array_almost_equal( project_point_to_line(points=p1, **common_kwargs), p1) np.testing.assert_array_almost_equal( project_point_to_line(points=p2, **common_kwargs), p2) other_point_on_line = np.array([0.0, 0.0, 2.0]) np.testing.assert_array_almost_equal( project_point_to_line(points=other_point_on_line, **common_kwargs), other_point_on_line, ) example_perpendicular_displacement = [ k * vg.perpendicular(vg.normalize(along_line), vg.basis.x) for k in [0.1, 0.5, -2.0] ] for point_on_line in [p1, p2, other_point_on_line]: for displacement in example_perpendicular_displacement: np.testing.assert_array_almost_equal( project_point_to_line(points=point_on_line + displacement, **common_kwargs), point_on_line, )
def __init__(self, point_on_plane, unit_normal): vg.shape.check(locals(), "point_on_plane", (3, )) vg.shape.check(locals(), "unit_normal", (3, )) if vg.almost_zero(unit_normal): raise ValueError("unit_normal should not be the zero vector") unit_normal = vg.normalize(unit_normal) self._r0 = np.asarray(point_on_plane) self._n = np.asarray(unit_normal)
def test_signed_distances_for_diagonal_plane(): np.testing.assert_array_almost_equal( signed_distance_to_plane( points=np.array([[425.0, 425.0, 25.0], [-500.0, -500.0, 25.0]]), # Diagonal plane @ origin - draw a picture! plane_equations=Plane( point_on_plane=np.array([1.0, 1.0, 0.0]), unit_normal=vg.normalize(np.array([1.0, 1.0, 0.0])), ).equation, ), np.array([ math.sqrt(2 * (425.0 - 1.0)**2), -math.sqrt(2 * (500.0 + 1.0)**2) ]), )
def world_to_view(position, target, up=vg.basis.y, inverse=False): """ Create a transform matrix which sends world-space coordinates to view-space coordinates. Args: position (np.ndarray): The camera's position in world coordinates. target (np.ndarray): The camera's target in world coordinates. `target - position` is the "look at" vector. up (np.ndarray): The approximate up direction, in world coordinates. inverse (bool): When `True`, return the inverse transform instead. Returns: np.ndarray: The `4x4` transformation matrix, which can be used with `polliwog.transform.apply_transform()`. See also: https://cseweb.ucsd.edu/classes/wi18/cse167-a/lec4.pdf http://www.songho.ca/opengl/gl_camera.html """ vg.shape.check(locals(), "position", (3, )) vg.shape.check(locals(), "target", (3, )) look = vg.normalize(target - position) left = vg.normalize(vg.cross(look, up)) recomputed_up = vg.cross(left, look) rotation = transform_matrix_for_rotation( np.array([left, recomputed_up, look])) if inverse: inverse_rotation = rotation.T inverse_translation = transform_matrix_for_translation(position) return compose_transforms(inverse_rotation, inverse_translation) else: translation = transform_matrix_for_translation(-position) return compose_transforms(translation, rotation)
def test_sliced_by_plane_open(): original = Polyline( np.array([ [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [1.0, 7.0, 0.0], [1.0, 8.0, 0.0], ]), is_closed=False, ) expected_vs = np.array([[1.0, 7.5, 0.0], [1.0, 8.0, 0.0]]) actual = original.sliced_by_plane( Plane(point_on_plane=np.array([0.0, 7.5, 0.0]), unit_normal=vg.basis.y)) np.testing.assert_array_almost_equal(actual.v, expected_vs) assert actual.is_closed is False expected_vs = np.array([ [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [1.0, 7.0, 0.0], [1.0, 7.5, 0.0], ]) actual = original.sliced_by_plane( Plane(point_on_plane=np.array([0.0, 7.5, 0.0]), unit_normal=vg.basis.neg_y)) np.testing.assert_array_almost_equal(actual.v, expected_vs) assert actual.is_closed is False with pytest.raises(ValueError): original.sliced_by_plane( Plane(point_on_plane=np.array([0.0, 15.0, 0.0]), unit_normal=vg.basis.neg_y)) actual = original.sliced_by_plane( Plane( point_on_plane=np.array([0.5, 0.0, 0.0]), unit_normal=vg.normalize(np.array([1.0, -1.0, 0.0])), )) expected_vs = np.array([[0.5, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.5, 0.0]]) np.testing.assert_array_almost_equal(actual.v, expected_vs) assert actual.is_closed is False
def surface_normals(points, normalize=True): """ Compute the surface normal of a triangle. The direction of the normal follows conventional counter-clockwise winding and the right-hand rule. Also works on stacked inputs (i.e. many sets of three points). """ points, _, transform_result = columnize(points, (-1, 3, 3), name="points") p1s = points[:, 0] p2s = points[:, 1] p3s = points[:, 2] v1s = p2s - p1s v2s = p3s - p1s normals = vg.cross(v1s, v2s) if normalize: normals = vg.normalize(normals) return transform_result(normals)
def test_surface_normals_from_points_vectorized(): from polliwog.shapes import triangular_prism p1 = np.array([3.0, 0.0, 0.0]) p2 = np.array([0.0, 3.0, 0.0]) p3 = np.array([0.0, 0.0, 3.0]) vertices = triangular_prism(p1, p2, p3, 1.0) expected_normals = vg.normalize( np.array( [ [1.0, 1.0, 1.0], [1.0, 1.0, -2.0], [1.0, 1.0, -2.0], [-2.0, 1.0, 1.0], [-2.0, 1.0, 1.0], [1.0, -2.0, 1.0], [1.0, -2.0, 1.0], [-1.0, -1.0, -1.0], ] ) ) np.testing.assert_allclose(surface_normals(vertices), expected_normals)
import numpy as np from polliwog import Plane import pytest from vg.compat import v2 as vg from ._slice_by_plane import slice_open_polyline_by_plane point_on_plane = np.array([1.0, 2.0, 3.0]) plane_normal = vg.normalize(np.array([3.0, 4.0, 5.0])) plane = Plane(point_on_plane=point_on_plane, unit_normal=plane_normal) def rand_nonzero(*shape): return 128 * np.random.rand(*shape) + 1e-6 def vertices_with_signs(signs): num_verts = len(signs) random_points_on_plane = plane.project_point(rand_nonzero(num_verts, 3)) random_displacement_along_normal = ( rand_nonzero(num_verts).reshape(-1, 1) * plane_normal) vertices = (random_points_on_plane + signs.reshape(-1, 1) * random_displacement_along_normal) # Because of rounding, the random points don't necessarily return 0 for # sign, so pick one that does. vertices[signs == 0] = plane.reference_point np.testing.assert_array_equal(plane.sign(vertices), signs) return vertices def intersect_segment_with_plane(p1, p2): from ..plane import intersect_segment_with_plane as _intersect_segment_with_plane
def test_v2_has_functions(): np.testing.assert_array_equal(vg.normalize(np.array([5, 0, 0])), np.array([1, 0, 0]))