Custom Nodes#
When the built-in node set doesn't cover a need, you can add a node to the
catalog. A node is a Python class; its name is its kind on the wire, so
every executing client (the iOS app) must implement a class of the same
name.
Client implementation required
Defining a node in Python only adds its interface and serialization. For a procedure that uses it to run on device, you must also implement the matching Swift class and register it — see Implementing the iOS side.
The @make_procedure decorator#
Every node is a frozen, keyword-only dataclass produced by
@make_procedure:
import capturegraph as cg
@cg.make_procedure
class RepeatString(cg.Procedure[cg.String]):
"""Repeat a string a fixed number of times."""
text: str
times: int
The return type is the type the node produces, written
as cg.Procedure[T] where T is a CGType (cg.String, cg.Image,
cg.Void, …). The class body is purely declarative — Python never executes
the node, it validates and serializes it.
Field classification#
Fields are classified automatically by what they hold:
| Field holds | Becomes | Example |
|---|---|---|
a Procedure |
a graph input (data flow) | image: cg.Procedure[cg.Image] |
a list of Procedures |
substeps (each cg.Void) |
steps: list[cg.Procedure[cg.Void]] |
| anything else | a plain JSON setting | times: int |
@cg.make_procedure
class ProcessImage(cg.Procedure[cg.Image]):
input_image: cg.Procedure[cg.Image] # input
brightness: float # setting
Validation#
Add checks in __post_init__ (call super() first):
@cg.make_procedure
class RepeatString(cg.Procedure[cg.String]):
text: str
times: int
def __post_init__(self) -> None:
super().__post_init__()
if self.times < 1:
raise ValueError("times must be at least 1")
Pass-through types#
A node that forwards a value's type (like IfThenElse) is generic and uses
forward_types, applied
under @make_procedure. The first argument bounds the forwarded fields;
the named fields must all produce the same type, which becomes the node's
return type:
@cg.make_procedure
@cg.forward_types(cg.CGType, "input_value")
class PassThrough[T: cg.CGType](cg.Procedure[T]):
input_value: cg.Procedure[T]
PassThrough(input_value=some_image_node) is then a Procedure[cg.Image].
Implementing the iOS side#
Every kind in the JSON must resolve to a Swift class with exactly the
same name. To add a node end to end:
- Define the class in the right module under
capturegraph-lib/capturegraph/procedures/nodes/{collection,process,destination}/. - Import it into the root
capturegraph/__init__.py(the single aggregation point), leaf-by-leaf asName as Name,— the deeperprocedures/__init__.pyis documentation-only, and a class that never reaches the root is invisible to the node editor and the registry-sync tests. - Create the matching Swift class under
capturegraph-ios-client/.../Models-Procedures/ProcedureNodes/Nodes/. - Add its
.selfentry toProcedureNodeRegistry.swift. - If it has UI, add a view under
Views-Procedures/Nodes/. - Run
cd capturegraph-lib && python -m pytest— the registry-sync tests catch any drift between the Python exports and the Swift registry.
Note
Renaming a node is a breaking change on both sides: the Python
__name__ and the Swift class name must change together, and previously
exported JSON keeps the old kind.
See Also#
- Types and Nodes — return types and the node set
- The JSON Format — how a node serializes
capturegraph.procedures— the node catalog