npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@scenoco-three/core

v0.4.0

Published

SceNoCo runtime: a component-based scene framework for Three.js (Engine, Component, registry, bundle loader)

Readme

@scenoco-three/core

The runtime of SceNoCo — a component-based scene framework for Three.js. Scene · Node · Component, Unity/Cocos-shaped, with the scene compiled to a compact bundle the runtime loads. No XML parser or validator ships here — that lives in @scenoco-three/compiler (build time only).

npm i @scenoco-three/core three

Quick start

import { Engine, Component, component, property } from '@scenoco-three/core';
import sceneBundle from './scene.bundle.json'; // produced at build time by the compiler

@component({ name: 'Rotator', description: 'Spins the host around Y' })
export class Rotator extends Component {
  @property({ type: 'float', default: 1, description: 'Radians/sec' }) speed = 1;
  override update(dt: number) { this.node.rotation.y += this.speed * dt; }
}

const engine = new Engine({ canvas: document.querySelector('canvas')! });
engine.loadScene(sceneBundle); // instantiates the Three.js scene + wires components

Headless (tests, servers) — omit the canvas and drive the loop by hand:

const engine = new Engine();        // no renderer
engine.loadScene(sceneBundle);
engine.tick(1 / 60);                // advance one frame
const ball = engine.findObject('ball'); // THREE.Object3D | undefined
engine.dispose();

Or build the scene by hand — no compiler, no bundle

You don't need a compiled scene to use components. engine.scene is a real THREE.Scene from construction, so you can populate it with plain Three.js objects and attach components imperatively — Unity's AddComponent. The component gets the full lifecycle (onLoad/ onEnable run immediately, start/update/… on tick) exactly as if it had been loaded from a bundle:

import * as THREE from 'three';
import { Engine } from '@scenoco-three/core';
import { Rotator } from './Rotator';

const engine = new Engine({ canvas: document.querySelector('canvas')! });

const cube = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshStandardMaterial());
engine.scene.add(cube);                            // plain Three.js — no wrapper
const rotator = engine.addComponent(cube, Rotator, { speed: 2 }); // attach + run onLoad now
//        engine.addComponent(cube, 'Rotator', { speed: 2 })       // …or by registered name

engine.scene.add(new THREE.DirectionalLight(0xffffff, 2).translateZ(3));
const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 100);
camera.position.z = 5;
engine.scene.add(camera);
engine.mainCamera = camera;                        // the loop draws through this

engine.start();                                    // rAF loop (or engine.tick(dt) by hand)

// Later — manipulate components on any object at runtime:
engine.getComponents(cube);          // readonly Component[] on this node
rotator.enabled = false;             // runs onDisable
engine.removeComponent(rotator);     // runs onDisable + onDestroy

Bundles and hand-built objects coexist: you can engine.loadScene(bundle) and still engine.addComponent(...) onto objects you add yourself, or skip bundles entirely. The compiler/XML is one way to produce a scene; the runtime only cares about THREE.Object3Ds and the components attached to them.

Concepts

| Concept | What it is | | --- | --- | | Node | A THREE.Object3D — used directly, no wrapper. this.node.rotation.y += dt just works. | | Component | Behaviour attached to a node (Unity MonoBehaviour / Cocos cc.Component). | | Attachment | Leaf config on a node: geometry, material, or a <Scene> setting (fog/background). | | System | A batch processor for all components of one type (physics, etc.), on a public seam. | | Bundle | The compiled, indexed scene the runtime loads. The runtime never sees XML. | | Prefab | A reusable subtree spawned at runtime via engine.instantiate() or a PrefabRef. |

The registry decorators (@component/@node/@attachment/@system/@property) are the single source of truth — the compiler, validator, and exporters are generated views of the same metadata, so the schema can never drift from the code.


API reference

Engine

new Engine(options?: EngineOptions)

| Option | Type | Default | Meaning | | --- | --- | --- | --- | | canvas | HTMLCanvasElement | — | Render target. Omit for headless. | | autoStart | boolean | true | Start the rAF loop when a canvas is given. | | fixedTimeStep | number | 1/60 | Seconds per fixed step (fixedUpdate, physics). | | editor | boolean | false | Editor mode: instantiate but run no hooks except @executeInEditor. | | preserveDrawingBuffer | boolean | false | Keep the buffer so canvas.toDataURL() can capture a frame. |

Properties: scene: THREE.Scene · renderer?: WebGLRenderer · mainCamera?: Camera (first camera found in the scene; the loop draws through it) · input: InputManager · editorMode: boolean.

Scene lifecycle

