Class reference

AssetStore & Asset

The sidecar for bytes

A Kinogaki Document is purely numeric. Every property is a Value: a float, an int, a bool, a vector, an array of those. That is what makes a document diffable, connectable, and evaluable through time. It is also why a texture, a font, or a baked mesh has no home there. Those are opaque byte blobs. They do not interpolate, they do not animate, and you never want them inlined into a data array you have to scroll past.

So they live in a sidecar: the AssetStore. Each blob is an Asset, keyed by the Path of the Element that owns it. A document, in full, is a Document plus an AssetStore. The Document holds the graph; the store holds the bytes the graph points at.

#include "kinogaki/AssetStore.h"

Document doc;
doc.append(Path("/world/wood"), "texture");   // the host Element: numeric metadata lives here

AssetStore assets;
assets.set(Path("/world/wood"), readFile("wood.png"), "image/png");  // the bytes live here

The Element at /world/wood carries the type (texture) and any numeric properties (tiling, gamma, a name string). The PNG itself rides in the store under that same Path. The rest of the graph reaches the texture the ordinary way: a connection to /world/wood. Nothing in the Document changes shape because a blob exists.

Why path-keyed

A Path is the only identity in Core. There are no UUIDs. An Element is named by where it sits, an endpoint of a connection is a Path, and an Asset is keyed by a Path too. One identity scheme, end to end.

This buys consistency and costs discipline. Because the key is a Path, the store must track the same Document edits connections track. Rename a subtree and every connection endpoint under it is rewritten; the asset keys under it must move the same way. Remove a subtree and every connection touching it goes; the asset blobs under it must go too. The store does not watch the Document, so the host runs the matching fixup by hand. Document::rename and Document::remove deliberately know nothing about the store. AssetStore is a sidecar, not a member.

The Asset type

One blob: the bytes, plus an optional MIME hint.

struct Asset {
    std::string mime;    // "image/png", "model/obj", "font/ttf" — informational
    std::string bytes;   // the raw payload, byte-exact
    bool operator==(const Asset&) const = default;
};

bytes is the payload, stored verbatim and round-tripped byte-for-byte. mime is a hint for downstream consumers, nothing more. The authoritative type discriminator is the host Element's type token in the Document, the same texture or font you read off the graph. Treat mime as advisory; treat the Element type as truth.

std::string here is a byte container, not text. It holds arbitrary bytes including NULs.

The AssetStore

A map from element Path to Asset, kept in sorted order so the same document always serializes to the same bytes.

set — install or replace a blob

void set(const Path& elementPath, std::string bytes, std::string mime = {});

Installs the bytes at elementPath, replacing anything already there. The Path's slot, if you pass one, is ignored: the key is always the Element, never a property of it. mime is optional.

assets.set(Path("/world/wood"), readFile("wood.png"), "image/png");
assets.set(Path("/fonts/body"), readFile("Inter.ttf"), "font/ttf");
assets.set(Path("/world/wood.albedo"), bytes);   // slot dropped → keyed at "/world/wood"

There is no separate insert and update. set is both: the last write at a Path wins.

get — read a blob back

const Asset* get(const Path& elementPath) const;

Returns a pointer to the Asset at elementPath, or nullptr if none is stored there. The slot, if present, is dropped before the lookup, matching set. The pointer is borrowed; it stays valid until the next mutation of the store.

if (const Asset* a = assets.get(Path("/world/wood"))) {
    decodePNG(a->bytes);
    // a->mime == "image/png"
}

erase — drop one blob

void erase(const Path& elementPath);

Removes the blob at elementPath if one is there, and does nothing if not. Use this when a single Element loses its payload. To drop a whole branch, reach for removeSubtree.

assets.erase(Path("/world/wood"));

empty, size, assets — inspect the store

bool empty() const;
std::size_t size() const;
const std::map<Path, Asset>& assets() const;   // ordered = deterministic

empty and size answer how many blobs are held. assets() exposes the backing map, kept sorted by Path so iteration order is stable and serialization is byte-stable. Walk it to enumerate or re-key every blob.

