Skip to content

Property, Functional Port, and Transform Synchronization System

This document describes the design and usage of the uLib Property and Port architecture across the C++ Core, Mathematical Transforms, VTK Viewport & Gizmo Manipulators, Qt GUI Property Panels, NodeGraph Editor, and the Python Recipe Scripting engine.

graph TD
    subgraph UI & Viewport Layer
        HW["VTK HandlerWidget\n(Interactive 3D Gizmo)"]
        VP["VTK Viewport Callback"]
        PW["Qt GUI PropertyWidgets\n(UnitLineEdit / VectorWidget)"]
        NG["NodeGraphQt UI\n(Green / Red / Blue Ports)"]
    end

    subgraph Core C++ Model
        P3D["uLib::Vtk::Prop3D"]
        TRS["uLib::TRS (Transform)\nPosition / Rotation / Scale"]
        Prop["uLib::Property<T>\nFunctionalProperty<T>"]
        RefProp["uLib::ReferenceProperty<T>\nFunctionalReferenceProperty<T>"]
        Rec["uLib::Recipe / Algorithm"]
    end

    subgraph Python Runtime
        FB["Recipe::FunctionBridge\n(PythonEngine)"]
        PyState["Python Module State\n(_state = {...})"]
        PyFuncs["get_prop() / set_prop()\nget_feed() / set_feed(obj)"]
        PyRun["run(ctx)\n(Recipe Logic)"]
    end

    %% Gizmo interaction
    HW -->|InteractionEvent| VP
    VP -->|SyncFromVtk()| P3D
    P3D -->|FromMatrix()| TRS
    TRS -->|Updated()| Prop
    Prop -->|PropertyChanged Signal| PW

    %% GUI Edit
    PW -->|m_Prop->Set(v)| Prop
    Prop -->|Updated()| TRS
    TRS -->|Object::Updated()| P3D
    P3D -->|ApplyProp3DTransform()| HW

    %% NodeGraph & Ports
    NG <-->|Wire Drag / Push| RefProp
    NG <-->|Value Edit / Push| Prop
    RefProp <-->|ConnectFrom / Push| Rec

    %% Python Bridge
    Prop <-->|getValue / setValue| FB
    RefProp <-->|getReference / setReference| FB
    FB <-->|Invokes| PyFuncs
    PyFuncs <-->|Read / Mutate| PyState
    PyRun <-->|Reads & Writes| PyState

1. Overview and Core Concepts

The uLib property architecture provides a unified, introspectable, serializable, and reactive system for data members, mathematical transforms, algorithm ports, and Python-scripted parameters.

Core Roles of the Property System

  1. Introspection & Reflection: Allows GUI inspectors (such as Qt property editors) and node graphs to inspect property names, types, units, ranges, and descriptions dynamically at runtime without hardcoded UI forms.
  2. Reactive Synchronization: Changes made via 3D viewport gizmos, typed GUI input, automated scripts, or graph node execution emit signals (PropertyChanged, Updated) to keep all views, downstream nodes, and VTK actors synchronized.
  3. Dataflow & Port Connections: Properties declare PortDirection (Input, Output, InputOutput, Internal) and PortAccess (Reference, ReadOnlyReference, Value), allowing objects and algorithms to be wired together into directed acyclic computation graphs in app/gcompose.
  4. Python Scripting Interoperability: Recipes written in Python can expose functional properties and ports directly to the C++ core and GUI via Recipe::FunctionBridge.

2. C++ Property Hierarchy (src/Core/Property.h & src/Core/uprops/)

The property hierarchy separates type-erased metadata and serialization from concrete type storage and function-backed accessors.

