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_t…int64_t | 1·2·4·8 | signed integers | | U8 U16 U32 U64 | uint8_t…uint64_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 (int8…uint32), 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.
setwrites matrix (6-tuple) and spectrum (32-tuple), and the C ABI exposesget_matrix/get_spectrum. But the PythonHandleonly binds theget_float2/get_float3readers — there are noget_matrix,get_spectrum, or variable-length array (Float2Array/FloatArray/IntArray) readers yet. Read those througheval, 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.