3dom

3DOM / $S Specification (v0.1)

Status: Draft, stable surface. The v0.x line may add ops and selectors but will not remove or repurpose the ones below without a major-version bump.

📖 Repo: https://github.com/tejaswigowda/3dom 🕹️ Live demo: examples/bare.html

3DOM (“three-D Object Model”) is a runtime-free model for addressing and editing a three.js scene graph. $S is its jQuery-style binding. The model knows nothing about any editor: every mutation is routed through a Host.


1. Scope & principles


2. Binding: createS

const $S = createS( sceneOrHost, opts );

$S.listSelectors()

Enumerates every addressable class and id selector in the current scene, with match counts. Returns an array sorted by count descending:

$S.listSelectors();
// → [{ selector: '.wheel', count: 4 }, { selector: '#body', count: 1 }, …]

selector is either a selector string (§3) or an array of nodes.


3. Selector grammar

A CSS subset, matched against the scene graph:

Form Meaning
type object type via is* flags: mesh group light camera sprite line points bone object3d. An unknown bare token matches nothing (not everything).
.class node has this class (author-set on userData.classes, or auto-labelled).
#id userData.label === id, falling back to node.name === id.
.a.b compound: has class a AND class b.
A B descendant combinator.
A > B child combinator.
* every node in scope.
:selected / :lasso resolves to the host app’s live selection, not a graph query. On a bare DefaultHost this is empty.

No union (,), attribute or state pseudos in v0.1.

Class names are normalised: lower-cased, spaces → -, non-[a-z0-9-] stripped.


4. Auto-labelling: autoLabel(root, opts?)

Two phases, both idempotent:

  1. indexSubtree(root, force): compute per-node descriptors into userData.descriptors (plus a geometry hash for symmetry pairing). Skips nodes already indexed for the same geometry unless opts.force === true.
  2. deriveAllClasses(root): write userData.classes from type, descriptors, and name stem.

Class sources (all deterministic):

role (leaf/group) is computed but deliberately not emitted as a class.


5. Op set

Ops are the closed set of undoable mutations. Chain methods and op-JSON both route here. Every op resolves its selector, builds Host commands, and executes them as one undoable step. When more than one command results, it becomes a multi.

Op Chain method Guard / clamp
recolor .recolor(color) clone-on-write for shared materials; textured materials warned, not silently overwritten.
scale .scale(factor, axis?) factor clamped to [0.1, 10].
move .move(x, y, z) per-axis delta clamped ±100; y grounded ≥ 0.
rotate .rotate(axis, degrees) degrees clamped ±360.
delete .delete() skips merged-mesh sub-nodes and detached nodes.
duplicate .duplicate(x?, y?, z?) bakes world transform, applies offset.
setMaterial .setMaterial(props) builds a MeshStandardMaterial from props.
setOpacity .setOpacity(v) sets transparent as needed.
setVisible .setVisible(v) none
wireframe .wireframe(on?) none
castShadow .castShadow(v?) object prop.
receiveShadow .receiveShadow(v?) object prop.
metalness .metalness(v) material prop.
roughness .roughness(v) material prop.

OP_SET exports the canonical op-name list.

Op-JSON contract

{ "op": "recolor", "selector": ".wheel", "args": { "color": "#111" } }

dispatchOp(host, json) runs one. dispatchOps(host, list) runs many in order and returns per-op results { success, message? }.


6. Host contract

The Host is the only injection seam between the model and a runtime. The ops layer never imports a concrete command class. It calls Host factories.

A Host is any object providing:

interface Host {
  scene: THREE.Object3D;
  execute( command ): void;      // apply + record for undo
  undo(): void;
  redo(): void;
  multi( commands[] ): command;  // group into one undoable step
  notify( kind: string, payload? ): void;
  // command factories, each returns { name, execute(), undo() }
  setPosition, setRotation, setScale, setValue,
  setColor, setMaterialColor, setMaterialValue, setMaterial,
  addObject, removeObject
}

resolveHost(x, opts) returns x unchanged if it already looks like a Host (has .execute and .scene), else wraps it in a DefaultHost.


7. three.js peer resolution

The library imports three as a bare specifier. Consumers provide it via bundler resolution, an import map, or a global. setThree(instance) / getThree() allow overriding the resolved instance (ES-module live binding, so importers see the update).


8. Versioning

MIT © Tejaswi Gowda.