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.
Navigating the schema#
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.fielddescends into a struct field.map[key]addresses acg.Mapentry. The key is a node producing the map's key type (herecg.CaptureTime()keys thecg.Datemap — 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#
- Types and Nodes — the schema you navigate
- Control Flow —
cg.do,cg.while_repeat,cg.when,cg.optional - The JSON Format — what
to_json()produces