← TeiaViewer examples
Scene and objects

A mesh of your own

A SceneMesh with its engine object, its controller (what the transform tool may do), an undoable add command, moved with the transform tool or from code.

CustomMeshObject.ts
import {
  AddObjectCommand, type AddObjectCommandOptions, type ObjectTransformConfig, SceneMesh,
  SceneMeshController, SetObjectTransformCommand, Viewer,
} from '@stereograph/teiaviewer';
import { GeometryBuffersConverter } from '@stereograph/teiaviewer-engine';
import {
  Color, ConeGeometry, Matrix4, Vector3,
} from 'three';

/**
 * A mesh of your own: a `SceneMesh` holds its engine object (its proxy), which the engine draws,
 * bounds and picks. Its state (visibility, selection, colour…) goes through the viewer as for any
 * loaded object: commands, undo, object state.
 */
export class Marker extends SceneMesh {
  /** Tells a marker, without `instanceof`. */
  readonly isMarker = true;

  constructor(readonly label: string, uuid?: string) {
    super(uuid);
  }
}

/**
 * The controller of the markers: their place in the scene (added, removed, disposed), and what
 * the transform tool may do to them: moved on the floor only.
 */
export class MarkerController extends SceneMeshController<Marker> {
  override getTransformConfig(_marker: Marker): ObjectTransformConfig {
    return {
      translate: {
        x: true,
        y: false,
        z: true,
      },
      rotate: {
        x: false,
        y: false,
        z: false,
      },
      scale: {
        x: false,
        y: false,
        z: false,
      },
    };
  }
}

// One geometry, converted once (with its sharp edges), shared by every marker.
const coneGeometry = new GeometryBuffersConverter().convertGeometry(new ConeGeometry(0.3, 1, 16));

/** Adds a marker at a position, undoable (the undo removes it, the redo adds it back). */
export class AddMarkerCommand extends AddObjectCommand<Marker> {
  readonly name = 'AddMarkerCommand';

  constructor(
    viewer: Viewer,
    controller: MarkerController,
    private readonly _label: string,
    private readonly _position: Vector3,
    options: AddObjectCommandOptions = {},
  ) {
    super(viewer, controller, options);
  }

  protected override async _createObject(): Promise<Marker> {
    const marker = new Marker(this._label);
    marker.name = this._label;
    // Its engine object: one part, the cone, in its material.
    const [proxy] = this.viewer.scene.engineScene.meshController.addMeshes([
      {
        uuid: marker.uuid,
        name: marker.name,
        parts: [
          {
            geometry: coneGeometry,
            matrix: new Matrix4().elements,
            material: {
              color: new Color(0x2f80ed),
              emissive: new Color(0x000000),
              opacity: 1,
              roughness: 0.6,
              metalness: 0,
            },
          },
        ],
      },
    ]);
    marker.setProxy(proxy);
    marker.setPosition(this._position);

    return marker;
  }
}

export async function addAndMoveMarker(viewer: Viewer) {
  // The controller goes into the scene once (e.g. in a plugin's `onRegister`).
  const markers = new MarkerController(viewer);
  viewer.scene.addObjectController(markers);

  // Added and selected: the default operator shows the transform tool on it.
  const marker = await new AddMarkerCommand(viewer, markers, 'Entrance', new Vector3(2, 0, 3), {
    selected: true,
  }).execute();

  // Or attached to the transform tool from code, in translation (the only mode it allows).
  const { transformController } = viewer.viewport;
  transformController.attachObjects([marker]);
  transformController.setMode('translate');

  // Moved from code, undoable as a move with the tool.
  const moved = new Matrix4().makeTranslation(4, 0, 3);
  await new SetObjectTransformCommand(viewer, marker.uuid, moved).execute();
  await viewer.commandHistory.undoLastCommand();

  // Gone for good: out of the scene, its engine object freed.
  transformController.detachObjects();
  markers.removeObject(marker);
  markers.disposeObject(marker);
}