Class reference

Value

Value — the reference

A Value is the atom of a Prism document. It is two things over one flat buffer: a dtype (the element type) and a shape (how the elements are laid out). Empty shape is a scalar; {N} is a 1-D run; {M, N, …} is a multi-dimensional array. Every property, every animation sample, every slot you read through a connection is a Value. The familiar graphics aggregates — Vec3, an affine matrix, a Spectrum — are not separate types; they are fixed shapes of float32 with names.

Hold one in your hand:

#include <kinogaki/Value.h>
using namespace kinogaki;

Value radius = Float(1.5);                 // f32 scalar
Value albedo = Float3(0.9, 0.2, 0.2);      // f32 shape {3}
Value name   = Str("ball");                // a string scalar

radius.dtype();      // DType::F32
albedo.shape();      // {3}
albedo.as<Vec3>();   // std::array<float,3>{0.9, 0.2, 0.2}

In Python there is no Value wrapper. A native float, int, bool, str, or a tuple of numbers is the value — Handle.set infers the Prism type, and the get_* readers hand you native values back.

import kinogaki as kg
ball = doc.edit("/world/ball")
ball.set("radius", 1.5)                    # → float
ball.set("albedo", (0.9, 0.2, 0.2))        # → float3
ball.set("name", "ball")                   # → str
ball.get_float3("albedo")                  # (0.9, 0.2, 0.2)
def object "ball" {
    float    radius = 1.5
    float3   albedo = (0.9, 0.2, 0.2)
    str      name   = "ball"
}

The DType enum

DType names the element type. One Value holds elements of exactly one dtype.

enum class DType : std::uint8_t {
    Bool, Char, I8, I16, I32, I64, U8, U16, U32, U64, F32, F64, Str
};

| DType | C++ element | Bytes | Notes | |-------|-------------|-------|-------| | Bool | bool | 1 | true / false | | Char | char | 1 | one byte, distinct from a string | | I8 I16 I32 I64 | int8_tint64_t | 1·2·4·8 | signed integers | | U8 U16 U32 U64 | uint8_tuint64_t | 1·2·4·8 | unsigned integers | | F32 | float | 4 | the default dtype | | F64 | double | 8 | | | Str | std::string | 0 | variable-length, stored out of band |

dtypeSize(d) gives the byte width of one element. Str reports 0 — strings live in a separate list, not the numeric buffer, because they are variable-length. dtypeIsFloat(d) is true for F32 and F64 only.

dtypeSize(DType::I16);    // 2
dtypeSize(DType::Str);    // 0  — out of band
dtypeIsFloat(DType::F32); // true

A default-constructed Value is a float32 scalar holding 0.

Value v;             // dtype F32, scalar, value 0.0f

Python. The C ABI exposes a smaller, role-shaped type code (float, int, bool, float2, float3, matrix, spectrum, the arrays) rather than the full dtype lattice. So Python sees float/int/bool/str and the float aggregates; it does not expose the narrow integer widths (int8uint32), char, or float64 as distinct reader types. An int set from Python lands as a 64-bit integer; a float lands as float32.

Shape

The shape is a std::vector<std::uint32_t>. Empty is a scalar. The element count is the product of the shape (a scalar counts as 1).

DType                          d = albedo.dtype();   // DType::F32
const std::vector<uint32_t>&   s = albedo.shape();   // {3}
std::size_t                    r = albedo.rank();    // 1
bool                        flat = albedo.isScalar();// false
std::size_t                    n = albedo.count();   // 3

| Method | Returns | Meaning | |--------|---------|---------| | dtype() | DType | the element type | | shape() | const vector<uint32_t>& | dimensions; empty = scalar | | rank() | size_t | number of dimensions | | isScalar() | bool | true when shape is empty | | count() | size_t | product of the shape (scalar → 1) |

Python. Shape is implicit in the value you pass. A 3-tuple is shape {3}; a bare float is a scalar. There is no shape() reader in the binding — you know the shape by which getter you call (get_float vs get_float3).

The Type role aliases and their fixed shapes

The graphics aggregates are typedefs over std::array / std::vector of float, and each maps to one fixed float32 shape. Construct a Value from one and the shape is set for you; read it back with as<T>().

using Vec2        = std::array<float, 2>;   // f32 shape {2}
using Vec3        = std::array<float, 3>;   // f32 shape {3}
using Affine2     = std::array<float, 6>;   // f32 shape {2,3} — 2×3 affine, row-major (a b c d tx ty)
using Spectrum    = std::array<float, 32>;  // f32 shape {32}  (SPECTRAL_BINS == 32)
using Float2Array = std::vector<Vec2>;      // f32 shape {N,2}
using FloatArray  = std::vector<float>;     // f32 shape {N}
using IntArray    = std::vector<int64_t>;   // i64 shape {N}

