Repository

SDK

What a Kinogaki Studio plugin compiles against.

GitHub
kinogaki/studio-sdk
Folder
sdk/
Crates
10
Rust lines
76,561 in src, 10,683 in tests
This repository Used by other repositories Kinogaki Studio's edit contracts: what core and the kernels agree on when a value is selected and changed.edit5.5k lines Kinogaki Studio's standard geometry values.geometry19k lines MaterialX 1.39 for Kinogaki Studio: the material layer every engine, format and core share, in our own implementation (MaterialX's C++ library is a reference and a test oracle, never a dependency).mtlx4.6k lines Kinogaki's render interface: what a host (Studio) sends a render engine and what comes back, with no GPU or OS API.render-api9.3k lines Kinogaki Studio's headless plugin API.studio-api19k lines The SDK's test support (a dev-dependency only): [`assert_refused`] for admission tests, [`read_hostile`] for a format's tests of hostile files, [`format`](mod@format) for its scratch folders and round trips, [`workers`] as a host installs them, with `wgpu` an ...studio-api-testing2.9k lines Kinogaki Studio's UI layer for plugins.studio-ui-api8.7k lines The UI layer's test support (a dev-dependency only): headless fakes of the host's UI for plugin unit tests: [`RecordingUi`] and [`RecordingPainter`] record what a plugin asks to show and draw, and play back scripted user actions, with no window and no GPU ...studio-ui-api-testing2.6k lines Kinogaki Studio's value model: the one way the SDK says what a value is and holds.value2.8k lines Kinogaki's viewport packet contract: what a data type hands Studio's viewport to draw, with no GPU or UI API.viewport-api1.9k lines lib-math1 crate lib-ui1 crate lib-viewport2 crates Comp2 crates OpenEXR2 crates FBX2 crates glTF2 crates JPEG2 crates Layout2 crates MaterialX2 crates Mesh4 crates OBJ2 crates Pixel2 crates PNG2 crates Spectre3 crates USD2 crates core19 crates
depends on (an edge implied by others shows when you point at a box)used by tests alone

Crates

CrateWhat it isFilesLinesTest lines
kinogaki-editKinogaki Studio's edit contracts: what core and the kernels agree on when a value is selected and changed.245,5112,198
kinogaki-geometryKinogaki Studio's standard geometry values.8218,9412,986
kinogaki-mtlxMaterialX 1.39 for Kinogaki Studio: the material layer every engine, format and core share, in our own implementation (MaterialX's C++ library is a reference and a test oracle, never a dependency).164,597476
kinogaki-render-apiKinogaki's render interface: what a host (Studio) sends a render engine and what comes back, with no GPU or OS API.369,26824
kinogaki-studio-apiKinogaki Studio's headless plugin API.8619,3253,176
kinogaki-studio-api-testingThe SDK's test support (a dev-dependency only): assert_refused for admission tests, read_hostile for a format's tests of hostile files, format for its scratch folders and round trips, workers as a host installs them, with wgpu an ...152,85340
kinogaki-studio-ui-apiKinogaki Studio's UI layer for plugins.428,6911,783
kinogaki-studio-ui-api-testingThe UI layer's test support (a dev-dependency only): headless fakes of the host's UI for plugin unit tests: RecordingUi and RecordingPainter record what a plugin asks to show and draw, and play back scripted user actions, with no window and no GPU ...82,6390
kinogaki-valueKinogaki Studio's value model: the one way the SDK says what a value is and holds.122,8080
kinogaki-viewport-apiKinogaki's viewport packet contract: what a data type hands Studio's viewport to draw, with no GPU or UI API.101,9280

Example plugins

CrateWhat it isFilesLinesTest lines
example-box-nodeA Kinogaki Studio plugin that adds a Box node.26800
example-flat-engineFlat: an example render engine.55860
example-pin-boardA Kinogaki Studio plugin that brings a context with its UI.23850
example-points-formatXYZ: an example file format.26370

README

studio-sdk

What a Kinogaki Studio plugin compiles against. Studio is an environment: with no packages installed it organises a scene (Parts, Cameras, an empty Material Library, a File node that reads nothing). Meshes, CAD bodies, file formats and render engines all come from packages, and this repository holds the contract between Studio and those packages. Its version is the version plugins and Studio agree on.

CrateRole
kinogaki-value (crates/value)The value model: declared ids (one type per kind of thing), Data (a type-erased value of a registered data type), Value and ValueKind (what every node parameter, format option, render setting, declared prim value and material port holds, its limits and options), Condition, and the structured values ramp::Ramp (every ramp's encoding and evaluation: nodes, the ramp widget and engines alike) and ToneCurve, RefFilter and ObjectRef. It depends on kinogaki-geometry alone.
kinogaki-geometry (crates/geometry)The SDK's base crate, which every other one stands on: the one Error and Diagnostic, Time and Rate, Units, the shared workers (parallel::Workers), Column, and the image unit with its sources (image: Image, ImageSource, EmbeddedFile). And the standard geometry values every package shares: Geometry (polygons, polylines and curves over one shared Primitives table: rows of point ids with a Kind each, its Basis and whether it closes, in any order, with its canonical edge index; each primitive read as a Curve of linear, Bézier, B-spline, Catmull-Rom or NURBS basis), brep::Brep (boundary-representation bodies with exact analytic and NURBS geometry, persistent names and history, built by BrepBuilder), the typed attribute model (Attributes of Attributes: storage × width × meaning, arrays, per Domain; indexed Text, groups as Elements, the standard names table, unique stable ids), Selection (Elements of a domain, with its members' ids) and the Incidence selection rules read, Detail and Tolerances. Each is a validated, immutable value over shared Columns, with typed cache slots and the cache rule in its docs. Kernels, formats and engines read and build these; Studio's core registers, displays and saves them.
kinogaki-mtlx (crates/mtlx)MaterialX 1.39, our own implementation: Document (read from and written to MaterialX's XML as MaterialX writes it, 1.36 to 1.38 upgraded on reading), the library of MaterialX 1.39.5's definitions (stdlib, pbrlib, the bxdf shading models and their translation graphs, cmlib, nprlib, lights; vendored verbatim in libraries/ with MaterialX's Apache-2.0 licence and their checksums, libraries.sha256) and Kinogaki's (kinogaki/), each definition's typed inputs (NodeDef, Port), the graphs implementing them resolved (Graph), translation chains between shading models, and evaluation of a node of values. It depends on kinogaki-geometry alone, for the SDK's Error.
kinogaki-render-api (crates/render-api)The render interface between a host and its engines: prims (shapes: triangles or curves with typed primvars; instancers, cameras, materials, textures) and the prim types an engine declares (lights, fog: parameter schemas, the standard UsdLux ones shared), coalesced scene updates with dirty bits, material networks and an engine's vocabulary, settings and outputs by id, the RenderEngine trait and EngineDescription, the shape source contract (ShapeSource) every data type renders through, and the SDK's preview shape (PreviewRequest). An engine's renderer depends on this crate alone; its package also on kinogaki-studio-api.
kinogaki-viewport-api (crates/viewport-api)The viewport packet contract: what a data type hands Studio's viewport to draw. Surface packets (SurfaceSource: point positions, vertex shading records, triangles, line classes with their looks, point markers, sparse draw updates, derived shading plans, see-through face runs) and packet_as to read one back as its own type; element sets and selections; live preview patches and GPU motions; overlay, guide and scene line layers. Its maths is the SDK's geometry vocabulary (Vec3, Xform, Bounds). The bytemuck feature derives Pod on the records a renderer uploads. Kernels depend on this crate alone for drawing.
kinogaki-edit (crates/edit)The edit contracts core and the kernels share, over kinogaki-geometry alone: the editor (Editor, the one way a stored value changes: points and primitives added, removed, welded, split and moved, attributes, groups and ids following, unchanged columns shared, and a Change saying what changed; gather joining placed values), selection rules (select::convert by Coverage, over any value's Incidence; select::reresolve by stable ids), the stable-id rule (ids::fresh, ids::merged) and modes (Mode, Expand::Connected pieces), a move as data (deform::Deform: a Move, per-point weights and a Symmetry; place and apply, the one function a drag's preview and its commit share) and a drag's moving points (preview::Moving). Re-exported as kinogaki_studio_api::edit.
kinogaki-studio-api (crates/api)The headless API: ids, Data and Typed, DataType with its optional facets (Displayable, Pickable, Tabular, Renderable), Components, DataCodec, the node cook contract (NodeType, CookCx, Outputs) and HeldEdit, ObjectBody and ObjectKindSpec, FileFormat, Imported and Reading, SessionSlot, CancelToken, Time and Rate (a moment of the scene on the tick grid, and frames a second: the geometry crate's), Key and Interp (a parameter's keys over time), Icon, kernel (a kernel's Verbs, which any package runs by id while the kernel is loaded), script (the host's script engine, which code nodes run through), Package (with its render engines' descriptions, and their makers behind the wgpu feature), export_package! (a plugin library's entry point) and the Registry; the standard data types (standard: the ids, handles and codecs of geometry's values) and their conformance suite (conformance); and the plain types they exchange: the geometry vocabulary, params, element, viewport and render
kinogaki-studio-ui-api (crates/ui-api, library studio_ui_api)The UI layer: how a plugin's panels, tool cards, overlays and settings pages ask the host to draw. Geometry (Pos, Vec2, Rect, Align2), Color, the host-filled Theme, fonts and icons by name, Id, plain-data UserInput, the Painter and Shapes, the Ui trait (immediate widgets, layout, and the host's composites: property sheets, cards, popovers, value fields) with UiExt; property sheets as data (sheet::Sheet); the pure viewport maths of the transform gizmo and point labels; the host interface (host: the capabilities a package's UI asks of Studio through one call context) and a package's UI (package: UiPackage, its contexts' parts, WithUi::with_ui, which adds it to the package); and testing's headless RecordingUi, RecordingPainter, FakeHost (with in-memory networks, FakeNetwork) and Hosting. Plugins never see Studio's UI library or egui: the host draws.
kinogaki-studio-api-testing, kinogaki-studio-ui-api-testing (crates/api-testing, crates/ui-api-testing)Test support, dev-dependencies only: the conformance suite's fixtures and invariants, a format's scratch folders and round trips (format), the workers a host installs (workers), and the UI layer's headless fakes (FakeHost, FakeNetwork, RecordingUi, RecordingPainter)

docs/ARCHITECTURE.md describes the API and the UI layer: what each part is for, the rules every package follows, and how the environment uses it.

To write a plugin, follow the walkthrough for its kind:

  • Write your first node plugin: a data type, a node type that makes it, the context and object kind that make the node reachable from Tab, tests, and building against the plugin kit.
  • Write your first context plugin: a context with its own UI (levels, commands, an inspector row) and its tests on the fake host.
  • Your first render engine: the render contract, from the description to previews.
  • Your first file format: reading files into the standard values and writing them back, materials, images and lights included, and untrusted files.

Who depends on it

  • Studio (the environment) implements the host side, loads plugin libraries and admits their packages.
  • Plugins implement packages against it: Studio's own (Mesh, CAD, rendering, Comp, formats) and third parties' alike.

Plugins never link Studio's editor; they see only the SDK.

Not written yet

  • The conformance suite's checks of a package's own data types and node types (stamps, codec round trips, empty values, pure parameter hooks). The conformance suite checks the standard data types today.

Self-contained

The SDK never depends on a private Kinogaki crate; Studio adapts at the boundary.

The SDK depends on the Rust standard library, on two crates.io crates at exact versions for its standard geometry values (blake3 1.8.7 for the topology fingerprint and bytemuck 1.25.2 for plain records and casts into columns), and, behind optional features only, on wgpu (kinogaki-render-api's wgpu feature) and bytemuck (kinogaki-viewport-api's bytemuck feature). Every type the API exposes is its own:

ModuleWhat it definesStudio converts it to and from
geometry (re-exported at the root)the one vocabulary: Error (an ErrorKind and the Diagnostic the user reads: what every fallible call, a kernel's operation and a node's cook alike, returns), Vec3, Xform (f64 affine, column vectors), Bounds, Ray, Projective, Units, Time (ticks of 1/705,600,000 s), Rate, Sampledits maths library
paramsValue, Parameters, Schema (sections and typed parameters: number with unit and range, integer, toggle, choice, text, code, file, path, reference with its RefFilter, color, vector, float or color ramp with params::ramp's encoding, kinogaki-value's ramp re-exported; whether each can be keyed, Parameter::animatable)its inspector's property schemas
elementElementWriter / ElementReader (typed fields, metadata, children, shared values) and MemoryElementits project documents
viewportSurface (the simple display packet, a SurfaceSource itself) and Hit; packets, ElementKind and ElementSet are kinogaki-viewport-api's, re-exportedits viewport's picks
renderRenderItem; the source contract (ShapeSource, Topology, Ranges) is kinogaki-render-api's, re-exportedits render scenes
geometry::parallelWorkers, the host's shared workers, installed once per process; map, map_blocks and chunks_mut run on them (on the calling thread until a host installs its own)its shared worker threads
the ui-api crateGeometry, colors, the theme, input, the painter, widgets and property sheetsits UI library (each call maps to the call it replaces)

So a package written against the SDK compiles against nothing private, and the SDK builds and tests from a plain clone. A plugin's kernel may use any library it likes inside; it converts at its own boundary, as Studio does at its.

Studio's architecture check enforces the rule: the SDK's crates depend on no Kinogaki crate but their own. Its third-party dependencies are pinned exactly and resolved from crates.io as normal dependencies: blake3 1.8.7 and bytemuck 1.25.2 with kinogaki-geometry (the standard values' fingerprint, plain records and casts), and two with one feature each: wgpu 30.0.1 with kinogaki-render-api's wgpu (a wgpu host and its engines share the device and display textures, so they must agree on the version), and bytemuck 1.25.2 with kinogaki-viewport-api's bytemuck (a renderer casts the packet records it uploads). The default build depends on blake3 and bytemuck alone.

Example plugins

examples/box-node is a whole node plugin in one file: a box data type that shows in the viewport, renders as twelve triangles and saves its size and center; a Boxes object kind opening into a Boxes network context; and a Box node, offered and listed in Tab there, that makes a box from its size and center, with a box handle Studio drags. Its tests cook the node the way Studio does, and add and edit it in a network on the fake host. Write your first node plugin builds it step by step; https://studiosdk.kinogaki.com walks through it too.

examples/flat-engine is a render engine: a CPU ray caster on the SDK's render thread, its frames shown through a texture on the host's device; its test runs the SDK's engine conformance. Your first render engine is its contract.

examples/points-format is a file format: XYZ, a text file of points, faces over them and the materials they show, read into the standard Geometry and materials (OpenPBR Surfaces, a picture through an Image node) and written from them, with Scale and Embed Textures options; its tests read and write it both ways and run the conformance suite's fixtures through it as a Consumer. Your first file format walks through it.

examples/pin-board is a plugin with UI: a Pin Board object opening into a context of its own (its networks and its UI), with two selection levels, a Pin command and an inspector row, all in the one package it exports. Its tests drive the UI on the fake host, as Studio drives it. Write your first context plugin builds it step by step.

Studio's own tests build all four against its plugin kit, load them as plugin libraries, run the pin board's commands and cook the Box node in a network.

Building

rust-toolchain.toml pins the exact compiler Studio and its plugins are built with: a plugin is a Rust dylib sharing Studio's standard library, so it must come from the same compiler build.

The wgpu feature is portable code with no platform-specific parts; it is verified on macOS so far.

Checks

Studio runs the same checks on the SDK.

License

The SDK is licensed under the Kinogaki Studio SDK license, LICENSE.md.

Third-party notices

Some of kinogaki-studio-ui-api's geometry and color arithmetic follows egui's, formula for formula, so Studio's egui-based UI library converts its values bit for bit. THIRD_PARTY_NOTICES.md credits it, and egui's licenses are in licenses/, with MaterialX's, whose libraries kinogaki-mtlx vendors. The SDK copies no third-party code otherwise; its crates.io dependencies (blake3 and bytemuck; wgpu behind its feature; and the crates they use) carry their own licenses, which THIRD_PARTY_NOTICES.md lists.

Its documents

The repository's architecture document as written there (code blocks and links left out), and the opening of each other document.

Architecture (sdk/docs/ARCHITECTURE.md)

Architecture

The SDK's two main crates are what a package is written against:

  • kinogaki-studio-api (crates/api) is the headless API: everything a package registers and exchanges that has no UI.
  • kinogaki-studio-ui-api (crates/ui-api, library studio_ui_api) is the UI layer: how a plugin's panels, cards, overlays and settings pages ask the host to draw. It is described in The UI layer below.

Leaf crates sit below them, each a contract another crate can depend on without the whole API:

  • kinogaki-value (crates/value) is the value model: declared ids, Data, and the one Value and ValueKind every node parameter, format option, render setting, declared prim value and material port holds (see Values). It depends on kinogaki-geometry alone (for Diagnostic and Domain).
  • kinogaki-geometry (crates/geometry) holds the standard geometry values (see Standard data types).
  • kinogaki-edit (crates/edit, depending on kinogaki-geometry alone) holds the edit contracts core and the kernels share: the editor every stored value changes through, selection rules and modes, deformations, a drag's moving points (see Edit contracts). It is re-exported as kinogaki_studio_api::edit.
  • kinogaki-mtlx (crates/mtlx, depending on kinogaki-geometry alone, for the SDK's Error) is MaterialX 1.39: documents and their XML, MaterialX 1.39.5's libraries vendored as data with their Apache-2.0 licence, Kinogaki's own definitions, and resolving, expanding, translating and evaluating definitions. A document's materials read as flat networks (Document::materials: its includes read first with Document::include, its node graphs and its own graph-defined nodes inlined, each value's color space kept) and a document is written of materials (Document::of_materials, carrying the Kinogaki definitions they use): what a format of MaterialX documents, and USD's MaterialX, convert at their boundary. MaterialX's C++ library is a reference and a test oracle, never a dependency.
  • kinogaki-render-api (crates/render-api, depending on kinogaki-value, kinogaki-geometry and kinogaki-mtlx) is the render interface between a host and its engines (see Rendering).
  • kinogaki-viewport-api (crates/viewport-api) is the viewport packet contract: what a data type hands the viewport to draw (see The viewport).

The test support crates kinogaki-studio-api-testing and kinogaki-studio-ui-api-testing are dev-dependencies only. A kernel is a package like any other: it depends on kinogaki-studio-api, which carries the verbs it offers and re-exports the geometry values and the edit contracts it works through (see Kernels).

This page explains how their parts fit together and the rules packages follow. The rustdoc on each item is the reference. To write a plugin step by step, start with Write your first node plugin, then Write your first context plugin.

The headless API

Values

Every value the SDK describes is a Value of a ValueKind (kinogaki-value, re-exported as kinogaki_studio_api::value and through params): a node's parameters and a format's options (Parameters, values by name, described by a params::Schema of Parameters), an engine's settings and its declared prims' values (Parameters in Sections: a name, a kind, a default, its section, when it shows and applies as Conditions), a material node's settings and its sockets' unwired values, an engine's presets. One description, Parameter, so a host draws, checks, keys over time and saves every one of them the same way.

  • Exact. Numbers are f64 and whole numbers i64 (Value::Integer) wherever they are held, authored or sent; an engine narrows what it keeps.
  • Choices by key. A Value::Choice holds its Choice::key; files save the key, so relabelling or reordering entries never changes a value.
  • Typed structures. A ramp is a Ramp of keys (any number; an engine's capacity is its kind's most, and a longer ramp is refused, never cut short), tone curves are ToneCurves, a color is Value::Color (linear: the working space as authored, ACEScg as an engine receives it).
  • Units are owned. Unit::Suffix holds its text (" EV", " K"); the host shows lengths in the project's unit.
  • Files and textures are two things. A declared prim's image is authored as its file's path (ValueKind::File, a Value::Text), and a material node's is the host's Image node wired into an Image input (of MaterialX's filename type), named by an ImageSource (a path or an EmbeddedFile kept in the project, and how its colors read); an engine receives the Texture prim the host read it to, in the declared prim's or material node's textures, never the source. A Texture prim is an Image, the SDK's one decoded picture, exactly as the host's ImageFiles answer a package: RGBA half floats, colors premultiplied in linear ACEScg or data as stored.
  • Kinds carry their options (a choice's entries, an attribute row's names, a number's unit and limits), checked by ValueKind::check, which core runs on every value it is handed. What only the host knows it offers: a ValueKind::ColorSpace row (a Value::Choice: one of its own entries, or a space by its name) lists its entries, then every space of the host's color configuration (an Image node's Color: Auto, Data, then the spaces). How a value is presented (segments or a menu) is the UI layer's; how a number's row looks and moves (a slider or a stepper, its places and drag speed) is a NumberLook beside the kind, on the Parameter or the sheet's Field, never in it.

Environment and packages

Studio is the environment. With no packages installed, it is the editor, networks, undo, files, Part, Camera, an empty Material Library and a File node that can read nothing. Everything else is a package:

KindRegisters hereExample
Kernelverbs (Verb, Package::with_verb): functions by id over standard values that other packages run; never data types of its ownStudio's Mesh kernel: booleans, subdivision, remeshing, UVs
Node packagenode types (NodeType: description and cook), the data types they make (DataType), how held edits apply to them (HeldEdit), session state (SessionSlot), the network contexts they live in (NetworkContext) and object kinds (ObjectKindSpec); the contexts' UI through the UI layerMesh objects, the Mesh nodes and the Mesh context
Formatreaders and writers (FileFormat)OBJ, STEP, USD
Renderengines (Package::engines, their descriptions, and Package::makers, the wgpu feature); they consume Prims, never kernel typesSpectre, Pixel

The environment registers packages into a Registry in dependency order. The loader resolves that order from the plugins' manifests, so a kernel always comes before the packages that build on it. Every package goes through the registry's checks (Registry::check, Registry::register): its id and every id it declares (its stored values' and render engines' too) are new and under its vendor; each data type it names (its ports' among them) and each context it names (where its node types are offered, what its object kinds hold) is its own or an earlier package's; each compiled context has one compiler; no node type declares a parameter named as spares are (cook::SPARE); each of its contexts adopts a node type at most once; its icons are well formed and named once, and each package icon it shows is one it or an earlier package carries; and its UI's contexts are its own. A package that fails is refused whole: Studio lists it as unavailable in Settings ▸ Plugins, says why in the Console, and keeps running.

A package with a UI carries it: Package::ui holds the parts the host UI interface (kinogaki-studio-ui-api) made, added with its WithUi::with_ui. The package's id, label and session slots are the Package's alone.

Plugin libraries

A plugin is a Rust dylib that exports its one Package, UI included, through export_package!(make): the macro writes the entry Studio looks up (entry::SYMBOL, of type entry::Entry), catches a panic in make, and keeps unsafe out of the plugin's own code, so its crate keeps unsafe_code = "forbid". Studio and its plugins share one standard library and one build of each shared crate, so values cross as plain Rust types and TypeIds agree.

A plugin is therefore built with the exact compiler this repository pins (rust-toolchain.toml) against the plugin kit of the Studio release it runs in: the build of the SDK that release ships, with its standard library. The plugin's root names kinogaki_shared::link!() once, which links the kit's shared library and records the build of it the plugin is compiled against. The kit's README gives the build command. A plugin is developed and unit-tested with plain cargo against this repository, and built against the kit to ship.

Studio looks for plugins in its own plugin folder and the user's (macOS: ~/Library/Application Support/Kinogaki Studio/plugins; Windows: %APPDATA%\Kinogaki Studio\plugins; Linux: $XDG_CONFIG_HOME/kinogaki-studio/plugins, else ~/.config/kinogaki-studio/plugins), in one folder per kind (kernels/, nodes/, formats/, renders/). Each plugin is its library and a manifest beside it, <name>.prisma, naming its package id, kind, version and the packages it depends on. Studio reads the manifests first, refuses a plugin missing a dependency without opening it, opens the rest in dependency order, refuses one whose library records another build of the shared library than the one Studio runs with before calling anything in it, admits each package through the registry, and never unloads a plugin it loaded. Settings ▸ Plugins lists every plugin found, loaded or not, with the reason.

Ids

An id is written <vendor>.<package>.<name>, for example kinogaki.mesh.extrude. Each id is declared once in code and never derived from a Rust name. Project files, shelves and settings store ids, so renaming code changes nothing a user has saved.

Each kind of thing has its own id type:

  • DataTypeId
  • ObjectKindId
  • ContextId
  • FormatId
  • NodeTypeId
  • SlotId
  • StoredId
  • MesherId
  • VerbId
  • CreationId
  • EngineId (a render engine's, re-exported by render-api)
  • PackageId (two segments only)

Every id is interned in one table and is a Copy pointer to its text. Packages are never unloaded, so the table never needs to free anything.

A package declares only ids under its own vendor. The registry refuses a package that does otherwise.

Data

A Data holds one value, which is either on a wire or an object's result. It has two parts:

  • the value's DataTypeId;
  • the value itself, as an Arc<dyn Any + Send + Sync>.

Values are immutable and shared:

  • Cloning a Data shares the same value.
  • A change produces a new Data.
  • Caches key on identity (ptr_eq, identity).

When identity is not enough, a cache keys on content instead, using DataType::stamp. This covers values that outlive an allocation, such as a value read back from a file.

A kernel reads its own values through a Typed<T> handle. The handle pairs the id with the Rust type, so get returns None for any other type. This works across packages because Studio and its plugins are built by one compiler and share one copy of each kernel crate, so a type's TypeId is the same everywhere.

The environment never looks inside a Data. It only calls the DataType registered for the value's id. A value whose type is not registered is kept as it was read when a project is read, and written back unchanged; the Console says so (see Saving).

Standard data types

Studio defines each kind of geometry once, in the kinogaki-geometry crate (re-exported as kinogaki_studio_api::geometry and kinogaki_render_api::geometry), and every package works on the same values. Core registers them as data types (kinogaki.core.geometry and kinogaki.core.brep, read and wrapped through standard::geometry() and standard::brep()), and a camera's lens and a light beside them (standard::camera(): render-api's Camera, unplaced, in UsdGeomCamera's units; standard::light(): render-api's standard Light, unplaced, in UsdLux's terms; what core's Camera and light objects hold and formats read and write). Those two handles are Checked: wrapping a value checks it and drops its placement, so no camera or light value breaks its rules, so they exist with no package installed, and core displays, picks, tabulates, saves and renders them. Kernels are operations over them: a kernel never registers a type of its own for the same data, and several kernels may offer operations over one type with no conversion between them.

ValueWhat
Geometrypolygons, polylines and curves over shared points: positions, an Arc<Primitives> (one row table: each primitive's Kind, a Basis and closed, in any order; CurveData with degrees, knots and weights when a curve is there; its canonical Index, a cheap edge_count, polygons() and fingerprint) and Attributes. Geometry::curve reads a primitive as a Curve: its domain, its Spans and the one exact evaluation (point, on a span) every display, engine and kernel shares; how finely a view samples it is the viewport's. One NURBS knot rule serves its curves and the B-rep's (clamped as a parameter)
brep::Brepboundary-representation bodies (brep::Body: shared brep::Geometry, BrepTopology, persistent Names and Attributes, placements and the positional tolerance it was built to), free curves (a Geometry of curves and polylines, no polygons) and datums; its faces, edges and vertices are numbered as one across bodies, free curves and datums (Brep::count, Brep::element, a datum plane a face, an axis or curve an edge, a datum point a vertex), and Brep::fingerprint is what a selection of it keeps (its bodies' structure and persistent names alone, never geometry, so a resize keeps a selection and a face added or split makes it stale); made by BrepBuilder, which checks structure and consistency at ends and joins. The module's rustdoc is the contract two kernels agree on: charts, senses, loops, seams, poles, tolerances. A kernel's mesher turns a body into Geometry; core doesn't tessellate
Attributesnamed, typed columns (Attribute of an AttributeType: Storage F32, F64, I32, I64 or indexed Text × width 1–255 × Meaning None, Point, Vector, Normal, Color, TextureCoord, Rotation, Transform, optionally an array per element) and groups (Elements) per Domain (Detail, Object, Instance, Primitive, Edge, Point, Vertex); Attribute::tuples adopts a Vec<[f32; 3]> without copying and as_tuples views it typed; one fixed table of standard names (P, N, uv, Cd, Alpha, v, id (unique within its domain), pscale, scale, orient, up, rest, width, material, material_map (an instance's pairs of a material its prototype names and the one it renders instead), name, part, the sharp/smooth Edge groups and the .hidden Primitive group) fixes each one's type and domains
Elements, Selection, IncidenceElements is the one set of a domain's elements (every one, a short id list or bits; equal by members), what groups and selections hold. A Selection is Elements of one domain, the active one, the fingerprint of the value it was made on (Selection::validate refuses it on another) and, when that value has stable ids, the members' ids, so kinogaki_edit::select::reresolve finds it again after a change upstream; a standard type's Components hold one (Components::selection). Incidence is a value's structure and identity as selection rules read it: elements per domain, which touch which, the fingerprint, the stable ids; Geometry and Brep implement it
Detailhow fine geometry is made for a view or a render: Viewport(DisplayQuality) or Render { chord, angle } (see Rendering)
DisplayQualityhow finely an object whose display is tessellated shows: Coarse, Medium, Fine, Finest, which a producer maps to presets of its own; the project's (a project value core keeps), unless the object overrides it
Tolerancesthe project's modeling tolerances (positional, fit, tangent, curvature, with their ranges, and the model unit), a project value core keeps
image::Imagethe one image unit: what files decode to, Texture prims hold, Comp and the 2D canvas exchange (standard::image(), saved by standard::ImageCodec). A data window of stored pixels inside a display window, a pixel aspect, Detail Attributes as metadata, and layers ("" the main one, others by name), each 1–4 named channels of one Sample (U8, U16, F16, F32, U32 for ids) and one Encoding: colors with a ColorTag (a configuration's Space, an Icc profile, Primaries, or Unknown, as a file says until the host resolves it to a space) and whether they are premultiplied, or Raw data. Pixels are 64² shared tiles, copy-on-write, or held by the host (HeldPixels): a texture on its GPU (render-api's display::Texture, frame-scoped and never saved: ImageCodec refuses it, so a consumer that keeps pixels reads them back first), or a result shown live (facets::Live: what a cooked value showing a render holds, resolved by the host each frame and saved as its reference). A layer is read at points through Image::sampler, a Sampler every reader of an image at coordinates shares so they agree to the bit (Attribute from Image, Wrangle's sample): 0–1 over the display window with v 0 its bottom row, values as stored, a Wrap per axis, and a SampleFilter (Nearest, Bilinear, Bicubic, or Footprint, which reads the mean levels Pyramid derives once per sampler). The metadata's standard names are image::metadata's (chromaticities, which a writer copies into a file; pixelated, pixel art a host magnifies through the nearest texel) and Cryptomatte's keys (render-api's aov::cryptomatte). A final render is one Image (render-api's aov::image)

Validated values. Every value's fields are private; it is made only through a constructor or builder that checks its rules (each type's rustdoc lists them), and refused with an Error naming the element at fault (the SDK's one error: an ErrorKind a caller branches on and the Diagnostic the user reads, so a kernel's operations and a node's cook return the same Error and pass it on with ?). So code reading a value relies on its rules: offsets in range, points that exist, attributes sized to their domain. An enum the SDK grows, whose values a plugin meets without naming them all (ValueKind, Value, a value's Domain, ErrorKind, a handle's HandleKind, a RenderItem, a Prim and its PrimKind), is #[non_exhaustive]: a match on it outside its crate ends in a wildcard that says what it does with a variant added since. A closed set a match must answer whole (an alignment, an axis, a storage) is exhaustive. The product limits (limits: 2³² − 2 points, primitives and vertices, so every id fits a u32 with u32::MAX free as "no element"; 256-byte names; finite positions within 1e100) hold for every value; memory is the budget's to bound.

Shared and immutable. Arrays are Columns: shared cells of about 64 KB that adopt a Vec without copying it. A ColumnWriter copies only the cells an edit writes, so its new memory follows the edit. Read by cell (column[i], iter, runs) where an edit or a drag passes: as_slice is free for a column made in one piece, one kept copy for a column an edit left in pieces. Edits make new values sharing whatever they didn't touch: Geometry::with_positions keeps the primitive table and attributes, Geometry::with_moved shares every cell of the positions no moved point falls in and checks only the points given, Attributes::with shares every other attribute. An attribute checks its own values once, when it is made, and a value's with_attributes checks only the entries it doesn't already hold, so replacing one column checks that column alone. A drag of 1M points re-checks positions and nothing else. Text is an index per element into a table of distinct strings; an empty string is no value (Text::NONE), and a renderer uses the index as a slot.

Caches. A kernel's acceleration data is a cache derived from a standard value, never the value:

  1. A cache is keyed by the identity of the shared part it was derived from: the Primitives table for adjacency, edges, half-edges and triangulation; the positions Column for BVHs and bounds; an attribute's Column for anything built from it.
  2. It lives in that part's typed slot (Primitives::derived, Column::derived): as long as the part, shared by every value sharing the part, so a points-only edit keeps every topology cache. Never in a global map keyed by address.
  3. Only the crate that defines a cache's type reads it; a slot is never a channel between plugins.
  4. A cache is never saved, compared or treated as the value.
  5. key() and ptr_eq hold within a session while the part lives; the fingerprint or a stamp is for anything longer.
  6. Two kernels' caches over one part are two slots.

The topology's Index (edges in canonical order, polygons and polylines per edge and per point, each primitive's edges) is the one standard cache: every kernel and core share its numbering, and Edge attributes follow it.

Saving. The SDK defines how the standard types save: standard::GeometryCodec, standard::BrepCodec, standard::CameraCodec and render::lights::LightCodec are the codecs core registers (core's Camera objects save their lens through CameraCodec too; a light value is unplaced and names a dome's image as authored, a file's path or op: and the path of the Image node reading one: the Texture prim a host decodes it to is one scene's, never the value's), writing each topology once per save for every value sharing it and reading every rule again. Projects save only frozen and held results; everything else is cooked again on load.

Scripts

A code node runs code over a standard value through the host's script engine (kinogaki_studio_api::script; core's own package brings it, and the registry refuses a second): script::engine()?.run(ScriptRun { code, over, on, inputs, parameters, time, rate, cancel }). Over says what it runs once for (each element of a Domain, or a count of numbers), and the engine's binding of the value says what code may name on it. The node holds rows: a ValueKind::Code row for the text (the kind names no language: the node type's language does), and it answers NodeType::spares and reads from ScriptEngine::facts, NodeType::language from ScriptEngine::language. A fault is an Error whose Diagnostic holds its line, column and lowest failing element; nothing partial is kept. Core's own package brings the engine (Package::with_script_engine), and a new standard value gains its binding with it.

A NodeType::language serves its node's code field through params::code::CodeLanguage: colors, completion, hover and check. While check finds an error the host keeps the row's text from the node's parameters, so the node goes on cooking the last text that held none.

Edit contracts

Core owns the geometric values; kernels bring the operations and the maths; plugins bring tools and nodes. What core and a kernel must agree on when a value is selected and changed is kinogaki-edit (re-exported as kinogaki_studio_api::edit), so kinogaki-geometry stays pure values:

ModuleWhat
editorthe one way a stored Geometry changes: an Editor over a base value collects changes (moving, adding and removing points and primitives, welding, splitting, rewiring, deform by a Deform) and finish makes the new value and a Change saying what moved, was added or went. Unchanged cells stay shared, attributes follow their sources by their type's rule, groups and ids follow, numbers stay put until the finish. gather joins several placed values into one by the same rules. A tool's cook, an Edit node's stored step and a kernel's verb all change values through it
selectconvert (the standard rule: whole into Primitive and Object and from Point into Edge, any otherwise) and convert_by a Coverage (Any, Whole) turn a selection into another domain over any Incidence (polygons and B-rep alike); resolve spells it out; reresolve finds a selection on a later value of the one it was made on (itself while the numbering holds, by its members' stable ids after a change upstream, None without ids). A context's level declares a Mode: the domain it selects and how a pick Expands there (None, or Connected: the pieces it touches, joined through shared points, a non-blank name kept together); expand grows a selection so. Mesh's Pieces level is Mode { domain: Primitive, expand: Connected }, a selection mode, not a domain. grow, shrink and boundary give what lies next to a selection; the selection commands' rules over a polygon value are here too, so every context finds the same: loops, rim, border, shortest_path, similar, by_normal, flood_by_angle and hard_edges (within ANGLE). Ways is the one shortest-way search (Dijkstra's) the Shortest Path command and a soft selection's reach along the surface (along_edges) share
deforma move as data: a Deform is a Move (translation, then rotation and scale about a pivot, along its axes), each moved point's weight (a soft move) and a Symmetry (a mirror plane on an Axis, its primary Side). Deform::place puts one point where the move takes it at its weight; apply places a run of points. The viewport's drag preview and the kernel's commit place points with the same function, so the preview is the commit
idsstable ids, one rule for every kernel: kept elements keep their id, new or copied ones take fresh ids above every id the value holds, and editor::gather, a Merge's and a realize's one path, keeps the first input's ids and gives later collisions and missing ones fresh ids
previewMoving: the points a drag moves (resolved once when it begins) and its Deform, which a context hands as Edited::moving and core previews over the shown value with no cook

Conformance. The conformance suite (the companion crate kinogaki-studio-api-testing, as the UI layer's fakes are kinogaki-studio-ui-api-testing: test support, only ever a dev-dependency) runs every producer's values through every consumer: a package's tests implement Producer for the values it makes and Consumer for what it draws, renders, writes or reads (the SDK's examples/points-format checks the fixtures as a Consumer, as core's own package does for the standard types), each beside the SDK's Fixtures (every shape and attribute kind), Codecs (a bit-exact round trip) and Invariants (rebuilt through the validating constructors, canonical edges).

Data types and facets

Every data type answers a fixed set of questions:

  • its label and wire color;
  • the value's stamp, memory, bounds and info rows;
  • its codec;
  • optionally its empty value (empty: what a bypassed node gives) and whether a value holds nothing (is_empty: the menus' "Create geometry first"). Hosts ask is_empty of shown values every frame, so it reads the value's counts; a stamp hashes the whole value and belongs to cooks.

Facets are optional:

FacetUsed byNotes
Displayablethe viewportlevels (ElementKinds), prepare → Drawn (pieces: packets with their placements, roles and how they draw while the object is open, plus type-owned detail) over the value shown before, refine once a value rests, highlight → ElementSet, convert, the status bar's counts (tally → Tallys) and a selection's box for the pivot (bounds_of)
Pickableviewport pickingthe host finds packet elements (Hits, box ElementSets); the facet turns them into Components and combines selections
ObjectPicking (picking)the object levelshow an object holding the value is hit by a Ray (at its world Xform) and framed; without it core frames the value by its bounds and doesn't pick it
Look (look)shadingthe pieces' colors while shaded, the images they show over them (SurfaceTextures: each polygon vertex's coordinates, image and Sampling, how it reads past the image's edges and between texels) and whether the value renders as a volume; without it the value shows as clay
Snappable (snapping)gizmo moveswhat a move may land on in the value, for the user's SnapTargets (a set of SnapKinds: points, edges' middles, faces' centers, crossings, edges, surfaces): a SnapQuery answered with SnapHits, a surface's with its normal there; the host keeps the rest of snapping (the master switch, the grid, ranking across objects, feedback)
Tabularthe spreadsheettables, columns, and cells read a range of rows at a time
Renderablerender syncRenderItem::Shape over a ShapeSource (triangles or curves), or RenderItem::Instances of other items

If a type does not provide a facet, the surface that needs it simply leaves the type's values out.

A type whose values hold other values (the standard Instances, their prototypes) draws and renders each held value through what the host gives in the request (DisplayRequest::held, RenderCx::held: a Held), handing it the call that would make the value's drawing or items: the host keeps what was made by the held value's identity while a holder holds it, so a value many objects hold is prepared, uploaded and sent to an engine once. Without one (a type's own tests), the type makes its own.

Components is a component selection described in the data type's own terms. A mesh uses element indices. A CAD body uses persistent topology names. The environment stores, undoes and counts a selection, but only the data type that made it reads what is inside.

A Mesher (Package::with_mesher) is a kernel's display mesh of a B-rep body (takes names the data type it meshes, the B-rep's, which the host shows at a display quality while it is installed): its faces' polygons in face order (face), then one polyline per edge over the same shared points (edge), at a Detail (in a view, Viewport of the object's display quality, one mesher preset a step) and the project's Tolerances, with the faces it couldn't mesh and why. The B-rep value holds no triangles; core caches each body's mesh, and draws, picks, renders and exports from it. Registry::meshers lists them in registration order; which one meshes is the host's choice (a user setting), never a vendor's. With none installed a body shows grey, without faces.

Kernels

A kernel is a heavy, shared algorithm library (Mesh: booleans, subdivision, remeshing, solvers, UVs; CAD: surface maths, B-rep booleans, a sketch solver). It is a package of Verbs (Package::with_verb): functions by id over Data and Parameters, returning Outputs, each declaring its inputs and outputs as Ports (Verb::input, Verb::output) as a node type does; the registry admits a verb only when its ports' data types are registered, and a run refuses inputs that don't fit. Registering a package offers its verbs to the whole process, and the registry refuses a verb another package offers already: it is the one table, and no package writes it. Any package's cook, render or worker reaches one by its id alone (kernel::find, kernel::run), with None or a Missing error while the kernel isn't loaded. A kernel's types never enter the SDK and no consumer links its crate: what a consumer needs is the kernel's documentation (its verbs' ids, inputs, rows and results), as plugins' other crossings need only standard values. A run is a cook's shape: rows through a CookCx, the caller's cancellation and project (a verb reads the project's Tolerances from CookCx::project's stored values), the process's workers; nothing of it is kept.

Cooking

Studio's core owns the cooking DAG for every context:

  • evaluation order, dirty propagation and invalidation;
  • the per-node result cache, its memory budget and reuse;
  • latest-wins scheduling, cancellation, worker threads and wakes;
  • time dependency and held edits.

A package owns only what a node is and computes. It implements NodeType:

  • description(): its label, its ports (Port: key, label, data type, multi, optional, why it can't cook unwired) and its parameters as a params::Schema at their defaults. The environment reads it once, when the package registers;
  • the description's handles: viewport handles bound to its parameters, as Houdini's handle bindings. A Handle names its kind (Translate, Rotate, Orient and Scale on Studio's transform gizmo, shown while the matching transform tool is active; Pivot, Distance, Radius, Direction, Angle, Turn, Box and Sphere as dots and rings on their shape), the parameters it reads and writes (a Vector is one three-number parameter or three Scalars; a Scalar is a number parameter, one number of a vector parameter, or fixed), its space (object or world), an optional placement Frame (Position, Up and Turn) its places are in, an anchor (which point of a shape its center names: a box built on its base), a constraint (one axis or one plane), a range for the numbers it writes, and the parameter values it shows for. Studio draws, hit-tests, snaps and drags them while the node is selected, writes the parameters as the pointer moves (the node recooks, incrementally where it can) and records one undo step a drag (inside a live tool's gesture, the drag is part of it). A handle whose place is on what the cook made (an extrusion's arrow on its profile, a revolve's ring about its axis) comes from the cook: Outputs::handles holds handles with fixed places and numbers bound by name, which Studio shows after the declared ones while the node is active and runs the same way; one that names a parameter the node type doesn't have, or one of another kind, is left out with a warning on the node (Outputs::check_handles). NodeInfo::handles reads them back while the node's stamp holds. A tool that needs more than a binding draws its own handles;
  • cook(cx, inputs): the outputs (Outputs: one Data per output port, the selection output 0 carries downstream, warnings) or an Error (cancelled, or failed with its diagnostic). An output nothing reads (cx.wants(k) is false) needn't be made;
  • reads(parameters): what besides inputs and parameters invalidates a node with those parameters, as data (time, input names, selection). What a node doesn't declare, core withholds from its CookCx, so it can't depend on it by accident;

CookCx gives a cook:

  • parameters by name (Parameters of params::Value; typed reads, CookCx::number, integer, toggle, choice (its key), text, fail with the parameter's name; they are ParametersExt's, which reads any Parameters the same way, an Edit step's or a compiler's);
  • which inputs are on which port (port), and input names when declared;
  • the component selection the node acts on and the one input 0 carries;
  • cancellation (check) and time; parallel work goes through geometry::parallel (chunks_mut, map_blocks) onto the Workers the host installs once per process, which receive each caller's location so the host can size runs per call site, so packages never start threads;
  • a core-owned scratch slot per node (scratch::<T>()), for state such as a tessellation cache. Core may empty it at any time, so it may only make cooks faster, never change their results.

Packages keep no evaluator, worker or private cache.

Held edits

Edit Mesh and Edit CAD hold an authored delta over their input. Core owns the mechanism: capturing the input once as the base, keeping one result per held state that its copies (undo snapshots) share, applying the delta anew when it changes, and undo. A package supplies HeldEdit for its deltas: the registry keys it by the deltas' data type, which a held node's type names (NodeType::held), so one package declares how one type of delta applies. It gives the base data type, the delta's data type (so a delta saves, stamps and counts memory like any value), which other inputs a delta reads (reads_input: what it projects onto), and apply(cx, base, delta, inputs), which returns the node's outputs and may keep prepared work in the node's scratch slot. How deltas are authored interactively is the package's UI, through studio-ui-api.

Networks and contexts

A context is one ContextId naming a kind of editing (Mesh, Comp, a package's own). It has two halves, each declared by the package that owns the context:

  • its networks, as data: a NetworkContext, registered with Package::with_context. It says how the context's networks evaluate (Evaluation::Cook, node by node, or Compile, below), which core flags their nodes offer (Flags: Bypass, Template, Frozen), whether its networks are chains, where deleting a node heals the chain, or libraries of results (NetworkKind), how the network pane draws their nodes (NodeLayout), the element they are stored under (NetworkStorage), and other packages' node types they offer too, each under a section of the context's (adopts: a trees context's networks may take a mesh package's Import; a node of an adopted type that isn't installed loads as a placeholder);
  • its UI: a ContextPart of the package's UiPackage (see The UI layer): what Tab, keys, menus and the viewport offer while the user works in it, its selection levels, its inspector.

A headless package declares only the first half: its context's nodes are then drawn, edited and inspected by Studio's own UI.

An object kind names the context its objects open into (ObjectKindSpec::context). Core keeps one network of that context beside each such object's body: it creates, edits, validates, cooks, undoes and saves it, as Houdini's host owns its node graphs, and the body never holds it. The object's result is what its network yields (NetworkRead::yielded): its NodeRole::Output node, else the node its display flag is on.

Node types say where they are offered. Description::context names each context whose networks offer a type, and Description::listed says where Tab, the menus and the shelves list it there: a section ("Create"), a Category that colors its tile, a family within the section and search aliases. A type without a listing is still added by code and by files, never by Tab.

Data parameters. A node whose authored state is a structured record (a sketch's curves and constraints, a stroke) holds it as one parameter of a registered data type: ValueKind::Data(DataTypeId) holding Value::Data, declared with Parameter::data at the type's empty value. Values compare by type and allocation (an edit always makes a new Arc), by the type's stamp where content counts (across a save and reopen). The inspector shows no row for it unless the node's schema adds its own (a summary, buttons); its node's tools set it through set_parameter, once per gesture (one undo step), and it is never keyed. Core saves it through the type's codec as a data child of the node's element, includes its stamp in the node's cook key and counts its memory through the type. A node whose data type isn't installed is kept as read and saved back verbatim.

One node shape. Every node of every context is its type, its Parameters, its input wires, the core flags and, optionally, a held delta or the outputs it keeps while frozen. Core stores a network as the network child of its object's element, typed as NetworkStorage::element says, with an element per node: its type, name and position, its flags, its wires, its parameters in one layout every type shares, and its delta and kept outputs through their data types' codecs. Packages never write networks themselves.

Reading and editing. A package reads a network through NetworkRead (its nodes by the host's NodeRefs, each node's NodeInfo, the displayed and the yielded node) and edits it through NetworkEdit: add a node (NewNode, inserted by Insertion::At, After a node or Grown from what the level displays), rename, move, set parameters, connect and disconnect, set flags and the display, remove, hold a delta, freeze. Each call refuses what doesn't fit (an unknown node, a port the node hasn't got, a value its parameter doesn't take, a flag the context doesn't offer) and changes nothing then. New parameters go through the type's created and edited ones through its edited, on every path (Tab, the inspector, NetworkEdit), so all paths agree. A package's UI reaches an object's network through HostNetwork (network, edit_network); each edit_network is one undo step, checked whole when it commits (no cycles, required values) and dropped when that fails.

Compiled contexts

A context declared compiled() (Comp's, shading's) isn't cooked node by node. Its node types are ordinary NodeTypes for their descriptions, ports and rows, but core never calls their cook. Core flattens the network into a Flat (subnets resolved, nodes in cook order) and hands it, with the output to show, to the context's NetworkCompiler. The compiler returns a Compiled: the package's own plan, opaque to the host, and the render outputs the plan reads (CompileCx::outputs lists those the render writes). Core keeps the plan until the network's topology or a flattened node's parameters change.

A compiler sees the installed node types through CompileCx::node_type. It reads what a type declares (its description, ports and spares) through the trait first. NodeType is Any, so the compiler may upcast a type to &dyn Any and downcast it to its own concrete type, but only for what that type alone knows (a Comp operator's lib-composit type, a shader's engine vocabulary).

A Material's Shading network is compiled the same way, by core: its node types are core's MaterialX nodes (one a MaterialX node, its Signature row picking the definition) and render::shader::ShaderNodes, one over each material node (NodeDef) an engine declares, and core compiles the network to the MaterialNetwork engines receive, each adopted for its engine (render-api's material module; see Materials). A Material Inside made from a MaterialNetwork (NetworkSeed::from) gives a MaterialX node its signature as material::mtlx::SIGNATURE.

The package that declares a compiled context registers its compiler (Package::with_compiler, exactly one per compiled context: the registry refuses a compiled context without one, and a compiler or runner for any other context). With the wgpu feature it also brings the plan's runner (Package::runners: a PlanRunner (network::run, the wgpu feature) makes a Runner on the host's device, which runs plans on the GPU). A runner reads the film as an Image of device layers (RunCx::film: the newest pass's outputs, each layer held as its display::Texture) and returns an Image of one such layer; its colors' space says whether it is scene-referred (RENDER_SPACE) or a display's, and the host shows a scene-referred one through the same view transform as the viewport's. What its File Outs write is an Image each (Runner::written).

Color. The host owns color management and lends it as one color::ColorService: the project's working space, conversions between spaces named as its configuration names them (convert, to_working, working_to), view transforms as WGSL, and a revision that moves whenever any of them would. Scene colors are linear in the project's working space and engines take and output linear ACEScg (render-api's RENDER_SPACE); formats convert a file's colors through Reading::colors and Writing::colors, cooks through the project they read (CookCx::colors), and the host's render target converts the working space to RENDER_SPACE. No package carries color maths of its own; conformance::Unconverted stands in for checks that convert nothing.

Saving

A project is a tree of elements. Each element has a kind, typed fields (scalars and flat arrays), text metadata and named children. Each data type and each object kind writes and reads its own element through its codec, DataCodec or ObjectCodec: the environment creates the element and hands the codec an ElementWriter to fill, or an ElementReader to read. Packages never see the file format; Studio implements both traits over its project documents, and element::MemoryElement implements them in memory for package tests.

DataCodec receives the base value an edit was derived from. This lets a codec write only what changed: for example, a mesh whose points moved stores no topology.

A value held in several places (two frozen nodes keeping the same mesh) is written once with ElementWriter::share and read once with read_shared, which hands every place the same Arc.

Codecs follow the project rules:

  • reading is strict both ways: the typed reads fail naming the field and the element, and Studio refuses a field, metadata entry or child a codec's read leaves unread, so a codec writes only what it reads back;
  • the element is the codec's alone: the host keeps its own fields elsewhere (an object's placement on the object's element, its body in a body child);
  • there are no defaults for missing values;
  • there are no version fields.

ObjectCodec::read also receives a ReadCx, a read-only view of what is installed (the render engines' descriptions, engines()). A body that needs something no installed package provides, such as a light of a type no installed engine declares, reads as an ErrorKind::Missing error with a plain message saying what is missing.

If a project uses a package that is not installed, it still opens, and nothing of it is lost. Everything no installed package reads is kept as it was read:

  • the nodes of that package's types: they show grey with their type's name, don't cook (nothing after them cooks), and their parameters are read-only, though they can be moved, wired around and deleted;
  • the objects of its kinds, and the bodies that read as Missing: they show grey, and can be renamed, moved in the tree and deleted;
  • its stored values, and values of its data types.

The Console says once per missing package what it kept ("Couldn't load 3 nodes and 1 object: acme.trees isn't installed"). Saving writes all of it back unchanged, so once the package is installed the project opens as it was.

Objects

An ObjectKindSpec describes one kind of object. It gives:

  • the label and icon;
  • whether its objects are placed in the scene;
  • the context of the network inside them (core keeps that network beside each object's body, and creates, edits, cooks, undoes and saves it; the body never holds it);
  • the data type of their result;
  • a new body;
  • a codec;
  • adopted and adopts (none by default): the data types it can be made from, and what an object made from an imported value starts as (an Adoption: its body, its network's content as a NetworkSeed when the kind builds it, and the references to the file's keys that now resolve inside it);
  • for a kind that holds no network, value(body, path): what a reference naming the object reads (a light's value). The path is the object's own, so a rename, like a body change, makes core read it again and recook its readers;
  • creations and create, the ways an object level makes its objects, given a CreateCx: the installed engines.

An ObjectBody is the part of an object that the package owns: the kind's own state (a Mesh object's display settings, a Render Output object's engine and settings). The environment keeps the object's name, its place in the hierarchy, its placement, its flags and its network. Bodies are immutable and are replaced whole, so undo keeps the previous body.

A kind whose objects are render prims themselves (an engine's lights and fog) says so through an optional facet, so the host's render sync never reads the body:

  • renders(), a RenderedKind: what an object is, as Declared prims of types its engine declares with every value it holds (declared), and the body holding an edit of them (with_declared). The host keeps, for the active engine, the values that engine's type declares, places the prims at the object, decodes the standard ones (UsdLux lights) for the viewport and exports, and draws the inspector rows from the types' schemas.
  • at_root(): its objects live only at the scene's root (an engine's fog, Render Output).

Each engine's package brings its own lights, and its fog if it renders one, as object kinds with this facet; the standard light and fog types (kinogaki_render_api::standard) are a shared vocabulary of plain data, so a light value from a file is adopted by any engine's Light kind. Render Output, the scene's render outputs (which engine renders through which camera, and the Comp network that corrects the result), are core's own kind.

Formats

A FileFormat declares:

  • its label and extensions, and optionally detects, for a file recognized by its first bytes;
  • the data types it produces (none for a format that only writes, such as G-code);
  • its options: defaults by name, a params::Schema for a Purpose (File ▸ Import's Objects, or Value, one value of the type the host wants: a File node's output, an image of the host's image store, which leaves out options that shape objects), and how they are saved (write_options/read_options; by default one field per option).

It reads an Input (read(&input, options, &Reading)): a file on disk (Input::file), a file kept in the project (Input::embedded, an EmbeddedFile, its bytes shared), or bytes lent from memory with a name (Input::memory). An Input gives its name (the extension and the project's file rules read from it), its first bytes (head, what detects sees), all its bytes, and its path when it has one, beside which a format finds the files it names (an OBJ's MTL); held gives all its bytes as one shared EmbeddedFile, whose ranges (EmbeddedFile::slice) a format keeps as the files packed in it (a usdz's textures) without a copy. The host picks the reader by type, then by the name's extension, then by the first bytes. A format reads the input into Imported, a list of FileItems with parents listed before their children; an item with no data is a group, and each is placed relative to its parent and shown or hidden. A Reading carries the purpose, the project's Units (lengths convert into them), the host's ColorService (colors convert from the file's space into the working space), a format::ReadBudget (what one read may spend: bytes of the files it names, one allocation, all allocations, nesting depth, a decoder's expansion; and its cancellation, which every check polls), and for Value the data type it wants (wants: a STEP file read as B-rep or as polygons). For Value a format reads one item. A format may also provide a FormatWriter, which writes items (write(path, items, &Writing, options), returning its warnings: what was written with loss or skipped) with options of its own: defaults by name (FormatWriter::options, none by default) and an Export dialog (FormatWriter::schema). Export hands a writer its own options, never the format's reading ones. A Writing carries the project's Units, the host's ColorService (the items' colors are in its working space), a CancelToken long writes (and their meshing) poll, and meshed (a value as polygons through the installed mesher, for a writer that writes only those).

format::options holds what formats share about options: reading a typed option (option; a choice is held and saved by its key, so a saved setting survives a reordered list), the rows most import dialogs show (scale, toggle, embed: Embed Textures, options::EMBED, off by default), and the rows a node holds a format's options in (row: <format id>/<option>, a File node's for its readers, a node writing files' for its writers; from_rows reads them back over the defaults).

Untrusted files. A reader checks every size, count and depth a file claims against the read's budget before acting on it (a picture's size against ReadLimits::pixels, ReadBudget::pixels), follows each reference once by the identity of what it names, and reaches the files a file names through format::files::Named alone: wherever they are; regular files only, never a device, a pipe or a folder; within the budget; what it left unresolved listed in one warning a reason. A read ends in its items or an Error, never a panic, a stack overflow or an aborted allocation. FIRST-FORMAT.md has the policy in full.

Images. A format of pictures (PNG, JPEG, OpenEXR) reads a file as one item holding one standard::image() value, as the file holds it, converting nothing: its samples as stored; each channel group a layer (an OpenEXR's parts are the layers of one Image); color layers tagged as the file says (ColorTag: a space's name, an ICC profile, primaries, else unknown, linear for float samples), straight or premultiplied as the file has it; data layers raw; every attribute the file carries as the Image's metadata. The host resolves the tag to a space of its configuration and converts; a format holds no color maths. A writer of pictures takes one item holding an Image the host made and tagged for the file: as a display shows it, through the view a render is seen in, when the writer says its files are display_referred (PNG, JPEG), else linear light (OpenEXR). It copies the tag (the sRGB display is color::SRGB_DISPLAY, the interop name configurations alias) and image::metadata::CHROMATICITIES, writes each layer as the file can hold it, and warns of what it left out.

Materials and images. A file's materials travel beside its geometry as one item of standard::materials() (kinogaki.core.materials, which core registers with MaterialsCodec), first among the items: Materials { materials, images }. An ImportedMaterial holds the key its polygons (or instances) name in their material attribute, a library name, inside (its whole Shading network as the file records it, every node kept, those its surface doesn't read too, each texture's file an Image node made by image_files::image_node::reading wired into the texture node's file), standard (its OpenPBR Surface form, which a host takes when it lacks a node type inside holds: an engine's own material, as the engine approximates it; none when inside is MaterialX's nodes alone) and double_sided (the Material's Double Sided). An ImportedImage holds a key, a name and the ImageSource its Image node reads (a path, or an EmbeddedFile with its name, and its color); a light names one by image_files::OP and its key (op:sky in a dome's image). Keys are one namespace. File ▸ Import adopts the item as a Material Library: each Material, double sided as the file says, holds inside when every node type in it is installed, else standard, else nothing, with one warning a package missing (Adoption::warnings); each image becomes an Image node of the library; the keys become paths (Adoption::renamed) in the polygons' and instances' material and the domes' op:. Export hands a writer the same item: each Material its items name, its whole network as authored, its standard form and its Double Sided, and the images its domes name, keyed by the node's path; every path absolute. A writer of one shading model reads a material as a network (MaterialNetwork::try_from(&NetworkSeed): its MaterialX nodes, the Image nodes wired into them as their images) and takes it to its model with MaterialNetwork::translated (the chain of translation graphs to standard_surface, gltf_pbr, UsdPreviewSurface, folded, textures as they are); format::files::Beside names the images a document names, a file by its path relative to the document, an embedded file written beside it once for equal bytes.

What formats share. format::Conversion changes a file's space into the project's: a uniform scale and a turn between up axes, or a mirroring for a left-handed file; points, directions, lengths, a value's placement (C·M·C⁻¹) and a camera's or a light's (Q·M), and its inverse for a writer, which divides by the scale the reading multiplies by. format::Unique gives names out once each, counted per base name (Unique::files numbers before the extension and ignores case). format::files::replace writes a file whole or not at all, every writer's files through it. format::materialx converts a MaterialX document's materials to and from ImportedMaterials (read, write) and a texture's colorspace to and from an ImageColor (image_color, colorspace). Spaces are named by their interop names (color::LIN_REC709, color::SRGB_TEXTURE, color::SRGB_DISPLAY), and an ImageColor of a space is made by ImageColor::space, which refuses auto and data, the keys of the other two (ImageColor::key). Parallel work runs on the host's workers (geometry::parallel::map, map_blocks, chunks_mut), never on threads of a package's own.

To import, the environment passes each value to the first object kind that adopts it, lending it the engines; the kind says what it approximates as the adoption's warnings. Formats are offered the types kinds declare as adopted, not their results: what an object can be made from and what it gives are separate. With no formats installed, nothing can be imported.

Session slots

Some state belongs to a package and lasts only for the session (a tool's picks, a board's pins). The component selection is the host's, whatever the context (HostSelection::selected_components, select_components): a pick is an undo step like a slot's edit. A package declares each such piece of state as a value type implementing Slot and registers it (SessionSlot::of::<T>(), Package::with_slot):

  • id and initial; undo, redo and a cancelled gesture restore it with the document;
  • restored: after an undo, a redo or an edit, the value brought in line with where the user is (SlotDoc);
  • estimated_bytes for the undo history's budget; values compare with PartialEq, so an unchanged value records no step.

SessionSlots holds every slot's value, shared and copy-on-write: a clone is a snapshot (one pointer a value), and changing a value copies it only while a snapshot still shares it. Studio keeps its undo history with these tables.

Session state is never saved in the project.

Stored values

Some values belong to a package and to the project rather than to an object: a kernel's tolerances for the project, a context's rows for each view; and some to the user, across projects: a tool's preferences. A package declares each as a value type implementing Stored and registers it (StoredValue::of::<T>(), Package::with_stored):

  • id, a StoredId (kinogaki.cad.tolerances) naming its element in the project, under the package's vendor and declared once among the installed packages (the registry refuses a second);
  • scope: one value for the project (Scope::Project), one per view (Scope::View), or one for the user (Scope::User, kept in the user's settings folder, never in a project);
  • defaults, what a new project or view starts with;
  • write and read, its codec; reading is strict, as codecs' is.

The host keeps them in StoredValues tables (the project's, and one per view), shared and copy-on-write like session slots, and saves each into the project under an element naming its package, so a project holding a value of a package that isn't installed skips it. Core keeps one view value of its own in every view's table: what the view draws, its display popover's state with the per-view display options the user set there (see "The display popover and display options"). A package's UI reads and sets the project's and the user's through HostDoc (typed through HostDocExt::stored); a project value's change is a change to the project, saved with it, and a user's is saved with the user's settings. A cook reads the project's values when its node declares Reads::project (CookCx's Project::stored); a change recooks it. Cooks never read the user's, so a project cooks alike for every user.

Icons

Studio owns a named icon set, and its icons are the default: wherever the API asks for an icon by name (ObjectKindSpec::icon, Description::icon, the UI layer's buttons, painter icons and library entries), a plain name such as "cube" is Studio's. A package adds icons of its own with Package::with_icon: an Icon is a name within the package and an SVG document the package embeds. Others name it by the package's id, a / and that name: icon leaf of acme.trees is "acme.trees/leaf". A package registers icons under its own id alone, so packages' names never meet each other's or Studio's.

Studio draws a package's icon exactly as it draws its own: as pixel art, its viewBox its size in icon pixels (whole numbers, at most icon::MAX_PIXELS either way, the size of Studio's own), every pixel solid or clear and tinted by the widget that shows it, rasterized once per size and cached. So an icon is one color, drawn in white or currentColor.

Studio checks each icon when the package registers: its name, its size (at most icon::MAX_BYTES), that its document reads and that its viewBox fits. An icon that fails is left out and the Console says why, once; the rest of the package registers as usual. A name that finds no icon draws Studio's neutral icon, and the Console names it once.

Icons mark what has no text of its own: toolbar buttons, tab buttons, the kinds of objects and nodes in the outliner. Menus and marking menus show text alone, and a node tile shows its category by color.

Cancellation and time

Long work polls a CancelToken: preparing a display, converting, reading a file.

Time is the scene moment that cooks and render prims read: a whole number of ticks (1/705,600,000 s, so frames at the common rates are exact); Rate is frames a second, checked finite and above zero, and converts between the two. A value across a frame's shutter is Sampled (one sample holds still): render prims' placements are Sampled<Xform>.

Rendering

Engines never see kernel types. A data type's Renderable facet is asked through a RenderCx (the scene time, a Detail of Viewport(DisplayQuality), the object's, or Render { chord, angle } that says how fine to make geometry such as a CAD body's tessellation, a standard value that render-api re-exports, the project's Tolerances an approximate value is made to, a cancel token, and the host's Held), and returns RenderItems:

  • RenderItem::Shape { part, source, label }: part is a key stable across recooks (a CAD body's index), so the host matches the items of successive values; source is a ShapeSource, an immutable snapshot with shared columns; label is the name Cryptomatte gives the object.
  • RenderItem::Instances { part, prototypes, which, xforms, primvars, label }: placements of other items, which an engine instances: what it made of each prototype is shared however many instances place it. An instance's material renders its prototype in that one material, its material_map some of the prototype's materials as others (standard::material_map, standard::instance_materials), and the host sends them as the Instancer's subsets and material_maps, the prototype still one shape.

The source contract (ShapeSource, Topology, Ranges) is kinogaki-render-api's, the render interface engines build on, and render re-exports it; so a source a data type makes is what a host hands its engines, with no conversion. RenderItem stays here.

An engine's renderer builds on kinogaki-render-api alone, and its package (its lights, its export) on kinogaki-studio-api too: FIRST-RENDER-ENGINE.md is the contract, end to end, from the description to previews.

The host keeps engine scenes in step cheaply: a source with the last identity is skipped; one with the same topology is a points update narrowed by moved_since; primvars and subsets are resent only when their columns change. Shading normals (N) are asked for lazily, after a drag ends.

The viewport

Studio's core draws and picks. What it draws is a packet behind kinogaki-viewport-api's SurfaceSource trait: shared point positions, vertex shading records, triangles, line segments in line classes, each class with its own look and ids, point markers, and the ranges that changed since the packet it was derived from. A kernel that needs full control (smooth and authored normals, curvature, hulls and control vertices, see-through faces, live transform previews) makes its own packets and reads them back as their own type with packet_as. The contract speaks the SDK's geometry vocabulary: points Vec3, placements Xform, boxes Bounds, quaternions [x, y, z, w]; bulk columns stay plain arrays.

The simpler path is a viewport::Surface, a SurfaceSource itself: points, polygon corners (point, face, normal), triangles over the corners, and line segments with edge ids. During a drag a type returns a surface derived from the last one, with the points that moved, and the host patches only those. Picks come back as Hits and box selections as ElementSets, in the ids the packet gave.

Displayable::prepare returns a Drawn: the value's pieces in drawing order. A piece is a packet, where it sits in its object (none, or several placements sharing one upload), its role (the object's own surface, or a templated node's faint result the open context marks). Core draws every piece while its object is open too. prepare receives what the type made for the value shown before, so a type whose values change a little at a time builds on it, and refine offers a finer display once the value rests.

Self-contained

The SDK depends on the standard library and two crates.io crates its standard geometry values need, at exact versions: blake3 1.8.7 (the topology fingerprint) and bytemuck 1.25.2 (plain records and free casts into columns). Beyond those, two optional features: kinogaki-render-api's wgpu (the display contract), which brings wgpu 30.0.1, and kinogaki-viewport-api's bytemuck (byte casts of the records a renderer uploads), from crates.io. kinogaki-studio-api's wgpu feature turns the first on for the makers of packages' render engines (Package::makers; their descriptions, Package::engines, are headless), and is off by default. Every type above is the SDK's own, so a package compiles against nothing private and the SDK builds from a plain clone. Studio converts between these types and its own libraries' at the boundary (its maths, its property schemas, its project documents, its viewport and render libraries, its worker threads).

The UI layer

kinogaki-studio-ui-api is how a plugin shows things: an inspector, a tool's card, viewport overlays, a settings page, heads-up value fields.

The host draws

Plugins never see Studio's UI library or egui. Every type a plugin draws with is this crate's own: geometry, colors, the theme, fonts and icons by name, input, a painter and the widgets. Studio implements the Ui and Painter traits over its private UI library, and each call maps to exactly the call it replaces there. So the SDK stays self-contained, a plugin compiles against nothing private, and Studio can change how it draws without breaking a plugin.

The crate depends on kinogaki-studio-api (for parameter values, units and diagnostics) and the standard library, nothing else.

What a plugin uses

From the most declarative to the most direct:

PartWhat it isUsed for
sheet::Sheet<K>Bands and typed fields with their values, as data; the host draws it with Ui::sheet and reports Editsinspectors, a tool's card body, settings pages (PageRows::rows(…) -> Sheet, then apply the Edits)
CardSpec, PopoverSpecFloating panels the host frames around a body (a popover as the host's raised popup or a flat panel)a tool's parameters; a picker beside a handle; a list at the pointer
ValueFieldsThe heads-up boxes a drawing tool types into; Ui::value_fields draws them and holds the keys while showna line's length and angle while drawing
UiImmediate widgets (labels, buttons, toggles, number fields, sliders, text fields, choices, swatches, menus) and layoutsmall panels the schema can't express
PainterSegments, circles, rectangles, polylines, text and icons, single or in batches; text measuring; reserved slotsviewport overlays: handles, guides, badges, labels
UserInputThis frame's pointer, keys and typing, as plain datatools reading the pointer and shortcuts

A node's parameter schema (params::Schema) becomes a sheet with From, keyed by parameter name, so the host shows every node's parameters the same way.

A node type shapes its rows from what its node reads (NodeType::schema with a SchemaCx: which ports are wired, what each input held at the last cook, what the node itself made then while its stamp holds (SchemaCx::output: an Auto choice can name what it came to), the names of the nodes behind each port's wires). It hides rows a wire or a choice makes moot, fills its kinds' options (ValueKind::Attribute's menu of the input's attribute names; ValueKind::Integer's menu, the names behind the wires, for an integer choosing a wire), marks a faulty value with its problem, and adds Button rows. A button holds no value: pressing it asks NodeType::pressed, and core applies the answer as one edit on the inspector's own path, so NodeType::edited answers it too. A choice shows as segments while it has three entries or fewer, 18 characters together at most (sheet::segments), else as a menu. A held node's delta shows its own rows (HeldEdit::rows, over the node's held result), written back through HeldEdit::apply_rows and HeldEdit::pressed. SchemaCx::purpose says where the rows show: the inspector, or a tool's card (RowsFor::Card { more }), where a type may show fewer.

Every kind of value has a sheet kind, drawn by Studio's own widget for it, like Houdini's parameter templates: numbers, vectors, colors (with an emission's swatch, Trailing::emission), linked RGBA channels, choices, toggles, text, code, secrets, file paths, free paths in the scene's tree, references (Kind::Reference(RefFilter): the field says what fits, such as objects of some kinds, results of some data types or Materials, and Studio fills the searchable picker, takes drops from the outliner and networks, and resolves the path from the sheet's holder), float and color ramps (Value::Ramp; kinogaki_value::ramp, re-exported as params::ramp and sheet::ramp, is their one maths: nodes, Studio's ramp widget and render engines evaluate them alike; a ramp row may fix one interpolation for every key), tone curves (Value::ToneCurves) and a material input's connection button (Socket). A file path's folder button opens the host's file dialog as its FileDialog says, and the chosen file arrives as an edit of the field; a host may give it nodes too, a picker beside the button of the nodes it may name instead (an image row's Image nodes, written op: and their paths). A field's animation (Animation: static, animated, keyed at the current time, or overridden: set from elsewhere than its own) is its value field's tint; Studio sets it on the rows it animates (node parameters, objects' placement and kind rows) and marks them keyable, so an Alt+click on the label reports Action::Key and Studio sets or removes the key there. A package's own rows are not keyable unless it marks them. Raw Ui widgets are for custom panels only.

Meshes, CAD surfaces and wireframes never go through the painter. They are the data types' display surfaces (the Displayable facet), drawn by the host's viewport; the painter is for the few hundred marks a tool puts over them.

Rules

  • Immediate mode, on the UI thread. A plugin calls its Ui every frame, inside the host's frame, and what it called is what shows. Nothing it draws persists. No call blocks; long work belongs on the host's workers.
  • Bodies run once. Layout methods take their body as &mut dyn FnMut(&mut dyn Ui) so the traits stay object-safe. The host calls it exactly once before returning; menu bodies run only while the menu is open. UiExt wraps the layouts for closures that return a value (horizontal_with and so on).
  • Ids name what a widget is, never where it is. Make them from stable keys with Id::new and Id::with. UiExt::sheet_keyed turns any hashable field key into a FieldKey by hashing it, so a row keeps its focus or open dropdown when rows above it come and go.
  • Colors come from the theme. Theme is filled in by the host. Use the token that names the role (surface, muted, accent), and ink a fill with Theme::ink_on, so a plugin follows the host's theme. Color is premultiplied sRGBA bytes; its arithmetic matches the host's to the bit.
  • Icons are named. A plain name ("cube") is one of Studio's icons, the default set; a package adds icons of its own (Package::with_icon), named by its id, a / and the icon's name ("acme.trees/leaf"). An unknown name draws Studio's neutral icon, and the Console says so once.
  • Values are edited in place. A widget writes what it edits into the &mut the plugin passed and reports changed. A sheet writes new values into its schema and reports Edits: the fields changed, the actions taken, and the gesture's start and finish, which one undo step spans.

Viewport maths

Some pure maths the host's own tools use lives here, so a plugin's tools hit, drag and label exactly as Studio's do:

  • projection: a view's ViewProjection (clip space from the edited object's space and back, and the direction towards the viewer), filled in by the host from its camera for one view rectangle's aspect;
  • gizmo: the transform gizmo's frame, handles, hit testing, and what a drag of a handle means (move along an axis or plane, scale factors, angle changes). The host paints the gizmo;
  • labels::point_labels: where to put point numbers so a dense mesh shows an evenly spread, readable subset at a bounded cost.

The host interface

A package's UI never sees the editor. Each call into it gets a context, &mut dyn host::Cx, reaching the host's capabilities, one trait each: HostUndo (gestures, undo, redo), HostSlots (typed through HostSlotsExt: slot, and the one mutator edit_slot(label, |t| …), an undo step named at the call, part of the open gesture's), HostSelection, HostDoc (where the user is, objects and their bodies, a body set as one undo step with set_object_body, stored values, an object's shown result and the polygons it draws as with shown_polygons), HostNetwork (objects' networks, read and edited), HostView, HostStatus, HostShell, HostTransform and HostTools (the tools' remembered parameters and the node a tool's card still adjusts); plus the plain HostPrefs, the wake for the package's workers, latest-wins jobs on the host's lanes, typed by a LatestKey (HostLatestExt::latest(asked, job), take_latest() -> Option<(Asked, Out)>: the host runs a job only when what it is asked for changes, a newer ask cancels the older, only the newest ask's result is given, and the host wakes the package when it lands, so a package asks every frame and compares nothing itself), the host's current theme (Cx::theme, the one Ui::theme draws with), and a service over the host's drawing: UnitsService (numbers in the user's units). The viewport parts' calls about one view (LiveTools::tools, Panels::panels, Display::overlay and edited) get a ViewCx (the view as data: its rectangle and the pane's, its response, what the pointer is over as a PointerOver, its camera; this frame's UserInput; its snaps and its display options), the inspector an InspectorCx. Among the capabilities: HostUndo::begin_gesture gives the gesture's GestureToken, which alone finishes or cancels it (none while another's gesture is open: what follows is part of that one, so a drag inside a live tool's run stays the tool's step), HostUndo::back_to(token.start()) puts back what the gesture changed and keeps it open (a live tool recomputing from its start stays one step), HostView::rendering says whether a view renders, HostStatus::report writes a Diagnostic to the Console, about a subject the context spells (an error shows on the error line too), and HostShell::copy_text/paste_text use the clipboard. Core's node sheet draws a node's rows; Inspector::row_action (a group or pick row's pick in the viewport, RowAction::Pick, from the inspector or a tool's card) and Inspector::typing (the text as typed, for live feedback) tell the context, and core owns file, path and reference rows.

A package's UI (package::UiPackage) is a state type, its contexts' UI halves (ContextPart<S>, under the context's id: a Library and optional parts, each a capability: Levels; in the viewport LiveTools, Panels, Transforms, Display; Inspector, Vocabulary; a context has the parts it gives and nothing else; what an object level creates is each object kind's own, ObjectKindSpec::creations; the network half is the context's NetworkContext, see Networks and contexts) and its pages of the Settings window (SettingsPage { scope, label, rows }: PageRows over a draft of one scope's stored values, drawn and applied lent the package's state and a Cx, so a page shows what is live; a SettingsPage::user page is a page of the window, its values saved with the user's settings and never undone; a SettingsPage::project page is a section of Settings ▸ Project over the project's values, the standard ones such as ColorConfiguration and the package's own of Scope::Project, and OK or Apply makes its changes one document edit); Levels::hovered and Levels::picked hand it what the host picked at the active level (a ray, box or lasso through the open value's Pickable, with the host's pick policy and a Combine from Shift and Command), and the context decides what the hit selects; its id, label and slots are its Package's. Package::with_ui (package::WithUi) adds it to the package and erases the state's type, and the host reads it back with package::hosting::Registered::of (the erased forms of the parts are made from each part trait's one definition, so no call can be left unforwarded; the hosting module is the host's alone). A context whose networks are core's needs no UI half: the host gives it its network pane, Tab and menus listing the context's listed node types (with Display, Bypass and Collapse into Subnet), and its node inspector, and a ContextPart adds to these or replaces them (its library's items first and what it sets winning, its vocabulary running its own items, its inspector drawn under the node sheet). The host keeps the state, and for each call the host takes the state out, lends it to the part with the call's context, and puts it back. Host methods never call back into a package; what would (an undo ending a live tool, an action) is queued until the call returns. Undo never holds a package's state: it restores the document and the slots.

The library. What a context offers the host's menus is data, read once when the package registers: its items (each tool, command or node by its stored name, with its label, family, search words, Tab section, the levels it applies at and which surfaces offer it), its key store and default bindings, its A/S/D marking menus and shelves (the user's to edit, kept under the menus' store name), its right-click menus and menu bar sections (each for a level, a selection or none, inside a held edit or outside one), how Tab lists its items (tools over the views while it edits, else nodes; a Suggested category) and the viewport chrome it takes (viewport: None for core's view of the scene, ViewportSpec::scene(), or exactly a spec of its own: its display options, the starting pivot, the host's transform tools). A context may share another's keys or menus (Shared::Of). The host builds every one of those surfaces from it. An entry that can't apply is left out, never greyed: its When (level, selection, held edit) and the vocabulary's shown decide; label renames an item by its state ("Repeat Bevel"). The vocabulary runs the items.

Select All. Edit ▸ Select All runs core's Select All in whichever context is open, and each context answers it its own way. A package's context answers it by holding a library item named command:select_all (core's id for Select All) and running that name in its Vocabulary, as an item invoked from the views (Invoked::Views). The item can also sit on the context's own menus. A context without that item, or without a vocabulary, doesn't answer, and Edit ▸ Select All is unavailable while it is open.

The edited surface. Core renders every view, and draws the open object's value itself (its drag previews too). While a context's object is open, its Display::edited (a query on its state, after Display::settle ran once that frame) returns, per view and frame, an Edited: what it adds over that value, never a surface of its own: overlay layers (each bound to the topology it was made for, so one made for the last cook drops out while the rest show) and guides, whether its points show at any level (points), which of the value's own pieces draw (pieces, chosen without building anything); which nodes' cooked values draw ghosted (ghosts: a tool's input beside what it makes, each in a GhostTone the host draws in its theme's roles: Neutral plain, Add the accent, Remove the error color); core draws the selection (Levels::components, one set per element kind, each in its kind's style: edges and faces of a Shift-mixed selection both show) and the hover (the hit it handed Levels::hovered) itself through the data type's Displayable::highlight. Everything in it is an Arc of the packet contract's types, so core borrows it into its renderer's scene without a copy and the renderer uploads only what changed allocation. A drag of polygons' points (the host's gizmo handing Transforms::transform its steps, or a tool's own drag) shows through Edited::moving, an edit::preview::Moving: the context resolves the moved points and their soft weights once when the drag begins and hands the same Arcs each frame with the current step in its edit::deform::Deform (its Symmetry for a symmetric move), placed by the function its commit cooks with. Core previews it over the shown value (no cook, no cached value, only the moved points' polygons sent to the GPU) and keeps the last step shown while the committed edit cooks.

Live tools. The rest of the viewport is core's: the left toolbar (the same manipulation tools everywhere), the status bar, framing, the gizmo, the pivot (the user's choice per context, starting at the spec's default_pivot, with Pick through the value's Snappable; HostTransform::pivot is where it resolves, and Levels::pivot_changed tells a live tool it moved) and component picks. Which gizmo shows is one question, Transforms::gizmo (GizmoUse::Selection: the host's gizmo at the selection's pivot, its drags handed to Transforms::transform; NodeHandles: the selected node's handles in the transform tool's place; None); a context without a Transforms part shows the node's handles. A context's LiveTools add its live tools. A frame asks holds (a live drag keeps the other views and Space), then each view's Panels::panels (where its floating panels are, Panel { id, rect, scope }, a View's own or over the whole Pane; the pointer over one is the panel's) and LiveTools::tools before the camera's navigation: the tool reads its view (ViewCx::view: PointerOver::Handle says a handle has the pointer, so a tool leaves the press to it) and the frame's input (ViewCx::input), and says what it Takes (picks, gizmo, handles, hover, wheel, menu, each kept from core apart; a tool that reads no wheel leaves it to the camera). A press with Alt held, or of the middle button, is the camera's until it is let go: neither the view's response nor the input shows it to the tools. Then core's gizmo, handles and picks as the tools left them, and the frame's commands; then Display::settle once (where the context adopts what changed and asks its latest-wins jobs), then each drawn view's Display::overlay (paint only: heads-up values, labels), each panel drawn once (Panels::panel: a tool's card through Ui::card, its body the tool's node's rows drawn by the host with Cx::node_rows, RowsFor::Card: a node type's schema may leave out rows the run isn't adjusted by, NodeRows::hidden says some were and the card offers More; their edits take the inspector's path, inside the tool's gesture), and each view's Display::edited. Keys come first: LiveTools::key sees a key or typed text before any binding (HostShell::keyboard_taken says a field has the keys), never Escape. Escape goes to the first of a handle drag (it puts back the drag), the host's open popup, popover, menu or Tab browser, the transform tool's Pick, and then LiveTools::cancel(Ended::Escape); an undo or a redo cancels the tools first (Ended::Undo). Going on to something else keeps a tool's work, as Enter does: the location leaving its context, a node picked or edited in the network, each ask LiveTools::accept, its gesture one undo step, and the navigation goes through. A tool snaps its own drags with ViewCx::snap (a screen point, the targets, a radius and what to snap on, SnapOn: the open object, another object's shown value, or a node's cooked value, a tool's input upstream of what its object shows): the host searches that value through its type's Snappable exactly as the gizmo does, answers in the open object's space and marks the hit as it marks the gizmo's. A node's value is cooked and prepared for display as an object's shown value is, kept while it stays; until it is there the snap finds nothing. The same values draw ghosted in a view through Edited::ghosts. While LiveTools::live_node names the tool's node, the open network's templated branches are pickable: a click wires one into the node's operand input inside the tool's gesture, and core marks the branch under the pointer and those wired in. Tab over the views lists the context's tools while its TabSpec::tools holds.

The display popover and display options. Each view has core's display popover beside its Camera menu, its rows drawn by core's one popover drawing (property sheets for its fields) and placed for tests at ["Popover", "Display", label]. It holds what the view draws, each view's own (saved with the project's view, never undone): Wireframe or Solid (Render is the Camera menu's), the grid, and under Solid Flat or Shaded, the wireframe over Solid, back faces tinted; back faces culled; and under Shaded the analysis modes (Zebra, Isophotes, Curvature, Draft, Normal colors) with the chosen mode's settings. Under Overlays come the open context's display options (ViewportSpec::display, each a DisplayOption: an id, a label, a scope and a kind): a Toggle or a Choice, each PerView (the view's own, kept in core's view value and saved with the project) or Context (the same in every view, kept in the user's settings); a section with nothing to show is absent. Neither is undone and nothing cooks when one changes. Any call reads one with HostView::displayed(view, id) (a view's call for its own view, settle for every view before they draw): as the user set it in its scope, else the option's default (a toggle's 1 or 0, a choice's index); 0 for one the open context doesn't declare. The context draws what an option shows itself, in edited or overlay. The Gear at the foot of the left toolbar holds the transform tool's settings here: core's space row while the context takes the host's transform tools (has a Transforms part), then the context's own sheet (Transforms::gear, its edits handed to apply_gear); it shows while it has rows.

Rules: every call runs on the UI thread and returns at once; a query gets &S and &dyn Cx and changes nothing; workers wake the host once per result and the host calls UiPackage::poll; no timers.

Testing

studio_ui_api_testing::FakeHost stands in for Studio's host interface (slots and objects' networks with a real undo history, selections, a place, views and stored values as plain fields, every request recorded as a HostCall), and studio_ui_api_testing::Hosting calls a registered package's parts on it as the host does.

FakeHost also keeps the clipboard, the views that render, the display options' values in each view (displayed, what HostView::displayed answers), the view call's input, and the latest-wins jobs (an ask equal to its kind's last runs nothing), which run_latest runs as the host's lanes would; studio_ui_api_testing::view and camera build a view to call the viewport parts about.

studio_ui_api_testing::FakeNetwork is one object's network in memory, made from the packages whose node types it holds. It answers NetworkRead and NetworkEdit as core does, refusing what core refuses, and cooks a node through its type (cook). Put into FakeHost::networks, it is what a package's tools edit through HostNetwork, one undo step an edit; so a tool's network edits, and a node type in a network, are tested without Studio. It is a test double, not core: it caches nothing and cooks every node anew, and its rustdoc lists where it is simpler than core.

studio_ui_api_testing::RecordingUi and studio_ui_api_testing::RecordingPainter are headless fakes: they lay widgets out simply, record every call and every shape, and play back scripted user actions (click a button, edit a sheet field, type into a field, pick a choice). A plugin's unit tests drive its panels with them, with no window and no GPU.

Write your first context plugin (sdk/docs/FIRST-CONTEXT-PLUGIN.md)

A context is a kind of editing: what Tab, the keys, the menus and the inspector offer while the user works inside an object, and the selection levels they work at. This walkthrough builds a plugin with a context of its own: a Pin Board object that opens into a pinning context with two selection levels, a Pin and a Clear command, an inspector row and an entry in Create. Its tests run the UI on the SDK's fake host, as Studio runs it.

Your first file format (sdk/docs/FIRST-FORMAT.md)

A file format is a package that reads files into the SDK's standard values and writes them back: Studio's File ▸ Import and Export, and the File node, use it. It knows no context and no kernel: what it makes is standard Geometry, materials, images, lights, cameras and instances, which any package's object kinds adopt.

Write your first node plugin (sdk/docs/FIRST-NODE-PLUGIN.md)

This walkthrough builds a plugin that adds a Box node to Kinogaki Studio: a box data type that shows in the viewport, renders and saves; a Box node that makes one from its size and center, with a handle Studio drags; and the Boxes context and object kind that make the node reachable from Tab. It ends with the plugin built against Studio's plugin kit and installed.

Your first render engine (sdk/docs/FIRST-RENDER-ENGINE.md)

A render engine is a package that renders what a host sends it. Its renderer depends on kinogaki-render-api alone, and sees no kernel, no document and no network: only prims, in batches, with what changed about each. Its package (the lights it brings as object kinds, export_package!) depends on kinogaki-studio-api too, with its wgpu feature.