diff --git a/docs/source/examples/18_splines.rst b/docs/source/examples/18_lines.rst
similarity index 50%
rename from docs/source/examples/18_splines.rst
rename to docs/source/examples/18_lines.rst
index 16186249..275bbc67 100644
--- a/docs/source/examples/18_splines.rst
+++ b/docs/source/examples/18_lines.rst
@@ -1,11 +1,11 @@
.. Comment: this file is automatically generated by `update_example_docs.py`.
It should not be modified manually.
-Splines
+Lines
==========================================
-Make a ball with some random splines.
+Make a ball with some random line segments and splines.
@@ -16,17 +16,35 @@ Make a ball with some random splines.
import time
import numpy as np
-
import viser
def main() -> None:
server = viser.ViserServer()
+
+ # Line segments.
+ #
+ # This will be much faster than creating separate scene objects for
+ # individual line segments or splines.
+ N = 2000
+ points = np.random.normal(size=(N, 2, 3)) * 3.0
+ colors = np.random.randint(0, 255, size=(N, 2, 3))
+ server.scene.add_line_segments(
+ f"/line_segments",
+ points=points,
+ colors=colors,
+ line_width=3.0,
+ )
+
+ # Spline helpers.
+ #
+ # If many lines are needed, it'll be more efficient to batch them in
+ # `add_line_segments()`.
for i in range(10):
- positions = np.random.normal(size=(30, 3)) * 3.0
+ points = np.random.normal(size=(30, 3)) * 3.0
server.scene.add_spline_catmull_rom(
- f"/catmull_{i}",
- positions,
+ f"/catmull/{i}",
+ positions=points,
tension=0.5,
line_width=3.0,
color=np.random.uniform(size=3),
@@ -35,9 +53,9 @@ Make a ball with some random splines.
control_points = np.random.normal(size=(30 * 2 - 2, 3)) * 3.0
server.scene.add_spline_cubic_bezier(
- f"/cubic_bezier_{i}",
- positions,
- control_points,
+ f"/cubic_bezier/{i}",
+ positions=points,
+ control_points=control_points,
line_width=3.0,
color=np.random.uniform(size=3),
segments=100,
diff --git a/examples/18_lines.py b/examples/18_lines.py
new file mode 100644
index 00000000..036d7afc
--- /dev/null
+++ b/examples/18_lines.py
@@ -0,0 +1,59 @@
+"""Lines
+
+Make a ball with some random line segments and splines.
+"""
+
+import time
+
+import numpy as np
+import viser
+
+
+def main() -> None:
+ server = viser.ViserServer()
+
+ # Line segments.
+ #
+ # This will be much faster than creating separate scene objects for
+ # individual line segments or splines.
+ N = 2000
+ points = np.random.normal(size=(N, 2, 3)) * 3.0
+ colors = np.random.randint(0, 255, size=(N, 2, 3))
+ server.scene.add_line_segments(
+ f"/line_segments",
+ points=points,
+ colors=colors,
+ line_width=3.0,
+ )
+
+ # Spline helpers.
+ #
+ # If many lines are needed, it'll be more efficient to batch them in
+ # `add_line_segments()`.
+ for i in range(10):
+ points = np.random.normal(size=(30, 3)) * 3.0
+ server.scene.add_spline_catmull_rom(
+ f"/catmull/{i}",
+ positions=points,
+ tension=0.5,
+ line_width=3.0,
+ color=np.random.uniform(size=3),
+ segments=100,
+ )
+
+ control_points = np.random.normal(size=(30 * 2 - 2, 3)) * 3.0
+ server.scene.add_spline_cubic_bezier(
+ f"/cubic_bezier/{i}",
+ positions=points,
+ control_points=control_points,
+ line_width=3.0,
+ color=np.random.uniform(size=3),
+ segments=100,
+ )
+
+ while True:
+ time.sleep(10.0)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/18_splines.py b/examples/18_splines.py
deleted file mode 100644
index 8566dbb9..00000000
--- a/examples/18_splines.py
+++ /dev/null
@@ -1,41 +0,0 @@
-"""Splines
-
-Make a ball with some random splines.
-"""
-
-import time
-
-import numpy as np
-
-import viser
-
-
-def main() -> None:
- server = viser.ViserServer()
- for i in range(10):
- positions = np.random.normal(size=(30, 3)) * 3.0
- server.scene.add_spline_catmull_rom(
- f"/catmull_{i}",
- positions,
- tension=0.5,
- line_width=3.0,
- color=np.random.uniform(size=3),
- segments=100,
- )
-
- control_points = np.random.normal(size=(30 * 2 - 2, 3)) * 3.0
- server.scene.add_spline_cubic_bezier(
- f"/cubic_bezier_{i}",
- positions,
- control_points,
- line_width=3.0,
- color=np.random.uniform(size=3),
- segments=100,
- )
-
- while True:
- time.sleep(10.0)
-
-
-if __name__ == "__main__":
- main()
diff --git a/src/viser/_messages.py b/src/viser/_messages.py
index 50c019f3..9922c0f2 100644
--- a/src/viser/_messages.py
+++ b/src/viser/_messages.py
@@ -1183,6 +1183,26 @@ class ThemeConfigurationMessage(Message):
colors: Optional[Tuple[str, str, str, str, str, str, str, str, str, str]]
+@dataclasses.dataclass
+class LineSegmentsMessage(Message, tag="SceneNodeMessage"):
+ """Message from server->client carrying line segments information."""
+
+ name: str
+ props: LineSegmentsProps
+
+
+@dataclasses.dataclass
+class LineSegmentsProps:
+ points: npt.NDArray[np.float32]
+ """A numpy array of shape (N, 2, 3) containing a batched set of line
+ segments. Synchronized automatically when assigned."""
+ line_width: float
+ """Width of the lines. Synchronized automatically when assigned."""
+ colors: npt.NDArray[np.uint8]
+ """Numpy array of shape (N, 2, 3) containing a color for each point.
+ Synchronized automatically when assigned."""
+
+
@dataclasses.dataclass
class CatmullRomSplineMessage(Message, tag="SceneNodeMessage"):
"""Message from server->client carrying Catmull-Rom spline information."""
@@ -1193,6 +1213,7 @@ class CatmullRomSplineMessage(Message, tag="SceneNodeMessage"):
@dataclasses.dataclass
class CatmullRomSplineProps:
+ # TODO: consider renaming positions to points and using numpy arrays for consistency with LineSegmentsProps.
positions: Tuple[Tuple[float, float, float], ...]
"""A tuple of 3D positions (x, y, z) defining the spline's path. Synchronized automatically when assigned."""
curve_type: Literal["centripetal", "chordal", "catmullrom"]
diff --git a/src/viser/_scene_api.py b/src/viser/_scene_api.py
index 02c6d9b0..2c847a82 100644
--- a/src/viser/_scene_api.py
+++ b/src/viser/_scene_api.py
@@ -28,6 +28,7 @@
HemisphereLightHandle,
ImageHandle,
LabelHandle,
+ LineSegmentsHandle,
MeshHandle,
MeshSkinnedBoneHandle,
MeshSkinnedHandle,
@@ -562,9 +563,58 @@ def add_glb(
message = _messages.GlbMessage(name, _messages.GlbProps(glb_data, scale))
return GlbHandle._make(self, message, name, wxyz, position, visible)
+ def add_line_segments(
+ self,
+ name: str,
+ points: np.ndarray,
+ colors: np.ndarray | tuple[float, float, float],
+ line_width: float = 1,
+ wxyz: tuple[float, float, float, float] | np.ndarray = (1.0, 0.0, 0.0, 0.0),
+ position: tuple[float, float, float] | np.ndarray = (0.0, 0.0, 0.0),
+ visible: bool = True,
+ ) -> LineSegmentsHandle:
+ """Add line segments to the scene.
+
+ Args:
+ name: A scene tree name. Names in the format of /parent/child can
+ be used to define a kinematic tree.
+ points: A numpy array of shape (N, 2, 3) defining start/end points
+ for each of N line segments.
+ colors: Colors of points. Should have shape (N, 2, 3) or be
+ broadcastable to it.
+ line_width: Width of the lines.
+ wxyz: Quaternion rotation to parent frame from local frame (R_pl).
+ position: Translation to parent frame from local frame (t_pl).
+ visible: Whether or not these line segments are initially visible.
+
+ Returns:
+ Handle for manipulating scene node.
+ """
+ points_array = np.asarray(points, dtype=np.float32)
+ if (
+ points_array.shape[-1] != 3
+ or points_array.ndim != 3
+ or points_array.shape[1] != 2
+ ):
+ raise ValueError("Points should have shape (N, 2, 3) for N line segments.")
+
+ colors_array = colors_to_uint8(np.asarray(colors))
+ colors_array = np.broadcast_to(colors_array, points_array.shape)
+
+ message = _messages.LineSegmentsMessage(
+ name=name,
+ props=_messages.LineSegmentsProps(
+ points=points_array,
+ colors=colors_array,
+ line_width=line_width,
+ ),
+ )
+ return LineSegmentsHandle._make(self, message, name, wxyz, position, visible)
+
def add_spline_catmull_rom(
self,
name: str,
+ # The naming inconsistency here compared to add_line_segments is unfortunate...
positions: tuple[tuple[float, float, float], ...] | np.ndarray,
curve_type: Literal["centripetal", "chordal", "catmullrom"] = "centripetal",
tension: float = 0.5,
@@ -581,6 +631,9 @@ def add_spline_catmull_rom(
This method creates a spline based on a set of positions and interpolates
them using the Catmull-Rom algorithm. This can be used to create smooth curves.
+ If many splines are needed, it'll be more efficient to batch them in
+ :meth:`add_line_segments()`.
+
Args:
name: A scene tree name. Names in the format of /parent/child can be used to
define a kinematic tree.
@@ -637,6 +690,9 @@ def add_spline_cubic_bezier(
positions and control points. It is useful for creating complex, smooth,
curving shapes.
+ If many splines are needed, it'll be more efficient to batch them in
+ :meth:`add_line_segments()`.
+
Args:
name: A scene tree name. Names in the format of /parent/child can be used to
define a kinematic tree.
diff --git a/src/viser/_scene_handles.py b/src/viser/_scene_handles.py
index a4c0dc00..280e2355 100644
--- a/src/viser/_scene_handles.py
+++ b/src/viser/_scene_handles.py
@@ -502,6 +502,14 @@ class GridHandle(
"""Handle for grid objects."""
+class LineSegmentsHandle(
+ SceneNodeHandle,
+ _messages.LineSegmentsProps,
+ _OverridableScenePropApi if not TYPE_CHECKING else object,
+):
+ """Handle for line segments objects."""
+
+
class SplineCatmullRomHandle(
SceneNodeHandle,
_messages.CatmullRomSplineProps,
diff --git a/src/viser/client/src/SceneTree.tsx b/src/viser/client/src/SceneTree.tsx
index ceccfa93..91d10287 100644
--- a/src/viser/client/src/SceneTree.tsx
+++ b/src/viser/client/src/SceneTree.tsx
@@ -2,6 +2,7 @@ import {
CatmullRomLine,
CubicBezierLine,
Grid,
+ Line,
PivotControls,
useCursor,
} from "@react-three/drei";
@@ -418,6 +419,52 @@ function useObjectFactory(message: SceneNodeMessage | undefined): {
),
};
}
+ case "LineSegmentsMessage": {
+ return {
+ makeObject: (ref) => {
+ // The array conversion here isn't very efficient. We go from buffer
+ // => TypeArray => Javascript Array, then back to buffers in drei's
+ // abstraction.
+ const pointsArray = new Float32Array(
+ message.props.points.buffer.slice(
+ message.props.points.byteOffset,
+ message.props.points.byteOffset + message.props.points.byteLength,
+ ),
+ );
+ const colorArray = new Uint8Array(
+ message.props.colors.buffer.slice(
+ message.props.colors.byteOffset,
+ message.props.colors.byteOffset + message.props.colors.byteLength,
+ ),
+ );
+ return (
+
+ [
+ pointsArray[i * 3],
+ pointsArray[i * 3 + 1],
+ pointsArray[i * 3 + 2],
+ ],
+ )}
+ color="white"
+ lineWidth={message.props.line_width}
+ vertexColors={Array.from(
+ { length: colorArray.length / 3 },
+ (_, i) => [
+ colorArray[i * 3] / 255,
+ colorArray[i * 3 + 1] / 255,
+ colorArray[i * 3 + 2] / 255,
+ ],
+ )}
+ segments={true}
+ />
+
+ );
+ },
+ };
+ }
case "CatmullRomSplineMessage": {
return {
makeObject: (ref) => {
diff --git a/src/viser/client/src/WebsocketMessages.ts b/src/viser/client/src/WebsocketMessages.ts
index 3acdd771..66291459 100644
--- a/src/viser/client/src/WebsocketMessages.ts
+++ b/src/viser/client/src/WebsocketMessages.ts
@@ -994,6 +994,15 @@ export interface ThemeConfigurationMessage {
]
| null;
}
+/** Message from server->client carrying line segments information.
+ *
+ * (automatically generated)
+ */
+export interface LineSegmentsMessage {
+ type: "LineSegmentsMessage";
+ name: string;
+ props: { points: Uint8Array; line_width: number; colors: Uint8Array };
+}
/** Message from server->client carrying Catmull-Rom spline information.
*
* (automatically generated)
@@ -1191,6 +1200,7 @@ export type Message =
| GuiUpdateMessage
| SceneNodeUpdateMessage
| ThemeConfigurationMessage
+ | LineSegmentsMessage
| CatmullRomSplineMessage
| CubicBezierSplineMessage
| GaussianSplatsMessage
@@ -1222,6 +1232,7 @@ export type SceneNodeMessage =
| SkinnedMeshMessage
| TransformControlsMessage
| ImageMessage
+ | LineSegmentsMessage
| CatmullRomSplineMessage
| CubicBezierSplineMessage
| GaussianSplatsMessage;
@@ -1263,6 +1274,7 @@ const typeSetSceneNodeMessage = new Set([
"SkinnedMeshMessage",
"TransformControlsMessage",
"ImageMessage",
+ "LineSegmentsMessage",
"CatmullRomSplineMessage",
"CubicBezierSplineMessage",
"GaussianSplatsMessage",