Toolbox
Open in GitHub

Xyz

State ownership primitives, a declarative Tweakpane debug layer, and Three.js scene warmup to kill first-frame hitches.

npm gzip

JOYCO's 3D toolkit. Keyed state ownership and ordered teardown for real-time apps, a declarative debug-pane layer over Tweakpane, and Three.js scene warmup that forces every shader compile and texture upload to happen before the player ever sees a hitch.

Mental model

Xyz is three independent pillars, each its own subpath so you only ship what you use:

  • Core (@joycostudio/xyz) — Derived and edge for state that multiple owners write to and one policy resolves; Disposer for teardown that unwinds in reverse.
  • Debug (@joycostudio/xyz/debug) — DebugManager owns URL/keyboard activation and lazy pane creation; a declarative schema (slider, toggles, select, color, section, button, …) binds to a live Tweakpane folder without hand-wired glue.
  • Three (@joycostudio/xyz/three) — Warmup discovers a scene's full renderable graph, exposes it long enough to push every texture/shader through the driver once, then restores every mutation exactly as it found it.

Features

FeatureDescription
Keyed ownership (Derived)Independent writers claim a slot by key; value resolves all current intents through one explicit policy.
Transition sync (edge)Turns a selector into a callback that only fires when the selected value actually changes.
Ordered teardown (Disposer)Registrations unwind in reverse order; disposal is idempotent and safe to register into after teardown.
Declarative debug schemaslider, sliders, bool, toggles, select, color, colorString, section, button build a DebugSchema with no Tweakpane import required.
URL + keyboard activationDebugManager lazy-mounts the pane on ?debug or Option+D, and stays in sync with popstate.
Survives enable/disableRegistrations remount from fresh definitions every cycle — no stale bindings after a toggle.
Scene warmupWarmup inventories geometries, materials, and textures (including TSL node graphs), pauses the driver, and forces one representative render before restoring visibility, culling, and draw ranges.
Late-resource auditingaudit() warns once per resource introduced after warmup — a throttled dev-only diagnostic, never a runtime upload.
No R3F dependencyThe Three.js utilities are raw Three.js — bring your own renderer/canvas ownership split.

Install

pnpm add @joycostudio/xyz

three and @tweakpane/core are optional peer dependencies — install them only if you use the /three or /debug subpaths.

Quick start

import { Derived, edge, maxOf } from '@joycostudio/xyz'
 
const blur = new Derived(0, maxOf)
blur.set('loader', 0.8)
blur.set('inspect', 0.35)
blur.value // 0.8
 
const syncPhase = edge(
  (state: { phase: string }) => state.phase,
  (next, previous) => console.log(previous, '→', next)
)

Quick start (debug)

import { DebugManager, property, slider, bool } from '@joycostudio/xyz/debug'
import { Pane } from 'tweakpane'
 
const state = { amount: 0.5, enabled: true }
 
const debug = new DebugManager({
  createPane: () => new Pane({ title: 'Scene' }),
})
 
debug.bind({
  title: 'scene',
  schema: () => ({
    amount: slider(property(state, 'amount'), { min: 0, max: 1 }),
    enabled: bool(property(state, 'enabled')),
  }),
})

Open with ?debug=true in the URL, or press Option+D.

Quick start (three)

import { Warmup } from '@joycostudio/xyz/three'
 
const warmup = new Warmup({
  pause: () => (clock.running = false),
  resume: () => (clock.running = true),
  initTexture: (texture) => renderer.initTexture(texture),
  compile: (scene, camera) => renderer.compileAsync(scene, camera),
  render: () => renderer.render(scene, camera),
  nextFrame: () => new Promise((resolve) => requestAnimationFrame(() => resolve())),
})
 
await warmup.start(scene, camera) // one representative frame, then the scene is restored
warmup.audit(scene) // dev-only: warns about resources introduced after warmup

Architecture

