Class reference

Composition, overlays & tombstones

Composition & layers

A Document never works alone. Two mechanisms join documents into one. Composition pulls another file's subtree in at a point — the USD-style reference, resolved when you compose. Layering stacks sparse opinions on top of a base, in a fixed strength order — overlay, the engine behind a proposal. Composition spans files; layering spans authoring stages. This page is the exhaustive reference for both: the reference/referencePath reserved metadata, compose/composeFile, the overlay rules, and the two layer tombstones, delete and disconnect.

#prisma 1.0
def group "garage" {
    def object "car1" { reference "car.prisma" }
    def object "car2" {
        reference "car.prisma"
        float3 color (0, 0, 1)        # local opinion overrides the referenced colour
    }
}

Compose that and car1/car2 carry car.prisma's whole subtree, car2 repainted. Model the car once, reference it twice, fix it in one place.

The strength order, weakest to strongest:

reference  <  local  <  proposal

A reference only fills in what the local document leaves unstated. A local opinion overrides what the reference would supply. A proposal layered on top overrides the local. Composition resolves the first relation; overlay resolves the second.

Authoring a reference

A reference is declared by two reserved metadata keys on an element:

In .prisma text the keys read as their own directives, not as string properties, so a reference always reads as a reference:

def object "car1" { reference "car.prisma" }
def object "wheel" { reference "parts.prisma" "/parts/wheel" }
def group "hardware" { reference "parts.prisma" "/parts/*" }

In code, reference is a method on the handle. Chain a local override straight after it:

doc.append(Path("/garage"), "group");
doc.append(Path("/garage/car1"), "object")
   .reference("car.prisma")
   .set("color", Float3(0, 0, 1));      // local override wins over the referenced colour

doc.append(Path("/garage/wheel"), "object").reference("parts.prisma", "/parts/wheel");

A concrete referencePath names one element, and that element becomes the referencing element: its subtree rebases under the referencing path, and its connections come along. A pattern names a set, and each match arrives as a child of the referencing element, which stays in place. * matches one segment (and globs a name within one); ** matches any depth. A pattern that matches nothing grafts nothing — not an error. See Composition for the pattern walkthrough.

Composing

Composition flattens a root document plus every document it transitively references into one Document. The reserved reference/referencePath keys are consumed in the result — the composed Document is a normal base again.

composeFile loads from disk and resolves each reference relative to its own document's directory:

Document doc;
doc.loadComposed("garage.prisma");        // resolve relative to the file on disk

composeFile trusts its document set — it does not sandbox the filesystem, so a reference may read outside the directory (../other.prisma, an absolute path). For untrusted documents, call compose with your own AssetResolver; the resolver is the security boundary, and it works against in-memory bytes, so composition needs no disk:

auto flat = compose(root, "garage.prisma",
    [&](const std::string& ref, const std::string& referrerId) -> std::optional<ResolvedDoc> {
        return lookUpSomehow(ref, referrerId);     // you decide where bytes come from
    });
if (!flat) { /* missing reference, malformed document, or a cycle */ }

compose returns nullopt on a missing reference, a malformed referenced document, a bad referencePath, or a cycle. References resolve recursively and memoize by resolved id, so a diamond loads each document once, not per occurrence. A document that transitively references itself is rejected.

From the CLI:

kinogaki compose garage.prisma > flat.prisma

Layering: overlay

overlay(base, layers) applies sparse layers on top of a base, matched by element path, later-wins at property and metadata granularity. This is how a proposal previews over a base, and how a committed proposal is written back as local opinions. It is a C++/server concern: overlay is not bound in Python today — the Python binding covers composition (compose_file / load_composed) and the editing surface, not layering.

Document preview = overlay(base, proposal);          // a single layer
Document preview = overlay(base, {layerA, layerB});  // many; later layers win

The rules, applied per layer in order:

Order within a layer is fixed: element tombstones, then connection tombstones, then elements, then connections.

A committed proposal is overlay(authoredBase, proposal) then save. References are preserved, never flattened away, and the result carries no override flags or tombstones — a normal base again. Overlays match by path: a base rename can orphan a layer. That is a known limit, handled a layer up for now.

The delete tombstone

A layer removes an element from the base with a delete directive — a quoted absolute path, its own top-level statement. It removes the element and its whole subtree (and every connection touching it). Tombstones are meaningful only in a layer; a normal base has none.

#prisma 1.0
over object "car2" {
    float3 color (0.9, 0.1, 0.1)      # repaint the inherited car
}
delete "/garage/car1"                 # and drop this one entirely

In code the tombstone is recorded on the layer Document:

Document layer;
layer.addDeletion(Path("/garage/car1"));
Document result = overlay(base, layer);    // /garage/car1 and its subtree are gone

Deletions are kept sorted and unique, so a layer round-trips deterministically. overlay applies each delete before it applies the layer's elements, and only when the path is present in the base.

The disconnect tombstone

disconnect is the connection analog of delete. A delete removes an element; a disconnect removes one connection — the incoming wire into a target slot — while both endpoints survive. Use it to sever a link the base draws without deleting either element.

The directive takes a quoted target slot, /element.property, kept whole:

#prisma 1.0
disconnect "/ball.albedo"             # the base wired a material into this slot; cut it

In code:

Document layer;
layer.addConnectionDeletion(Path("/ball.albedo"));
Document result = overlay(base, layer);    // /ball.albedo keeps no incoming wire; both ends remain

Connection tombstones are sorted and unique too, emitted after the element tombstones in the text form, and applied with them — before the layer's own elements and connections. A later connection in the same layer can wire a new source into the freed slot.

A complete layer combining the directives:

#prisma 1.0
over material "paint" {
    float3 out (0, 0, 1)              # restate one property; the rest falls through to base
}
def light "fill" {                    # add a new element (its parent exists in the base)
    float intensity 0.4
}
delete "/scene/extra_prop"           # remove an element and its subtree
disconnect "/ball.albedo"            # cut one connection, keep both endpoints

Layer that over a base: paint.out turns blue, a fill light appears, /scene/extra_prop and its subtree vanish, and /ball.albedo loses its incoming wire — every other base opinion untouched.

Why two mechanisms

Composition and layering answer different questions. Composition asks where do these bytes come from — link a shared asset instead of copying it, so "swap the shared material to the blue one" is one edit to one file, not a search-and-replace across dozens. Layering asks whose opinion wins — let a proposal preview non-destructively over a base, then commit it down to local opinions. The strength order reference < local < proposal is the single rule that orders them both. Identity is a path; a reference is a path to a file; a tombstone is a path withdrawn. See Connections for links within one document and Bundles for a whole directory tree expressed as one Document.