Skip to content

Recorder and Caching#

You write a procedure with the recorder: a function decorated with @cg.procedure(Schema) whose body navigates the schema and assigns captured values into it. The assignments record nodes — the function never runs your capture logic.

import capturegraph as cg


class Session(cg.Struct):
    photo: cg.Image
    notes: cg.String


class Survey(cg.Struct):
    reference: cg.Image
    sessions: cg.Map[cg.Date, Session]
    _metadata: cg.Metadata


@cg.procedure(Survey)
def survey(root: cg.Procedure[cg.Path[Survey]]):
    root.reference |= cg.CaptureImage(label="Reference")
    session = root.sessions[cg.CaptureTime()]
    session.photo &= cg.CaptureImage(label="Photo")
    session.notes &= cg.UserInputString(label="Notes")

root is a cg.Procedure[cg.Path[Survey]] — a cursor at the schema root.

The cursor walks the schema the same way the type tree reads:

root.reference                      # a struct field
root.sessions[cg.CaptureTime()]     # a new Map entry, keyed by capture time
session.photo                       # a field inside that entry
  • root.field descends into a struct field.
  • map[key] addresses a cg.Map entry. The key is a node producing the map's key type (here cg.CaptureTime() keys the cg.Date map — that capture instant becomes the session id).
  • An array position is appended to in a loop.

The recorder type-checks each step: descending into a field that isn't in the schema, or keying a map with the wrong type, fails immediately.

Caching operators#

A captured value lands on disk by assigning it to a path with a caching operator. There are two, differing only in what happens when the file already exists:

Operator Meaning
path \|= node Cache, skip if it already exists
path &= node Cache, overwrite
# Ask once and remember — config, reference imagery, calibration
root.reference |= cg.CaptureImage(label="Reference")

# Fresh every session
session.photo &= cg.CaptureImage(label="Photo")

A plain = assignment with a node is rejected — caching must be explicit. Assigning to the same position twice raises an error, so each slot is filled exactly once.

Reading a value back#

Reading an assigned position gives you its cached value, ready to feed another node:

root.reference |= cg.CaptureImage(label="Reference")

# Derive a thumbnail from the cached reference
root._metadata._thumbnail &= cg.ConvertImageToThumbnail(root.reference)

Loading a position you have not assigned is explicit — an unwritten slot used as a value is an authoring error, never a silent disk read:

# Wait for a stored value (a previous session's, or one an interceptor fills).
reference = root.reference.load()

# Or fall back when the slot is definitively empty. The fallback runs only on
# a definite "nothing stored here" answer — a value still downloading keeps
# the read pending — so a slow fetch is never mistaken for an absent value.
world_map = root.world_map.load_or(cg.ARKitNewScene())

A read-then-overwrite of the same slot is acyclic by construction (the load reads disk, never the cache that later overwrites the slot) — the pattern a procedure uses to carry a shared artifact forward across sessions:

capture = cg.ARKitCaptureScene(
    initial_world_map=root.world_map.load_or(cg.ARKitNewScene()),
)
session.frames &= capture.frames        # this session's keyframes
root.world_map &= capture.world_map     # the refreshed map, for the next session

cg.ARKitCaptureVideo shares the same world-map carry-forward, but records the dense, continuous counterpart — one clip with a pose on every frame and depth on the frames a sensor reached — instead of a sparse set of keyframes:

capture = cg.ARKitCaptureVideo(
    initial_world_map=root.world_map.load_or(cg.ARKitNewScene()),
)
session.capture &= capture              # the whole PosedVideo (clip + poses + depth)
root.world_map &= capture.world_map     # the refreshed map, for the next session

Reading part of a captured value#

When a node returns a composite value, you can drill into it the same way you navigate the schema — .field for a struct, [i] for an array element, and [key] for a map entry — and cache the part you want:

# CaptureImagesByLabel returns a Map[String, ImageSequence]: one capture
# button per label. Index it by label to pull out one label's sequence...
views = cg.CaptureImagesByLabel(label="Photograph each side",
                                labels=["Front", "Side", "Back"])
session.front &= views["Front"]      # one label's Array[Image]

# ...or cache the whole map into a Map[String, ImageSequence] slot.
session.views &= views

Indexing a returned map by a label it does not have produces no value (so the cache stores nothing) and warns — at authoring time when the label is a known mistake, and on the device when a key is absent at run time. Drilling composes: views["Front"][0] is the first image of the Front sequence, and cg.CapturePanorama().frames[1].angle walks a struct → array → struct down to a leaf scalar.

Serializing#

The decorated object serializes to the wire form:

json_str = survey.to_json()    # a {schema, root, nodes} string
data = survey.to_dict()        # the same, as a dict

See Also#