or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

boundaries-constraints.mdcamera-fitting.mdcamera-movement.mdcore-controls.mdevent-system.mdindex.mdinput-configuration.mdstate-management.md
tile.json

event-system.mddocs/

Event System

Comprehensive event handling for camera interactions, transitions, and state changes with the built-in EventDispatcher system.

Capabilities

EventDispatcher Base Class

CameraControls extends EventDispatcher, providing event management functionality.

/**
 * Event listener function type
 */
type Listener = (event?: DispatcherEvent) => void;

/**
 * Base event interface
 */
interface DispatcherEvent {
  type: string;
  [key: string]: any;
}

/**
 * Add event listener for specific event type
 * @param type - Event name to listen for
 * @param listener - Function to call when event fires
 */
addEventListener(type: string, listener: Listener): void;

/**
 * Check if specific listener exists for event type
 * @param type - Event name to check
 * @param listener - Listener function to check for
 * @returns True if listener exists
 */
hasEventListener(type: string, listener: Listener): boolean;

/**
 * Remove specific event listener
 * @param type - Event name
 * @param listener - Listener function to remove
 */
removeEventListener(type: string, listener: Listener): void;

/**
 * Remove all listeners for event type (or all events if no type specified)
 * @param type - Optional event name (removes all if omitted)
 */
removeAllEventListeners(type?: string): void;

/**
 * Fire an event to all registered listeners
 * @param event - Event object to dispatch
 */
dispatchEvent(event: DispatcherEvent): void;

Camera Controls Events

CameraControls fires specific events during camera operations and user interactions.

interface CameraControlsEventMap {
  update: { type: 'update' };
  wake: { type: 'wake' };
  rest: { type: 'rest' };
  sleep: { type: 'sleep' };
  transitionstart: { type: 'transitionstart' };
  controlstart: { type: 'controlstart' };
  control: { type: 'control' };
  controlend: { type: 'controlend' };
}

Event Descriptions:

  • update: Fired every frame when camera position/orientation changes
  • wake: Fired when controls become active (first user interaction)
  • rest: Fired when camera stops moving and reaches rest state
  • sleep: Fired when controls become inactive (no recent interaction)
  • transitionstart: Fired when programmatic transition begins
  • controlstart: Fired when user interaction starts (mouse down, touch start)
  • control: Fired during active user interaction (mouse move, touch move)
  • controlend: Fired when user interaction ends (mouse up, touch end)

Basic Event Handling

// Listen for camera updates
cameraControls.addEventListener('update', () => {
  console.log('Camera updated - position changed');
  renderer.render(scene, camera);
});

// Listen for user interactions
cameraControls.addEventListener('controlstart', () => {
  console.log('User started interacting with camera');
});

cameraControls.addEventListener('controlend', () => {
  console.log('User stopped interacting with camera');
});

// Listen for camera rest state
cameraControls.addEventListener('rest', () => {
  console.log('Camera has stopped moving');
});

Advanced Event Usage

Performance Optimization with Update Events:

let renderRequested = false;

cameraControls.addEventListener('update', () => {
  if (!renderRequested) {
    renderRequested = true;
    requestAnimationFrame(() => {
      renderer.render(scene, camera);
      renderRequested = false;
    });
  }
});

// Alternative: Only render on camera changes
function animate() {
  const delta = clock.getDelta();
  const hasControlsUpdated = cameraControls.update(delta);
  
  if (hasControlsUpdated) {
    renderer.render(scene, camera);
  }
  
  requestAnimationFrame(animate);
}

UI State Management:

// Show/hide UI elements based on camera state
const loadingIndicator = document.getElementById('loading');
const cameraInfo = document.getElementById('camera-info');

cameraControls.addEventListener('transitionstart', () => {
  loadingIndicator.style.display = 'block';
});

cameraControls.addEventListener('rest', () => {
  loadingIndicator.style.display = 'none';
});

cameraControls.addEventListener('controlstart', () => {
  cameraInfo.style.opacity = '0.5'; // Fade info during interaction
});

cameraControls.addEventListener('controlend', () => {
  cameraInfo.style.opacity = '1.0'; // Restore info after interaction
});

Sleep/Wake Cycle Management:

// Manage power consumption and updates
cameraControls.addEventListener('sleep', () => {
  console.log('Camera controls sleeping - reducing update frequency');
  // Reduce animation loop frequency or pause updates
});

cameraControls.addEventListener('wake', () => {
  console.log('Camera controls waking - resuming full updates');
  // Resume full update frequency
});

Interaction Tracking:

let interactionCount = 0;
let isInteracting = false;

cameraControls.addEventListener('controlstart', () => {
  isInteracting = true;
  interactionCount++;
  console.log(`User interaction #${interactionCount} started`);
});

cameraControls.addEventListener('control', () => {
  if (isInteracting) {
    // Update interaction UI, show coordinates, etc.
    updateCameraDebugInfo();
  }
});

cameraControls.addEventListener('controlend', () => {
  isInteracting = false;
  console.log('User interaction ended');
});