| Alias | dtype | Shape | Python value | Python reader | |-------|-------|-------|--------------|---------------| | Vec2 | F32 | {2} | 2-tuple | get_float2 | | Vec3 | F32 | {3} | 3-tuple | get_float3 | | Affine2 (matrix) | F32 | {2, 3} | 6-tuple | unbound — see below | | Spectrum | F32 | {32} | 32-tuple | unbound — see below | | Float2Array | F32 | {N, 2} | — | unbound | | FloatArray | F32 | {N} | — | unbound | | IntArray | I64 | {N} | — | unbound |

The matrix is a 2×3 affine, stored row-major as (a b c d tx ty), mapping (x, y) to (a·x + b·y + tx, c·x + d·y + ty). SPECTRAL_BINS is 32, so a Spectrum is 32 floats.

Value xform = Value(Affine2{1, 0, 0, 1, 5, 8});   // identity rotation, translate (5,8)
Value white = Value(Spectrum{/* 32 floats */});
Value path  = Value(Float2Array{{0,0}, {1,0}, {1,1}});  // shape {3,2}

Value::type() reports a legacy classifier (Type::Vec3, Type::Matrix, …) used by the time-sample interpolator and the C ABI. Any dtype or shape outside this fixed set classifies as Type::Generic — it still serializes and interpolates, it just has no role name.

Python. A sequence of length 2, 3, 6, or 32 (all numbers) maps to float2 / float3 / matrix / spectrum respectively — that mapping lives in Handle.set:

ball.set("offset", (1.0, 2.0))                       # → float2,  shape {2}
ball.set("albedo", (0.9, 0.2, 0.2))                  # → float3,  shape {3}
ball.set("xform",  (1, 0, 0, 1, 5, 8))               # → matrix,  shape {2,3}
ball.set("light",  tuple(spectrum32))                # → spectrum, shape {32}

A sequence of any other length raises TypeError:

ball.set("oops", (1, 2, 3, 4))   # TypeError: a sequence value must be 2, 3, 6, or 32 numbers

Unbound in Python. set writes matrix (6-tuple) and spectrum (32-tuple), and the C ABI exposes get_matrix / get_spectrum. But the Python Handle only binds the get_float2 / get_float3 readers — there are no get_matrix, get_spectrum, or variable-length array (Float2Array / FloatArray / IntArray) readers yet. Read those through eval, which returns the raw tuple of components, or reach for the C++/C ABI.

Constructors

A Value comes from an implicit scalar/aggregate constructor, a named static builder, or a role helper.

Scalars (implicit):

Value a = 1.5f;          // F32
Value b = 2.0;           // F64
Value c = true;          // Bool
Value d = 42;            // int → I64
Value e = std::int64_t{7};// I64
Value f = "hello";       // Str
Value g = std::string{"x"};

Fixed-shape aggregates (implicit): Value(Vec2), Value(Vec3), Value(Affine2), Value(Spectrum), Value(Float2Array), Value(IntArray), Value(FloatArray) — each sets dtype and shape as tabled above.

Static builders for the general case — any dtype, any rank:

Value u8  = Value::scalar<std::uint8_t>(255);            // U8 scalar
Value ch  = Value::scalar<char>('A');                    // Char scalar
Value s   = Value::string("title");                      // Str scalar
Value grid= Value::array<std::int32_t>({2, 3}, {1,2,3,4,5,6});  // I32 shape {2,3}
Value tags= Value::stringArray({2}, {"a", "b"});         // Str shape {2}
Value raw = Value::fromBytes(DType::F32, {3}, bytes);    // from a native-endian buffer (deserializers)

| Builder | Result | |---------|--------| | Value::scalar<T>(x) | a scalar of T's dtype | | Value::string(s) | a string scalar | | Value::array<T>(shape, data) | a typed numeric array of any rank | | Value::stringArray(shape, data) | a string array of any rank | | Value::fromBytes(d, shape, raw) | raw native-endian construction |

Role helpers — uniform, explicit construction so a scalar reads like a vector at the call site:

Float (1.5)               // F32 scalar
Double(1.5)               // F64 scalar
Int   (42)                // I64 scalar
Bool  (true)              // Bool scalar
Str   ("ball")            // Str scalar
Float2(1, 2)              // f32 shape {2}
Float3(0.9, 0.2, 0.2)     // f32 shape {3}

These keep a call chain even: set("radius", Float(1.5)).set("albedo", Float3(0.9, 0.2, 0.2)).