classDiagram
    class PropertyBase {
        +GetName() string
        +GetTypeId() string
        +GetPortDirection() PortDirection
        +GetPortAccess() PortAccess
        +IsReadOnly() bool
        +IsDeclaredReadOnly() bool
        +GetValueAsString() string
        +SetValueFromString(string) bool
        +Updated()
    }

    class Property~T~ {
        -T* m_value
        -T m_ownedValue
        +Get() const T&
        +Set(const T& val) bool
    }

    class FunctionalProperty~T~ {
        -getter_type m_getter
        -setter_type m_setter
        +Get() T
        +Set(const T& val) bool
    }

    class ReferencePropertyBase {
        +GetReferencedObject() Object*
        +SetReferencedObject(Object*) bool
        +ConnectFrom(ReferencePropertyBase*)
        +NotifyReferenceChanged()
    }

    class ReferenceProperty~T~ {
        -SmartPointer~T~* m_target
        +Get() T*
        +Set(T* val) bool
    }

    class FunctionalReferenceProperty~T~ {
        -getter_type m_getter
        -setter_type m_setter
        +Get() T*
        +Set(T* val) bool
    }

    PropertyBase <|-- Property~T~
    PropertyBase <|-- FunctionalProperty~T~
    PropertyBase <|-- ReferencePropertyBase
    ReferencePropertyBase <|-- ReferenceProperty~T~
    ReferencePropertyBase <|-- FunctionalReferenceProperty~T~

Property Classes

Class Purpose Data Backing
PropertyBase Type-erased base class containing metadata (name, units, min/max bounds, visibility, group, PortDirection, PortAccess). None (Interface)
Property<T> Concrete value property. Supports direct pointer binding or internal owned storage. Validates range bounds and emits PropertyChanged. Direct variable pointer or local value
FunctionalProperty<T> Value property backed by getter/setter lambdas (std::function<T()>, std::function<void(const T&)>). Ideal for computed or encapsulated values. Function / Lambda Callables
ReferencePropertyBase Type-erased base for object references and node graph wiring. Supports ConnectFrom() and SetReferencedObject(). Object* reference
ReferenceProperty<T> Concrete reference property pointing to a SmartPointer<T>. Automatically tracks object lifecycles and scene references. SmartPointer<T> member
FunctionalReferenceProperty<T> Reference property backed by getter/setter lambdas (std::function<T*()>, std::function<void(T*)>). Connects ports to existing object accessors without requiring a dedicated SmartPointer member. Function / Lambda Callables

3. Dynamic Functional Ports & Algorithm Macros

Instead of requiring every port to be a class member variable, uLib allows adding functional reference and value ports dynamically at runtime or in algorithm class declarations.

Runtime Port Helpers (src/Core/Property.h)

#include "Core/Property.h"

// 1. Re-pointable Reference Port (Getter + Setter) -> Green Port in NodeGraph
uLib::AddReferencePort<uLib::Object>(
    obj, "Target", 
    [obj]() -> uLib::Object* { return obj->GetTarget(); },
    [obj](uLib::Object* o) { obj->SetTarget(o); },
    uprops::PortDirection::Input, "Inputs"
);

// 2. Read-Only Reference Output Port (Getter Only) -> Red Port in NodeGraph
// Downstream nodes can read what was produced, but external wires cannot re-point it.
uLib::AddReferencePort<uLib::Object>(
    obj, "Result", 
    [obj]() -> uLib::Object* { return obj->GetResult(); },
    nullptr, // No setter
    uprops::PortDirection::Output, "Outputs"
);

// 3. By-Copy Value Port (Primitive or copyable type) -> Blue Port in NodeGraph
uLib::AddValuePort<double>(
    obj, "Gain",
    [obj]() -> double { return obj->GetGain(); },
    [obj](const double& v) { obj->SetGain(v); },
    uprops::PortDirection::InputOutput
);

Algorithm Class Macros (src/Core/Algorithm.h)

When defining subclasses of uLib::Algorithm, use the _FN macros to expose ports backed by accessors:

class CustomFilter : public uLib::Algorithm {
public:
    CustomFilter() {
        ULIB_ACTIVATE_PROPERTIES();
    }

    // Declares an input reference port bound to GetSource()/SetSource()
    ULIB_ALGORITHM_INPUT_FN(uLib::Image3D, Source, 
        [this]() { return GetSource(); },
        [this](uLib::Image3D* img) { SetSource(img); }
    );

    // Declares a read-only output reference port bound to GetOutputImage()
    ULIB_ALGORITHM_OUTPUT_FN(uLib::Image3D, Output,
        [this]() { return GetOutputImage(); },
        nullptr
    );
};