function updateCameraDebugInfo() {
  const position = new THREE.Vector3();
  const target = new THREE.Vector3();
  
  cameraControls.getPosition(position);
  cameraControls.getTarget(target);
  
  document.getElementById('debug-position').textContent = 
    `Position: ${position.x.toFixed(2)}, ${position.y.toFixed(2)}, ${position.z.toFixed(2)}`;
  document.getElementById('debug-target').textContent = 
    `Target: ${target.x.toFixed(2)}, ${target.y.toFixed(2)}, ${target.z.toFixed(2)}`;
}

Event-Driven Animation

Coordinated Animations:

// Chain animations using events
async function performCameraTour() {
  const positions = [
    { x: 10, y: 5, z: 10 },
    { x: -10, y: 8, z: 5 },
    { x: 0, y: 15, z: 0 },
    { x: 5, y: 2, z: -10 }
  ];
  
  for (const pos of positions) {
    // Start movement
    const movePromise = cameraControls.moveTo(pos.x, pos.y, pos.z, true);
    
    // Wait for transition to start
    await new Promise(resolve => {
      cameraControls.addEventListener('transitionstart', resolve, { once: true });
    });
    
    console.log(`Moving to position: ${pos.x}, ${pos.y}, ${pos.z}`);
    
    // Wait for movement to complete
    await movePromise;
    
    // Wait for camera to rest
    await new Promise(resolve => {
      cameraControls.addEventListener('rest', resolve, { once: true });
    });
    
    console.log('Position reached, pausing...');
    await new Promise(resolve => setTimeout(resolve, 1000));
  }
}

Event-Based State Machine:

enum CameraState {
  IDLE = 'idle',
  USER_CONTROLLING = 'user_controlling',
  TRANSITIONING = 'transitioning',
  SLEEPING = 'sleeping'
}

class CameraStateMachine {
  private currentState = CameraState.IDLE;
  
  constructor(controls: CameraControls) {
    controls.addEventListener('controlstart', () => this.setState(CameraState.USER_CONTROLLING));
    controls.addEventListener('controlend', () => this.setState(CameraState.IDLE));
    controls.addEventListener('transitionstart', () => this.setState(CameraState.TRANSITIONING));
    controls.addEventListener('rest', () => this.setState(CameraState.IDLE));
    controls.addEventListener('sleep', () => this.setState(CameraState.SLEEPING));
    controls.addEventListener('wake', () => this.setState(CameraState.IDLE));
  }
  
  private setState(newState: CameraState) {
    const oldState = this.currentState;
    this.currentState = newState;
    
    console.log(`Camera state: ${oldState} -> ${newState}`);
    
    // Trigger state-specific behaviors
    this.onStateChange(oldState, newState);
  }
  
  private onStateChange(from: CameraState, to: CameraState) {
    switch (to) {
      case CameraState.USER_CONTROLLING:
        document.body.classList.add('camera-interacting');
        break;
      case CameraState.TRANSITIONING:
        document.body.classList.add('camera-transitioning');
        break;
      case CameraState.IDLE:
        document.body.classList.remove('camera-interacting', 'camera-transitioning');
        break;
      case CameraState.SLEEPING:
        document.body.classList.add('camera-sleeping');
        break;
    }
  }
  
  getCurrentState(): CameraState {
    return this.currentState;
  }
}

const stateMachine = new CameraStateMachine(cameraControls);

Custom Event Handling

Creating Custom Events:

// Extend controls with custom events
class ExtendedCameraControls extends CameraControls {
  
  private targetObject: THREE.Object3D | null = null;
  
  focusOnObject(object: THREE.Object3D) {
    this.targetObject = object;
    
    // Fire custom event
    this.dispatchEvent({ 
      type: 'focus-start', 
      target: object 
    });
    
    // Perform focus operation
    this.fitToBox(object, true).then(() => {
      this.dispatchEvent({ 
        type: 'focus-complete', 
        target: object 
      });
    });
  }
}

// Use custom events
const extendedControls = new ExtendedCameraControls(camera, renderer.domElement);

extendedControls.addEventListener('focus-start', (event) => {
  console.log('Starting focus on object:', event.target);
});

extendedControls.addEventListener('focus-complete', (event) => {
  console.log('Focus complete on object:', event.target);
});

Event Cleanup

Proper Event Listener Management:

class CameraControlsManager {
  private controls: CameraControls;
  private listeners: Map<string, Listener[]> = new Map();
  
  constructor(camera: THREE.Camera, domElement: HTMLElement) {
    this.controls = new CameraControls(camera, domElement);
  }
  
  addEventListener(type: string, listener: Listener) {
    this.controls.addEventListener(type, listener);
    
    // Track listeners for cleanup
    if (!this.listeners.has(type)) {
      this.listeners.set(type, []);
    }
    this.listeners.get(type)!.push(listener);
  }
  
  dispose() {
    // Remove all tracked listeners
    for (const [type, listeners] of this.listeners) {
      for (const listener of listeners) {
        this.controls.removeEventListener(type, listener);
      }
    }
    this.listeners.clear();
    
    // Dispose controls
    this.controls.dispose();
  }
}

// Usage
const controlsManager = new CameraControlsManager(camera, renderer.domElement);

controlsManager.addEventListener('rest', () => {
  console.log('Camera at rest');
});

// Clean up when done
controlsManager.dispose();