Python. There are no constructors — the native value is the value. Float(1.5)1.5; Float3(0.9, 0.2, 0.2) ↔ the 3-tuple (0.9, 0.2, 0.2); Int(42)42; Str("ball")"ball". A 6-tuple is a matrix, a 32-tuple a spectrum. set reads the Python type and routes to the matching C ABI setter.

ball.set("radius", 1.5)               # Float(1.5)
ball.set("albedo", (0.9, 0.2, 0.2))   # Float3(...)
ball.set("count", 4)                  # Int(4)
ball.set("name", "ball")              # Str("ball")

Accessors

Read a Value back as a scalar, an aggregate, or a flat vector.

float   r   = radius.scalarAs<float>();   // scalar (or element 0) as T
float   any = radius.asFloat();           // best-effort: bool→0/1, ints→float, str→0
Vec3    rgb = albedo.as<Vec3>();          // as a fixed-shape aggregate
bool    ok  = albedo.is<Vec3>();          // does dtype+shape match Vec3?
std::vector<float> flat = albedo.values<float>();  // the whole numeric payload

| Method | Returns | Use | |--------|---------|-----| | scalarAs<T>() | T | the scalar, or the first element, reinterpreted as T | | asFloat() | float | best-effort element-0 read for UI/coercion | | is<T>() | bool | true when dtype + shape match T's aggregate | | as<T>() | T | read as a fixed-shape aggregate (Vec2/Vec3/Affine2/Spectrum/Float2Array/IntArray/FloatArray, or scalar float/double/bool/int64) | | values<T>() | vector<T> | flatten the numeric payload to a vector | | bytes() | const vector<byte>& | raw native-endian payload (serializers) | | strings() | const vector<string>& | the string payload |

values<T>() copies by what the buffer actually holds, not the declared shape — it defends itself against a mismatch. is<float>() is true only for a float32 scalar; is<Vec3>() checks F32 and shape exactly {3}. as<T>() on a wrong shape returns a zeroed T rather than throwing.

Value m = Value(Affine2{1,0,0,1,5,8});
m.is<Affine2>();        // true
m.is<Vec3>();           // false — shape is {2,3}, not {3}
m.as<Affine2>()[4];     // 5  (tx)

Python. Reads come back native. The permissive getters return a default when the value is absent or mistyped; the strict require_* getters raise PrismError. Every read takes an optional time so a value can be sampled along its animation curve.

r   = ball.get_float("radius", 1.0)        # float, or 1.0 if absent
rgb = ball.get_float3("albedo")            # 3-tuple, or None
off = ball.get_float2("offset", (0, 0))    # 2-tuple, or the default
n   = ball.require_float("radius")         # raises PrismError if absent/mistyped
rgb = ball.require_float3("albedo")        # raises PrismError if absent/mistyped
s   = ball.get_str("name", "")             # str, or the default

| Python reader | C++ analogue | |---------------|--------------| | get_float / require_float | scalarAs<float> / requireFloat | | get_int | scalarAs<int64_t> | | get_bool | scalarAs<bool> | | get_float2 | as<Vec2> | | get_float3 / require_float3 | as<Vec3> | | get_str / require_str | string payload |

The get_* readers cover scalars, float2, and float3. As noted above, matrix, spectrum, and variable-length arrays have no Python reader yet — go through doc.eval(slot), which returns the components as a tuple, or use the C++ API.

Equality

Two Values are equal when their dtype, shape, numeric payload, and string payload all match. Equality is exact and byte-wise — there is no float tolerance.

Float3(1, 2, 3) == Float3(1, 2, 3);   // true
Float(1.0f)     == Double(1.0);       // false — F32 vs F64
Int(1)          == Float(1.0f);       // false — I64 vs F32

Python. Compare the native values you read back; there is no Value object to compare. Whole documents compare by their serialized bytes (doc_a == doc_b).

Interpolation

Value::lerp(a, b, u) blends two values of the same dtype and shape, component-wise. Float dtypes interpolate; integers interpolate then round; bool, char, str, and any dtype/shape mismatch hold at a. This is the engine behind animated properties — see properties.

Value mid = Value::lerp(Float3(0, 0, 0), Float3(1, 1, 1), 0.5);  // (0.5, 0.5, 0.5)

Python. You don't call lerp directly. Author samples with Handle.animate (float scalars and float3s) and read the blended result with eval(slot, time).

ball.animate("radius", {0.0: 1.0, 1.0: 2.0}, interp="linear")
doc.eval("/world/ball.radius", time=0.5)   # 1.5

From here: properties give a Value a name and a time axis, and connections let one Value flow into another.