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:
reference— the asset path of the document to pull in, resolved by the host's resolver.referencePath— optional; which element inside that document. A concrete path, or a glob pattern. Absent, it defaults to the first top-level 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");doc.append("/garage", "group")
doc.append("/garage/car1", "object") \
.reference("car.prisma") \
.set("color", (0, 0, 1)) # local override wins over the referenced colour
doc.append("/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 diskdoc = kg.Document()
doc.load_composed("garage.prisma") # in place: replaces the document's contents
flat = kg.compose_file("garage.prisma") # or as a free function returning a new DocumentcomposeFile 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:
- An element matching a base path overrides that element's stated properties and metadata. Properties the layer does not state fall through to the base — so a base
referencesurvives a layered override. - A
defelement at a path the base lacks is added. Its parent should already exist. - An
overelement at a path the base lacks is skipped.overmeans "patch an existing element," so a typo cannot silently create an orphan. - A
delete "/path"tombstone removes that element and its subtree. Applied first. - A
disconnect "/target.slot"tombstone removes the incoming connection into that slot, keeping both endpoints. Applied with the element tombstones. - A connection in a layer replaces the base connection into the same target slot.
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.