engine.loadScene(bundleOrCompiledScene): void          // synchronous full swap; wires all refs before any onLoad
await engine.loadSceneAsync(bundleOrCompiledScene)     // awaits preload providers (assets) first, then instantiates
engine.disposeScene(): void                            // unload, free GPU resources, leave an empty scene
engine.dispose(): void                                 // stop loop + destroy everything

Async assets — register a PreloadProvider (an asset package does this) and the engine awaits it before onLoad, so components never see missing assets:

engine.addPreloadProvider(async (compiled) => { /* load this scene's assets into a cache */ });
await engine.loadSceneAsync(bundle);          // assets ready before any onLoad runs
const root = await engine.instantiateAsync(prefabBundle); // same guarantee for runtime spawns

loadScene stays synchronous (and asset-agnostic — assets stream in afterward); reach for the *Async variants only when a component's onLoad must see its assets already loaded.

Components

engine.addComponent(node, Rotator, { speed: 2 })  // by class (typed) …
engine.addComponent(node, 'Rotator', { speed: 2 }) // … or by registered name
engine.removeComponent(instance)
engine.getComponents(node): readonly Component[]
engine.getComponentsOfType<T>(Type): T[]           // every live instance of a type (Unity FindObjectsByType)
engine.findComponent<T>(id, type?): T | undefined  // type may be a class or a name string
engine.findObject(id): Object3D | undefined        // by scene id — indexed (O(1)), with a traversal fallback

Execution order (Unity Script Execution Order): @component({ order }) runs components in ascending order across every phase (onLoad/start/update/fixedUpdate/lateUpdate); equal orders keep instantiation order. Default 0.

Loop (headless or manual)

engine.tick(dt)   // fixedUpdate ×N → start/update → lateUpdate → systems → render
engine.start()    // begin the requestAnimationFrame loop (needs a canvas)
engine.stop()

Systems & attachments

engine.addSystem(new PhysicsSystem())
engine.getSystem(PhysicsSystem): T | undefined
engine.findAttachment(Physics)?.gravity        // live singleton config attachment

Prefabs / dynamic spawning

const root = engine.instantiate(prefabBundle, parent?)  // Unity Instantiate; returns the root
engine.destroy(root)                                    // remove a subtree + its components

Time & timeScale

engine.timeScale = 0.5   // slow-mo; 0 pauses gameplay (update still runs, deltaTime = 0)
engine.time              // scaled seconds elapsed   · engine.deltaTime  — scaled dt this frame
engine.unscaledTime      // wall-clock seconds       · engine.frameCount — frames ticked
engine.interpolationAlpha // [0,1) blend into the next fixed step (physics reads it to de-jitter)

Deterministic RNG (seed via new Engine({ seed })) — reproducible runs/replays/QA. Use it instead of Math.random():

engine.random()            // [0, 1)
engine.randomRange(2, 5)   // [2, 5)
engine.randomInt(0, 6)     // 0..5
engine.reseed(42); engine.seed

Camerasengine.cameras is every scene camera sorted by priority (Unity camera depth); the render loop draws them in order, one after another — the first paints the background, later ones overlay (depth-cleared) for UI/HUD passes. mainCamera (the first camera found, or set it yourself) is the one used for raycasting/pointer. Set priority in XML: <PerspectiveCamera priority="0" /><OrthographicCamera priority="10" />.

Raycasting & pointer events

engine.raycast()                       // hits under the pointer (input.mouseNdc) → Intersection[]
engine.raycast({ ndc, objects, recursive })  // custom ray/targets

Components receive Unity-style pointer hooks — the engine raycasts on press/release and bubbles the event up the hit object's ancestors:

class Button extends Component {
  override onPointerDown(e: PointerEventData) {}  // pressed over this object
  override onPointerUp(e: PointerEventData) {}    // released (delivered to the pressed object)
  override onPointerClick(e: PointerEventData) {} // press + release on the same object (tap/click)
  override onDrag(e: PointerEventData) {}         // held + moving after a press; e.delta = px moved
}

PointerEventData carries { object, point?, distance?, button, delta? }. Driven by the mouse and single touch (one finger maps to the primary pointer, so taps/drags work on mobile — set CSS touch-action: none on the canvas); inert in editor mode.

Scripted input (tooling/tests/replays): engine.input.injectKey / injectMouseButton / injectMouseMove / injectScroll / injectGamepad drive input with no DOM, committed on the next tick exactly like real events.

Toolingengine.getSceneGraph() returns a SceneGraph (compiled + Object3D/ Component ⇄ compiled-index maps) so an editor can patch live edits back to source.