LayerResponsibility
Core state (Derived, edge)State resolution primitives with no DOM or rendering dependency.
DisposerAn owner-local teardown stack shared by the debug and three layers (and your own code).
Debug schema (schema.ts)Pure DebugField/DebugDefinition builders — no Tweakpane import, fully testable in isolation.
Debug binding (bind.ts)Binds a DebugDefinition into a live FolderApi: tracks live-refresh fields, nested sections, and computed sources for copy.
DebugManagerOwns URL/keyboard activation and lazy pane creation; registrations survive disable/enable and remount fresh.
Scene discovery (warmup.ts)discoverScene walks the graph once, inventories every geometry/material/texture (materials, uniforms, TSL nodes), and temporarily exposes hidden/culled/zero-instance state.
Warmup orchestrationWarmup pauses the driver, runs registered Warmable.warmup() hooks, uploads textures, compiles, renders once, then restores everything in reverse — even on failure.

Core API

State

ExportDescription
Derived<In, Out>.set(key, value) / .clear(key) claim or release a slot; .value resolves through combine.
maxOfResolve to the highest active value, never lower than the fallback.
latestOfResolve to the most recently claimed slot.
edge(select, onChange, equals?)Returns a sync function: runs select every call, onChange only when the result changes.
Disposer.add(teardown), .listen(target, type, fn, options?), .dispose() — reverse-order, idempotent.

Debug

ExportDescription
DebugManager.register(source) / .bind(definition) to mount; .enable() / .disable() / .toggle() / .sync() control activation.
bindDebugSchema(host, definition, opts?)Binds a DebugDefinition into an existing Tweakpane folder; returns apply() / refresh() / values().
ref(read, write) / property(obj, key, changed?)Adapt a getter/setter pair, or an object property, into a ValueRef.
slider / slidersOne bounded numeric control, or several declared from an object's numeric keys at once.
bool / togglesOne boolean control, or several declared from an object's boolean keys at once.
select / enumFieldA string/number option control (enumField is an alias).
color / colorString / hexColorBind an RGB-compatible object, or a CSS color string (hexColor is an alias).
folder / debugFolder / sectionNest a DebugDefinition as a field; section adds a collapsed, copyable group.
buttonDeclare a sync or async debug action, with optional feedback text.
schemaValues(schema)Read every control's current value (recursing into folders, skipping buttons).

Three

ExportDescription
Warmup.add(entity) registers a Warmable; .start(scene, camera) runs once; .audit(scene); .dispose().
discoverScene(scene, camera)Inventories geometries/materials/textures and returns a restore() to undo the temporary exposure.
WarmableInterface for entities whose render state isn't already in the scene — implement warmup().
WarmupDriverRenderer-owned hooks the caller supplies: pause, resume, initTexture, compile, render, nextFrame.

Package exports

Import pathContents
@joycostudio/xyzVERSION, Disposer, Derived, maxOf, latestOf, edge, and their types.
@joycostudio/xyz/debugDebugManager, bindDebugSchema, ref, property, slider, sliders, bool, toggles, select, color, colorString, hexColor, enumField, folder, debugFolder, section, button, schemaValues, toFolders, editableTarget, matchesShortcut, parameterEnabled, and their types.
@joycostudio/xyz/threeWarmup, discoverScene, and their types (Warmable, WarmupDriver, WarmupCleanup, SceneDiscovery).

Demo

The JOYCO UI catalog in apps/demo gives every public runtime export its own interactive Next.js route: 8 core utilities, 22 debug utilities, and 2 Three.js utilities. The 3D routes use raw Three.js — there is no React Three Fiber dependency — and follow the same thin-canvas/renderer-owner split as Jam.

pnpm demo

Open the printed local URL and choose an export from the catalog. The DebugManager route demonstrates URL and Option+D activation, while each schema helper has a focused live Tweakpane. Use pnpm demo:build to verify the production integration.


Development

pnpm install
pnpm test          # Vitest suite (run); pnpm test:watch for watch mode
pnpm typecheck      # tsc --noEmit, plus the demo app
pnpm build          # tsup bundle (index, debug, three)
pnpm lint           # eslint --fix

Releases go through Changesets:

pnpm changeset          # describe the change and bump type
pnpm version:package    # apply pending changesets, update CHANGELOG
pnpm release             # build and publish to npm

Related Toolboxs