The scene language
@scenography/scene is the what: a small, serializable data model with zero
three.js. An editor, a database, or a backend can depend on it without ever
touching WebGL.
defineScene
defineScene(scene, plugins?) validates a definition and returns it unchanged.
It throws on structural errors so authoring mistakes surface early. The returned
value is plain JSON-serializable data.
The scene keys themselves (walls, artworks, floor, …) are contributed by
element plugins. Pass the data half of each element you use as the second
argument, so defineScene knows how to validate its key:
import { defineScene } from '@scenography/scene';
import { wallData } from '@scenography/plugin-wall/data';
import { artworkData } from '@scenography/plugin-artwork/data';
const scene = defineScene(
{
walls: [...],
artworks: [...],
camera: { start: { position: [0, 170, 0] } },
},
[wallData, artworkData],
);
Use validateScene(scene, plugins?) if you want the list of issues without
throwing (an editor's live feedback), and cloneScene(scene) for a JSON
round-trip clone.
Walls
Contributed by @scenography/plugin-wall (wallData for validation, wall()
for the runtime).
A wall is a top-down path: a polyline footprint the engine extrudes into a continuous run of walls. No need to declare each side separately.
interface WallStructure {
id: string;
path: [x: number, z: number][]; // top-down footprint
height?: number; // default 300 (≈3 m)
thickness?: number; // default 30
closed?: boolean; // connect the last vertex back to the first
edges?: { segment: number; name: string }[]; // name segments to hang art on
color?: string; // default white
}
Each edge names a segment of the path (segment runs from path[segment] to
path[segment + 1], wrapping when closed). Artworks reference these names.
Corners are simple butt joints for now and the wall follows the path at its center. Chamfered/rounded corners and inside/outside offset are on the roadmap.
Artworks
Contributed by @scenography/plugin-artwork. A framed image, placed via a
location. Today a piece either hangs on a named wall edge or floats freely.
interface Artwork {
id: string;
location: ArtworkLocation;
src: string;
size: [width: number, height: number];
title?: string;
technique?: string;
}
type ArtworkLocation =
| {
type: 'wall';
edge: string; // a NamedEdge name
position: number | `${number}%`; // along the edge: units, or a fraction
height?: number | `${number}%`; // world Y, a fraction of wall height, or (omitted) eye level
side?: 'right' | 'left'; // which face; 'right' = right-hand side along the edge
}
| {
type: 'floating';
point: [x: number, y: number, z: number];
yaw?: number; // radians
};
side defaults to 'right' — the right-hand side when walking the edge from its
start vertex to its end. Set it explicitly when a wall's winding makes the
visible face ambiguous (e.g. an S-shaped path).
Camera
Where the visitor starts, and how far they may roam.
interface Camera {
start: {
position: [x: number, y: number, z: number];
yaw?: number; // radians, default 0
};
bounds?: number; // square half-extent that clamps XZ movement
}
Richer camera paths and tours are a planned addition — and, true to the core principle, they will be data (waypoints, easings, durations), with the motion itself living in the engine.