Skip to main content

threejs-viewer

Lightweight Three.js viewer controlled from Python via WebSocket.

Toolpath demo

A Python client runs a WebSocket server that a browser-based Three.js viewer connects to. Designed for robotics visualization, scientific computing, and interactive 3D exploration.

VS Code integration

Features

  • Simple API: Add primitives, load models, update transforms
  • PBR materials: Roughness, metalness, transparency and environment reflections on all objects
  • GLB/PBR support: Load GLB models with PBR materials, embedded skeletal/morph animations
  • Animation support: Pre-compute animations, scrub timeline, adjust playback speed
  • Binary channels: Efficient transfer of large animations (100k+ objects × frames)
  • Toolpath visualization: Extruded bead tubes with per-point colors, draw_range animation, and automatic LOD — handles 1M+ point toolpaths at 60 fps with smooth camera-distance-adaptive simplification via Web Worker
  • 5-axis swept tool body: add_swept_tool lofts an oriented shank/holder profile about the per-station tool axis (decoupled from the path tangent) into a swept surface — visualizes 5-axis reorientation rate and tool-body collisions, with per-station heatmap colours and draw_range reveal
  • GPU point clouds: add_points renders millions of unconnected points (voxel fields, metrology/deviation clouds, LiDAR scans) in a single THREE.Points draw call, with per-point colormap colours, distance size-attenuation, and draw_range reveal
  • Live point streaming: append_points grows an existing cloud in place — only the new points cross the wire and only they are uploaded to the GPU, so a producer streaming for hours (a laser tracker at 1 kHz) stays O(new points) per update instead of re-sending the whole cloud
  • Per-point time windows: give each point a [birth, removal) lifetime and scrub a global time — points appear/disappear out of buffer order in the vertex shader (material-removal animations that a prefix reveal can't express), driven live or from the animation slider
  • Octree LOD streaming: add_points(lod=True) builds a Potree-style sampled octree in Python and streams node payloads on demand — draw a ~1.5M-point budget of the biggest-on-screen nodes out of clouds far larger than one draw call, refining as you zoom; composes with the time windows at every LOD
  • Auto-reconnect: Browser reconnects automatically, animations persist
  • Z-up coordinates: Robotics convention (matches ROS, URDF)
  • No build step: Self-contained HTML viewer, just open in browser

Installation

pip install threejs-viewer

Quick Start

from threejs_viewer import viewer

# Start server and wait for browser to connect
v = viewer()

# Add objects
v.add_sphere("ball", radius=0.3, color=0xFF0000, position=[0, 0, 0.5])
v.add_box("ground", width=5, height=5, depth=0.1, color=0x444444)

# Keep running
input("Press Enter to exit")

The viewer opens automatically in your default browser. To open it manually:

threejs-viewer open
# Or: threejs-viewer path  (prints path to viewer.html)

Usage

Objects

from threejs_viewer import Toolpath, viewer

# Primitives with PBR materials
v.add_box("box1", width=1, height=2, depth=0.5, color=0x4A90D9, roughness=0.5, metalness=0.1)
v.add_sphere("ball", radius=0.5, position=[2, 0, 0], roughness=0.3, metalness=0.7)
v.add_cylinder("cyl1", radius_top=0.3, radius_bottom=0.5, height=1)

# 3D models (binary transfer)
v.add_model_binary("robot", "robot.stl", format="stl")

# Polylines with colormaps
v.add_polyline("path", points, colors=z_values, colormap="viridis", line_width=3)

# Bead toolpath (parametric tube, built client-side)
tp = Toolpath.from_points(points, bead_width=0.3, bead_height=0.08)
tp.colorize(per_point_rgb)
v.add_toolpath("bead", tp)

# Transparency
v.set_opacity("box1", 0.5)
v.set_color("ball", 0xFF0000, opacity=0.3)

Transforms

# Single object
v.set_matrix("box1", matrix_4x4.flatten().tolist())

# Batch update (efficient for 60fps)
v.batch_update({
    "link1": {"position": [1, 2, 0.5]},
    "link2": {"position": [3, 0, 1], "rotation": [0, 0, 1.57]},
})

Picking points on lines & beads

# Click a point anywhere along any polyline or parametric tube; the pick
# travels back to Python. Hovering glides a marker to the closest point.
def on_pick(pick):
    print(f"{pick['kind']} {pick['id']}: {pick['fraction']:.1%} along  at {pick['point']}")

v.on_polyline_pick(on_pick)   # also enables picking

Each click sends {id, kind, fraction, point, local_point, segment, t} where kind is "line" or "tube". Movement is continuous — the marker never snaps to vertices. Picking is opt-out per object — pass pickable=False to add_polyline / add_parametric_tube to exclude one from picking (it stays rendered); adding objects costs nothing when picking is never enabled. For a tube, segment indexes the full-resolution spine 1:1 with the per-point arrays you built it from, so it doubles as a lookup key for other per-point data at the picked point. A browser embedder can subscribe in JS instead — viewer.onPolylinePick(cb) (click) / viewer.onPolylineHover(cb) (every hover move) — for a live readout with no Python round-trip. See examples/22_polyline_picking.py.

Point clouds: live append, time windows & octree LOD

# Live streaming: seed the cloud once, then append as data arrives. Both the
# wire payload and the GPU upload cover the new points only, so this holds up
# for hours at a few flushes per second (the browser grows the buffer with
# spare capacity, doubling when it runs out).
v.add_points("live", first_chunk, colors=first_values, colormap="turbo",
             cmin=-0.5, cmax=0.5)      # fix the colour range up front...
while streaming:
    v.append_points("live", chunk, colors=values)   # ...appends clamp to it

Appending to an id that was never added (or was deleted, cleared, or created with lod= / birth_times) raises ValueError — a stream must not lose data silently. A cloud is not replayed when the browser reloads, so re-seed with add_points after a reconnect.

# Per-point lifetimes: a point is visible while birth_time <= t < removal_time.
# NaN = unbounded (never born-late / never removed).
v.add_points("stock", positions, colors=depth, colormap="turbo",
             birth_times=birth, removal_times=removal)
v.set_points_time("stock", 3.0)   # scrub the time directly...

# ...or map the animation slider onto it (drag to scrub points in/out):
anim = Animation(loop=True)
anim.set_frame_times(times)
anim.set_point_time_data(["stock"], times.reshape(-1, 1))
v.load_animation(anim)

# Octree LOD for clouds beyond one draw call (10M+ points): nodes stream
# on demand from Python under a point budget as the camera moves. Keep the
# Python process running — it serves node payloads.
v.add_points("big", positions, colors=scalars, lod=True)
# tunable: lod={"node_capacity": 15000, "point_budget": 1_500_000,
#               "refine_pixels": 12, "seed": 0}

Both compose: octree node samples are stratified over the time values, so scrubbing thins every LOD level uniformly. On LOD clouds use the time window instead of set_draw_range (buffer order is spatial there). See examples/27_points_octree_lod.py — point count tunable from the command line.

Animations

from threejs_viewer import Animation

animation = Animation(loop=True)
for t in times:
    animation.add_frame(
        time=t,
        transforms=compute_transforms(t),
        colors={"robot": 0xFF0000 if collision else 0x00FF00},
        clip_times={"glb_model": t},  # drive embedded GLTF animations
    )
animation.add_marker(3.5, "Collision detected")

client.load_animation(animation)

Viewer controls: Space (play/pause), Arrow keys (step frames), 1-5 (speed), L (loop)

Binary Channels (Large Animations)

For animations with many objects or frames, use binary channels instead of Frame dicts for much faster serialization and transfer:

import numpy as np
from threejs_viewer import Animation

animation = Animation(loop=True)
animation.set_frame_times(np.arange(n_frames) / 60.0)

# Transforms: (n_frames, n_objects, 16) float32
animation.set_transform_data(object_ids, transform_array)

# Draw ranges: (n_frames, n_objects) float32
animation.set_draw_range_data(object_ids, draw_range_array)

# Clip times for embedded GLTF animations: (n_frames, n_objects) float32
animation.set_clip_time_data(object_ids, clip_time_array)

# Colors with indexed colormap: (n_frames, n_objects) uint8
animation.add_channel("colors", object_ids, color_indices, dtype="uint8",
                       metadata={"colormap": [0x44AA44, 0xFF3333]})

# Visibility: (n_frames, n_objects) uint8 (0=hidden, 1=visible)
animation.add_channel("visibility", object_ids, vis_data, dtype="uint8")

client.load_animation(animation)

Binary channels and Frame-based JSON can be mixed. A binary channel supersedes the same-named Frame field.

GLB Models with Embedded Animations

# Load a GLB with embedded animations (skeletal, morph targets)
client.add_model_binary("fox", "fox.glb", format="glb")

# Seek embedded animation to a specific time (seconds)
client.set_clip_time("fox", 1.5)

Documentation

CLI

threejs-viewer path    # Print path to viewer.html
threejs-viewer open    # Open in default browser
threejs-viewer code    # Open in VS Code (use "Show Preview" for docked view)

License

MIT

Metadata

Release files for threejs-viewer 0.0.57

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for threejs-viewer 0.0.57
File Size Uploaded
threejs_viewer-0.0.57.tar.gz 697.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for threejs-viewer 0.0.57
File Interpreter ABI Platform
threejs_viewer-0.0.57-py3-none-any.whl Python 3 none any Details

Total release size: 1.4 MB

Release files / threejs_viewer-0.0.57.tar.gz

Download URL threejs_viewer-0.0.57.tar.gz
Size 697.6 kB
Tags Source
SHA-256 checksum
How to use checksums
f48c301673a8747d7be85a638141904b887d9e9ac49340169d2bb35ea314a073
BLAKE2b-256 checksum
How to use checksums
d37223a1a2ef688ddc10d9e58f1695306e447f4fc7a0a30b5946fdb1e489164c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.

Transparency log

Release files / threejs_viewer-0.0.57-py3-none-any.whl

Download URL threejs_viewer-0.0.57-py3-none-any.whl
Size 700.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f8ff1f4bd6a5af98f3adb183116bfd438ec0070c8db9c25066134d6ee6c6c8de
BLAKE2b-256 checksum
How to use checksums
7399a7b79c96346fd081986b6ee079515aa01168bd574b8b1460768c0273d1db
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.

Transparency log

Release history Release notifications | RSS feed

0.0.69

2 release files

0.0.62

2 release files

0.0.61

2 release files

0.0.60

2 release files

0.0.59

2 release files

0.0.58

2 release files

This release

0.0.57 This release

2 release files

0.0.56

2 release files

0.0.55

2 release files

0.0.54

2 release files

0.0.53

2 release files

0.0.52

2 release files

0.0.47

2 release files

0.0.46

2 release files

0.0.45

2 release files

0.0.44

2 release files

0.0.38

2 release files

0.0.37

2 release files

0.0.36

2 release files

0.0.35

2 release files

0.0.34

2 release files

0.0.30

2 release files

0.0.29

2 release files

0.0.28

2 release files

0.0.24

2 release files

0.0.23

2 release files

0.0.22

2 release files

0.0.21

2 release files

0.0.20

2 release files

0.0.19

2 release files

0.0.13

2 release files

0.0.12

2 release files

0.0.11

2 release files

0.0.9

2 release files

0.0.8

2 release files

0.0.7

2 release files

0.0.6

2 release files

0.0.5

2 release files

0.0.4

2 release files

0.0.3

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page