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: Group

Scene 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. None means 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.

property overlays: List[Overlay]

Get the list of overlays added to this scene.

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 width is set, text wraps within that viewport-relative width.

Parameters:
  • text – The text content to display. When hover is 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 text is 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 None height keeps the image’s own aspect ratio (the viewer renders height: auto), as for add_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 (its visible_range not 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 pass alpha_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 a poster for 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 None height 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 autoplay is 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 require autoplay=True because 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 hover is 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 html is 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 new LuxarZarrCompiler if 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 LuxarZarrCompiler or luxar optimize instead.

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: object

A 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 GroupAttrs alias so its copy() — kept for parity with the cache dict this replaced — is visible to type checkers. _WriteThroughAttrs is a MutableMapping[str, Any], so it still satisfies NodeProtocol.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 Group node.

A kind=lod Group picks one of N alternative children at runtime by comparing the group’s on-screen size against each child’s coverage_fraction threshold; selector names the UNITS of those thresholds. Under selector="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 at 1.0 (fills-screen). The default selector="coverage" is the legacy diagonal metric (projected bbox diagonal over half the fitted screen axis, bounded by MAX_COVERAGE_FRACTION = 4.0), kept for hand-authored ladders and existing datasets whose values were tuned in those units. Children are added via the inherited add_* methods on the returned Group, each carrying a coverage_fraction attribute, in strictly increasing order (coarsest 0.0 first). The resolved display_type of 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_fraction thresholds: "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 (with kind="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 Group node.

A kind=partition Group is 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 of display_type.

For the common case where you want the partitioning to happen automatically, use the partition= convenience kwarg on add_points / add_lines / add_gsplats instead 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 (with kind="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 num_children: int

Get the number of direct children of this node.

property is_leaf: bool

Check if this node is a leaf (has no children).

property is_root: bool

Check if this node is the root (has no parent).

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 volumetric blending mode: it scales how strongly this node’s content attenuates what is behind it (kappa = 0 renders exactly like additive). 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_mode this does NOT substitute a default when unset — it returns None. 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.

__hash__() → int[source]

Hash based on path AND root identity in the scene graph.

Nodes from different scenes with the same path hash differently.

__repr__() → str[source]

String representation of the node.

Returns:

Human-readable string representation of the node

Group

class luxar.core.Group(name: str, parent: Node | None = None, writer: ZarrWriterProtocol | None = None, **attrs: Any)[source]

Bases: Node

A 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 colormap in attrs. Mutually exclusive with colors.

  • 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 / copy templates to substitute as {hover_key}. Same length rule and the same spatial reordering as labels — 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 with fill values 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) / False write no substitutive ladder. True / dict() use coarse="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 (use 1 to disable it). Both forms assemble a kind="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; None disables), and quality_stamps (measure per-level quality, default True). coarse="points" accepts method="subsample" or method="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, and max_aspect remain refused. Integer coarsen_dims entries name the scene-ordered position columns after dim_order has been applied. For stacked nodes, every discrete hidden coordinate must fit in the coarsest point level; otherwise authoring raises. Composes with additive_lod, which then describes how each level streams in (every level gets a streaming ladder by default; pass additive_lod=False to opt out). When combined with an explicit partition=, authors an overview topology: global coarse levels above a spatially partitioned finest Points branch, selected only once it fills the viewport. scalars``+``colormap points 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). See luxar.core.group.lod.points.resolve_substitutive_axis_points().

  • partition – Spatial-decomposition control. None (default) writes a single Points node. True decomposes via balanced median BSP with max_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=partition Group wrapper carrying display_type= "points"; the wrapper’s children are part_<i> Points nodes. When substitutive_lod= is also set, that wrapper is instead the finest child of a kind=lod Group. The partition wrapper’s position_bounds is the union of the children’s so picking treats the layer as one entity. image_labels is not supported alongside partition= (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=True lands on the outermost wrapper, not on a nested partition wrapper or each leaf part.

    • visible (bool): Initial visibility when scene loads (default True). Used by the Layers panel to start a layer hidden.

    • opacity, intensity, gamma, blending_mode, colormap: standard rendering attributes. An explicit None for colormap or coverage_fraction means “absent” — identical to omitting the key — so colormap=maybe_colormap is a safe call form. Every OTHER render attr still refuses a None.

    • absorption (float >= 0): absorption coefficient kappa, read by the "volumetric" blending mode; kappa=0 renders like additive. Defaults to 1.0.

Returns:

The created Points node, a kind=partition Group when partition= produces multiple parts, or a kind=lod Group when substitutive_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 colormap in attrs. Mutually exclusive with colors.

  • 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 / copy templates to substitute as {hover_key}. Same length rule and the same spatial reordering as labels — 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"). Use polyline for one continuous chain and indexed for 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 /False write 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 a kind="lod" Group whose finest child is the original Lines node. coarse="lines" writes either seeded whole-polyline subsamples or, with method="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 carries level_stats.quality unless the spec sets quality_stamps=False. Composes with additive_lod (which then describes how each level streams in; every level is laddered by default, pass additive_lod=False to opt out). Mutually exclusive with partition. See luxar.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 (default True).

    • opacity, intensity, gamma, blending_mode, colormap: standard rendering attributes. An explicit None for colormap or coverage_fraction means “absent” — identical to omitting the key — so colormap=maybe_colormap is a safe call form. Every OTHER render attr still refuses a None.

    • 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 at story=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. spatial defaults 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 with hidden= 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, or None.

  • hidden – {dimension_name: value} binding for a non-spatial clip. Mutually exclusive with positions.

  • spatial – Route through a panner (needs positions or attach_to). Defaults to positions 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 the viewer_config.waypoints entry whose when clause 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 – PannerNode distance knobs (spatial only). Distances left None default in the viewer from the scene scale (scale/20 and scale).

  • ref_distance – PannerNode distance knobs (spatial only). Distances left None default in the viewer from the scene scale (scale/20 and scale).

  • max_distance – PannerNode distance knobs (spatial only). Distances left None default in the viewer from the scene scale (scale/20 and scale).

  • rolloff – PannerNode distance knobs (spatial only). Distances left None default in the viewer from the scene scale (scale/20 and scale).

  • cone_inner_deg – PannerNode directional-cone knobs (spatial only).

  • cone_outer_deg – PannerNode directional-cone knobs (spatial only).

  • cone_outer_gain – PannerNode directional-cone knobs (spatial only).

  • orientation – PannerNode directional-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; with hidden= it defaults to every non-displayed dimension not named there.

  • **attrs – layer / visible / transform / nd_transform only — 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 vertices plus a faces triangle-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_lod IS supported: it writes a kind=lod group 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’ — no truncation_radius / max_aspect / device / seed, because those exist only for geometries that coarsen by lifting to gsplats, and method is {'auto', 'cluster', 'qem'} rather than the Gaussian-mixture reducers. See luxar.core.group.lod.mesh.resolve_substitutive_axis_mesh().

partition IS supported too. Returns the kind=partition wrapper Group instead of a Mesh when the split yields more than one part (a single part falls through to a plain leaf), matching add_points / add_gsplats. A mesh may also be added directly to a kind=partition group you built yourself, provided that group declares display_type='mesh' — a partition is homogeneous, so a mismatched declaration is refused. partition and substitutive_lod cannot be combined for Mesh or Lines; Points uses that pair for its overview shape.

additive_lod IS supported, as a reveal ladder and nothing else: it writes additive_<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. method accepts 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 are salience_kind and seed. Use substitutive_lod to 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’s 1/e(k) brightness compensation must not reach it), it degrades to a plain leaf with a UserWarning when labels or image_labels is 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 with substitutive_lod or partition — each pairing is refused by name, where Points and Lines compose both. See luxar.core.group.lod.mesh.resolve_additive_axis_mesh().

The viewer half ships too: a mesh node declaring n_additive_sublods > 1 is 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. See docs/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). Requires normal_dims.

  • normal_dims – The three dimension indices normals describes. Required with normals and 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 **attrs fails: 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 a colormap attr.

  • shading – "smooth", "flat", or unlit "none". Defaults to "smooth" when normals are 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 / copy templates to substitute as {hover_key}. Same length rule and the same spatial reordering as labels — 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 – True for the default cap, or {"max_elements": int, "rule": "median"|"midpoint"|"sah"}, to split the surface into spatially-culled parts. max_elements counts 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 vertices columns are in, for remapping onto the scene’s dimension order. faces is 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 with faces[:, [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 a kind=lod group of progressively DECIMATED copies of the surface (see above).

  • additive_lod – True / {...} to write a reveal ladder of additive_<i>/ levels inside the leaf. Keys: method ("radial" only), n_lods, counts (alias breakpoints), reveal_center, spatial_dims. Levels hold concentric shells of FACES — a triangle is the indivisible unit, as it is for partition — 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 controls ambient, specular, alpha_cutoff (each in [0, 1]), shade_exponent, and shininess (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]) and sheen_color ("#rrggbb"), plus the glass family: transmission ([0, 1]), ior ([1, 2.333]), thickness (>= 0), attenuation_color ("#rrggbb"), attenuation_distance (> 0), dispersion (>= 0) and refract_data (bool). A physical knob without material="physical" is refused; a physical mesh refuses the house-shader knobs, blending_mode, colormap, texture and shading="none"; and thickness / attenuation_* / dispersion / refract_data are refused without a transmission above zero — none of them means anything in those pairings. Glass refracts the background and other meshes; with refract_data=True it 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, default 1.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 within slab_tolerance cells 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. Note volumetric blending is rejected — it has no meaning for an opaque surface. An explicit None for colormap or coverage_fraction means “absent” — identical to omitting the key — so colormap=maybe_colormap is a safe call form. Every OTHER render attr still refuses a None.

Returns:

The created Mesh node — or, with substitutive_lod, the kind=lod Group wrapping the ladder, or with partition, the kind=partition Group wrapping the parts (matching add_points / add_lines). With additive_lod it 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 / copy templates to substitute as {hover_key}. Same length rule and the same spatial reordering as labels — 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. True decomposes via balanced median BSP with max_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=partition Group wrapper carrying display_type= "gsplats"; the wrapper’s children are part_<i> GSplats nodes. image_labels is not supported alongside partition=.

  • substitutive_lod – Substitutive-LOD control. The value vocabulary is the same as add_gsplats_from_data()’s historical lod_group=. When combined with partition=, 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; explicit partition= beside the ladder remains unsupported. On a stacked or hidden-dimension node, an authored ladder is inert when one already exists unless recompute=True, and slice_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. When partition= produces a wrapper, layer=True lands on the wrapper, not on each leaf part.

    • visible (bool): Initial visibility when scene loads (default True).

    • opacity, intensity, gamma, blending_mode, colormap: standard rendering attributes. An explicit None for colormap or coverage_fraction means “absent” — identical to omitting the key — so colormap=maybe_colormap is a safe call form. Every OTHER render attr still refuses a None.

    • absorption (float >= 0): absorption coefficient kappa, read by the "volumetric" blending mode; kappa=0 renders like additive. Defaults to 1.0. Like layer, on a partition= wrapper this lands on the wrapper node, not on each leaf part.

Returns:

The created GSplats node, or a kind=partition Group wrapper when partition= 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_lod or an additive_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_lod and additive_lod control the two LOD axes (see luxar.core.group.lod.gsplats.resolve_substitutive_axis_gsplats / resolve_additive_axis_gsplats for the full value vocabulary). lod_group= remains supported as an alias for substitutive_lod=; passing both is refused. When the resolved data has multiple substitutive levels, this method builds a kind="lod" Group containing one gsplats child per level (in coarsest→finest order, named child_<i>) and returns it; otherwise it returns a single GSplats node.

labels / image_labels may 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. Pass substitutive_lod=False (or its lod_group=False alias) to label the collapsed finest level, or build the kind="lod" group yourself with add_lod_group() and give each child its own labels.

An explicit ``None`` in ``**attrs`` means “absent”. For labels, image_labels, partition, colors, truncation_radius, colormap and coverage_fraction, passing None is exactly equivalent to omitting the key — so the idiomatic partition=maybe_partition / colormap=maybe_colormap call form is safe. Every OTHER attribute (opacity, blending_mode, layer, visible, gamma, intensity, absorption, …) rejects a None as an invalid value, so a typo is not silently swallowed.

The data’s own channels may not be passed as attributes. centers, amplitudes, cholesky_factors and a non-None colors each raise ValueError: this method supplies all four from result itself, 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 the GSplatData before calling, or use colormap= for appearance.

coverage_fraction may only be passed in **attrs when the result is single-substitutive AND the parent is itself a kind="lod" Group (the child is a leaf of an enclosing LOD group). Passing it on a multi-substitutive path raises ValueError — use substitutive_lod=dict(coverage_fractions=[...]) to override the auto-derived thresholds. (coverage_fraction=None is “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_splats or 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 a kind=lod Group), True (require stored levels), False (collapse to finest), dict(...) (compute via make_substitutive_lod()), or dict(..., recompute=True). Optional coverage_fractions=[...] inside the dict overrides the auto-derived thresholds. On a computed ladder, integer coarsen_dims entries name the raw result.centers columns before dim_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 to make_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 a kind=lod or kind=partition group) acts only when that reference exceeds 1.0, so data already in range is untouched. Children inserted into those specialized groups default to False because their exposure must be shared across siblings; pass True explicitly to override that rule. A positive number sets an explicit target. The factor used is recorded as amplitude_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. See luxar.core.group.gsplats_pipeline.amplitude_norm.

  • **attrs – Additional node attributes — the add_gsplats() vocabulary (including absorption) MINUS the four channels this method supplies from result, which are refused; see the two rules above for that and for None handling. On a nested kind=lod tree, 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=lod Group (one gsplats child per substitutive level); pass substitutive_lod=False to collapse to the finest level instead. lod_group=False remains the backward-compatible alias.