Wire Propagation and NotifyReferenceChanged()

When an object produces a new output or changes its internal reference without a setter being called from the outside: - Standard writable reference ports update via SetReferencedObject(). - Read-only reference ports (getter-only) do not have a setter. The producing object calls port->NotifyReferenceChanged(), which pushes the current object reference downstream to every connected node.


4. Port Direction, Port Access, and Node Graph Semantics

The node graph editor in app/gcompose visualizes properties and ports according to two distinct attributes:

  1. PortDirection (Where data flows):
  2. Input: Appears on the left side of the node.
  3. Output: Appears on the right side of the node.
  4. InputOutput: Appears as both input and output.
  5. Internal: Hidden from graph port edges; visible only in the Property Panel.

  6. PortAccess (What a reader receives):

  7. Determines what kind of data is handed across the connection and dictates the visual color of the port in NodeGraphQt.
uprops::PortAccess Port Color Semantics Typical Source
Reference Green A live, shared handle to the object itself. Re-pointable through this port. ReferenceProperty<T>, FunctionalReferenceProperty<T> with setter.
ReadOnlyReference Red The object itself, but read-only / non-repointable (e.g. const ref, getter-only port, or move-only handle). Getter-only reference port, readonly=True, or access="readonly".
Value Blue A copy: primitive numeric/string types, or objects created fresh on demand. Property<T>, FunctionalProperty<T>, primitive ports.
Action Violet Nothing. The port is invoked, not read: pressing it runs a function or raises a signal. Drawn as a button in the node body and in the Properties panel; a wire between two of them is a dotted ConnectionKind::Trigger. uprops::ActionProperty, uLib::ActionProperty, ULIB_ACTION, Action(...) in a recipe.

Access Derivation and Overrides

Port access is derived automatically: - Reference properties default to PortAccess::Reference. - Reference properties without a setter or marked read-only report PortAccess::ReadOnlyReference. - Value and primitive properties report PortAccess::Value.

For special cases (such as a std::unique_ptr output that is writable internally but cannot be shared as a live reference), the access kind can be explicitly pinned:

port->SetPortAccess(uprops::PortAccess::ReadOnlyReference); // Force red port
port->ClearPortAccessOverride(); // Restore automatic derivation


5. Python Recipe Properties and Functional Ports

In app/gcompose, recipes written in Python can define node properties and ports declaratively. Every read and write seamlessly crosses between C++ and Python via Recipe::FunctionBridge.

Python Declarations (src/Python/uLib/recipe_agent.py)

A recipe script can declare Property, InputPort, OutputPort and Action:

# Function-backed value property with getter and setter (Editable in GUI)
Gain = Property(
    "Gain",
    getter=get_gain,
    setter=set_gain,
    type="float",
    min=0.0,
    max=10.0,
    description="Multiplication gain factor"
)

# Read-only computed value property (Getter only)
Summary = Property(
    "Summary",
    getter=get_summary,
    type="string",
    description="Live computed summary string"
)

# Live reference input port (Green port)
Feed = InputPort(
    "Feed",
    getter=get_feed,
    setter=set_feed,
    description="Upstream object reference"
)

# Read-only output port (Red port)
Produced = OutputPort(
    "Produced",
    getter=get_produced,
    description="Object generated during run()"
)

# Explicitly pinned access handle (Red port despite having setter)
Handle = OutputPort(
    "Handle",
    getter=get_handle,
    setter=set_handle,
    access="readonly",
    description="Owned handle; writable locally but not shareable"
)

Key Python Declaration Options

  • getter= / setter=: Bind the property directly to Python functions defined in the recipe file.
  • type=: Explicit primitive type ("float", "int", "bool", "string").
  • min=, max=, step=: Numeric range constraints for Qt GUI sliders and line edits.
  • readonly=True: Prevents user or wire edits.
  • access="reference" | "readonly" | "value": Explicitly overrides the port access color.
  • handler= (Action only): the zero-argument function this button calls when pressed.

Actions: Buttons On A Recipe Node

An Action is not a value at all. It declares a button -- in the Properties panel and on the node itself -- which calls a zero-argument function of the same recipe every time it is pressed, through Recipe::FunctionBridge::callAction:

def do_reset():
    _state["gain"] = 1.0

Reset = Action("Reset", handler=do_reset, description="Back to defaults")
Fire  = Action("Fire")   # no handler: fires whatever is wired downstream

The panel's "+ Add > Action" writes both halves -- a def on_actionN(): stub and the declaration naming it -- so the button does something the moment it appears.

An action has no type, no default and no range; it is never serialized, so loading a document does not press one. It lands in the Actions group and reports PortAccess::Action. With no handler it still fires whatever is wired to it, which is how one node's button presses another's across the graph.

See assets/exmaples/actions.py.

Every Algorithm Has A Run Button

uLib::Algorithm carries RunAction, exposing Run() as an action named Run in the Algorithm group. It appears on every algorithm and every recipe, in the Properties panel and on the node.

It is synchronous: it runs on the caller's thread and returns when the run is over. That is the difference from gcompose's Play button, which calls Start() and hands the work to a worker thread so the GUI stays responsive and Pause/Stop keep working. Use the button to make something run now -- from a script, a remote client, or a trigger wire; use Play for a long run.

Because an action announces itself only after its handler returns, wiring one algorithm's Run output into the next one's Run input runs them in order, each on what the one before it wrote. A press while a run is already in flight is ignored.

Recipe Namespace and State Persistence

[!NOTE] A recipe module's global namespace outlives individual run() executions.

Global variables (e.g. _state = {...}) survive across runs. Getters are queried continuously by the Qt Properties panel and VTK sync even when run() is not currently executing.

Inside the recipe run(ctx) function: - Referencing a declared property name (e.g. print(Gain)) provides a snapshot of the value taken at the start of run(). - Calling the getter function directly (e.g. get_gain()) retrieves the live current state. - Modifying state via the setter (e.g. set_gain(1.5)) immediately informs the C++ Core and updates Qt UI widgets and downstream nodes.


6. Practical Python Examples

Example A: Functional Properties (assets/exmaples/functional_properties.py)

Demonstrates computed parameters, validation/clamping inside setters, and dynamic string summaries:

# Module-level state surviving across runs
_state = {
    "celsius": 20.0,
    "samples": 0,
}

def get_celsius():
    return _state["celsius"]

def set_celsius(value):
    # Clamping / validation in setter
    _state["celsius"] = max(-273.15, float(value))

def get_fahrenheit():
    # Read-only derived property: recomputed on every read
    return _state["celsius"] * 9.0 / 5.0 + 32.0

def get_summary():
    return "%.1f C = %.1f F after %d run(s)" % (
        get_celsius(),
        get_fahrenheit(),
        _state["samples"],
    )

# Editable in Properties panel (calls get_celsius / set_celsius)
Celsius = Property("Celsius", getter=get_celsius, setter=set_celsius, type="float")

# Read-only in Properties panel (getter only)
Fahrenheit = Property("Fahrenheit", getter=get_fahrenheit, type="float")
Summary = Property("Summary", getter=get_summary, type="string")

def run(ctx):
    _state["samples"] += 1
    print(f"Running: {Celsius} C -> {get_fahrenheit()} F")
    set_celsius(Celsius + 1.0)

Example B: Functional Object Ports (assets/exmaples/functional_ports.py)

Demonstrates input/output object wiring, upstream connection handling, and producing scene objects:

_wired = {
    "feed": None,      # Upstream object wired to Feed
    "produced": None,  # Object created by run()
    "handle": None,
}

def get_feed():
    return _wired["feed"]

def set_feed(obj):
    # Called when an edge is connected to Feed or upstream node pushes an update
    _wired["feed"] = obj
    print(f"Feed updated <- {obj.name if obj else 'None'}")

def get_produced():
    return _wired["produced"]

def get_handle():
    return _wired["handle"]

def set_handle(obj):
    _wired["handle"] = obj

# Green input port: live reference re-pointed when wire is attached
Feed = InputPort("Feed", getter=get_feed, setter=set_feed)

# Red output port: downstream nodes can read, but cannot re-point
Produced = OutputPort("Produced", getter=get_produced)

