Model Studio

Model Studio is the shell's generic viewer for portable object packages. It starts with the same authored people used at the beach. It does not contain a hard-coded scene for each model.

Use the viewer

  1. Open shell/?scene=studio over local HTTP.
  2. Choose an Available model, then Add or Replace selected.
  3. Select an instance in the viewport or Selected object list. Use Frame selected to centre it. The Camera panel selects orbit, pan or free look; scroll zooms. See CAMERA_AND_CONTROLLER.md for mouse, keyboard and Xbox mappings.
  4. The inspector contains scale, rotation, material roughness and the selected model's own properties/actions. The side panel scrolls; model-specific controls are below the shared controls.
  5. Remove releases the selected instance. An empty studio is supported. Reset returns to the starting person. Objects and edited values are session state, not a saved scene file.

Different objects keep their real sizes. The viewer frames the selected object automatically and places added models beside existing ones. The engine remains independent of this UI.

Load a model made later

Copy the entire compatible package into objects/my-model/ beside the other packages. In Studio, expand Load another package, enter my-model, and click Load package. Its models appear in the library; select one and click Add or Replace selected. No Studio source edits or catalogue changes are needed. A direct application-local path such as ../objects/my-model/object.json also works.

The browser reads files served by the local HTTP server. This is a package loader, not a desktop file manager: it does not copy folders into the project or import arbitrary mesh formats. Future packages follow the contract below. Package registration lasts for the current page session; load a custom package again after a full reload. Built-in packages are registered automatically.

The developer example at tests/fixtures/studio-object/ is a complete tiny package absent from the built-in catalogue. Load ../tests/fixtures/studio-object/object.json to verify the extension path. It exposes a color property and quarter-turn action.

Object-owned Studio contract, version 1

The existing object.json keeps its runtime entry and portable object protocol. Add:

"studio": { "entry": "studio.js", "protocol": 1 }

studio.js exports a models array. Each definition has:

FieldMeaning
id, labelStable package-local identifier and user-facing model name
create(api, context)Return a portable object, or a promise for it
propertiesOptional array of editable property definitions
actionsOptional array of labelled actions

The factory receives the engine API namespace. context contains an abort signal, quality, shared wind, flat terrain, camera and cameraPosition. Use local package imports and asset URLs relative to import.meta.url; do not import an engine path or shell file. This preserves independent object-folder portability.

The returned object supplies root (a Three.js Object3D), properties, update(dt, time, context), getBounds() and dispose(). The usual portable-object serialize/restore methods remain available to other consumers. getBounds() returns finite world bounds. The factory must release allocations if it fails before returning. The viewer owns successfully returned objects, disposes replaced/removed instances, and disposes a late return after scene cancellation. Object disposal must also cover resources not attached to its visible mesh.

A property specifies id, label, type, get(object) and set(object, value). Types are number, boolean, select, color. Numbers may declare min, max, step. Select options are strings or {value, label} entries; typed values such as booleans are preserved. Color values passed to setters are six-digit CSS hex strings. Setters must update the actual object through its API, not merely edit an unrelated metadata field. The UI validates numeric ranges and choices before invoking a setter.

An action specifies id, label, run(object). Setters/actions may return promises. Parameterized methods can be exposed through properties plus an action, keeping their object-specific logic in the package. Engine time drives animation; mechanisms such as the jet canopy need animation resumed to travel to their commanded positions.

Example from the fixture:

import {ExampleBlock} from './index.js';
export const models = [{
  id: 'block', label: 'Example block',
  create: api => new ExampleBlock(api),
  properties: [{
    id: 'color', label: 'Color', type: 'color',
    get: object => object.properties.color,
    set: (object, value) => object.setColor(value)
  }],
  actions: [{
    id: 'turn', label: 'Quarter turn',
    run: object => { object.root.rotation.y += Math.PI / 2; }
  }]
}];

Loading and verification boundaries

Packages are trusted local application code. The loader accepts manifests within the current application folder and origin, rejects manifest redirects and escaping module entry paths, and requires protocol 1. It does not sandbox the implementation of a package the application owner supplies. Built-in package modules and dependencies remain subject to the existing folder-closure checks.

shell/lib/model-library.js is the single deliberate dynamic-import exception in tools/verify.mjs, needed to load future packages. The exception is limited to its named import boundary. Other computed imports remain audit failures. Five Node checks cover validation/registration; tests/studio.html exercises real WebGL loading, actions, replacement, failures and late cancellation, including the uncatalogued fixture.

Camera pan, fixed-position free look and standard Xbox-controller navigation are available in 0.2.2. Model-library state remains independent of camera mode.

Hair color

Select a Person, then scroll to Model Properties & Actions. Hair color is directly below Hairstyle. The color picker updates the hair and eyebrows together. It retains the chosen style and all other person settings. The starting person now has brown hair; gray, black, blond and other colors are selectable. Bald remains hairless while its eyebrow color can change.