Set flatten=True to materialize the source tree’s default finest selection as one leaf before applying partition=, substitutive_lod=, or additive_lod=. Without it, partition= and additive_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 for substitutive_lod=.

labels / image_labels / keys are 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-part kind=lod / kind=partition subtree — refuses them, because each leaf holds its own set of splats (see add_gsplats_from_data). A single LADDERED leaf (gsplat lod --recipe stream, or the one-part output of --recipe tiles on a small dataset — a plain gsplat fit writes a FLAT leaf, which labels fine) is refused too, because the additive writer has no per-element string channel — gsplat flatten collapses the ladder if you need one.

The two **attrs rules of add_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 explicit None for labels / image_labels / keys / partition / colors / truncation_radius / colormap / coverage_fraction means “absent”, while centers / amplitudes / cholesky_factors and a non-None colors raise ValueError (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 a kind=lod or kind=partition group) acts only when that reference exceeds 1.0, so data already in range is untouched. Children inserted into those specialized groups default to False because their exposure must be shared across siblings; pass True explicitly to override that rule. A positive number sets an explicit target. The factor used is recorded as amplitude_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. See luxar.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 (including absorption) 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. Stamping blending_mode on 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=...) or add_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=lod or kind=partition group and disabled for children inserted directly into those groups so sibling exposure stays shared. Pass True to override that specialized-group default, False to preserve raw units, or a positive number to set an explicit target. The factor used is recorded as amplitude_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: DataNode