Component

abstract class Component<TNode extends Object3D = Object3D>

Type the host node for cast-free access: class Rig extends Component<PerspectiveCamera> { … this.node.fov … }.

Fields: node: TNode · engine: Engine · id?: string · enabled: boolean (setting it runs onEnable/onDisable).

Methods: getComponent(Type): T | null · getComponentInChildren(Type): T | null · getComponentsInChildren(Type): T[] (self + descendants, Unity-style) · destroy().

Lifecycle hooks (all optional, in order):

onLoad()                  // once, on attach (refs already wired)
onEnable()                // on enable (incl. right after onLoad)
start()                   // once, first update while enabled
update(dt)                // every frame while enabled
fixedUpdate(fixedDt)      // 0..N times/frame at the fixed step (physics)
lateUpdate(dt)            // after every component's update this frame (camera follow)
onDisable() / onDestroy()

System

@system(RigidBody)
class PhysicsSystem extends System<RigidBody> {
  override onAttach() { /* build the world */ }
  override fixedStep(dt: number) { for (const rb of this.components) { /* … */ } }
}

The engine auto-runs a registered system while the scene contains its component type, keeping this.components in sync. Hooks: onAttach · fixedStep(dt) · step(dt) · onDetach.

InputManager

engine.input, sampled once per tick; getKeyDown/getKeyUp are true for exactly one frame. Headless-safe (neutral values with no DOM).

input.getKey('Space'); input.getKeyDown('KeyW'); input.getKeyUp('Escape'); input.anyKey;
input.getAxis('Horizontal'); input.getAxis('Vertical');           // -1..1 from arrows/WASD
input.mousePosition; input.mouseNdc; input.mouseDelta; input.mouseScrollDelta; // Vector2
input.getMouseButton(0); input.getMouseButtonDown(0); input.getMouseButtonUp(2);
const pad = input.getGamepad(0); pad?.getButton('A'); pad?.getButtonDown('Start');

decorators

@component({ name, description, order? })  // behaviour class → an XML <Tag> in <Components>
@node({ name, description, kind?, children? })   // a Three.js Object3D tag
@attachment({ name, description, target, group, unique?, required?, children? })  // geometry/material/setting
@system(ComponentType)                    // batch processor for that component type
@executeInEditor                          // also run this component in editor mode

@property({ type, description, ... })      // declare an XML-exposed field

description is mandatory on every @component, @node, @attachment, and @property — it is the one-line summary people, the editor, and agents/RAG read to understand the API. The type checker requires it, and the registry rejects a missing/empty one at load. (Because a description is required, the old bare @property(SomeComponent) / @property([Type]) shorthand is gone — pass the reference as type in the object form.)

@property forms (all carry a description):

@property({ type: 'float', default: 1, description: 'Radians/sec' }) speed = 1;        // scalar
@property({ type: 'vec3', default: [0, 1, 0], description: 'Local offset' }) offset = new Vector3(0, 1, 0);
@property({ type: 'color', default: '#ff8800', description: 'Tint' }) tint = 0xff8800;
@property({ type: ['float'], description: 'Lane weights' }) weights: number[] = [];     // array, written "1 2 3"
@property({ type: TargetComponent, description: 'Node to track' }) target!: TargetComponent; // typed reference (#id)
@property({ type: 'prefab', description: 'Brick to spawn' }) brick: PrefabRef | null = null;
@property({ type: MySideEnum, default: 'front', description: 'Which faces render' }) side = FrontSide; // enum

Scalar types: float · int · bool · string · vec2 · vec3 · euler · color, plus node / prefab and an EnumDef. Math props are forgiving and consistent — a vec2/vec3/euler default (or field initializer) may be an array [x,y,z], a THREE.Vector3/Vector2/Euler, or {x,y,z}; a color may be 0xRRGGBB, a "#rrggbb" string, or a THREE.Color. At runtime the field is always the Three type (Vector3/Vector2/Euler/Color), whether the attribute was set in XML or left at its default — so you can always write this.offset.x without checking the shape.

enumOf

Register a set of named choices as a @property type. The field holds the native value; XML/XSD/editor surface the member names. Three input forms:

import { enumOf } from '@scenoco-three/core';
import { FrontSide, BackSide, DoubleSide } from 'three';

enumOf('Side', { front: FrontSide, back: BackSide, double: DoubleSide }); // value map
enumOf('Kind', MyTsEnum);                                                 // native TS enum
enumOf('UniformType', ['float', 'int', 'vec2']);                          // identity (name == value)