for (const auto& [path, asset] : assets.assets())
    std::printf("%s  %zu bytes  %s\n",
                path.str().c_str(), asset.bytes.size(), asset.mime.c_str());

reparent — move keys under a renamed subtree

void reparent(const Path& oldPrefix, const Path& newPrefix);

Rewrites every key that lies under oldPrefix to sit under newPrefix instead. This is the asset half of a Document rename. When the host moves /world to /scene, the connections rewrite themselves inside the Document; you run reparent so /world/wood becomes /scene/wood in the store. Same fixup, same prefixes, applied to the sidecar.

Path moved = doc.rename(Path("/world"), Path("/scene"));   // Document rewrites paths + connections
assets.reparent(Path("/world"), moved);                    // store keys follow

Pass the prefixes Document gave you. rename returns the final Path it settled on after uniquifying; feed that to reparent, not the name you asked for.

removeSubtree — drop a whole branch

void removeSubtree(const Path& subtreeRoot);

Drops every blob whose key lies at or under subtreeRoot, inclusive. This is the asset half of Document::remove. When the host removes /world and all its descendants from the Document, you run removeSubtree so no orphan blobs linger.

doc.remove(Path("/world"));            // Document drops the subtree + its connections
assets.removeSubtree(Path("/world"));  // store drops "/world", "/world/wood", "/world/sky/sun", …

reparent and removeSubtree are the only members that exist because Paths move. Keep them paired with the matching Document edit and the two halves never drift.

Equality

bool operator==(const AssetStore&) const = default;

Two stores compare equal when they hold the same blobs at the same keys with the same MIME. Useful in tests and round-trip checks.

Assets ride inside a package

You rarely carry an AssetStore around by itself. You bundle it. The package encoding (PRSMZ, the .usdz analog) writes a Document and an AssetStore into one self-contained, dependency-free file.

#include "kinogaki/Serialize.h"

std::string bytes = serializePackage(doc, assets);          // binary scene + every blob
// …later, or on another machine…
auto loaded = deserializePackage(bytes);                    // both halves come back
if (loaded) {
    auto& [doc2, assets2] = *loaded;
    // doc2 == doc, assets2 == assets
}

The package is a flat chunked archive: a table-of-contents at the front, blob payloads concatenated after. There is exactly one scene chunk — the Document, embedded as a binary crate by default or as .prisma text with serializePackage(doc, assets, /*textScene*/ true) when you want it diffable. Every Asset becomes one more entry, named by its element Path, carrying its MIME and its bytes. The TOC is sorted by name, so the same (Document, AssetStore) always yields the same bytes. The layout is mmap-friendly: read the TOC at the front, touch only the blobs you need.

Because the key in the package is the Path string, the round-trip is exact: deserializePackage parses each entry's name back into a Path and sets the bytes under it. The store you get out keys the same blobs you put in.

If you only have a package and only want the scene, plain deserialize accepts package bytes and hands back just the Document. Use deserializePackage whenever you need the blobs too.

Python

AssetStore is not bound in Python, and neither is the package codec. The Python kinogaki module is a thin layer over the C ABI, and the C ABI does not expose asset bytes: kinogaki_document_load auto-detects package input but returns only the scene, and there is no kinogaki_asset_* entry point. So from Python you can author and read the numeric Document in full, but you cannot reach into a package's blobs.

import kinogaki as kg

doc = kg.Document()
doc.append("/world/wood", "texture")        # the host element — fine from Python
# kg.AssetStore                             # ✗ no such name; assets are C++ only

There is a Python method named reference whose first argument is called asset:

doc.append("/car", "object").reference("car.prisma")

That is unrelated. reference is composition — it pulls in another document's subtree by filename. It has nothing to do with the AssetStore or byte blobs.

When a workflow needs to ship binary payloads alongside a document, do it in C++ (or the CLI's pack / unpack, which build on the same package encoding). Python stays on the numeric side of the line: it shapes the graph, and the bytes travel in a package built and opened by the C++ layer.