Points 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 n_elements: int

Number of primary elements (points).

Returns:

Number of points

property has_colors: bool

Check if points have colors.

property has_radii: bool

Check if points have radii.

property has_sharpness: bool

Check if points have sharpness.

property has_scalars: bool

Check if points have scalar values for colormap lookup.

property has_labels: bool

Check if points have per-element string labels for hover tooltips.

property has_image_labels: bool

Check if points have per-element image labels for hover thumbnails.

property has_keys: bool

Check if points have per-element machine-readable keys.

The string a link / copy template substitutes as {hover_key}, independent of has_labels.

property max_radius: float

Get maximum point radius.

property has_spatial_index: bool

Check if points have spatial indexing enabled.

property ordering: str

Get spatial ordering method (e.g., ‘morton’, ‘hilbert’, ‘none’).

property join: str | None

Get the line join style for this node.

Re-declared (identically to Node, None when unset rather than a substituted default) only so the setter below can be overridden — a property’s getter and setter travel together.

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: DataNode

Lines 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 n_elements: int

Number of primary elements (vertices).

Returns:

Number of vertices

property n_segments: int

Get number of line segments.

property line_type: Literal['segments', 'polyline', 'loop', 'indexed']

Get original line type (user-specified).

property has_colors: bool

Check if lines have colors.

