diff --git a/fury/actor.py b/fury/actor.py index fbeb0498c..4ddd8d0b0 100644 --- a/fury/actor.py +++ b/fury/actor.py @@ -100,3 +100,93 @@ def sphere( obj.local.position = centers[0] obj.prim_count = prim_count return obj + + +def box( + centers, + *, + directions=(1, 0, 0), + colors=(1, 0, 0), + scales=(1, 1, 1), + opacity=None, + material="phong", + enable_picking=True, + detailed=True, +): + """Visualize one or many boxes with different features. + + Parameters + ---------- + centers : ndarray, shape (N, 3) + Box positions. + directions : ndarray, shape (N, 3), optional + The orientation vector of the box. + colors : ndarray (N,3) or (N, 4) or tuple (3,) or tuple (4,), optional + RGB or RGBA (for opacity) R, G, B and A should be at the range [0, 1]. + scales : int or ndarray (N,3) or tuple (3,), optional + The size of the box in each dimension. If a single value is provided, + the same size will be used for all boxes. + opacity : float, optional + Takes values from 0 (fully transparent) to 1 (opaque). + If both `opacity` and RGBA are provided, the final alpha will be: + final_alpha = alpha_in_RGBA * opacity + material : str, optional + The material type for the boxes. Options are 'phong' and 'basic'. + enable_picking : bool, optional + Whether the boxes should be pickable in a 3D scene. + detailed : bool, optional + Whether to create a detailed box with 24 vertices or a simple box with + 8 vertices. + + Returns + ------- + mesh_actor : Actor + A mesh actor containing the generated boxes, with the specified + material and properties. + + Examples + -------- + >>> from fury import window, actor + >>> import numpy as np + >>> scene = window.Scene() + >>> centers = np.random.rand(5, 3) * 10 + >>> scales = np.random.rand(5, 3) + >>> box_actor = actor.box(centers=centers, scales=scales) + >>> scene.add(box_actor) + >>> show_manager = window.ShowManager(scene=scene, size=(600, 600)) + >>> show_manager.start() + """ + vertices, faces = fp.prim_box(detailed=detailed) + res = fp.repeat_primitive( + vertices, + faces, + directions=directions, + centers=centers, + colors=colors, + scales=scales, + ) + big_vertices, big_faces, big_colors, _ = res + prim_count = len(centers) + big_colors = big_colors / 255.0 + + if isinstance(opacity, (int, float)): + if big_colors.shape[1] == 3: + big_colors = np.hstack( + (big_colors, np.full((big_colors.shape[0], 1), opacity)) + ) + else: + big_colors[:, 3] *= opacity + + geo = buffer_to_geometry( + indices=big_faces.astype("int32"), + positions=big_vertices.astype("float32"), + texcoords=big_vertices.astype("float32"), + colors=big_colors.astype("float32"), + ) + mat = _create_mesh_material(material=material, enable_picking=enable_picking) + obj = create_mesh(geometry=geo, material=mat) + obj.local.position = centers[0] + + obj.prim_count = prim_count + + return obj diff --git a/fury/primitive.py b/fury/primitive.py index 67a94663d..7ce5bcd5e 100644 --- a/fury/primitive.py +++ b/fury/primitive.py @@ -256,46 +256,111 @@ def prim_square(): return vertices, triangles -def prim_box(): - """Return vertices and triangle for a box geometry. +def prim_box(detailed=True): + """Return vertices and triangles for a box geometry. + + Parameters + ---------- + detailed : bool, optional + If True, returns 24 vertices (no shared vertices between orthogonal faces). + If False, returns 8 unique vertices. Returns ------- - vertices: ndarray - 8 vertices coords that composed our box - triangles: ndarray - 12 triangles that composed our box + vertices : ndarray + Array of vertex coordinates. + triangles : ndarray + Array of triangle indices. """ - vertices = np.array( - [ - [-0.5, -0.5, -0.5], - [-0.5, -0.5, 0.5], - [-0.5, 0.5, -0.5], - [-0.5, 0.5, 0.5], - [0.5, -0.5, -0.5], - [0.5, -0.5, 0.5], - [0.5, 0.5, -0.5], - [0.5, 0.5, 0.5], - ] - ) - triangles = np.array( - [ - [0, 6, 4], - [0, 2, 6], - [0, 3, 2], - [0, 1, 3], - [2, 7, 6], - [2, 3, 7], - [4, 6, 7], - [4, 7, 5], - [0, 4, 5], - [0, 5, 1], - [1, 5, 7], - [1, 7, 3], - ], - dtype="i8", - ) + if detailed: + vertices = ( + np.array( + [ + [-1, -1, -1], + [1, -1, -1], + [1, 1, -1], + [-1, 1, -1], + [-1, -1, 1], + [1, -1, 1], + [1, 1, 1], + [-1, 1, 1], + [-1, -1, -1], + [-1, 1, -1], + [-1, 1, 1], + [-1, -1, 1], + [1, -1, -1], + [1, 1, -1], + [1, 1, 1], + [1, -1, 1], + [-1, 1, -1], + [1, 1, -1], + [1, 1, 1], + [-1, 1, 1], + [-1, -1, -1], + [1, -1, -1], + [1, -1, 1], + [-1, -1, 1], + ], + dtype=np.float32, + ) + * 0.5 + ) + + triangles = np.array( + [ + [2, 1, 0], + [3, 2, 0], + [4, 5, 6], + [4, 6, 7], + [8, 10, 9], + [11, 10, 8], + [12, 13, 14], + [12, 14, 15], + [16, 17, 18], + [16, 18, 19], + [20, 21, 22], + [20, 22, 23], + ], + dtype=np.uint32, + ) + + else: + vertices = ( + np.array( + [ + [-1, -1, -1], + [-1, -1, 1], + [-1, 1, -1], + [-1, 1, 1], + [1, -1, -1], + [1, -1, 1], + [1, 1, -1], + [1, 1, 1], + ], + dtype=np.float32, + ) + * 0.5 + ) + + triangles = np.array( + [ + [0, 6, 4], + [0, 2, 6], + [0, 3, 2], + [0, 1, 3], + [2, 7, 6], + [2, 3, 7], + [4, 6, 7], + [4, 7, 5], + [0, 4, 5], + [0, 5, 1], + [1, 5, 7], + [1, 7, 3], + ], + dtype=np.uint32, + ) + return vertices, triangles diff --git a/fury/tests/test_actor.py b/fury/tests/test_actor.py index f46a202c4..c39e6d425 100644 --- a/fury/tests/test_actor.py +++ b/fury/tests/test_actor.py @@ -72,3 +72,22 @@ def test_sphere(): assert len(vertices) == len(colors) npt.assert_array_almost_equal(len(faces), (2 * phi * (theta - 2))) + + +def test_box(): + scene = window.Scene() + centers = np.array([[0, 0, 0]]) + colors = np.array([[1, 0, 0]]) + scales = np.array([[1, 1, 7]]) + + box_actor = actor.box(centers=centers, colors=colors, scales=scales) + scene.add(box_actor) + + npt.assert_array_equal(box_actor.local.position, centers[0]) + + mean_vertex = np.mean(box_actor.geometry.positions.view, axis=0) + npt.assert_array_almost_equal(mean_vertex, centers[0]) + + assert box_actor.prim_count == 1 + + scene.remove(box_actor) diff --git a/fury/tests/test_primitive.py b/fury/tests/test_primitive.py index 9325eaa34..2d451f34a 100644 --- a/fury/tests/test_primitive.py +++ b/fury/tests/test_primitive.py @@ -9,16 +9,17 @@ def test_vertices_primitives(): # Tests the default vertices of all the built in primitive shapes. l_primitives = [ - (fp.prim_square, (4, 3), -0.5, 0.5, 0), - (fp.prim_box, (8, 3), -0.5, 0.5, 0), - (fp.prim_tetrahedron, (4, 3), -0.5, 0.5, 0), - (fp.prim_star, (10, 3), -3, 3, -0.0666666666), - (fp.prim_rhombicuboctahedron, (24, 3), -0.5, 0.5, 0), - (fp.prim_frustum, (8, 3), -0.5, 0.5, 0), + (fp.prim_square, (4, 3), -0.5, 0.5, 0, {}), + (fp.prim_box, (24, 3), -0.5, 0.5, 0, {"detailed": True}), + (fp.prim_box, (8, 3), -0.5, 0.5, 0, {"detailed": False}), + (fp.prim_tetrahedron, (4, 3), -0.5, 0.5, 0, {}), + (fp.prim_star, (10, 3), -3, 3, -0.0666666666, {}), + (fp.prim_rhombicuboctahedron, (24, 3), -0.5, 0.5, 0, {}), + (fp.prim_frustum, (8, 3), -0.5, 0.5, 0, {}), ] - for func, shape, e_min, e_max, e_mean in l_primitives: - vertices, _ = func() + for func, shape, e_min, e_max, e_mean, kwargs in l_primitives: + vertices, _ = func(**kwargs) npt.assert_equal(vertices.shape, shape) npt.assert_almost_equal(np.mean(vertices), e_mean) npt.assert_equal(vertices.min(), e_min)