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. In C++ a Value is an explicit wrapper. In Python there is no Value wrapper: a native float, int, bool, str, or a tuple of numbers is the value, set infers the Prism type, and the get_* readers hand you native values back.
#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}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, F16
};
| 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 |
F16 | (half) | 2 | IEEE binary16; no native C++ half, so the 16-bit pattern is stored and halfBitsToFloat/floatToHalfBits convert |
F32 | float | 4 | the default dtype |
F64 | double | 8 | |
Str | std::string | 0 | variable-length, stored out of band |
F16 is appended last in the enum so the binary dtype codes of the others stay stable. Build a half value with Value::half(x) / Value::halfArray(shape, data) (both take float inputs). 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 F16, F32, and F64.
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 ergonomic setters infer the common types. A native float lands as float32, an int as 64-bit, a tuple as a float aggregate. For the rest of the dtype lattice the binding mirrors the full model: Handle.set_double / get_double for float64; Handle.set_array(dtype, shape, values) / get_array for any dtype at any shape (narrow and unsigned ints, char, float16 via "f16", float64 matrices); Handle.value_type(key) reads a stored value's (dtype, shape). Python reaches every dtype, not only the role-shaped subset.
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(); // 3dtype, shape = ball.value_type("albedo") # ("f32", (3,))
rank = len(shape) # 1
flat = shape == () # False, it is a 3-tuple| 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. value_type(key) reads a stored value's (dtype, shape) back, the dtype as a name and the shape as a tuple (() for a scalar); the per-shape getters (get_float vs get_float3) tell you the shape by which one resolves.
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 | get_matrix |
Spectrum | F32 | {32} | 32-tuple | get_spectrum |
Float2Array | F32 | {N, 2} | list of pairs | get_float2_array |
FloatArray | F32 | {N} | list of floats | get_float_array |
IntArray | I64 | {N} | list of ints | get_int_array |
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}ball.set("xform", (1, 0, 0, 1, 5, 8)) # matrix, shape {2,3}
ball.set("light", tuple(spectrum32)) # spectrum, shape {32}
ball.set_float2_array("path", [(0, 0), (1, 0), (1, 1)]) # float2[], 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
Reading them in Python.
setwrites a matrix (6-tuple) and a spectrum (32-tuple);get_matrixandget_spectrumread them back, with strictrequire_matrix/require_spectrumpartners. Variable-length arrays write throughset_float2_array/set_float_array/set_int_arrayand read throughget_float2_array/get_float_array/get_int_array. The generalget_arrayreturns any numeric value as(dtype, shape, values).
Named types (the built-in catalog)
A property can declare a named type (vec4f, matrix4d, color3f) instead of a bare (dtype, shape). The name is a shorthand resolved through one shipped, built-in catalog; there are no user-defined aliases. The document stores the name, so two types that share a (dtype, shape) stay distinct and round-trip verbatim: vec4f and quatf (both F32×4), or point3f, normal3f, and color3f (all F32×3). The name lives on the property (Property::typeName()); the value underneath is the catalog's dtype and shape.
def object "mesh" {
matrix4d xform = [[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]]
color3f albedo = [0.9, 0.2, 0.2]
point3f pivot = [0, 1, 0]
}
The catalog:
| Group | Names | dtype · shape |
|---|---|---|
| float vectors | vec2f vec3f vec4f | F32 · {2} {3} {4} |
| double vectors | vec2d vec3d vec4d | F64 · {2} {3} {4} |
| half vectors | vec2h vec3h vec4h | F16 · {2} {3} {4} |
| int vectors | vec2i vec3i vec4i | I32 · {2} {3} {4} |
| matrices | matrix2d matrix3d matrix4d | F64 · {2,2} {3,3} {4,4} |
matrix4f | F32 · {4,4} | |
| quaternions | quath quatf quatd | F16 / F32 / F64 · {4} |
| roles (F32×3 / ×4) | point3f normal3f vector3f color3f · color4f | F32 · {3} · {4} |
| texture coords | texCoord2f texCoord3f | F32 · {2} {3} |
Existing plain spellings (float, float2, float3, float32, int64, …) still parse; the catalog is additive. Arrays of named types use the general dtype[N,…] form for now.
C ABI / Python. kinogaki_type_alias(name, …) resolves a name to its (dtype, shape); kinogaki_element_set_typed authors by name; kinogaki_element_get_type_name reads the declared name back. In Python set_typed authors by name and type_name reads it back:
mesh.set_typed("xform", "matrix4d", [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1])
mesh.set_typed("albedo", "color3f", [0.9, 0.2, 0.2])
mesh.type_name("albedo") # "color3f", distinct from a plain float3
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"};ball.set("a", 1.5) # f32
ball.set_double("b", 2.0) # f64
ball.set("c", True) # bool
ball.set("d", 42) # int → i64
ball.set("f", "hello") # strFixed-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)ball.set_array("u8", "u8", (), [255]) # u8 scalar
ball.set_array("ch", "char", (), [ord("A")]) # char scalar
ball.set("s", "title") # str scalar
ball.set_array("grid", "i32", (2, 3), [1,2,3,4,5,6]) # i32 shape {2,3}
ball.set_str_array("tags", ["a", "b"]) # str shape {2}| 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}ball.set("a", 1.5) # f32 scalar
ball.set_double("b", 1.5) # f64 scalar
ball.set("c", 42) # i64 scalar
ball.set("d", True) # bool scalar
ball.set("e", "ball") # str scalar
ball.set("f", (1, 2)) # f32 shape {2}
ball.set("g", (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 payloadr = ball.get_float("radius") # scalar as float
rgb = ball.get_float3("albedo") # as a 3-tuple
kind = ball.value_type("albedo") # ("f32", (3,)), the dtype and shape
_, _, flat = ball.get_array("albedo") # the whole numeric payload as a flat list| 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)ball.set("m", (1, 0, 0, 1, 5, 8)) # a matrix, shape {2,3}
ball.value_type("m") # ("f32", (2, 3)), not (3,)
ball.get_matrix("m")[4] # 5.0 (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. For the rest, get_matrix / require_matrix and get_spectrum / require_spectrum read the fixed shapes; get_float_array, get_int_array, and get_float2_array read the variable-length arrays; get_array reads any numeric dtype back as (dtype, shape, values).
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.