property has_sharpness: bool

Check if lines have sharpness.

property has_scalars: bool

Check if lines have scalar values for colormap lookup.

property has_labels: bool

Check if lines have per-element string labels for hover tooltips.

property has_image_labels: bool

Check if lines have per-element image labels for hover thumbnails.

property has_keys: bool

Check if lines have per-element machine-readable keys.

The string a link / copy template substitutes as {hover_key}, independent of has_labels.

property max_width: float

Get maximum line width.

property has_spatial_index: bool

Check if lines have spatial indexing enabled.

property ordering: str

Get spatial ordering method (e.g., ‘morton’, ‘hilbert’, ‘none’).

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: DataNode

GSplats 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 n_elements: int

Number of primary elements (splats).

Returns:

Number of splats

property has_colors: bool

Check if splats have colors.

property has_labels: bool

Check if gsplats have per-element string labels for hover tooltips.

property has_image_labels: bool

Check if gsplats have per-element image labels for hover thumbnails.

property has_keys: bool

Check if gsplats have per-element machine-readable keys.

The string a link / copy template substitutes as {hover_key}, independent of has_labels.

property ordering: str

Get spatial ordering type.

property amplitude_range: Dict[str, float]

Get amplitude range.

property center_bounds: Dict[str, list]

Get center coordinate bounds.

