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}
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
};
DTypeC++ elementBytesNotes
Boolbool1true / false
Charchar1one byte, distinct from a string
I8 I16 I32 I64int8_tint64_t1·2·4·8signed integers
U8 U16 U32 U64uint8_tuint64_t1·2·4·8unsigned integers
F16(half)2IEEE binary16; no native C++ half, so the 16-bit pattern is stored and halfBitsToFloat/floatToHalfBits convert
F32float4the default dtype
F64double8
Strstd::string0variable-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();   // 3
MethodReturnsMeaning
dtype()DTypethe element type
shape()const vector<uint32_t>&dimensions; empty = scalar
rank()size_tnumber of dimensions
isScalar()booltrue when shape is empty
count()size_tproduct 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}
AliasdtypeShapePython valuePython reader
Vec2F32{2}2-tupleget_float2
Vec3F32{3}3-tupleget_float3
Affine2 (matrix)F32{2, 3}6-tupleget_matrix
SpectrumF32{32}32-tupleget_spectrum
Float2ArrayF32{N, 2}list of pairsget_float2_array
FloatArrayF32{N}list of floatsget_float_array
IntArrayI64{N}list of intsget_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}

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. set writes a matrix (6-tuple) and a spectrum (32-tuple); get_matrix and get_spectrum read them back, with strict require_matrix / require_spectrum partners. Variable-length arrays write through set_float2_array / set_float_array / set_int_array and read through get_float2_array / get_float_array / get_int_array. The general get_array returns 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:

GroupNamesdtype · shape
float vectorsvec2f vec3f vec4fF32 · {2} {3} {4}
double vectorsvec2d vec3d vec4dF64 · {2} {3} {4}
half vectorsvec2h vec3h vec4hF16 · {2} {3} {4}
int vectorsvec2i vec3i vec4iI32 · {2} {3} {4}
matricesmatrix2d matrix3d matrix4dF64 · {2,2} {3,3} {4,4}
matrix4fF32 · {4,4}
quaternionsquath quatf quatdF16 / F32 / F64 · {4}
roles (F32×3 / ×4)point3f normal3f vector3f color3f · color4fF32 · {3} · {4}
texture coordstexCoord2f texCoord3fF32 · {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"};

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)
BuilderResult
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
MethodReturnsUse
scalarAs<T>()Tthe scalar, or the first element, reinterpreted as T
asFloat()floatbest-effort element-0 read for UI/coercion
is<T>()booltrue when dtype + shape match T's aggregate
as<T>()Tread 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 readerC++ analogue
get_float / require_floatscalarAs<float> / requireFloat
get_intscalarAs<int64_t>
get_boolscalarAs<bool>
get_float2as<Vec2>
get_float3 / require_float3as<Vec3>
get_str / require_strstring 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.