Helpers: isEnumType(x) · getEnum(name) · allEnums().

assets (textures & models)

External assets are referenced by URL in a @property({ type: 'string', asset }) field, loaded into engine.assets (a URL-keyed AssetCache) by a preload provider, and read by the attachment at attach. Built-in support:

<Mesh><BoxGeometry /><MeshStandardMaterial map="brick.png" normalMap="brick_n.png" /></Mesh>
<Group><Model src="hero.glb" /></Group>   <!-- glTF/GLB loaded as a cloned child subtree -->
await engine.loadSceneAsync(bundle); // textures/models load before onLoad (see Scene lifecycle)
engine.assets.registerLoader(['ktx2'], async (url) => /* custom loader */); // extend formats

Loaders are picked by extension (images → TextureLoader; .glb/.gltf → a lazily-imported GLTFLoader, so a model-free build never ships the glTF addon). Cache-owned resources are shared across scenes and freed by engine.assets.dispose() — scene unload leaves them intact. Declare your own asset attrs with @property({ type: 'string', asset: 'texture' | 'model' }); collectAssetRefs(scene) enumerates them. With loadSceneAsync assets are ready before onLoad; with synchronous loadScene they stream in and appear when their load finishes. The @scenoco-three/vite plugin rewrites local asset URLs to bundler-served assets (hashed + HMR-watched); absolute/public URLs are fetched as-is.

registry

Registry (components) and the node/attachment registry. Built-in tags self-register on import; projects add their own the same way.

import { Registry, getNodeDef, nodeTags, getAttachmentDef, attachmentTags } from '@scenoco-three/core';
Registry.getComponent('Rotator'); Registry.componentNames();
nodeTags(); getNodeDef('Mesh'); attachmentTags(); getAttachmentDef('BoxGeometry');

PrefabRef

What a @property({ type: 'prefab' }) field receives — a handle that spawns a build-time-embedded prefab. spawn(options) mirrors Unity Instantiate(prefab, position, rotation, parent)placement only:

@property({ type: 'prefab' }) brick: PrefabRef | null = null;
// …
const root = this.brick!.spawn({ position: [x, y, 0] }); // parent defaults to this.node
// later: this.engine.destroy(root);  // you own spawned subtrees

position/rotation accept native Three.js types (Vector3, Euler, Quaternion) or plain [x,y,z] arrays (rotation arrays are radians) — the same value you'd set on any Object3D:

const bullet = this.prefab!.spawn({ position: muzzle.clone(), rotation: ship.quaternion });

Setting fields / wiring references on a spawned instance is done the Unity/Cocos way — there is no override bag on spawn. Take the returned root and GetComponent, then set fields (references are just live instances you already hold):

const root = this.prefab!.spawn({ position: p });
const ctrl = this.engine.getComponents(root).find((c) => c instanceof Controller) as Controller;
ctrl.gameManager = this.engine.findComponent('gm', GameManager); // wire a scene reference

Or let the prefab's own script resolve the scene object in start(), Unity FindObjectOfType / Cocos find style:

override start() {
  this.gameManager = this.engine.getComponentsOfType(GameManager)[0]   // FindObjectOfType
    ?? this.engine.findComponent('GameManager', GameManager);          // or find by id/name
}

Author-time placement (a <PrefabInstance> in a scene) can wire prefab internals to scene objects declaratively with <Override> — see the @scenoco-three/compiler README.

bundle

import { deserializeBundle, instantiateScene, BUNDLE_VERSION, bundleKind } from '@scenoco-three/core';
const compiled = deserializeBundle(bundle);   // Bundle → CompiledScene
const live = instantiateScene(compiled, ctx); // CompiledScene → Three.js objects

You rarely call these directly — engine.loadScene / engine.instantiate do it. Types (CompiledScene, CompiledNode, CompiledComponent, CompiledAsset, CompiledUniform, Bundle, …) are exported for tooling. ShaderMaterial uniforms use the UNIFORM_TYPES / UniformType set (custom GLSL with <Uniform> children, validated at compile time).


Standalone & dependencies

Zero runtime dependencies (three is the only peer). Core is usable entirely on its own: author components in TypeScript, build a THREE.Scene — either by attaching components to Three.js objects by hand or by loading a compiled bundle (produced by @scenoco-three/compiler at build time) — and drive the gameplay loop. No XML parser, validator or CLI ever reaches the browser. Everything above core (compiler, vite, editor, rapier, box2d) is optional and builds on its public seams — the registry decorators and the System API.

See the repository and ARCHITECTURE.md for the full design.

License

MIT