property join: str | None

Get the line join style for this node.

Re-declared (identically to Node, None when unset rather than a substituted default) only so the setter below can be overridden — a property’s getter and setter travel together.

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: DataNode

Mesh 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 n_faces: int

Get number of triangles.

property has_normals: bool

Check if the mesh has per-vertex normals.

property normal_dims: List[int] | None

The three dimension indices the stored normals describe.

None when 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_colors: bool

Check if the mesh has per-vertex colors.

property has_scalars: bool

Check if the mesh has scalar values for colormap lookup.

property has_labels: bool

Check if the mesh has per-vertex string labels for hover tooltips.

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 / copy template substitutes as {hover_key}, independent of has_labels.

property shading: Literal['smooth', 'flat', 'none']

Get the shading mode (‘smooth’, ‘flat’, or ‘none’).

property double_sided: bool

Whether back faces render.

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, None when 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: DataNode

Sound 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 positions array 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

property n_elements: int

Number of source positions (0 for a non-spatial clip).

property n_positions: int

Number of rows in the positions array (0 when absent).

property spatial: bool

True when the clip plays through a panner at its position(s).

property trigger: str

Playback trigger (continuous, once, on_depart, or on_arrive).

property bus: str

Mixer bus the clip is routed to (ambient / voice / effects).

property attach_to: str | None

Name of the node whose bounding-box centre the source follows, if any.

property format: str

Clip codec as sniffed by the writer (mp3 or aac).

property audio_file: str

Filename of the clip inside the node group (audio.mp3 / audio.m4a).

property duration_ms: float | None

Clip duration in ms when the writer could measure it, else None.

Dimensions

class luxar.core.Dimensions(dimensions: List[Dimension] = <factory>)[source]

Bases: object

Complete dimension specification for a scene.

Defines the coordinate system including dimension order, properties, and display configuration.

dimensions: List[Dimension]
__post_init__() → None[source]

Validate dimensions configuration.

property ndim: int

Number of dimensions.

__len__() → int[source]

Return the number of dimensions.

property names: List[str]

List of dimension names in order.

property displayed: List[int]

Indices of displayed dimensions.

property non_displayed: List[int]

Indices of non-displayed dimensions.

property spatial_extend_dims: List[bool]

List of spatial extension flags for all dimensions.

get_dimension(name: str) → Dimension | None[source]

Get dimension by name.

get_index(name: str) → int[source]

Get dimension index by name.

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

to_dict() → Dict[str, Any][source]

Convert to dictionary for serialization.

classmethod from_dict(data: Dict[str, Any]) → Dimensions[source]

Create from dictionary.

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).

classmethod from_positions(positions: ndarray, names: List[str] | None = None) → Dimensions[source]

Infer dimensions from positions array.

Parameters:
  • positions – Positions array

  • names – Optional dimension names

Returns:

Inferred dimensions

__init__(dimensions: List[Dimension] = <factory>) → None
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: object

Definition of a single dimension in a scene.

name

Identifier for the dimension (e.g., “x”, “time”, “channel”)

Type:

str

unit

Physical unit (e.g., “um”, “s”, “px”)

Type:

str

range

Optional min/max values as tuple

Type:

Tuple[float, float] | None

step

Default step size for navigation (None = auto-calculate)

Type:

float | None

display

Whether dimension should be displayed (max 3 can be True)

Type:

bool

discrete

Whether dimension has discrete values (for channels, indices)

Type:

bool

cyclic

Whether dimension wraps around (for angles, periodic states)

Type:

bool

scale

Physical scale factor (default 1.0)

Type:

float

spatial

Whether points extend through this dimension (None = auto-determine)

Type:

bool | None

categories

Optional category labels for categorical dimensions

Type:

List[str] | None

description

Optional human-readable description

Type:

str

__post_init__() → None[source]

Validate dimension parameters and auto-determine spatial flag.

get_step() → float[source]

Get the step size for navigation.

Returns auto-calculated step if not explicitly set.

property is_categorical: bool

Check if this dimension is categorical.

to_dict() → Dict[str, Any][source]

Convert to dictionary for serialization.

classmethod from_dict(data: Dict[str, Any]) → Dimension[source]

Create from dictionary.

__init__(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 = '') → None

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-major

The 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 transformBoundingBox in 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().

luxar.core.transforms.scaling(*args: Any, **kwargs: Any) → NDArray[float32][source]

Alias for scale().

luxar.core.transforms.rotation(*args: Any, **kwargs: Any) → NDArray[float32][source]

Alias for rotate().