Core Package
The core package provides fundamental data structures for building Luxar scenes.
Core data structures for Luxar scene graph and points.
Scene
- class luxar.core.Scene(writer: ZarrWriterProtocol, dimensions: Dimensions, viewer_config: ViewerConfig | None = None, citation: Mapping[str, str] | None = None)[source]
Bases:
GroupScene root node representing the top level of a scene hierarchy.
The Scene class is a Group that must be created through LuxarZarrCompiler for progressive writing and memory-efficient handling of large datasets.
Scene dimensions are REQUIRED and serve as the single source of truth for the coordinate system. All data nodes must conform to these dimensions.
Data-adding methods (add_points, add_lines, add_gsplats, etc.) are inherited from Group and work identically on Scene.
Example
>>> from luxar import LuxarZarrCompiler, Dimensions, Dimension >>> dims = Dimensions([ ... Dimension("X", display=True), ... Dimension("Y", display=True), ... Dimension("Z", display=True), ... ]) >>> with LuxarZarrCompiler('output.luxar.zarr') as compiler: ... scene = compiler.create_scene(dimensions=dims) ... scene.add_points('points', huge_array) # Written immediately ... ... # Groups also support add_points, add_lines, add_gsplats: ... group = scene.add_group("my_group") ... group.add_points('nested_pts', more_data)
- Parameters:
writer – Writer interface for progressive writing (required)
dimensions – Scene-level dimension definitions (REQUIRED)
- __init__(writer: ZarrWriterProtocol, dimensions: Dimensions, viewer_config: ViewerConfig | None = None, citation: Mapping[str, str] | None = None) None[source]
Initialize a new Luxar scene.
- Parameters:
writer – Writer interface for progressive writing (required)
dimensions – Scene-level dimension definitions (REQUIRED). Defines the coordinate system for all data in the scene.
viewer_config – Optional viewer configuration hints. Stored in the zarr file and used by the viewer as scene-specific defaults.
citation – Optional credit for whoever produced the underlying dataset –
{"short", "ref"?, "doi"?, "license"?, "url"?}. Written to the store’s root attributes so the attribution travels with the data rather than only with the page that happens to show it.Nonemeans no external dataset to credit.
- Raises:
ValueError – If writer is None, dimensions is None, the citation is malformed, or initialization fails
- property citation: dict[str, str] | None
Dataset credit stamped into the store root, or None if none is owed.
- add_group(name: str, **attrs: Any) Group[source]
Create and add a child group node to the scene.
- Parameters:
name – Name of the group
**attrs – Additional attributes for the group. Supports: opacity: float (0.0-1.0, default 1.0) - Node opacity absorption: float (>=0, default 1.0) - Volumetric kappa gamma: float (0.1-10.0, default 1.0) - Gamma correction blending_mode: str (“normal”, “additive”, “max”, “opaque”, “luminous”, “volumetric”; default “additive”)
- Returns:
The created group node
- Raises:
ValueError – If group creation fails or rendering attributes are invalid
- get_store_path() str[source]
Get the path to the backing Zarr store.
- Returns:
Path to the Zarr store backing this scene. Archive-backed writers return their staging directory before finalization and the archive path afterward.
- property dimensions: Dimensions
Get scene-level dimensions.
- Returns:
Dimensions object (always present - required at construction)
- property viewer_config: ViewerConfig | None
Get viewer configuration hints.
- Returns:
ViewerConfig if set, None otherwise.
- add_text(text: str, position: Tuple[float, float], *, name: str | None = None, font_size: float = 0.025, font: str = 'sans', color: str = 'white', opacity: float = 1.0, anchor: str = 'top-left', width: float | None = None, text_align: str = 'left', line_height: float = 1.4, background: str | None = None, padding: float = 0.005, stroke_color: str | None = None, stroke_width: float = 0.002, visible_range: Dict[str, float | Tuple[float, float]] | None = None, transition: str = 'none', transition_duration: float = 0.3, interactive: bool = False, blend_mode: str = 'normal', hover: bool = False) Overlay[source]
Add a text overlay to the scene.
Text is rendered as an HTML element over the viewer canvas. If
widthis set, text wraps within that viewport-relative width.- Parameters:
text – The text content to display. When
hoveris True, this is a template with{hover_label},{hover_key},{hover_node},{hover_index}placeholders.position – (x, y) in normalized screen coords [0, 1]. Origin is top-left.
name – Optional overlay name. Auto-generated if None.
font_size – Font size as fraction of viewport height (default 0.025 = 2.5vh).
font – Font preset (‘sans’, ‘serif’, ‘mono’) or CSS font-family string.
color – CSS color string (default ‘white’).
opacity – Opacity 0.0-1.0 (default 1.0).
anchor – Anchor point for positioning (default ‘top-left’).
width – Optional width as fraction of viewport width. Enables word wrapping.
text_align – Text alignment: ‘left’, ‘center’, ‘right’, ‘justify’.
line_height – CSS line-height multiplier (default 1.4).
background – Optional CSS background color for a backing rectangle.
padding – Padding around text as fraction of viewport height (default 0.005).
stroke_color – Optional text stroke/outline color.
stroke_width – Stroke width as fraction of viewport height (default 0.002).
visible_range – Optional dimension-based visibility filter. Maps dimension names to values or (min, max) range tuples.
transition – Transition type: ‘none’ or ‘fade’ (default ‘none’).
transition_duration – Transition duration in seconds (default 0.3).
interactive – If True, overlay captures pointer events (default False).
blend_mode – CSS mix-blend-mode (default ‘normal’). Use ‘difference’ for XOR-style text that inverts the background colors.
hover – If True, this overlay is a hover tooltip updated by GPU picking. The
textis treated as a template (default False).
- Returns:
Overlay metadata object.
Example
>>> scene.add_text("Scale: 10um", position=(0.05, 0.95), font_size=0.02) >>> scene.add_text( ... "This is a paragraph with wrapping.", ... position=(0.02, 0.85), ... width=0.3, ... text_align='left', ... background='rgba(0,0,0,0.6)', ... )
- add_image(image: Any, position: Tuple[float, float], *, name: str | None = None, size: Tuple[float, float | None] | None = None, opacity: float = 1.0, anchor: str = 'top-left', blend_mode: str = 'normal', format: str = 'png', visible_range: Dict[str, float | Tuple[float, float]] | None = None, transition: str = 'none', transition_duration: float = 0.3, interactive: bool = False) Overlay[source]
Add an image overlay to the scene.
The image is stored directly in the zarr directory and rendered as an HTML
<img>element over the viewer canvas.- Parameters:
image – Image data. Accepts: file path (str/Path), raw bytes, numpy array (HWC uint8 or float 0-1), PIL Image. Pre-encoded bytes and path payloads must be PNG, JPEG, or WebP; a recognized path extension must match the payload.
position – (x, y) in normalized screen coords [0, 1].
name – Optional overlay name. Auto-generated if None.
size – Optional (width, height) as fractions of viewport dimensions. A
Noneheight keeps the image’s own aspect ratio (the viewer rendersheight: auto), as foradd_video().opacity – Opacity 0.0-1.0 (default 1.0).
anchor – Anchor point for positioning (default ‘top-left’).
blend_mode – CSS blend mode: ‘normal’, ‘multiply’, ‘screen’, ‘overlay’, ‘additive’, or ‘difference’ (default ‘normal’).
format – Encoding format for array/PIL inputs: ‘png’, ‘jpeg’, or ‘webp’ (default ‘png’). Pre-encoded bytes and paths retain their detected format.
visible_range – Optional dimension-based visibility filter.
transition – Transition type: ‘none’ or ‘fade’ (default ‘none’).
transition_duration – Transition duration in seconds (default 0.3).
interactive – If True, overlay captures pointer events (default False).
- Returns:
Overlay metadata object.
Example
>>> scene.add_image('logo.png', position=(0.9, 0.05), anchor='top-right') >>> scene.add_image( ... numpy_heatmap, ... position=(0.0, 0.0), ... size=(1.0, 1.0), ... opacity=0.5, ... blend_mode='multiply', ... )
- add_video(video: Any, position: Tuple[float, float], *, name: str | None = None, size: Tuple[float, float | None] | None = None, opacity: float = 1.0, anchor: str = 'top-left', blend_mode: str = 'normal', loop: bool = True, autoplay: bool = True, muted: bool = True, playback_rate: float = 1.0, poster: Any = None, alpha_matte: str | None = None, visible_range: Dict[str, float | Tuple[float, float]] | None = None, transition: str = 'none', transition_duration: float = 0.3, interactive: bool = False) Overlay[source]
Add a looping video overlay to the scene.
The file is stored verbatim inside the zarr directory (like an image overlay) and rendered as an HTML
<video>over the canvas. Muted autoplay is what browsers allow without a user gesture, so that is the default and the only autoplay mode accepted. A hidden video (itsvisible_rangenot matching) is paused, so ten story turntables cost one decode at a time.Captures: video overlays stay synchronized in real-time recordings. Offline capture runs on a synthetic frame clock while the HTML video follows wall time, so its recorded playback speed is not preserved.
Transparency: encode the clip as a STACKED ALPHA MATTE — the colour on top and the alpha channel as a grey matte of the same size below, one ordinary opaque frame twice as tall (ffmpeg:
split[c][a];[a]alphaextract[a];[c][a]vstack) — and passalpha_matte="stacked"; the viewer recombines the halves in a shader, so the clip is transparent in every browser, WKWebView included. A VP9 WebM with an alpha plane (yuva420p) plays transparent only in Chrome and Firefox: Safari decodes it and DROPS the alpha, showing the clip on a black square. Always supply aposterfor a transparent video.- Parameters:
video – Raw bytes or a file path. Must already be WebM or MP4; there is no re-encoding. A recognized path suffix must match the payload.
position – (x, y) in normalized screen coords [0, 1]. Origin is top-left.
name – Optional overlay name. Auto-generated if None.
size – Optional (width, height) as fractions of the viewport. A
Noneheight keeps the video’s own aspect ratio.opacity – Opacity 0.0-1.0 (default 1.0).
anchor – Anchor point for positioning (default ‘top-left’).
blend_mode – CSS blend mode (default ‘normal’).
loop – Loop playback (default True).
autoplay – Start on load / when it becomes visible (default True). False shows native controls and makes the overlay capture pointer events so those controls can be used.
muted – Required True while
autoplayis True (default True).playback_rate – Speed multiplier in (0, 16] (default 1.0).
poster – Optional still shown before play and where the video cannot be decoded (PNG/JPEG/WebP bytes, path, array or PIL image).
alpha_matte –
None(an ordinary clip) or"stacked"(the clip is colour over a grey alpha matte, see above; the viewer shows only the top half, made transparent by the bottom half). Stacked mattes requireautoplay=Truebecause their source video is hidden.visible_range – Optional dimension-based visibility filter.
transition – ‘none’ or ‘fade’ (default ‘none’).
transition_duration – Transition duration in seconds (default 0.3).
interactive – If True, overlay captures pointer events (default False). Non-autoplay videos capture them regardless of this setting.
- Returns:
Overlay metadata object.
Example
>>> scene.add_video( ... "turntable.webm", position=(0.06, 0.5), anchor="center-left", ... size=(0.26, None), poster="turntable.png", ... visible_range={"story": 3}, transition="fade", ... )
- add_html(html: str, position: Tuple[float, float], *, name: str | None = None, width: float | None = None, opacity: float = 1.0, anchor: str = 'top-left', visible_range: Dict[str, float | Tuple[float, float]] | None = None, transition: str = 'none', transition_duration: float = 0.3, interactive: bool = False, blend_mode: str = 'normal', hover: bool = False, hover_image_size: Tuple[float, float] | None = None) Overlay[source]
Add an HTML overlay to the scene.
HTML is sanitized to a safe subset of tags and attributes before storage. Allowed tags include: b, i, em, strong, a, span, div, br, img, ul, ol, li, p, h1-h6, sub, sup, code, pre, table elements. Inline styles are allowed. Script tags and event handlers are stripped.
- Parameters:
html – HTML content string (will be sanitized). When
hoveris True, this is a template with{hover_label},{hover_key},{hover_image_label},{hover_node},{hover_index}placeholders.position – (x, y) in normalized screen coords [0, 1].
name – Optional overlay name. Auto-generated if None.
width – Optional width as fraction of viewport width.
opacity – Opacity 0.0-1.0 (default 1.0).
anchor – Anchor point for positioning (default ‘top-left’).
visible_range – Optional dimension-based visibility filter.
transition – Transition type: ‘none’ or ‘fade’ (default ‘none’).
transition_duration – Transition duration in seconds (default 0.3).
interactive – If True, overlay captures pointer events (default False).
blend_mode – CSS mix-blend-mode (default ‘normal’). Use ‘difference’ for XOR-style content that inverts the background colors.
hover – If True, this overlay is a hover tooltip updated by GPU picking. The
htmlis treated as a template (default False).hover_image_size – Optional (width, height) as viewport fractions for hover image thumbnails. Controls the size of
{hover_image_label}images.
- Returns:
Overlay metadata object.
Example
>>> scene.add_html( ... '<span style="color:red;font-weight:bold">Warning</span>: ' ... '<span style="color:#aaa">low signal region</span>', ... position=(0.5, 0.9), ... interactive=True, ... )
- to_zarr(path: PathLike) None[source]
Finalize and copy the backing Zarr store to
path.Luxar scenes are written progressively as nodes are added; the scene object itself does not keep an in-memory copy of geometry arrays. This method therefore exports by finalizing the current backing store and copying that on-disk Zarr directory to a new location.
Calling this method finalizes the associated writer. Do not add more nodes to this scene after calling
to_zarr(); create a newLuxarZarrCompilerif additional writes are needed.- Parameters:
path – Destination path for the Zarr store. A directory destination must not already exist unless it is the current backing store. For an archive-backed scene, the only supported destination is the selected archive path, which is replaced if it exists. To create an archive from a directory-backed scene, use
LuxarZarrCompilerorluxar optimizeinstead.- Raises:
FileExistsError – If a directory destination already exists and is not the current backing store.
ValueError – If the destination is inside the source store, the source store is unavailable, or an archive-backed writer is asked to publish anywhere except its selected archive path, or a directory-backed writer is asked to copy to an archive path.
Node
- class luxar.core.Node(name: str, parent: Node | None = None, writer: ZarrWriterProtocol | None = None, **attrs: Any)[source]
Bases:
objectA node in the Luxar scene graph.
This class represents a single node in the hierarchical scene graph structure. Nodes are lightweight metadata containers that write data immediately through the writer interface without keeping Zarr groups in memory.
- Parameters:
name – Name of the node
parent – Parent node in the hierarchy
writer – Writer interface for progressive writing
**attrs – Additional attributes for the node
- __init__(name: str, parent: Node | None = None, writer: ZarrWriterProtocol | None = None, **attrs: Any) None[source]
Initialize a scene graph node.
- Parameters:
name – Name of the node
parent – Parent node in the scene hierarchy
writer – Writer interface for progressive data writing
**attrs – Additional attributes to set on the node
- property attrs: _WriteThroughAttrs
Get node attributes.
Annotated with the concrete view rather than the
GroupAttrsalias so itscopy()— kept for parity with the cache dict this replaced — is visible to type checkers._WriteThroughAttrsis aMutableMapping[str, Any], so it still satisfiesNodeProtocol.attrs.- Returns:
Mutable mapping backed by the cache and the zarr store.
- add_group(name: str, **attrs: Any) Group[source]
Create and add a child group node.
The returned Group has add_points(), add_lines(), and add_gsplats() methods for adding data children directly.
- Parameters:
name – Name of the child group
**attrs – Additional attributes for the group
- Returns:
The created Group node
- Raises:
ValueError – If group creation fails
- add_lod_group(name: str, *, selector: str = 'coverage', default_level: int = 0, **attrs: Any) Group[source]
Create and add a child kind=lod
Groupnode.A kind=lod
Grouppicks one of N alternative children at runtime by comparing the group’s on-screen size against each child’scoverage_fractionthreshold;selectornames the UNITS of those thresholds. Underselector="screen-area"(what every auto-derived ladder stamps) a threshold is a literal screen-area fraction — the node’s projected bbox rect area over the viewport area — so a derived whole-object ladder reads[0, …, 1/4, 1/2](full detail while the node occupies at least half the screen; one level coarser per halving of occupied area) and a partition tile anchors at1.0(fills-screen). The defaultselector="coverage"is the legacy diagonal metric (projected bbox diagonal over half the fitted screen axis, bounded byMAX_COVERAGE_FRACTION= 4.0), kept for hand-authored ladders and existing datasets whose values were tuned in those units. Children are added via the inheritedadd_*methods on the returnedGroup, each carrying acoverage_fractionattribute, in strictly increasing order (coarsest0.0first). The resolveddisplay_typeof the finest child becomes the group’s user-facing geometry type.Example:
lod = scene.add_lod_group("multires") lod.add_gsplats_from_data("c", coarse, coverage_fraction=0.0) lod.add_gsplats_from_data("m", medium, coverage_fraction=0.5) lod.add_gsplats_from_data("f", fine, coverage_fraction=1.0)
- Parameters:
name – Name of the lod-kind group.
selector – Units of the children’s
coverage_fractionthresholds:"coverage"(legacy diagonal metric, the default for hand-built ladders) or"screen-area"(literal screen-area fractions — what derived ladders use).default_level – Initial active level index for the manual-override UI (0-based).
**attrs – Additional node attributes (transform, layer, etc.).
- Returns:
The created
Group(withkind="lod"in its attrs).- Raises:
ValueError – If selector mode is unknown or group creation fails.
- add_partition_group(name: str, *, display_type: str, max_elements: int, **attrs: Any) Group[source]
Create and add a child kind=partition
Groupnode.A kind=partition
Groupis a compile-time decomposition of a single large geometry node into multiple smaller children for per-child frustum culling and per-child LOD. The user does not see the decomposition — the layers panel presents one logical layer ofdisplay_type.For the common case where you want the partitioning to happen automatically, use the
partition=convenience kwarg onadd_points/add_lines/add_gsplatsinstead of constructing the wrapper yourself.- Parameters:
name – Name of the partition-kind group.
display_type – Geometry type the user sees this layer as (
"points","lines", or"gsplats"). All children must resolve to this same display type — homogeneity is mandatory for a partition.max_elements – Cap that drove the BSP recursion (recorded on the group for diagnostics and for future partition-aware tools).
**attrs – Additional node attributes.
- Returns:
The created
Group(withkind="partition"in its attrs).
- walk(depth: int = 0) Generator[Tuple[int, Any], None, None][source]
Walk the node hierarchy depth-first.
- Parameters:
depth – Current depth in the hierarchy (used for indentation)
- Yields:
Tuple of (depth, node) for each node in the hierarchy
- Raises:
ValueError – If traversal encounters an error
- property transform: NDArray[float32] | None
Get the transformation matrix for this node.
- Returns:
4x4 transformation matrix if set, None otherwise
- property world_transform: NDArray[float32]
Get the world transformation matrix by composing all parent transforms.
Walks up the parent chain, collecting local transforms, and composes them root-outermost: this node’s own transform is applied first (innermost) and the root transform last (outermost), i.e.
world = root @ ... @ leaf.- Returns:
4x4 world transformation matrix. Identity if no transforms are set.
- property nd_transform: Dict[str, Any] | None
Get the nD transform for non-displayed dimensions.
- Returns:
Dict mapping dimension names to per-dim transforms, or None. Affine entries: {“scale”: float, “offset”: float} Permutation entries: {“permutation”: [int, …]}
- property world_nd_transform: Dict[str, Any]
Get the composed world nD transform by walking the parent chain.
- Returns:
Composed nD transform dict. Empty dict means identity.
- property layer: bool
Whether this node is exposed as a layer in the viewer’s Layers panel.
- Returns:
True if this node should appear in the Layers panel, False otherwise
- property visible: bool
Initial visibility in the viewer (default True).
This controls whether the layer starts visible when the scene loads. Authoring-time property only — runtime toggling is done from the viewer’s Layers panel (eye icon).
- Returns:
True if the layer should start visible, False to start hidden.
- property opacity: float
Get the opacity value for this node.
- Returns:
Opacity value (0.0 to 1.0), defaults to 1.0 if not set
- property absorption: float
Get the absorption coefficient (volumetric kappa) for this node.
Only read by the
volumetricblending mode: it scales how strongly this node’s content attenuates what is behind it (kappa = 0 renders exactly likeadditive). Composes multiplicatively down the scene graph with identity 1.0, like opacity.- Returns:
Absorption coefficient (>= 0), defaults to 1.0 if not set
- property gamma: float
Get the gamma value for this node.
- Returns:
Gamma value (0.1 to 10.0), defaults to 1.0 if not set
- property intensity: float
Get the intensity value for this node.
- Returns:
Intensity value (0.0 to 100.0), defaults to 1.0 if not set
- property offset: float
Get the offset value for this node.
- Returns:
Offset value (-10.0 to 10.0), defaults to 0.0 if not set
- property blending_mode: str
Get the blending mode for this node.
- Returns:
Blending mode string, defaults to “additive” if not set. Valid modes: “normal”, “additive”, “max”, “opaque”, “luminous”, “volumetric”
- property join: str | None
Get the line join style for this node (issue #790).
Lines-only: the strategy the line vertex stage uses at a degree-2 polyline joint. Compositing, so it may equally be set on a wrapper Group, from where it flows down to the lines descendants.
Unlike
blending_modethis does NOT substitute a default when unset — it returnsNone. The default belongs to the viewer, where a?lineJoin=override can still win over it; reporting one here would invite writing it back and freezing today’s default into the file.- Returns:
"none"or"miter"if set, None otherwise
- set_opacity(value: Any) Node[source]
Set opacity and return self for chaining.
- Parameters:
value – Opacity value (0.0 to 1.0)
- Returns:
Self for method chaining
- set_absorption(value: Any) Node[source]
Set absorption (volumetric kappa) and return self for chaining.
- Parameters:
value – Absorption coefficient (>= 0, finite)
- Returns:
Self for method chaining
- set_gamma(value: Any) Node[source]
Set gamma and return self for chaining.
- Parameters:
value – Gamma value (0.1 to 10.0)
- Returns:
Self for method chaining
- set_intensity(value: Any) Node[source]
Set intensity and return self for chaining.
- Parameters:
value – Intensity value (0.0 to 100.0)
- Returns:
Self for method chaining
- set_offset(value: Any) Node[source]
Set offset and return self for chaining.
- Parameters:
value – Offset value (-10.0 to 10.0)
- Returns:
Self for method chaining
- set_blending_mode(value: Any) Node[source]
Set blending mode and return self for chaining.
- Parameters:
value –
Blending mode string. Valid modes:
”normal”: Standard alpha blending (semi-transparent)
”additive”: Classic additive blending, ignores depth (renders on top)
”max”: Maximum of source and destination (brightest wins)
”opaque”: Solid rendering with depth write (closest wins)
”luminous”: Same as additive visually, but respects depth occlusion
”volumetric”: Emission-absorption — adds light AND absorbs what’s behind, scaled by the
absorption(kappa) attr
- Returns:
Self for method chaining
- set_join(value: Any) Node[source]
Set the line join style and return self for chaining.
- Parameters:
value – Join style string (“none” or “miter”)
- Returns:
Self for method chaining
- property colormap: Any | None
Get the colormap for this node.
- Returns:
Colormap name (str) or LUT array, or None if not set.
- set_colormap(value: str) Node[source]
Set colormap and return self for chaining.
Only string colormap names are supported after creation. Custom array colormaps must be set at node creation time.
- Parameters:
value – Colormap name (str).
- Returns:
Self for method chaining
- __eq__(other: object) bool[source]
Equality based on path AND root identity in the scene graph.
Nodes from different scenes with the same path are NOT equal.
Group
- class luxar.core.Group(name: str, parent: Node | None = None, writer: ZarrWriterProtocol | None = None, **attrs: Any)[source]
Bases:
NodeA group node that can contain data children (Points, Lines, GSplats, Mesh).
Groups provide add_points(), add_lines(), add_gsplats(), and add_mesh() methods for adding data nodes. They access the root Scene for dimension validation and the writer interface.
Groups are created via add_group() on any Node, Scene, or Group:
scene = compiler.create_scene(dimensions=dims) group = scene.add_group("my_group") group.add_points("pts", positions) # data written under my_group/
Dimension mapping (
dim_order) allows adding lower-dimensional data to higher-dimensional scenes:# 3D splats fitted from a volume → 4D scene with Time dimension scene.add_gsplats_from_data( "splats", result_3d, dim_order=["Z", "Y", "X"], # maps data cols to scene dims fill={"Time": 0.0}, # fixed value for unmapped dim )
- Parameters:
name – Name of the group
parent – Parent node in the hierarchy
writer – Writer interface for progressive writing
**attrs – Additional attributes (opacity, blending_mode, etc.)
- add_points(name: str, positions: NDArray[float32] | NDArray[float16] | ndarray[Any, Any] | Sequence[Sequence[float]], colors: NDArray[float32] | NDArray[uint8] | NDArray[uint16] | ndarray[Any, Any] | Sequence[float | int] | None = None, radii: ndarray[Any, dtype[float32]] | ndarray[Any, Any] | float | None = None, sharpness: ndarray[Any, dtype[float32]] | ndarray[Any, Any] | float | None = None, scalars: ndarray[Any, dtype[float32]] | ndarray[Any, Any] | float | None = None, labels: List[str] | Sequence[str] | None = None, image_labels: Any | None = None, keys: List[str] | Sequence[str] | None = None, parent: Node | None = None, extend_to_all: List[str] | str | None = None, dim_order: List[str] | None = None, fill: Dict[str, float] | None = None, partition: Any = None, additive_lod: Any = None, substitutive_lod: Any = None, **attrs: Any) Points | Group[source]
Add a points node.
- Parameters:
name – Name of the points node
positions – Array of shape (N, D) for point positions
colors – Optional (N, 3) RGB or (N, 4) RGBA array (the alpha column is per-point opacity in [0, 1]), RGB tuple, or None
radii – Optional (N,) array, scalar, or None (default 0.5)
sharpness – Optional (N,) array, scalar, or None
scalars – Optional (N,) array or scalar for colormap lookup. Requires
colormapin attrs. Mutually exclusive withcolors.labels – Optional list of strings, one per point. Used for hover tooltips. Length must equal the number of points. Empty strings are treated as null labels (no tooltip shown on hover).
image_labels – Optional per-element images for hover thumbnails. Accepts List[bytes], List[PIL.Image], List[ndarray], List[Path], or Dict[int, Any] for sparse assignment. Prefer pre-encoded JPEG/WebP blobs for best compression.
keys – Optional list of machine-readable strings, one per point, for
link/copytemplates to substitute as{hover_key}. Same length rule and the same spatial reordering aslabels— a key stays paired with its element — but kept separate so the visible label can stay readable prose while the URL is built from a bare id.parent – Parent node (default: this group)
extend_to_all – Visibility extension across non-displayed dimensions
dim_order – Map data columns to scene dimensions by name. E.g.,
["Y", "X"]for 2D data in a 3D scene. Unmapped dims are filled withfillvalues and auto-extended.fill – Fixed coordinate values for unmapped scene dimensions when using
dim_order. Defaults to 0.0 for unspecified dims.substitutive_lod – Substitutive-LOD control.
None(default) /Falsewrite no substitutive ladder.True/dict()usecoarse="gsplats"(the default): each point is lifted to an isotropic Gaussian and reduced by the gsplat substitutive pipeline.coarse="points"instead writes spatially stratified subsamples as Points children, preserving the original radius; under effective additive/luminous blending their RGB values are scaled to preserve the finest level’s summed point energy, widening to float32 HDR only when the gain is not 1.brightness_compensation="auto"selects that rule, while a numeric value applies that per reduction level (use1to disable it). Both forms assemble akind="lod"Group whose finest child is the original Points node.dict(...)keys:compression_factor(K),coarse,brightness_compensation,levels(n_lods),method,truncation_radius,device,seed,coverage_fractions,coarsen_dims,max_aspect(anisotropy cap on the coarse levels, default 3.0;Nonedisables), andquality_stamps(measure per-level quality, defaultTrue).coarse="points"acceptsmethod="subsample"ormethod="merge". Merge writes moment-matched representatives, preserves discrete hidden coordinates, uses quantized colour as a soft ordering preference, bakes scalar colormaps, and drops identity channels on coarse levels. Under additive/luminous blending it conserves per-bin light by reducing radius before increasing RGB, so SDR colours remain representable. It refuses numeric brightness compensation.truncation_radius,device,coarsen_dims, andmax_aspectremain refused. Integercoarsen_dimsentries name the scene-ordered position columns afterdim_orderhas been applied. For stacked nodes, every discrete hidden coordinate must fit in the coarsest point level; otherwise authoring raises. Composes withadditive_lod, which then describes how each level streams in (every level gets a streaming ladder by default; passadditive_lod=Falseto opt out). When combined with an explicitpartition=, authors an overview topology: global coarse levels above a spatially partitioned finest Points branch, selected only once it fills the viewport.scalars``+``colormappoints are supported by baking scalars→RGB where coarse-level brightness compensation is required (the finest Points child stays scalar-driven; a live colormap change then re-colours only the finest level). Seeluxar.core.group.lod.points.resolve_substitutive_axis_points().partition – Spatial-decomposition control.
None(default) writes a single Points node.Truedecomposes via balanced median BSP withmax_elements = DEFAULT_MAX_ELEMENTS.dict(max_elements=N, rule=...)uses an explicit cap and rule ("median"default,"midpoint", or"sah"). When the decomposition yields more than one part, returns a kind=partitionGroupwrapper carryingdisplay_type= "points"; the wrapper’s children arepart_<i>Points nodes. Whensubstitutive_lod=is also set, that wrapper is instead the finest child of a kind=lodGroup. The partition wrapper’sposition_boundsis the union of the children’s so picking treats the layer as one entity.image_labelsis not supported alongsidepartition=(the sparse-dict semantics complicate slicing).**attrs –
Additional node attributes. Common ones:
layer(bool): Expose this node in the viewer’s Layers panel for per-node control. When structural options produce wrappers,layer=Truelands on the outermost wrapper, not on a nested partition wrapper or each leaf part.visible(bool): Initial visibility when scene loads (defaultTrue). Used by the Layers panel to start a layer hidden.opacity,intensity,gamma,blending_mode,colormap: standard rendering attributes. An explicitNoneforcolormaporcoverage_fractionmeans “absent” — identical to omitting the key — socolormap=maybe_colormapis a safe call form. Every OTHER render attr still refuses aNone.absorption(float >= 0): absorption coefficient kappa, read by the"volumetric"blending mode; kappa=0 renders like additive. Defaults to 1.0.
- Returns:
The created
Pointsnode, a kind=partitionGroupwhenpartition=produces multiple parts, or a kind=lodGroupwhensubstitutive_lod=produces coarse levels (with the partition as its finest child when both controls are combined).
- add_lines(name: str, vertices: NDArray[float32] | NDArray[float16] | ndarray[Any, Any] | Sequence[Sequence[float]], widths: ndarray[Any, dtype[float32]] | ndarray[Any, Any] | float, colors: NDArray[float32] | NDArray[uint8] | NDArray[uint16] | ndarray[Any, Any] | Sequence[float | int] | None = None, sharpness: ndarray[Any, dtype[float32]] | ndarray[Any, Any] | float | None = None, scalars: ndarray[Any, dtype[float32]] | ndarray[Any, Any] | float | None = None, labels: List[str] | Sequence[str] | None = None, image_labels: Any | None = None, keys: List[str] | Sequence[str] | None = None, indices: ndarray[Any, Any] | None = None, line_type: str = 'polyline', parent: Node | None = None, extend_to_all: List[str] | str | None = None, dim_order: List[str] | None = None, fill: Dict[str, float] | None = None, additive_lod: Any = None, substitutive_lod: Any = None, partition: Any = None, **attrs: Any) Lines | Group[source]
Add a lines node.
- Parameters:
name – Name of the lines node
vertices – Array of shape (N, D) for vertex positions
widths – (N,) array or scalar for line widths
colors – Optional (N, 3) array, RGB tuple, or None
sharpness – Optional (N,) array, scalar, or None
scalars – Optional (N,) array or scalar for colormap lookup. Requires
colormapin attrs. Mutually exclusive withcolors.labels – Optional list of strings, one per vertex. Used for hover tooltips.
image_labels – Optional per-element images for hover thumbnails.
keys – Optional list of machine-readable strings, one per vertex, for
link/copytemplates to substitute as{hover_key}. Same length rule and the same spatial reordering aslabels— a key stays paired with its element — but kept separate so the visible label can stay readable prose while the URL is built from a bare id.indices – Vertex-index pairs for
line_type="indexed", as a flat even-element(2E,)array or an(E, 2)pair array. Connected edges must reference the same vertex row for joint continuity; duplicated rows at equal coordinates remain independent endpoints.line_type – Connectivity (
"segments","polyline","loop", or"indexed"). Usepolylinefor one continuous chain andindexedfor multiple chains or graph topology with shared joints.parent – Parent node (default: this group)
extend_to_all – Visibility extension across non-displayed dimensions
dim_order – Map data columns to scene dimensions by name
fill – Fixed values for unmapped dimensions when using dim_order
substitutive_lod – Substitutive-LOD control (peer of Points’).
None/Falsewrite no substitutive ladder.True/dict()synthesise coarse LOD levels as Gaussian splats: each segment is lifted to isotropic “bead” gaussians and reduced by the gsplat substitutive pipeline, assembled as akind="lod"Group whose finest child is the original Lines node.coarse="lines"writes either seeded whole-polyline subsamples or, withmethod="merge", equal-vertex-count centroid polylines with transverse moment-matched widths. Merge preserves hidden coordinates, uses orientation and colour as soft ordering preferences, bakes scalar colormaps, and drops identity channels on coarse levels. Additive/luminous levels conserve per-bin light through width up to the transition’s pixel-floor cap, then through residual HDR colour, materialising a colour channel when needed. The coarsest level must represent every occupied discrete hidden coordinate. Each level carrieslevel_stats.qualityunless the spec setsquality_stamps=False. Composes withadditive_lod(which then describes how each level streams in; every level is laddered by default, passadditive_lod=Falseto opt out). Mutually exclusive withpartition. Seeluxar.core.group.lod.lines.resolve_substitutive_axis_lines().**attrs –
Additional node attributes. Common ones:
layer(bool): Expose this node in the viewer’s Layers panel for per-node control.visible(bool): Initial visibility when scene loads (defaultTrue).opacity,intensity,gamma,blending_mode,colormap: standard rendering attributes. An explicitNoneforcolormaporcoverage_fractionmeans “absent” — identical to omitting the key — socolormap=maybe_colormapis a safe call form. Every OTHER render attr still refuses aNone.absorption(float >= 0): absorption coefficient kappa, read by the"volumetric"blending mode; kappa=0 renders like additive. Defaults to 1.0.join(str): join style at degree-2 polyline joints –"miter"(the default) or"none". Without join geometry a turn leaves an uncovered wedge on the outside of the bend and a double-covered lens inside;"miter"rotates each quad’s end edge onto the shared miter edge so the two tile exactly. Gated in-shader by rendered width and a miter limit, so"none"is rarely worth authoring. An unrecognised value is rejected rather than silently treated as"none".
- Returns:
The created Lines node
- add_sound(name: str, clip: bytes | bytearray | str | Path, *, positions: ndarray | None = None, hidden: Mapping[str, float] | None = None, spatial: bool | None = None, trigger: str = 'continuous', delay_ms: float = 0.0, gain: float = 1.0, bus: str = 'ambient', fade_in_ms: float = 0.0, fade_out_ms: float = 0.0, distance_model: str = 'inverse', ref_distance: float | None = None, max_distance: float | None = None, rolloff: float | None = None, cone_inner_deg: float | None = None, cone_outer_deg: float | None = None, cone_outer_gain: float | None = None, orientation: Sequence[float] | None = None, attach_to: str | None = None, ambisonic: str | None = None, license: str = '', attribution: str = '', source_url: str = '', parent: Node | None = None, extend_to_all: List[str] | str | None = None, **attrs: Any) Sound[source]
Add a sound node — an MP3/AAC clip that plays in the viewer.
The one node type that is heard rather than drawn (
docs/guides/specs/SOUND_SPEC.md). Four placements:Everywhere —
positions=None, hidden=None: an ambient bed that plays whatever the sliders say.Bound to a hidden-dimension value —
hidden={"story": 3}: sugar for one(1, ndim)row atstory=3, extended over every other non-displayed dimension, so the slab rule that decides which points are visible decides when this clip is live. Non-spatial.Spatial —
positions=[[3, 7.3, -7.4, -0.3]]: one nD row per place the source exists; the clip plays through a panner there and gets louder as the camera approaches.spatialdefaults to True.Attached —
attach_to="cluster_hsp70": the source follows the bounding-box centre of the named node (“the cluster hums” without authoring coordinates). Spatial by default; combine withhidden=to make it live at one hidden-dimension value only.
- Parameters:
name – Node name (no
/).clip – Encoded MP3 or AAC (
.m4a) bytes, or a path to such a file. Ogg/Opus is refused (Safari cannot decode it); WAV/FLAC are refused (wrong size class for a hosted store).positions –
(K, ndim)source positions, orNone.hidden –
{dimension_name: value}binding for a non-spatial clip. Mutually exclusive withpositions.spatial – Route through a panner (needs
positionsorattach_to). Defaults topositions is not None or attach_to is not None.trigger –
"continuous"(looped while audible, fades on the slab edge),"once"(plays once each time the node becomes audible),"on_depart"/"on_arrive"(plays once when a story flight leaves / lands on theviewer_config.waypointsentry whosewhenclause this node’s row satisfies — a node without rows belongs to every waypoint).attach_to – Name of the node whose bounding-box centre the source follows. Mutually exclusive with
positions.ambisonic –
"foa"for a first-order ambisonic FIELD — a 4-channel AmbiX clip (AAC only) the viewer rotates against the camera so the field stays fixed to the world. Non-spatial and position-free by nature;hidden=still decides when it is live.delay_ms – Delay after the trigger fires,
>= 0.gain – Per-node linear gain,
>= 0.bus –
"ambient"(default) /"voice"/"effects". The voice bus ducks ambient while it plays.fade_in_ms – Ramp lengths on the audible edge,
>= 0.fade_out_ms – Ramp lengths on the audible edge,
>= 0.distance_model –
PannerNodedistance knobs (spatial only). Distances leftNonedefault in the viewer from the scene scale (scale/20andscale).ref_distance –
PannerNodedistance knobs (spatial only). Distances leftNonedefault in the viewer from the scene scale (scale/20andscale).max_distance –
PannerNodedistance knobs (spatial only). Distances leftNonedefault in the viewer from the scene scale (scale/20andscale).rolloff –
PannerNodedistance knobs (spatial only). Distances leftNonedefault in the viewer from the scene scale (scale/20andscale).cone_inner_deg –
PannerNodedirectional-cone knobs (spatial only).cone_outer_deg –
PannerNodedirectional-cone knobs (spatial only).cone_outer_gain –
PannerNodedirectional-cone knobs (spatial only).orientation –
PannerNodedirectional-cone knobs (spatial only).license – REQUIRED provenance for the clip (e.g.
"CC0", the author, the URL it came from).attribution – REQUIRED provenance for the clip (e.g.
"CC0", the author, the URL it came from).source_url – REQUIRED provenance for the clip (e.g.
"CC0", the author, the URL it came from).parent – Parent node (default: this group).
extend_to_all – As for
add_points; withhidden=it defaults to every non-displayed dimension not named there.**attrs –
layer/visible/transform/nd_transformonly — a sound has no appearance attrs.
- Returns:
The created Sound node.
- add_mesh(name: str, vertices: ndarray, faces: ndarray, normals: ndarray | None = None, normal_dims: Sequence[int] | None = None, colors: Any | None = None, scalars: Any | None = None, uvs: ndarray | None = None, texture: Any | None = None, *, texture_encoding: str = 'raw', texture_width: int | None = None, texture_height: int | None = None, texture_channels: int | None = None, texture_color_space: str = 'srgb', texture_ktx2_mode: str = 'uastc', texture_ktx2_quality: int | None = None, texture_ktx2_rdo_l: float | None = None, texture_ktx2_zcmp: int | None = None, shading: str | None = None, double_sided: bool = True, labels: Sequence[str] | None = None, image_labels: Any | None = None, keys: List[str] | Sequence[str] | None = None, partition: Any = None, parent: Node | None = None, extend_to_all: List[str] | str | None = None, dim_order: List[str] | None = None, fill: Dict[str, float] | None = None, substitutive_lod: bool | Dict[str, Any] | None = None, additive_lod: bool | Dict[str, Any] | None = None, **attrs: Any) Mesh | Group[source]
Add a triangle mesh (surface) node to this group.
The surface geometry type: nD
verticesplus afacestriangle-index array. Unlike Points / Lines / GSplats, a mesh has no per-element size — a triangle’s extent comes from its own vertices — so it also contributes no extent padding to the scene’s bounds.substitutive_lodIS supported: it writes akind=lodgroup whose coarse children are progressively DECIMATED copies of the surface and whose finest child is the original. Its vocabulary is shorter than the sibling adders’ — notruncation_radius/max_aspect/device/seed, because those exist only for geometries that coarsen by lifting to gsplats, andmethodis{'auto', 'cluster', 'qem'}rather than the Gaussian-mixture reducers. Seeluxar.core.group.lod.mesh.resolve_substitutive_axis_mesh().partitionIS supported too. Returns thekind=partitionwrapperGroupinstead of aMeshwhen the split yields more than one part (a single part falls through to a plain leaf), matchingadd_points/add_gsplats. A mesh may also be added directly to akind=partitiongroup you built yourself, provided that group declaresdisplay_type='mesh'— a partition is homogeneous, so a mismatched declaration is refused.partitionandsubstitutive_lodcannot be combined for Mesh or Lines; Points uses that pair for its overview shape.additive_lodIS supported, as a reveal ladder and nothing else: it writesadditive_<i>/levels inside the leaf, each holding one concentric shell of faces, innermost first, which the viewer draws cumulatively so the surface grows outward from its centre as it streams.methodaccepts only"radial"— a prefix of an arbitrarily ordered index buffer is a surface with holes rather than a coarser one, so"random"/"salience"and the element samplers are refused, as aresalience_kindandseed. Usesubstitutive_lodto make a surface genuinely coarser. The ladder carries no energy stamps by construction (a reveal is a partial surface at FULL brightness, so the viewer’s1/e(k)brightness compensation must not reach it), it degrades to a plain leaf with aUserWarningwhenlabelsorimage_labelsis set (a level re-indexes its own vertices, so there is no single index space for a union label CSR), and it cannot yet be combined withsubstitutive_lodorpartition— each pairing is refused by name, where Points and Lines compose both. Seeluxar.core.group.lod.mesh.resolve_additive_axis_mesh().The viewer half ships too: a mesh node declaring
n_additive_sublods > 1is loaded by its own progressive loader, which fetches the levels in order and commits each grown prefix into the same buffers.Not supported for meshes (raises rather than silently degrading):
blending_mode='volumetric'— a zero-thickness surface has no path length to integrate. Seedocs/specs/MESH_NODE_SPEC.md§9.- Parameters:
name – Name of the mesh node.
vertices – Vertex positions of shape
(V, D).faces – Triangle vertex indices,
(F, 3)or flat(3F,). Wound counter-clockwise as seen with the authored spatial triple in ascending index order.normals – Optional per-vertex normals of shape
(V, 3). Requiresnormal_dims.normal_dims – The three dimension indices
normalsdescribes. Required withnormalsand rejected without them — it is not inferable, and an implicit “first three dimensions” is wrong for any mesh whose leading dimension is not spatial (for a(t, x, y, z)mesh those are(t, x, y)). Passing it through**attrsfails: it is writer-reserved metadata.colors – Per-vertex colors
(V, 3|4), a broadcast RGB(A) tuple/list, or None. A 4th component is per-vertex opacity.scalars – Per-vertex scalars
(V,)or a single value for colormap lookup. Requires acolormapattr.shading –
"smooth","flat", or unlit"none". Defaults to"smooth"whennormalsare given, else"flat". An explicit value is stored as given —"flat"renders faceted even with normals present,"smooth"without normals falls back to derived flat normals at render time, and"none"computes no lighting normal.double_sided – Whether back faces render (default
True).labels – Optional per-vertex strings for hover tooltips.
image_labels – Optional per-vertex images for hover thumbnails. Not supported alongside
partition.keys – Optional list of machine-readable strings, one per vertex, for
link/copytemplates to substitute as{hover_key}. Same length rule and the same spatial reordering aslabels— a key stays paired with its element — but kept separate so the visible label can stay readable prose while the URL is built from a bare id.partition –
Truefor the default cap, or{"max_elements": int, "rule": "median"|"midpoint"|"sah"}, to split the surface into spatially-culled parts.max_elementscounts FACES — the BSP recurses on face centroids, so a triangle is the indivisible unit. Faces are assigned whole (never cut) and each part gathers and renumbers the vertices its own faces use, so vertices on a cut are duplicated between neighbouring parts.parent – Optional explicit parent node (defaults to this group).
extend_to_all – Dimension name(s) across which this mesh stays visible.
dim_order – Names of the dimensions the
verticescolumns are in, for remapping onto the scene’s dimension order.facesis index data addressing vertex rows and is never reordered. An orientation-reversing order flips face handedness relative to the scene frame; the writer warns but does not repair it. Reverse the corner order withfaces[:, [0, 2, 1]]when needed; see the mesh spec §3.7.fill – Fill values for scene dimensions absent from
dim_order.substitutive_lod –
True/{...}to write akind=lodgroup of progressively DECIMATED copies of the surface (see above).additive_lod –
True/{...}to write a reveal ladder ofadditive_<i>/levels inside the leaf. Keys:method("radial"only),n_lods,counts(aliasbreakpoints),reveal_center,spatial_dims. Levels hold concentric shells of FACES — a triangle is the indivisible unit, as it is forpartition— and are cumulative when concatenated, so the surface grows outward as it loads.**attrs – Additional attributes —
opacity,intensity,offset,gamma,colormap,layer,visible,transform,nd_transform,blending_mode, and the mesh-only appearance controlsambient,specular,alpha_cutoff(each in[0, 1]),shade_exponent, andshininess(both strictly positive and finite). Also mesh-only:material("luxar", the default house shader, or"physical"for three’s physically based material lit by the viewer’s scene environment —MESH_PHYSICAL_MATERIALS_SPEC.md) and the knobs it unlocks,roughness,metalness,clearcoat,clearcoat_roughness,iridescence,sheen(each in[0, 1]) andsheen_color("#rrggbb"), plus the glass family:transmission([0, 1]),ior([1, 2.333]),thickness(>= 0),attenuation_color("#rrggbb"),attenuation_distance(> 0),dispersion(>= 0) andrefract_data(bool). A physical knob withoutmaterial="physical"is refused; a physical mesh refuses the house-shader knobs,blending_mode,colormap,textureandshading="none"; andthickness/attenuation_*/dispersion/refract_dataare refused without atransmissionabove zero — none of them means anything in those pairings. Glass refracts the background and other meshes; withrefract_data=Trueit also refracts the points, lines and splats BEHIND it, while data in front of the glass stays crisp on top (the viewer partitions each data fragment by depth against the glass; spec §3.4). Also mesh-only, and a LOADING knob rather than an appearance one:slab_tolerance(strictly positive and finite, default1.0) — the half-width, IN CELLS, of the nD membership slab a continuous hidden dimension is culled against: a vertex is inside when it is withinslab_tolerancecells of the slice. A mesh renders a triangle only when all three of its vertices fall inside that slab, so on a continuous hidden axis it shows “the surface near this slice” rather than a planar cut, and this is the only control over how thick “near” is. It has no effect on a discrete hidden axis (time, channel), which uses a half-cell membership rule instead. Notevolumetricblending is rejected — it has no meaning for an opaque surface. An explicitNoneforcolormaporcoverage_fractionmeans “absent” — identical to omitting the key — socolormap=maybe_colormapis a safe call form. Every OTHER render attr still refuses aNone.
- Returns:
The created Mesh node — or, with
substitutive_lod, thekind=lodGroup wrapping the ladder, or withpartition, thekind=partitionGroup wrapping the parts (matchingadd_points/add_lines). Withadditive_lodit is still the Mesh node: a reveal ladder lives INSIDE the leaf, so the caller’s “one node” is unchanged.
- add_gsplats(name: str, centers: ~numpy._typing._array_like.NDArray[~numpy.float32] | ~numpy._typing._array_like.NDArray[~numpy.float16] | ~numpy.ndarray[~typing.Any, ~typing.Any] | ~typing.Sequence[~typing.Sequence[float]], amplitudes: ~numpy.ndarray[~typing.Any, ~numpy.dtype[~numpy.float32]] | ~numpy.ndarray[~typing.Any, ~typing.Any] | float, cholesky_factors: ~numpy.ndarray[~typing.Any, ~numpy.dtype[~numpy.float32]] | ~numpy.ndarray[~typing.Any, ~typing.Any], colors: ~numpy._typing._array_like.NDArray[~numpy.float32] | ~numpy._typing._array_like.NDArray[~numpy.uint8] | ~numpy._typing._array_like.NDArray[~numpy.uint16] | ~numpy.ndarray[~typing.Any, ~typing.Any] | ~typing.Sequence[float | int] | None = None, label_ids: ~numpy.ndarray[~typing.Any, ~typing.Any] | None = None, label_vocabulary: ~typing.Dict[int, str] | None = None, labels: ~typing.List[str] | ~typing.Sequence[str] | None = None, image_labels: ~typing.Any | None = None, keys: ~typing.List[str] | ~typing.Sequence[str] | None = None, parent: ~luxar.core.node.node.Node | None = None, extend_to_all: ~typing.List[str] | str | None = None, dim_order: ~typing.List[str] | None = None, fill: ~typing.Dict[str, float] | None = None, fill_sigma: ~typing.Dict[str, float] | None = None, partition: ~typing.Any = None, substitutive_lod: ~typing.Any = <object object>, additive_lod: ~typing.Any = <object object>, _source_dtype: str | None = None, **attrs: ~typing.Any) GSplats | Group[source]
Add a Gaussian splats node.
- Parameters:
name – Name of the gsplats node
centers – Array of shape (N, D) for splat centers
amplitudes – (N,) array or scalar for intensities
cholesky_factors – (N, k) packed lower-triangular factor L of the covariance (Σ = L·Lᵀ), k=D*(D+1)/2. The diagonal is scale-like: isotropic std σ uses [σ, 0, σ, 0, 0, σ], not 1/sigma.
colors – Optional (N, 3) RGB or (N, 4) RGBA array (the alpha column is per-splat opacity in [0, 1]), RGB tuple, or None
label_ids – Optional non-negative integer class id per splat.
label_vocabulary – Explicit mapping from every stored class id to its name.
labels – Optional list of strings, one per splat. Used for hover tooltips.
image_labels – Optional per-element images for hover thumbnails.
keys – Optional list of machine-readable strings, one per splat, for
link/copytemplates to substitute as{hover_key}. Same length rule and the same spatial reordering aslabels— a key stays paired with its element — but kept separate so the visible label can stay readable prose while the URL is built from a bare id.parent – Parent node (default: this group)
extend_to_all – Visibility extension across non-displayed dimensions
dim_order – Map data columns to scene dimensions by name. Also reorders and embeds Cholesky factors automatically.
fill – Fixed coordinate values for unmapped dimensions
fill_sigma – Standard deviations for unmapped dimensions in the Cholesky embedding (default 1.0). Controls splat extent in unmapped dims.
partition – Spatial-decomposition control.
None(default) writes a single GSplats node.Truedecomposes via balanced median BSP withmax_elements = DEFAULT_MAX_ELEMENTS.dict(max_elements=N, rule=...)uses an explicit cap and rule ("median"default,"midpoint", or"sah"). When the decomposition yields more than one part, returns a kind=partitionGroupwrapper carryingdisplay_type= "gsplats"; the wrapper’s children arepart_<i>GSplats nodes.image_labelsis not supported alongsidepartition=.substitutive_lod – Substitutive-LOD control. The value vocabulary is the same as
add_gsplats_from_data()’s historicallod_group=. When combined withpartition=, every substitutive level is partitioned independently.additive_lod – Additive-LOD control. The value vocabulary matches
add_gsplats_from_data(). Requesting an additive ladder opts this node out of compiler auto-partitioning; explicitpartition=beside the ladder remains unsupported. On a stacked or hidden-dimension node, an authored ladder is inert when one already exists unlessrecompute=True, andslice_dims=is the only way to give every slice the same absolute budget.**attrs –
Additional node attributes. Common ones:
layer(bool): Expose this node in the viewer’s Layers panel for per-node control. Whenpartition=produces a wrapper,layer=Truelands on the wrapper, not on each leaf part.visible(bool): Initial visibility when scene loads (defaultTrue).opacity,intensity,gamma,blending_mode,colormap: standard rendering attributes. An explicitNoneforcolormaporcoverage_fractionmeans “absent” — identical to omitting the key — socolormap=maybe_colormapis a safe call form. Every OTHER render attr still refuses aNone.absorption(float >= 0): absorption coefficient kappa, read by the"volumetric"blending mode; kappa=0 renders like additive. Defaults to 1.0. Likelayer, on apartition=wrapper this lands on the wrapper node, not on each leaf part.
- Returns:
The created
GSplatsnode, or a kind=partitionGroupwrapper whenpartition=produced more than one part.
- add_gsplats_from_data(name: str, result: GSplatData, parent: Optional[Node] = None, extend_to_all: Optional[Union[List[str], str]] = None, dim_order: Optional[List[str]] = None, fill: Optional[Dict[str, float]] = None, fill_sigma: Optional[Dict[str, float]] = None, lod_group: Any = <object object>, additive_lod: Any = None, normalize_amplitudes: Any = <object object>, substitutive_lod: Any = <object object>, **attrs: Any) GSplats | 'Group'[source]
Add Gaussian splats from a GSplatData object.
Multi-additive-LOD data (from
make_additive_lodor anadditive_lod=spec) is written with per-sub-LOD subgroups directly under the gsplats node (<node>/additive_<i>/...) for progressive (prefix-sum) loading. Single-LOD data uses the flat layout (arrays at the node path).substitutive_lodandadditive_lodcontrol the two LOD axes (seeluxar.core.group.lod.gsplats.resolve_substitutive_axis_gsplats/resolve_additive_axis_gsplatsfor the full value vocabulary).lod_group=remains supported as an alias forsubstitutive_lod=; passing both is refused. When the resolved data has multiple substitutive levels, this method builds akind="lod"Groupcontaining one gsplats child per level (in coarsest→finest order, namedchild_<i>) and returns it; otherwise it returns a singleGSplatsnode.labels/image_labelsmay not be passed when the resolved result is multi-substitutive: every level is its own set of merged representative splats with its own count, so no single list has a per-element correspondence to carry. Passsubstitutive_lod=False(or itslod_group=Falsealias) to label the collapsed finest level, or build thekind="lod"group yourself withadd_lod_group()and give each child its own labels.An explicit ``None`` in ``**attrs`` means “absent”. For
labels,image_labels,partition,colors,truncation_radius,colormapandcoverage_fraction, passingNoneis exactly equivalent to omitting the key — so the idiomaticpartition=maybe_partition/colormap=maybe_colormapcall form is safe. Every OTHER attribute (opacity,blending_mode,layer,visible,gamma,intensity,absorption, …) rejects aNoneas an invalid value, so a typo is not silently swallowed.The data’s own channels may not be passed as attributes.
centers,amplitudes,cholesky_factorsand a non-Nonecolorseach raiseValueError: this method supplies all four fromresultitself, so a keyword of the same name collides with the value already being passed, and on a multi-child result it could not be split per child anyway. Set them on theGSplatDatabefore calling, or usecolormap=for appearance.coverage_fractionmay only be passed in**attrswhen the result is single-substitutive AND the parent is itself akind="lod"Group(the child is a leaf of an enclosing LOD group). Passing it on a multi-substitutive path raisesValueError— usesubstitutive_lod=dict(coverage_fractions=[...])to override the auto-derived thresholds. (coverage_fraction=Noneis “absent” per the rule above, so it is accepted on any path.)- Parameters:
name – Name of the gsplats (or kind=lod group) node.
result – GSplatData from
fit_gaussian_splatsor similar.parent – Parent node (default: this group).
extend_to_all – Visibility extension across non-displayed dimensions.
dim_order – Map data columns to scene dimensions by name.
fill – Fixed coordinate values for unmapped dimensions.
fill_sigma – Standard deviations for unmapped dims in Cholesky embedding.
substitutive_lod – Substitutive-axis control.
None(default; auto-lower a multi-substitutive pyramid into akind=lodGroup),True(require stored levels),False(collapse to finest),dict(...)(compute viamake_substitutive_lod()), ordict(..., recompute=True). Optionalcoverage_fractions=[...]inside the dict overrides the auto-derived thresholds. On a computed ladder, integercoarsen_dimsentries name the rawresult.centerscolumns beforedim_order; dimension names remain scene names.lod_group – Backward-compatible alias for
substitutive_lod. Passing both is refused.additive_lod – Additive-axis control, uniform across substitutive levels. Same value vocabulary as
substitutive_lod;dict(...)routes tomake_additive_lod().normalize_amplitudes –
Scale amplitudes so a robust upper reference (the 99.9th percentile) lands at 1.0, applied as ONE factor across every substitutive level and additive rung.
True/"auto"(the default outside akind=lodorkind=partitiongroup) acts only when that reference exceeds 1.0, so data already in range is untouched. Children inserted into those specialized groups default toFalsebecause their exposure must be shared across siblings; passTrueexplicitly to override that rule. A positive number sets an explicit target. The factor used is recorded asamplitude_normalization_factor.On by default for standalone insertion because raw fitted amplitudes cannot be corrected at display time. A fit stores source units (detector counts), and while the colormap window feeds only the LUT index — clamped to
[0, 1], so it picks a colour — emitted radiance and volumetric optical depth are both LINEAR in the raw stored amplitude and nothing windows them. Seeluxar.core.group.gsplats_pipeline.amplitude_norm.**attrs – Additional node attributes — the
add_gsplats()vocabulary (includingabsorption) MINUS the four channels this method supplies fromresult, which are refused; see the two rules above for that and forNonehandling. On a nestedkind=lodtree, compositing attributes (e.g.absorption) land on the wrapper node while the rest (e.g.colormap) are copied onto each leaf.
Example
>>> result = fit_gaussian_splats(volume_3d) >>> # Plain flat gsplats node >>> scene.add_gsplats_from_data("splats", result, ... dim_order=["Z", "Y", "X"], fill={"Time": 0}) >>> >>> # Auto-build a 3-level kind=lod Group with a 4-step >>> # additive ladder per level, computed from a flat input. >>> scene.add_gsplats_from_data( ... "multires", flat_result, ... substitutive_lod=dict(compression_factor=4, levels=2), ... additive_lod=dict(n_lods=4), ... )
- add_gsplats_from_file(name: str, path: str | Path, parent: Node | None = None, extend_to_all: List[str] | str | None = None, dim_order: List[str] | None = None, fill: Dict[str, float] | None=None, fill_sigma: Dict[str, float] | None=None, normalize_amplitudes: Any = <object object>, partition: Any = None, substitutive_lod: Any = <object object>, lod_group: Any = <object object>, additive_lod: Any = None, flatten: bool = False, **attrs: Any) GSplats | Group[source]
Add Gaussian splats by loading from a .gsplats.zarr file.
If the source file carries multiple substitutive levels, the pyramid is auto-lowered into a
kind=lodGroup (one gsplats child per substitutive level); passsubstitutive_lod=Falseto collapse to the finest level instead.lod_group=Falseremains the backward-compatible alias.Set
flatten=Trueto materialize the source tree’s default finest selection as one leaf before applyingpartition=,substitutive_lod=, oradditive_lod=. Without it,partition=andadditive_lod=retain a nested source tree and apply to each leaf;substitutive_lod=requires flattening that tree first.lod_group=is retained as an alias forsubstitutive_lod=.labels/image_labels/keysare accepted only when the file is a single leaf with NO additive ladder. Any multi-LEAF result — an auto-lowered pyramid, or a grafted multi-partkind=lod/kind=partitionsubtree — refuses them, because each leaf holds its own set of splats (seeadd_gsplats_from_data). A single LADDERED leaf (gsplat lod --recipe stream, or the one-part output of--recipe tileson a small dataset — a plaingsplat fitwrites a FLAT leaf, which labels fine) is refused too, because the additive writer has no per-element string channel —gsplat flattencollapses the ladder if you need one.The two
**attrsrules ofadd_gsplats_from_data()apply here identically, and identically on BOTH of this method’s branches (a matrix-shaped file and a grafted nested one): an explicitNoneforlabels/image_labels/keys/partition/colors/truncation_radius/colormap/coverage_fractionmeans “absent”, whilecenters/amplitudes/cholesky_factorsand a non-NonecolorsraiseValueError(they come from the file itself). Nothing is written when either rule refuses.- Parameters:
name – Name of the gsplats node
path – Path to .gsplats.zarr file
parent – Parent node (default: this group)
extend_to_all – Visibility extension across non-displayed dimensions
dim_order – Map data columns to scene dimensions by name
fill – Fixed coordinate values for unmapped dimensions
fill_sigma – Standard deviations for unmapped dims in Cholesky embedding
normalize_amplitudes –
Scale amplitudes so a robust upper reference (the 99.9th percentile) lands at 1.0, applied as ONE factor across every substitutive level and additive rung.
True/"auto"(the default outside akind=lodorkind=partitiongroup) acts only when that reference exceeds 1.0, so data already in range is untouched. Children inserted into those specialized groups default toFalsebecause their exposure must be shared across siblings; passTrueexplicitly to override that rule. A positive number sets an explicit target. The factor used is recorded asamplitude_normalization_factor.On by default for standalone insertion because raw fitted amplitudes cannot be corrected at display time. A fit stores source units (detector counts), and while the colormap window feeds only the LUT index — clamped to
[0, 1], so it picks a colour — emitted radiance and volumetric optical depth are both LINEAR in the raw stored amplitude and nothing windows them. Seeluxar.core.group.gsplats_pipeline.amplitude_norm.partition – Spatial partition control applied to the loaded matrix, or to each leaf of a retained nested tree.
substitutive_lod – Substitutive-LOD control applied after loading; nested trees require
flatten=True.lod_group – Backward-compatible alias for
substitutive_lod.additive_lod – Additive-LOD control applied to the loaded matrix, or independently to each leaf of a retained nested tree.
flatten – Collapse stored structure to the default finest selection before applying the requested structure.
**attrs – Additional node attributes — the
add_gsplats()vocabulary (includingabsorption) MINUS the four channels the file supplies, which are refused; see the rules above. On a nested tree, compositing attributes (blending_mode,absorption,opacity, …) land on the wrapper node ONLY — the viewer resolves them down the ancestry — while the rest (e.g.colormap) are copied onto each leaf. Stampingblending_modeon the parts too would SHADOW the wrapper (it is nearest-setter-wins), leaving the layer’s Blend control inert.
- add_gsplats_from_volume(name: str, volume: ndarray, seeds: int | float | None = None, n_iters: int = 1000, device: str | None = None, progressive: bool = False, max_splats_per_pass: int = 5000, psnr_patience: float = 0.5, max_passes: int | None = None, parent: Node | None = None, extend_to_all: List[str] | str | None = None, dim_order: List[str] | None = None, fill: Dict[str, float] | None=None, fill_sigma: Dict[str, float] | None=None, opacity: float | None = None, absorption: float | None = None, blending_mode: str | None = None, normalize_amplitudes: Any = <object object>, **fit_kwargs: Any) GSplats | Group[source]
Fit Gaussian splats to a volume and add them in one step.
- Parameters:
name – Name of the gsplats node
volume – Input n-dimensional volume to fit
seeds – Number of splats (int), compression ratio (float), or None. In progressive mode, this is the total max splats budget.
n_iters – Optimization iterations (default: 1000). In progressive mode, this is iterations per pass.
device – Compute device (“cuda”, “mps”, “cpu”, or None for auto)
progressive – Optimize in several passes against residuals, returning one flat splat set. To build a streaming ladder, use
add_gsplats(..., additive_lod=...)oradd_gsplats_from_file(..., additive_lod=...).max_splats_per_pass – Max splats per progressive pass (default: 5000)
psnr_patience – Stop progressive fitting if PSNR gain < this (dB)
max_passes – Max number of progressive passes (None = unlimited)
parent – Parent node (default: this group)
extend_to_all – Visibility extension across non-displayed dimensions
dim_order – Map fitted data columns to scene dimensions by name
fill – Fixed coordinate values for unmapped dimensions
fill_sigma – Standard deviations for unmapped dims in Cholesky embedding
opacity – Node opacity (0.0-1.0)
absorption – Absorption coefficient kappa (>= 0) read by the “volumetric” blending mode; kappa=0 renders like additive
blending_mode – Blending mode (“normal”, “additive”, “max”, “opaque”, “luminous”, “volumetric”)
normalize_amplitudes – Scale amplitudes so a robust upper reference (the 99.9th percentile) lands at 1.0. The default is enabled outside a
kind=lodorkind=partitiongroup and disabled for children inserted directly into those groups so sibling exposure stays shared. PassTrueto override that specialized-group default,Falseto preserve raw units, or a positive number to set an explicit target. The factor used is recorded asamplitude_normalization_factor.**fit_kwargs – Extra kwargs for fitting function
Points
- class luxar.core.Points(name: str, metadata: Dict[str, Any] | None = None, parent: 'DataNode' | None = None, writer: 'ZarrWriterProtocol' | None = None, **attrs: Any)[source]
Bases:
DataNodePoints node that holds metadata about points data.
This class is a lightweight metadata container. Actual point data is written immediately to Zarr via the writer interface and not kept in memory.
Points are created internally by Scene.add_points() and should not be instantiated directly by users.
- Parameters:
name – Name of the points node
metadata – Metadata dictionary about the written points
parent – Parent node in hierarchy
writer – Writer interface for progressive writing
**attrs – Additional attributes
- __init__(name: str, metadata: Dict[str, Any] | None = None, parent: 'DataNode' | None = None, writer: 'ZarrWriterProtocol' | None = None, **attrs: Any) None[source]
Initialize a Points node.
- Parameters:
name – Name of the points node
metadata – Metadata dictionary about the written points
parent – Parent node in the scene graph
writer – Writer interface for progressive writing
**attrs – Additional attributes for the node
- property has_image_labels: bool
Check if points have per-element image labels for hover thumbnails.
Lines
- class luxar.core.Lines(name: str, metadata: Dict[str, Any] | None = None, parent: 'DataNode' | None = None, writer: 'ZarrWriterProtocol' | None = None, **attrs: Any)[source]
Bases:
DataNodeLines node that holds metadata about line/curve data.
This class is a lightweight metadata container. Actual line data is written immediately to Zarr via the writer interface and not kept in memory.
Lines are created internally by Scene.add_lines() and should not be instantiated directly by users.
- Parameters:
name – Name of the lines node
metadata – Metadata dictionary about the written lines
parent – Parent node in hierarchy
writer – Writer interface for progressive writing
**attrs – Additional attributes
- __init__(name: str, metadata: Dict[str, Any] | None = None, parent: 'DataNode' | None = None, writer: 'ZarrWriterProtocol' | None = None, **attrs: Any) None[source]
Initialize a Lines node.
- Parameters:
name – Name of the lines node
metadata – Metadata dictionary about the written lines
parent – Parent node in the scene graph
writer – Writer interface for progressive writing
**attrs – Additional attributes for the node
- property line_type: Literal['segments', 'polyline', 'loop', 'indexed']
Get original line type (user-specified).
GSplats
- class luxar.core.GSplats(name: str, metadata: Dict[str, Any] | None = None, parent: 'DataNode' | None = None, writer: 'ZarrWriterProtocol' | None = None, **attrs: Any)[source]
Bases:
DataNodeGSplats node that holds metadata about Gaussian splat data.
This class is a lightweight metadata container. Actual splat data is written immediately to Zarr via the writer interface and not kept in memory.
GSplats are created internally by Scene.add_gsplats() and should not be instantiated directly by users.
- Parameters:
name – Name of the gsplats node
metadata – Metadata dictionary about the written gsplats
parent – Parent node in hierarchy
writer – Writer interface for progressive writing
**attrs – Additional attributes
- __init__(name: str, metadata: Dict[str, Any] | None = None, parent: 'DataNode' | None = None, writer: 'ZarrWriterProtocol' | None = None, **attrs: Any) None[source]
Initialize a GSplats node.
- Parameters:
name – Name of the gsplats node
metadata – Metadata dictionary about the written gsplats
parent – Parent node in the scene graph
writer – Writer interface for progressive writing
**attrs – Additional attributes for the node
- property has_image_labels: bool
Check if gsplats have per-element image labels for hover thumbnails.
Mesh
- class luxar.core.Mesh(name: str, metadata: Dict[str, Any] | None = None, parent: 'DataNode' | None = None, writer: 'ZarrWriterProtocol' | None = None, **attrs: Any)[source]
Bases:
DataNodeMesh node that holds metadata about triangle-surface data.
This class is a lightweight metadata container. Actual mesh data is written immediately to Zarr via the writer interface and not kept in memory.
Meshes are created internally by Scene.add_mesh() and should not be instantiated directly by users.
- Parameters:
name – Name of the mesh node
metadata – Metadata dictionary about the written mesh
parent – Parent node in hierarchy
writer – Writer interface for progressive writing
**attrs – Additional attributes
- __init__(name: str, metadata: Dict[str, Any] | None = None, parent: 'DataNode' | None = None, writer: 'ZarrWriterProtocol' | None = None, **attrs: Any) None[source]
Initialize a Mesh node.
- Parameters:
name – Name of the mesh node
metadata – Metadata dictionary about the written mesh
parent – Parent node in the scene graph
writer – Writer interface for progressive writing
**attrs – Additional attributes for the node
- property n_elements: int
Number of primary elements (vertices).
Vertices, not faces — matching
Lines.n_elements, which also counts vertices rather than segments. The primary element is the thing the per-element attribute arrays (colors / scalars / normals / labels) are indexed by, and for a mesh that is the vertex.- Returns:
Number of vertices
- property normal_dims: List[int] | None
The three dimension indices the stored normals describe.
Nonewhen the mesh has no normals. Never inferred: normals are a display-space quantity, and which three dimensions they belong to is recorded explicitly (an implicit “first three” is wrong for any mesh whose leading dimension is not spatial).
- property has_image_labels: bool
Check if the mesh has per-vertex image labels for hover thumbnails.
- property has_keys: bool
Check if the mesh has per-element machine-readable keys.
The string a
link/copytemplate substitutes as{hover_key}, independent ofhas_labels.
- property shading: Literal['smooth', 'flat', 'none']
Get the shading mode (‘smooth’, ‘flat’, or ‘none’).
- property blending_mode: str
Get the blending mode for this node.
Re-declared (identically to
Node) only so the setter below can be overridden — a property’s getter and setter travel together.
- property join: str | None
Get the line join style for this node.
Re-declared (identically to
Node,Nonewhen unset rather than a substituted default) only so the setter below can be overridden — a property’s getter and setter travel together.
- property ordering: str
Get spatial ordering method.
Always
"none"for a mesh: v1 has no spatial index because the loader is whole-node, so there is nothing for a chunk index to skip. The property exists for symmetry with the sibling geometry types, and reads the stamped attr rather than returning a literal so it stays honest if that changes.
Sound
- class luxar.core.Sound(name: str, metadata: Dict[str, Any] | None = None, parent: 'DataNode' | None = None, writer: 'ZarrWriterProtocol' | None = None, **attrs: Any)[source]
Bases:
DataNodeSound node: an opaque MP3/AAC clip, optionally positioned in nD.
Like the geometry nodes this is a lightweight metadata container — the clip and the optional
positionsarray are written to Zarr immediately by the writer and never kept in memory. A sound node is heard rather than drawn: it has no appearance attrs, contributes nothing to the scene bounds, and is audible only where the slab rule on its hidden-dimension coordinates says so (docs/guides/specs/SOUND_SPEC.md§3).Sounds are created by
luxar.Group.add_sound()and should not be instantiated directly by users.- Parameters:
name – Name of the sound node
metadata – Metadata dictionary about the written clip
parent – Parent node in hierarchy
writer – Writer interface for progressive writing
**attrs – Compositing attributes (
layer/visible/transform/nd_transform)
- __init__(name: str, metadata: Dict[str, Any] | None = None, parent: 'DataNode' | None = None, writer: 'ZarrWriterProtocol' | None = None, **attrs: Any) None[source]
Initialize a Sound node.
- Parameters:
name – Name of the sound node
metadata – Metadata dictionary about the written clip
parent – Parent node in the scene graph
writer – Writer interface for progressive writing
**attrs – Additional attributes for the node
Dimensions
- class luxar.core.Dimensions(dimensions: List[Dimension] = <factory>)[source]
Bases:
objectComplete dimension specification for a scene.
Defines the coordinate system including dimension order, properties, and display configuration.
- validate_positions(positions: ndarray, name: str = 'positions') None[source]
Validate that positions array matches scene dimensions.
- Parameters:
positions – Array to validate
name – Name for error messages
- Raises:
ValueError – If positions don’t match scene dimensions
- classmethod default_2d() Dimensions[source]
Create default 2D dimensions (x, y).
- classmethod default_3d() Dimensions[source]
Create default 3D dimensions (x, y, z).
- classmethod default_timeseries(n_timepoints: int = 100, time_unit: str = 's') Dimensions[source]
Create default time series dimensions (t, x, y, z).
- classmethod default_multichannel(n_channels: int = 3) Dimensions[source]
Create default multichannel dimensions (c, x, y, z).
- class luxar.core.Dimension(name: str, unit: str = '', range: Tuple[float, float] | None = None, step: float | None = None, display: bool = True, discrete: bool = False, cyclic: bool = False, scale: float = 1.0, spatial: bool | None = None, categories: List[str] | None = None, description: str = '')[source]
Bases:
objectDefinition of a single dimension in a scene.
Transforms
luxar.core.transforms – Transform utilities for creating and manipulating 4x4 transformation matrices.
This module provides convenient functions for creating common transformations used in 3D graphics, including translation, rotation, and scaling matrices. All matrices are 4x4 homogeneous transformation matrices suitable for use with the Luxar scene graph.
- luxar.core.transforms.identity() NDArray[float32][source]
Create a 4x4 identity transformation matrix.
- Returns:
4x4 identity matrix as float32 array
Example
>>> t = identity() >>> print(t) [[1. 0. 0. 0.] [0. 1. 0. 0.] [0. 0. 1. 0.] [0. 0. 0. 1.]]
- luxar.core.transforms.translate(x: float = 0.0, y: float = 0.0, z: float = 0.0) NDArray[float32][source]
Create a translation transformation matrix.
- Parameters:
x – Translation along X axis
y – Translation along Y axis
z – Translation along Z axis
- Returns:
4x4 translation matrix as float32 array
Example
>>> t = translate(5, 0, 0) # Move 5 units along X >>> print(t[:, 3]) # Translation column [5. 0. 0. 1.]
- luxar.core.transforms.scale(x: float = 1.0, y: float = 1.0, z: float = 1.0, uniform: float | None = None) NDArray[float32][source]
Create a scaling transformation matrix.
- Parameters:
x – Scale factor for X axis (ignored if uniform is set)
y – Scale factor for Y axis (ignored if uniform is set)
z – Scale factor for Z axis (ignored if uniform is set)
uniform – If provided, scales all axes equally
- Returns:
4x4 scaling matrix as float32 array
Example
>>> t1 = scale(2, 2, 2) # Scale 2x on all axes >>> t2 = scale(uniform=2) # Same as above >>> t3 = scale(x=2, y=1, z=0.5) # Non-uniform scaling
- luxar.core.transforms.rotate_x(degrees: float) NDArray[float32][source]
Create a rotation matrix around the X axis.
- Parameters:
degrees – Rotation angle in degrees
- Returns:
4x4 rotation matrix as float32 array
Example
>>> t = rotate_x(90) # Rotate 90 degrees around X
- luxar.core.transforms.rotate_y(degrees: float) NDArray[float32][source]
Create a rotation matrix around the Y axis.
- Parameters:
degrees – Rotation angle in degrees
- Returns:
4x4 rotation matrix as float32 array
Example
>>> t = rotate_y(45) # Rotate 45 degrees around Y
- luxar.core.transforms.rotate_z(degrees: float) NDArray[float32][source]
Create a rotation matrix around the Z axis.
- Parameters:
degrees – Rotation angle in degrees
- Returns:
4x4 rotation matrix as float32 array
Example
>>> t = rotate_z(180) # Rotate 180 degrees around Z
- luxar.core.transforms.rotate(degrees: float, axis: Literal['x', 'y', 'z'] | Tuple[float, float, float] | NDArray[float32]) NDArray[float32][source]
Create a rotation matrix around a specified axis.
- Parameters:
degrees – Rotation angle in degrees
axis – Either ‘x’, ‘y’, ‘z’ for principal axes, or a 3D vector for arbitrary axis
- Returns:
4x4 rotation matrix as float32 array
Example
>>> t1 = rotate(45, 'z') # Rotate around Z axis >>> t2 = rotate(30, (1, 1, 0)) # Rotate around diagonal axis >>> t3 = rotate(90, np.array([0, 0, 1])) # Rotate around Z using vector
- luxar.core.transforms.compose(*transforms: NDArray[float32]) NDArray[float32][source]
Compose multiple transformation matrices into a single matrix.
Matrices are applied in order from left to right. For example, compose(T1, T2, T3) creates a matrix that first applies T1, then T2, then T3.
- Mathematical Note:
To apply transforms in user order (T1, then T2, then T3), we compute: result = T3 @ T2 @ T1
This is because matrix multiplication is right-associative when applied to vectors: (T3 @ T2 @ T1) @ v = T3 @ (T2 @ (T1 @ v))
So the rightmost matrix (T1) is applied first to the vector.
- Parameters:
*transforms – Variable number of 4x4 transformation matrices
- Returns:
4x4 composed transformation matrix as float32 array
Example
>>> t1 = translate(5, 0, 0) # Move right 5 units >>> t2 = rotate_z(45) # Rotate 45 degrees >>> t3 = scale(2, 2, 2) # Scale by 2x >>> combined = compose(t1, t2, t3) # Translate, then rotate, then scale
- luxar.core.transforms.inverse(transform: NDArray[float32]) NDArray[float32][source]
Compute the inverse of a transformation matrix.
- Parameters:
transform – 4x4 transformation matrix
- Returns:
4x4 inverse transformation matrix as float32 array
- Raises:
ValueError – If matrix is not invertible
Example
>>> t = translate(5, 0, 0) >>> t_inv = inverse(t) # Translates -5, 0, 0
- luxar.core.transforms.look_at(eye: Tuple[float, float, float], target: Tuple[float, float, float], up: Tuple[float, float, float] = (0, 1, 0)) NDArray[float32][source]
Create a “look at” transformation matrix.
This creates a transformation that positions an object at ‘eye’ and orients it to look at ‘target’ with the given ‘up’ vector.
- Parameters:
eye – Position of the viewer
target – Position to look at
up – Up direction vector (default: Y-up)
- Returns:
4x4 transformation matrix as float32 array
Example
>>> t = look_at((10, 5, 10), (0, 0, 0)) # Look at origin from position (10, 5, 10)
- luxar.core.transforms.to_list(transform: NDArray[float32]) list[float][source]
Convert a 4x4 transformation matrix to a flat list for storage.
Note: The matrix is transposed before flattening to match THREE.js column-major format requirements.
- Parameters:
transform – 4x4 transformation matrix (row-major, NumPy format)
- Returns:
List of 16 float values in column-major order (for THREE.js)
Example
>>> t = translate(1, 2, 3) >>> values = to_list(t) # Returns transposed, flattened matrix
- luxar.core.transforms.from_list(values: list[float]) NDArray[float32][source]
Create a 4x4 transformation matrix from a flat list.
Note: The values are assumed to be in THREE.js column-major format and are transposed back to NumPy row-major format.
- Parameters:
values – List of 16 float values in column-major order (THREE.js format)
- Returns:
4x4 transformation matrix in row-major order (NumPy format)
- Raises:
ValueError – If values is not a list of 16 numbers
Example
>>> values = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 5, 0, 0, 1] >>> t = from_list(values) # Creates translation matrix (5, 0, 0)
- luxar.core.transforms.prepare_transform_for_zarr(transform: Any) list[float][source]
Prepare any transform format for Zarr storage (THREE.js compatible).
This centralizes the logic for converting transforms to the format expected by the Zarr storage and THREE.js viewer. The transform is validated and converted to a 16-element list in column-major order.
All inputs are interpreted as row-major (NumPy convention): - 4x4 numpy array: standard NumPy row-major matrix - 16-element list: flattened row-major (same as
matrix.ravel().tolist()) - 16-element flat numpy array: flattened row-majorThe output is always column-major (THREE.js convention) for zarr storage.
- Parameters:
transform – Transform in any supported format: - 4x4 numpy array (row-major) - 16-element list (row-major, flattened) - 16-element numpy array (row-major, flat)
- Returns:
16-element list in column-major order for THREE.js
- Raises:
ValueError – If transform is invalid or wrong shape
See also
read_transform_from_zarr: Reverse operation to read from storage
- luxar.core.transforms.read_transform_from_zarr(transform_list: list[float]) NDArray[float32][source]
Read a transform from Zarr storage and convert to NumPy format.
This is the inverse of prepare_transform_for_zarr(). It converts a 16-element list in THREE.js column-major format back to a NumPy row-major 4x4 matrix.
- Parameters:
transform_list – 16-element list in column-major order (THREE.js format)
- Returns:
4x4 transformation matrix in row-major order (NumPy format)
- Raises:
ValueError – If transform_list is invalid
Example
>>> # Read transform from zarr attributes >>> transform_list = node_attrs['transform'] >>> matrix = read_transform_from_zarr(transform_list) >>> print(matrix.shape) # (4, 4)
See also
prepare_transform_for_zarr: Inverse operation to write to storage
- luxar.core.transforms.transform_bounding_box(matrix: Any, lo: Any, hi: Any) Tuple[NDArray[float64], NDArray[float64]][source]
Transform an axis-aligned 3D bounding box by a 4x4 matrix.
Transforms all 8 corners of the box and returns the smallest axis-aligned box that encloses the transformed corners. This is the mathematically correct way to transform an AABB under rotation / shear — transforming only the (min, max) corner pair underestimates the rotated extent and is the classic bug that makes peripheral geometry get clipped/culled.
Mirrors
transformBoundingBoxin the TypeScript viewer (scene/scene-manager/clipping/bounds-math.ts).- Parameters:
matrix – 4x4 transformation matrix (row-major, NumPy convention). Accepts a 4x4 array or a flat 16-element row-major array.
lo – Lower corner
[x, y, z]of the box.hi – Upper corner
[x, y, z]of the box.
- Returns:
Tuple
(new_lo, new_hi)of the enclosing box, each a length-3 float64 array. Corners with|w| < W_EPSILON (1e-12)are omitted; if all corners are omitted, the input box is returned unchanged.
Example
>>> m = translate(3, 0, 0) >>> lo, hi = transform_bounding_box(m, [-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]) >>> lo.tolist(), hi.tolist() ([2.5, -0.5, -0.5], [3.5, 0.5, 0.5])
- luxar.core.transforms.translation(*args: Any, **kwargs: Any) NDArray[float32][source]
Alias for translate().