# -*- coding: utf-8 -*-
"""Planar actors for rendering 2D shapes in 3D space."""
import logging
import numpy as np
from scipy.spatial.transform import Rotation as R
from fury.actor import (
Group,
Points,
actor_from_primitive,
create_image,
create_point,
create_text,
)
from fury.colormap import normalize_colors
from fury.geometry import (
buffer_to_geometry,
line_buffer_separator,
)
from fury.lib import (
PointsMarkerMaterial,
PointsMaterial,
PointsShader,
register_wgpu_render_function,
)
from fury.material import (
_create_image_material,
_create_points_material,
_create_text_material,
validate_opacity,
)
import fury.primitive as fp
from fury.shader import LineProjectionComputeShader
[docs]
def square(
centers,
*,
directions=(0, 0, 0),
colors=(1, 1, 1),
scales=(1, 1, 1),
opacity=None,
material="phong",
enable_picking=True,
wireframe=False,
wireframe_thickness=1.0,
):
"""
Create one or many squares with different features.
Parameters
----------
centers : ndarray, shape (N, 3)
Square positions.
directions : ndarray, shape (N, 3) or tuple (3,), optional
The orientation vector of the square.
colors : ndarray, shape (N, 3) or (N, 4) or tuple (3,) or tuple (4,), optional
RGB or RGBA colors. Accepts values in [0, 255] (int), [0, 1] (float),
or hex strings (e.g. "#FF0000"). Values above 1.0 are treated as
[0, 255] and normalized internally.
scales : ndarray, shape (N, 3) or tuple (3,) or float, optional
The size of the square in each dimension. If a single value is provided,
the same size will be used for all squares.
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 squares. Options are 'phong' and 'basic'.
enable_picking : bool, optional
Whether the squares should be pickable in a 3D scene.
wireframe : bool, optional
Whether to render the mesh as a wireframe.
wireframe_thickness : float, optional
The thickness of the wireframe lines.
Returns
-------
Actor
A mesh actor containing the generated squares, 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
>>> colors = np.random.rand(5, 3)
>>> square_actor = actor.square(centers=centers, colors=colors)
>>> _ = scene.add(square_actor)
>>> show_manager = window.ShowManager(scene=scene, size=(600, 600))
>>> show_manager.start()
"""
vertices, faces = fp.prim_square()
return actor_from_primitive(
vertices,
faces,
centers=centers,
colors=colors,
scales=scales,
directions=directions,
opacity=opacity,
material=material,
enable_picking=enable_picking,
wireframe=wireframe,
wireframe_thickness=wireframe_thickness,
)
[docs]
def star(
centers,
*,
dim=2,
directions=(0, 0, 0),
colors=(1, 1, 1),
scales=(1, 1, 1),
opacity=None,
material="phong",
enable_picking=True,
wireframe=False,
wireframe_thickness=1.0,
):
"""
Create one or many stars with different features.
Parameters
----------
centers : ndarray, shape (N, 3)
Star positions.
dim : int, optional
The dimensionality of the star (2D or 3D).
directions : ndarray, shape (N, 3) or tuple (3,), optional
The orientation vector of the star.
colors : ndarray, shape (N, 3) or (N, 4) or tuple (3,) or tuple (4,), optional
RGB or RGBA colors. Accepts values in [0, 255] (int), [0, 1] (float),
or hex strings (e.g. "#FF0000"). Values above 1.0 are treated as
[0, 255] and normalized internally.
scales : ndarray, shape (N, 3) or tuple (3,) or float, optional
The size of the star in each dimension. If a single value is
provided, the same size will be used for all stars.
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 stars. Options are 'phong' and 'basic'.
enable_picking : bool, optional
Whether the stars should be pickable in a 3D scene.
wireframe : bool, optional
Whether to render the mesh as a wireframe.
wireframe_thickness : float, optional
The thickness of the wireframe lines.
Returns
-------
Actor
A mesh actor containing the generated stars, 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
>>> colors = np.random.rand(5, 3)
>>> star_actor = actor.star(centers=centers, colors=colors)
>>> _ = scene.add(star_actor)
>>> show_manager = window.ShowManager(scene=scene, size=(600, 600))
>>> show_manager.start()
"""
vertices, faces = fp.prim_star(dim=dim)
return actor_from_primitive(
vertices,
faces,
centers=centers,
colors=colors,
scales=scales,
directions=directions,
opacity=opacity,
material=material,
enable_picking=enable_picking,
wireframe=wireframe,
wireframe_thickness=wireframe_thickness,
)
[docs]
def disk(
centers,
*,
colors=(1.0, 1.0, 1.0),
radii=0.5,
sectors=36,
scales=(1.0, 1.0, 1.0),
directions=(0.0, 0.0, 0.0),
opacity=None,
material="phong",
enable_picking=True,
wireframe=False,
wireframe_thickness=1.0,
):
"""
Visualize one or many disks with different features.
Parameters
----------
centers : ndarray, shape (N, 3)
Disk positions.
colors : ndarray (N,3) or (N, 4) or tuple (3,) or tuple (4,), optional
RGB or RGBA colors. Accepts values in [0, 255] (int), [0, 1] (float),
or hex strings (e.g. "#FF0000"). Values above 1.0 are treated as
[0, 255] and normalized internally.
radii : float or ndarray (N,) or tuple, optional
The radius of the disks, single value applies to all disks,
while an array specifies a radius for each disk individually.
sectors : int, optional
The number of divisions around the disk's circumference .
Higher values produce smoother disk.
scales : int or ndarray (N,3) or tuple (3,), optional
The size of the disks in each dimension. If a single value is provided,
the same size will be used for all disks.
directions : ndarray, shape (N, 3), optional
The orientation vector of the disk.
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 disk. Options are 'phong' and 'basic'.
enable_picking : bool, optional
Whether the disk should be pickable in a 3D scene.
wireframe : bool, optional
Whether to render the mesh as a wireframe.
wireframe_thickness : float, optional
The thickness of the wireframe lines.
Returns
-------
Actor
A mesh actor containing the generated disks, 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
>>> colors = np.random.rand(5, 3)
>>> disk_actor = actor.disk(centers=centers, colors=colors)
>>> _ = scene.add(disk_actor)
>>> show_manager = window.ShowManager(scene=scene, size=(600, 600))
>>> show_manager.start()
"""
n_centers = len(centers)
radii_arr = fp._normalize_geom_param(radii, n_centers, "radii")
all_uniform = np.all(radii_arr == radii_arr[0])
if all_uniform:
vertices, faces = fp.prim_disk(radius=radii_arr[0], sectors=sectors)
return actor_from_primitive(
vertices,
faces,
centers=centers,
colors=colors,
scales=scales,
directions=directions,
opacity=opacity,
material=material,
enable_picking=enable_picking,
wireframe=wireframe,
wireframe_thickness=wireframe_thickness,
)
_, faces = fp.prim_disk(radius=radii_arr[0], sectors=sectors)
all_verts = [
fp.prim_disk(radius=radii_arr[i], sectors=sectors)[0] for i in range(n_centers)
]
vertices = np.concatenate(all_verts)
return actor_from_primitive(
vertices,
faces,
centers=centers,
colors=colors,
scales=scales,
directions=directions,
opacity=opacity,
material=material,
enable_picking=enable_picking,
wireframe=wireframe,
wireframe_thickness=wireframe_thickness,
have_tiled_verts=True,
)
[docs]
def triangle(
centers,
*,
directions=(0, 0, 0),
colors=(1, 1, 1),
scales=(1, 1, 1),
opacity=None,
material="phong",
enable_picking=True,
wireframe=False,
wireframe_thickness=1.0,
):
"""
Create one or many triangles with different features.
Parameters
----------
centers : ndarray, shape (N, 3)
Triangle positions.
directions : ndarray, shape (N, 3) or tuple (3,), optional
The orientation vector of the triangle.
colors : ndarray, shape (N, 3) or (N, 4) or tuple (3,) or tuple (4,), optional
RGB or RGBA colors. Accepts values in [0, 255] (int), [0, 1] (float),
or hex strings (e.g. "#FF0000"). Values above 1.0 are treated as
[0, 255] and normalized internally.
scales : ndarray, shape (N, 3) or tuple (3,) or float, optional
The size of the triangle in each dimension. If a single value is provided,
the same size will be used for all triangles.
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 triangles. Options are 'phong' and 'basic'.
enable_picking : bool, optional
Whether the triangles should be pickable in a 3D scene.
wireframe : bool, optional
Whether to render the mesh as a wireframe.
wireframe_thickness : float, optional
The thickness of the wireframe lines.
Returns
-------
Actor
A mesh actor containing the generated triangles, 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
>>> colors = np.random.rand(5, 3)
>>> triangle_actor = actor.triangle(centers=centers, colors=colors)
>>> _ = scene.add(triangle_actor)
>>> show_manager = window.ShowManager(scene=scene, size=(600, 600))
>>> show_manager.start()
"""
vertices, faces = fp.prim_triangle()
return actor_from_primitive(
vertices,
faces,
centers=centers,
colors=colors,
scales=scales,
directions=directions,
opacity=opacity,
material=material,
enable_picking=enable_picking,
wireframe=wireframe,
wireframe_thickness=wireframe_thickness,
)
[docs]
def point(
centers,
*,
size=4.0,
colors=(1.0, 0.0, 0.0),
material="basic",
map=None,
aa=True,
opacity=1.0,
enable_picking=True,
):
"""
Create one or many points with different features.
Parameters
----------
centers : ndarray, shape (N, 3)
The positions of the points.
size : float, optional
The size (diameter) of the points in logical pixels.
colors : ndarray, shape (N, 3) or (N, 4) or tuple (3,) or tuple (4,), optional
RGB or RGBA colors. Accepts values in [0, 255] (int), [0, 1] (float),
or hex strings (e.g. "#FF0000"). Values above 1.0 are treated as
[0, 255] and normalized internally.
material : str, optional
The material type for the points.
Options are 'basic', 'gaussian'.
map : TextureMap or Texture, optional
The texture map specifying the color for each texture coordinate.
aa : bool, optional
Whether or not the points are anti-aliased in the shader.
opacity : float, optional
Takes values from 0 (fully transparent) to 1 (opaque).
enable_picking : bool, optional
Whether the points should be pickable in a 3D scene.
Returns
-------
Actor
A point actor containing the generated points with the specified material
and properties.
Examples
--------
>>> from fury import window, actor
>>> import numpy as np
>>> scene = window.Scene()
>>> centers = np.random.rand(1000, 3) * 10
>>> colors = np.random.rand(1000, 3)
>>> point_actor = actor.point(centers=centers, colors=colors)
>>> _ = scene.add(point_actor)
>>> show_manager = window.ShowManager(scene=scene, size=(600, 600))
>>> show_manager.start()
"""
colors = normalize_colors(colors, n_points=len(centers))
geo = buffer_to_geometry(
positions=centers.astype("float32"),
colors=colors,
)
mat = _create_points_material(
size=size,
material=material,
map=map,
aa=aa,
opacity=opacity,
enable_picking=enable_picking,
)
obj = create_point(geo, mat)
return obj
[docs]
def marker(
centers,
*,
size=15,
colors=(1.0, 0.0, 0.0),
marker="circle",
edge_color="black",
edge_width=1.0,
opacity=1.0,
enable_picking=True,
):
"""
Create one or many markers with different features.
Parameters
----------
centers : ndarray, shape (N, 3)
The positions of the markers.
size : float, optional
The size (diameter) of the points in logical pixels.
colors : ndarray, shape (N, 3) or (N, 4) or tuple (3,) or tuple (4,), optional
RGB or RGBA colors. Accepts values in [0, 255] (int), [0, 1] (float),
or hex strings (e.g. "#FF0000"). Values above 1.0 are treated as
[0, 255] and normalized internally.
marker : str or MarkerShape, optional
The shape of the marker.
Options are "●": "circle", "+": "plus", "x": "cross", "♥": "heart",
"✳": "asterix".
edge_color : str or tuple or Color, optional
The color of line marking the edge of the markers.
edge_width : float, optional
The width of the edge of the markers.
opacity : float, optional
Takes values from 0 (fully transparent) to 1 (opaque).
enable_picking : bool, optional
Whether the points should be pickable in a 3D scene.
Returns
-------
Actor
A marker actor containing the generated markers with the specified material
and properties.
Examples
--------
>>> from fury import window, actor
>>> import numpy as np
>>> scene = window.Scene()
>>> centers = np.random.rand(1000, 3) * 10
>>> colors = np.random.rand(1000, 3)
>>> marker_actor = actor.marker(centers=centers, colors=colors)
>>> _ = scene.add(marker_actor)
>>> show_manager = window.ShowManager(scene=scene, size=(600, 600))
>>> show_manager.start()
"""
colors = normalize_colors(colors, n_points=len(centers))
geo = buffer_to_geometry(
positions=centers.astype("float32"),
colors=colors,
)
mat = _create_points_material(
material="marker",
size=size,
marker=marker,
edge_color=edge_color,
edge_width=edge_width,
opacity=opacity,
enable_picking=enable_picking,
)
obj = create_point(geo, mat)
return obj
[docs]
def text(
text,
*,
colors=(1.0, 1.0, 1.0),
position=(0.0, 0.0, 0.0),
font_size=1.0,
family="Arial",
anchor="middle-center",
max_width=0.0,
line_height=1.2,
text_align="start",
outline_color=(0.0, 0.0, 0.0),
outline_thickness=0.0,
opacity=1.0,
):
"""
Create text with different features.
Parameters
----------
text : str or list of str
The plain text to render. When a list is given, each item becomes
a separate text actor and a Group is returned.
colors : tuple (3,) or tuple (4,) or list of tuple, optional
RGB or RGBA values in the range [0, 1]. When ``text`` is a list,
this can be a single color applied to all actors, or a list of
colors (one per text item).
position : tuple (3,) or list of tuple (3,), optional
The (x, y, z) coordinates to place the text in 3D space. When
``text`` is a list, this can be a single position applied to all
actors, or a list of positions (one per text item).
font_size : float, optional
The size of the font, in object coordinates or pixel screen coordinates.
family : str, optional
The name(s) of the font to prefer.
anchor : str, optional
The position of the origin of the text. Can be "top-left",
"top-center", "top-right", "middle-left", "middle-center",
"middle-right", "bottom-left", "bottom-center", "bottom-right".
max_width : float, optional
The maximum width of the text. Words are wrapped if necessary.
line_height : float, optional
A factor to scale the distance between lines. A value of 1 means the
"native" font's line distance.
text_align : str, optional
The horizontal alignment of the text. Can be "start",
"end", "left", "right", "center", "justify" or "justify_all".
Text alignment is ignored for vertical text.
outline_color : tuple, optional
The color of the outline of the text.
outline_thickness : float, optional
A value indicating the relative width of the outline. Valid values are
between 0.0 and 0.5.
opacity : float, optional
Takes values from 0 (fully transparent) to 1 (opaque).
Returns
-------
Text or Group
A single Text actor if ``text`` is a string, or a Group of Text
actors if ``text`` is a list.
Raises
------
TypeError
If ``text`` is not a string or list of strings.
ValueError
If ``colors`` or ``position`` is a list whose length does not match
the length of ``text``.
Examples
--------
>>> from fury import window, actor
>>> scene = window.Scene()
>>> text_actor = actor.text(text='FURY')
>>> _ = scene.add(text_actor)
>>> show_manager = window.ShowManager(scene=scene, size=(600, 600))
>>> show_manager.start()
"""
if isinstance(text, (list, tuple)):
if not all(isinstance(t, str) for t in text):
raise TypeError("All items in text list must be strings.")
n = len(text)
is_color_seq = (
isinstance(colors, (list, tuple))
and len(colors) > 0
and isinstance(colors[0], (list, tuple))
)
if is_color_seq:
if len(colors) != n:
raise ValueError(
"Length of colors list must match length of text list."
)
colors_list = colors
else:
colors_list = [colors] * n
is_pos_seq = (
isinstance(position, (list, tuple))
and len(position) > 0
and isinstance(position[0], (list, tuple))
)
if is_pos_seq:
if len(position) != n:
raise ValueError(
"Length of position list must match length of text list."
)
position_list = position
else:
position_list = [position] * n
group = Group()
for t, c, p in zip(text, colors_list, position_list, strict=True):
mat = _create_text_material(
color=c,
opacity=opacity,
outline_color=outline_color,
outline_thickness=outline_thickness,
)
obj = create_text(
text=t,
material=mat,
font_size=font_size,
family=family,
anchor=anchor,
max_width=max_width,
line_height=line_height,
text_align=text_align,
)
obj.local.position = p
group.add(obj)
return group
if not isinstance(text, str):
raise TypeError("text must be a string.")
mat = _create_text_material(
color=colors,
opacity=opacity,
outline_color=outline_color,
outline_thickness=outline_thickness,
)
obj = create_text(
text=text,
material=mat,
font_size=font_size,
family=family,
anchor=anchor,
max_width=max_width,
line_height=line_height,
text_align=text_align,
)
obj.local.position = position
return obj
[docs]
def image(
image,
*,
position=(0.0, 0.0, 0.0),
directions=(0.0, 0.0, 1.0),
visible=True,
clim=None,
map=None,
gamma=1.0,
interpolation="nearest",
):
"""
Visualize a 2D image from a NumPy array or image file.
Parameters
----------
image : str or ndarray
The image input. Can be a file path (string) or a NumPy array.
position : tuple, optional
The position of the image in 3D space.
directions : ndarray, shape (3,) or tuple (3,), optional
The orientation vector of the image.
visible : bool, optional
Whether the image should be visible.
clim : tuple, optional
Contrast limits for image scaling.
map : TextureMap or Texture, optional
The texture map used to convert image values into color.
gamma : float, optional
Gamma correction to apply to the image.
Must be greater than 0.
interpolation : str, optional
Interpolation method for rendering the image.
Either 'nearest' or 'linear'.
Returns
-------
ImageActor
An image actor containing the rendered 2D image.
Examples
--------
>>> from fury import window, actor
>>> import numpy as np
>>> scene = window.Scene()
>>> image_data = np.random.rand(256, 256)
>>> image_actor = actor.image(image=image_data)
>>> _ = scene.add(image_actor)
>>> show_manager = window.ShowManager(scene=scene, size=(600, 600))
>>> show_manager.start()
"""
mat = _create_image_material(
clim=clim,
map=map,
gamma=gamma,
interpolation=interpolation,
)
obj = create_image(
image_input=image,
material=mat,
visible=visible,
)
if interpolation not in ["nearest", "linear"]:
raise ValueError(
f"Interpolation must be 'nearest' or 'linear', but got {interpolation}."
)
if position is None:
position = (0.0, 0.0, 0.0)
if isinstance(position, (list, tuple, np.ndarray)) and len(position) == 3:
position = np.asarray(position, dtype=np.float32)
else:
raise ValueError(f"Position must have a length of 3. Got {position}.")
if isinstance(directions, (list, tuple, np.ndarray)) and len(directions) == 3:
directions = np.asarray(directions, dtype=np.float32)
else:
raise ValueError(f"Directions must have a length of 3. Got {directions}.")
obj.local.position = position
default_normal = np.array([0, 0, 1])
target_normal = np.asarray(directions)
target_normal = target_normal / np.linalg.norm(target_normal)
rotation_axis = np.cross(default_normal, target_normal)
dot_product = np.dot(default_normal, target_normal)
rotation_angle = np.arccos(np.clip(dot_product, -1.0, 1.0))
if np.linalg.norm(rotation_axis) > 1e-6:
rotation_axis = rotation_axis / np.linalg.norm(rotation_axis)
rot = R.from_rotvec(rotation_angle * rotation_axis)
else:
rot = R.from_quat([0, 0, 0, 1])
obj.local.rotation = rot.as_quat()
return obj
[docs]
def ring(
centers,
*,
inner_radius=0.5,
outer_radius=1.0,
radial_segments=1,
circumferential_segments=32,
directions=(0, 0, 0),
colors=(1, 1, 1),
scales=(1, 1, 1),
opacity=None,
material="phong",
enable_picking=True,
):
"""
Create one or many rings with different features.
Parameters
----------
centers : ndarray, shape (N, 3)
Ring positions.
inner_radius : float or ndarray, shape (N,), optional
The inner radius of the ring (radius of the hole). A single value
applies to all rings, while an array specifies a value per ring.
outer_radius : float or ndarray, shape (N,), optional
The outer radius of the ring. A single value applies to all rings,
while an array specifies a value per ring.
radial_segments : int, optional
Number of segments along the radial direction.
circumferential_segments : int, optional
Number of segments around the circumference.
directions : ndarray, shape (N, 3) or tuple (3,), optional
The orientation vector of the ring.
colors : ndarray, shape (N, 3) or (N, 4) or tuple (3,) or tuple (4,), optional
RGB or RGBA colors. Accepts values in [0, 255] (int), [0, 1] (float),
or hex strings (e.g. "#FF0000"). Values above 1.0 are treated as
[0, 255] and normalized internally.
scales : ndarray, shape (N, 3) or tuple (3,) or float, optional
The size of the ring in each dimension. If a single value is provided,
the same size will be used for all rings.
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 rings. Options are 'phong' and 'basic'.
enable_picking : bool, optional
Whether the rings should be pickable in a 3D scene.
Returns
-------
Actor
A mesh actor containing the generated rings, 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
>>> colors = np.random.rand(5, 3)
>>> ring_actor = actor.ring(centers=centers, colors=colors)
>>> _ = scene.add(ring_actor)
>>> show_manager = window.ShowManager(scene=scene, size=(600, 600))
>>> show_manager.start()
"""
n_centers = len(centers)
inner_arr = fp._normalize_geom_param(inner_radius, n_centers, "inner_radius")
outer_arr = fp._normalize_geom_param(outer_radius, n_centers, "outer_radius")
all_uniform = np.all(inner_arr == inner_arr[0]) and np.all(
outer_arr == outer_arr[0]
)
if all_uniform:
vertices, faces = fp.prim_ring(
inner_radius=inner_arr[0],
outer_radius=outer_arr[0],
radial_segments=radial_segments,
circumferential_segments=circumferential_segments,
)
return actor_from_primitive(
vertices,
faces,
centers=centers,
colors=colors,
scales=scales,
directions=directions,
opacity=opacity,
material=material,
enable_picking=enable_picking,
)
_, faces = fp.prim_ring(
inner_radius=inner_arr[0],
outer_radius=outer_arr[0],
radial_segments=radial_segments,
circumferential_segments=circumferential_segments,
)
all_verts = [
fp.prim_ring(
inner_radius=inner_arr[i],
outer_radius=outer_arr[i],
radial_segments=radial_segments,
circumferential_segments=circumferential_segments,
)[0]
for i in range(n_centers)
]
vertices = np.concatenate(all_verts)
return actor_from_primitive(
vertices,
faces,
centers=centers,
colors=colors,
scales=scales,
directions=directions,
opacity=opacity,
material=material,
enable_picking=enable_picking,
have_tiled_verts=True,
)
class LineProjection(Points):
"""
Initialize the line projection object.
Parameters
----------
lines : sequence
A list of lines to be projected.
plane : tuple, optional
The plane equation (a, b, c, d) for the projection.
colors : tuple, optional
The color of the lines.
lengths : list, optional
A list of lengths for each line.
offsets : list, optional
A list of offsets for each line.
thickness : float, optional
Thickness of the cross-section.
outline_color : tuple, optional
The color of the outline.
outline_thickness : float, optional
The thickness of the outline.
opacity : float, optional
The opacity of the lines.
lift : float, optional
A small lift applied to the projected points along the plane normal
to avoid z-fighting.
"""
uniform_type = dict(
Points.uniform_type,
plane="4xf4", # (a, b, c, d) plane equation ax + by + cz + d = 0
lift="f4",
)
def __init__(
self,
lines,
*,
plane=(0, 0, -1, 0),
colors=(1, 0, 0),
lengths=None,
offsets=None,
thickness=1.0,
outline_color=(0, 0, 0),
outline_thickness=0.2,
opacity=1.0,
lift=0.0,
):
"""
Initialize the line projection object.
Raises
------
ValueError
If any of the input parameters are invalid.
"""
super().__init__()
self.num_lines = len(lines)
if lengths is None:
lengths = np.asarray([len(line) for line in lines], dtype="int32")
elif len(lengths) != self.num_lines:
raise ValueError(
f"Lengths must have a length of {self.num_lines}. Got {lengths}."
)
if offsets is None:
offsets = np.zeros((self.num_lines,), dtype="int32")
for i in range(1, self.num_lines):
offsets[i] = len(lines[i - 1]) + offsets[i - 1]
elif len(offsets) != self.num_lines:
raise ValueError(
f"Offsets must have a length of {self.num_lines}. Got {offsets}."
)
if lift is None:
lift = 0.0
logging.info("No lift provided, defaulting to 0.0.")
elif not isinstance(lift, (int, float)):
raise ValueError(f"Lift must be a single float value. Got {lift}.")
self.plane = plane
self.lengths = lengths
self.offsets = offsets
self.lift = lift
self.lines, _ = line_buffer_separator(lines)
if colors is None:
colors = np.ones((self.num_lines, 4), dtype="float32")
else:
colors = np.asarray(colors, dtype="float32")
if colors.ndim == 1:
colors = np.tile(colors, (self.num_lines, 1))
if colors.shape[0] != self.num_lines or colors.shape[-1] not in (3, 4):
raise ValueError(
f"colors must have a length of 1 or {self.num_lines}"
f" with 3 or 4 channels. Got {colors.shape}."
)
elif colors.shape[0] == self.num_lines and colors.shape[-1] == 3:
colors = np.concatenate(
[colors, np.ones((self.num_lines, 1), dtype="float32")], axis=-1
)
if outline_color is None:
outline_color = np.ones((self.num_lines, 4), dtype="float32")
else:
outline_color = np.asarray(outline_color, dtype="float32")
if outline_color.ndim == 1:
outline_color = np.tile(outline_color, (self.num_lines, 1))
if outline_color.shape[0] != self.num_lines or outline_color.shape[-1] not in (
3,
4,
):
raise ValueError(
f"outline_color must have a length of 1 or {self.num_lines}"
f" with channels 3 or 4. Got {outline_color.shape}."
)
elif outline_color.shape[0] == self.num_lines and outline_color.shape[-1] == 3:
outline_color = np.concatenate(
[outline_color, np.ones((self.num_lines, 1), dtype="float32")], axis=-1
)
positions = np.empty((self.num_lines, 3), dtype="float32")
self.geometry = buffer_to_geometry(
positions, colors=colors, edge_colors=outline_color
)
if not isinstance(thickness, (int, float)):
raise ValueError(
f"Thickness must be a single float value. Got {thickness}."
)
if not isinstance(outline_thickness, (int, float)):
raise ValueError(
"Outline thickness must be a single float value. Got"
f"{outline_thickness}."
)
opacity = validate_opacity(opacity)
self.material = PointsMarkerMaterial(
size=thickness,
edge_width=outline_thickness,
opacity=opacity,
pick_write=False,
color_mode="vertex",
edge_color_mode="vertex",
)
@property
def plane(self):
"""
Get the plane equation.
Returns
-------
ndarray
The plane equation coefficients.
"""
return self.uniform_buffer.data["plane"]
@plane.setter
def plane(self, value):
"""
Set the plane equation.
Parameters
----------
value : {tuple, list, np.ndarray}
The plane equation coefficients.
Raises
------
ValueError
If the input parameters are invalid.
"""
if value is None:
value = (0, 0, -1, 0)
logging.info("No plane provided, defaulting to (0, 0, -1, 0).")
elif not isinstance(value, (list, tuple, np.ndarray)) or len(value) != 4:
raise ValueError(f"Plane must have a length of 4. Got {value}.")
self.uniform_buffer.data["plane"] = value
self.uniform_buffer.update_full()
@property
def lift(self):
"""
Get the lift value to avoid z-fighting.
Returns
-------
float
The lift value applied to the projected points.
"""
return self.uniform_buffer.data["lift"]
@lift.setter
def lift(self, value):
"""
Set the lift value to avoid z-fighting.
Parameters
----------
value : float
The lift value to apply.
Raises
------
ValueError
If the input parameter is invalid.
"""
if not isinstance(value, (int, float)):
raise ValueError(f"Lift must be a single float value. Got {value}.")
self.uniform_buffer.data["lift"] = value
self.uniform_buffer.update_full()
[docs]
def line_projection(
lines,
*,
plane="XY",
colors=(1, 0, 0),
lengths=None,
offsets=None,
thickness=1.0,
outline_color=(0, 0, 0),
outline_thickness=0.2,
opacity=1.0,
lift=0.0,
):
"""
Initialize the line projection object.
This projection is best visualized when the plane normal
is aligned with the camera view direction.
Parameters
----------
lines : sequence
A list of lines to be projected.
plane : {str, tuple}, optional
The plane equation (a, b, c, d) for the projection.
colors : {tuple, list, ndarray}, optional
The color of the cross-section point. It can be a single color or
a list of colors for each line.
lengths : list, optional
A list of lengths for each line.
offsets : list, optional
A list of offsets for each line.
thickness : float, optional
Thickness of the cross-section.
outline_color : tuple, optional
The color of the outline.
outline_thickness : float, optional
The thickness of the outline.
opacity : float, optional
The opacity of the lines.
lift : float, optional
A small lift applied to the projected points along the plane normal
to avoid z-fighting.
Returns
-------
LineProjection
The created line projection object.
"""
if isinstance(plane, str):
if plane.upper() == "XY":
plane = (0, 0, -1, 0)
elif plane.upper() == "XZ":
plane = (0, -1, 0, 0)
elif plane.upper() == "YZ":
plane = (-1, 0, 0, 0)
else:
raise ValueError(
f"Plane must be 'XY', 'XZ', 'YZ' or a tuple of 4 elements. Got {plane}."
)
return LineProjection(
lines,
plane=plane,
colors=colors,
lengths=lengths,
offsets=offsets,
thickness=thickness,
outline_color=outline_color,
outline_thickness=outline_thickness,
opacity=opacity,
lift=lift,
)
@register_wgpu_render_function(LineProjection, PointsMaterial)
def register_render_line_projection(wobject):
"""
Register the line projection render function.
Parameters
----------
wobject : LineProjection
The line projection object to register.
Returns
-------
tuple
The created line projection shaders.
"""
return (LineProjectionComputeShader(wobject), PointsShader(wobject))