# Dalaran Blueprint

> Design a Dalaran blueprint from the data, then iterate on it from headless screenshots. Read this when laying out a recording or dataset in the viewer, designing a default blueprint, or deciding which views show which entities. Covers archetype-to-view mapping, layout reasoning, the rrb construction API, the contents grammar, and the screenshot loop.

- Skill: `flaminis/dalaran-blueprint` (Agent Skill)
- Install (CLI): `npx skillmds@latest add flaminis/dalaran-blueprint`
- Raw SKILL.md: https://api.skillmd.com/api/skills/flaminis/dalaran-blueprint/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: flaminis (https://skillmd.com/u/flaminis)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/flaminis/dalaran-blueprint

---


# Dalaran blueprint

A blueprint decides how a recording is shown; the recording decides what exists.
Read the data, design a first layout, then **iterate from screenshots** until it
reads at a glance. The constructors are discoverable (`dir(rrb)`,
`help(rrb.Spatial3DView)`); this skill covers what you can't guess. Import as
`import dalaran.blueprint as rrb`.

## 1. Read the data

Enumerate every `(entity_path, archetype)` pair first — the archetype picks the
view, the entity path scopes it. For a dataset (see `dalaran-catalog-queries`):

```python
for c in dataset.schema().component_columns():
    print(c.entity_path, c.archetype, c.component_name)
```

For a local recording, stream it with `RrdReader` and read `entity_path` and
the archetype off each chunk (see `dalaran-chunk-processing`).

## 2. Map archetype to view

| Archetype family                                                                      | View                                                |
| ------------------------------------------------------------------------------------- | --------------------------------------------------- |
| `Points2D`, `Image`, `EncodedImage`, `Boxes2D`, `LineStrips2D`, `Pinhole` projections | `Spatial2DView`                                     |
| `Points3D`, `Mesh3D`, `Boxes3D`, `LineStrips3D`, `Transform3D`, `Asset3D`             | `Spatial3DView`                                     |
| `Scalars`, `SeriesLines`, `SeriesPoints`                                              | `TimeSeriesView`                                    |
| `TextLog` / `TextDocument` / `BarChart`                                               | `TextLogView` / `TextDocumentView` / `BarChartView` |
| `Tensor`, `DepthImage` (heatmap)                                                      | `TensorView`                                        |
| `GeoPoints`, `GeoLineStrings`                                                         | `MapView`                                           |
| `GraphNodes`, `GraphEdges`                                                            | `GraphView`                                         |
| tabular / catalog data                                                                | `DataframeView`                                     |

## 3. Group entities into views

This step is just deciding which entities share a view. Sizing and placement are
mechanics you tune by eye in the screenshot loop, not rules to settle up front.

- **Group by shared path prefix.** Entities under a common prefix usually belong
  in one view. Don't give every entity its own pane: dozens of raw message
  entities go in one `DataframeView`, or get left out, rather than a pane each.
- **One spatial frame per spatial view.** A whole `Transform3D` tree goes in one
  `Spatial3DView` at the common ancestor; a `Pinhole` camera gets its own
  `Spatial2DView` rooted at the camera so images inherit the projection.
- **Collapse scalars.** Many `Scalars` under a prefix → one `TimeSeriesView` over
  the prefix, not one each. Split only when value ranges clash.
- **`column_shares`/`row_shares` are relative weights** on a container's
  children, equal by default. Tune them from the screenshots.

## 4. Construct it

Containers (`Grid`, `Horizontal`, `Vertical`, `Tabs`) hold views. Default a flat
set to `Grid`; use the others for a deliberate split, `Tabs` for alternatives
competing for one slot (left/right/depth cameras). **Always set an explicit
`origin` and `name`** — `origin` defaults to `/`, which dumps the whole tree into
one view (the usual cause of an unreadable blob).

```python
blueprint = rrb.Blueprint(
    rrb.Horizontal(
        rrb.Spatial3DView(origin="/world", name="Scene"),
        rrb.Vertical(
            rrb.Spatial2DView(origin="/world/camera", name="Camera"),
            rrb.TimeSeriesView(origin="/sensors", name="Sensors"),
        ),
        column_shares=[3, 2],
    ),
    rrb.TextLogView(origin="/logs", name="Logs"),
    collapse_panels=True,
)
```

`contents` defaults to `"$origin/**"`. Scope a view with include/exclude rules,
e.g. `contents=["+ $origin/**", "- $origin/internal/**"]`. A bare line is an
include; `/**` is the only wildcard (matches a subtree). Most-specific rule wins,
ties go to the last, unmatched paths are excluded.

**Coordinate frames.** A spatial view only renders entities it can place relative
to its target frame. `Transform3D`/`Pinhole` logged on entities compose down the
tree, so `origin` is enough. But **named frames** (`CoordinateFrame(frame=...)`,
common in ROS/MCAP) live in a separate frame graph — point the view at a frame
the data occupies via `spatial_information=rrb.SpatialInformation(target_frame="<frame>")`.
The tell is an empty 3D view with "No transform path from `<frame>`…" errors:
`origin="/"` targets the root `tf#/`, which connects to nothing if the tf tree
was never materialized. Read the `CoordinateFrame:frame` values, target the one
the main 3D content sits in, and exclude entities in unconnected frames.

## 5. Iterate from screenshots

Spawn the viewer and load the recording once, then re-send blueprints into the
same viewer; each send + screenshot is one cheap iteration. Render, look, revise.

The blueprint binds to the data by store identity, **both application id and
recording id**. A loaded `.dlr` keeps its own identity, so build the stream from
the dlr's values (set both; `recording_id` otherwise defaults to a random one).
Mismatch either and the viewer keeps blueprint and data as separate recordings
and never applies the blueprint.

```python
import time
import dalaran as rr
import dalaran.blueprint as rrb
from dalaran.experimental import ViewerClient, RrdReader

store = RrdReader("segment.dlr").recordings()[0]  # the dlr's application id + recording id

with ViewerClient.spawn(headless=True, port=9879) as viewer:
    rec = rr.RecordingStream(store.application_id, recording_id=store.recording_id)
    rec.connect_grpc(url=viewer.url)
    rec.log_file_from_path("segment.dlr")

    def shot(blueprint, path):
        rec.send_blueprint(blueprint, make_active=True, make_default=True)
        time.sleep(2)  # let the import finish and a frame render; bump if a view is blank
        viewer.save_screenshot(path)  # view_id=view.id for a single view

    shot(blueprint_v1, "bp_v1.png")
    # Read bp_v1.png, revise, send the next one into the same viewer.
    shot(blueprint_v2, "bp_v2.png")
```

To bake a finished blueprint in instead, pass `default_blueprint=` to `rr.init` /
`spawn` / `connect_grpc` / `save`, or `blueprint=` to `notebook_show`.

## Gotchas

- A `DataframeView` shows "Unknown timeline" without a query:
  `query=rrb.archetypes.DataframeQuery(timeline="<timeline>", apply_latest_at=True)`.
- A view back blank? The importer may not have finished (bump the settle) or the
  cursor sits before the data (add `rrb.TimePanel(play_state=rrb.components.PlayState.Following)`).
- `rrb` views and `ViewerClient` (`dalaran.experimental`) are unstable. Check
  `help()` if a constructor argument is rejected.

## See also

- `dalaran-data-model` — entities, archetypes, timelines.
- `dalaran-catalog-queries` — enumerate entities in a dataset.