# Red output port: explicitly pinned as readonly access
Handle = OutputPort("Handle", getter=get_handle, setter=set_handle, access="readonly")

# Blue value property
Scale = Property("Scale", default=1.0, min=0.1, max=10.0)

def run(ctx):
    box = ctx.find("FunctionalBox")
    if box is None:
        box = ctx.create("ContainerBox", "FunctionalBox")

    if Feed is not None:
        print(f"Processing upstream input: {Feed.name}")

    box.Size = [100.0 * Scale, 50.0 * Scale, 25.0 * Scale]
    box.Updated()

    # Move output port; notifies all downstream connected nodes
    _wired["produced"] = box
    set_handle(box)

7. Mathematical Transforms & VTK Gizmo Synchronization

The uLib::TRS (Translation-Rotation-Scale) spatial transform implementation bridges Eigen Affine3f transforms to functional properties and VTK props:

sequenceDiagram
    participant User as User (3D Gizmo)
    participant HW as Vtk::HandlerWidget
    participant VP as Vtk::Viewport
    participant P3D as Vtk::Prop3D
    participant TRS as uLib::TRS
    participant PW as Qt VectorPropertyWidget

    User->>HW: Drag translation handle
    HW->>VP: InteractionEvent (Matrix4f)
    VP->>P3D: SyncFromVtk()
    P3D->>TRS: tr->FromMatrix(userMatrix)
    TRS->>TRS: NotifyPropertiesUpdated()
    TRS-->>PW: PropertyChanged Signal (Updates LineEdits)

    Note over PW,TRS: Alternatively: User edits Position X in Qt GUI
    PW->>TRS: Position.Set(newVec)
    TRS->>TRS: Update() -> Object::Updated()
    TRS-->>P3D: Prop3D::Update()
    P3D-->>HW: ApplyProp3DTransform(actor)

Transform Properties (src/Math/Transform.h)

uLib::TRS exposes three FunctionalProperty<Vector3f> members: - Position: getter GetPosition(), setter SetPosition(v) - Rotation: getter GetRotationEuler(), setter SetRotationEuler(v) (radians) - Scale: getter GetScale(), setter SetScale(v)

When individual vectors are modified, TRS updates its internal matrix m_T, emits this->Updated(), and triggers NotifyPropertiesUpdated().

VTK Handler Widget Coordinate Frames (src/Vtk/vtkHandlerWidget.h)

The 3D interactive manipulator supports multiple reference frames: - LOCAL: Axis aligned with object matrix (Prop3D->GetMatrix()). - CENTER_LOCAL: Aligned with object orientation, centered at bounding box center. - GLOBAL: Axis aligned to world coordinates (Identity), positioned at object origin. - CENTER: Axis aligned to world coordinates, centered at bounding box center.


8. Summary Checklist

When implementing new properties, algorithms, or recipes in uLib:

  • [ ] Inheritance: Inherit domain objects from uLib::Object or uLib::Algorithm.
  • [ ] Macro Activation: Use ULIB_PROPERTY for members and call ULIB_ACTIVATE_PROPERTIES() in constructors.
  • [ ] Dynamic Ports: Use AddReferencePort() / AddValuePort() or ULIB_ALGORITHM_INPUT_FN / ULIB_ALGORITHM_OUTPUT_FN when exposing functions rather than direct fields.
  • [ ] Port Access Colors: Check that GetPortAccess() returns the intended node graph color (Green for shared references, Red for read-only references, Blue for values/copies).
  • [ ] Read-Only Output Signaling: For getter-only reference outputs, invoke NotifyReferenceChanged() whenever the internal reference updates.
  • [ ] Python Recipe Properties: Use Property, InputPort, and OutputPort with getter=/setter= to bind module state.
  • [ ] Actions: For something the user should run rather than edit, declare Action("Name", handler=fn) in a recipe, or uLib::ActionProperty / ULIB_ACTION in C++ -- not a bool nobody resets.
  • [ ] Reserved Recipe Names: Avoid naming custom ports with reserved recipe properties (Title, Description, Source, AutoRun, LastOutput, NumThreads, Status).