Skip to main content

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.