| name | thatopen-impl-navigation |
| description | Use when configuring camera navigation modes, switching between orbit and first-person views, or implementing plan view for floor plans. Prevents incorrect projection usage and navigation mode conflicts. Covers OrthoPerspectiveCamera modes (Orbit, FirstPerson, Plan), projection switching, fit() framing, custom navigation modes, camera controls, frustum settings, user input toggling. Keywords: navigation, camera, orbit, first person, plan, projection, perspective, orthographic, fit, controls, zoom, pan, rotate, fly to, look at, camera position, walk through model.
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires @thatopen/components 3.3.x. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
ThatOpen Navigation: OrthoPerspectiveCamera
Overview
OrthoPerspectiveCamera extends SimpleCamera and provides three built-in
navigation modes (Orbit, FirstPerson, Plan) with switchable perspective and
orthographic projections. It wraps the camera-controls library and manages
dual Three.js cameras (PerspectiveCamera + OrthographicCamera).
Package: @thatopen/components (core โ works in browser and Node.js)
Dependency: camera-controls >= 3.1.2
Prerequisites
- World must be created with
OrthoPerspectiveCamera as the camera type.
See thatopen-impl-viewer for full viewer setup.
components.init() MUST be called after camera assignment for the render
loop to start.
import * as OBC from "@thatopen/components";
const components = new OBC.Components();
const worlds = components.get(OBC.Worlds);
const world = worlds.create<
OBC.SimpleScene,
OBC.OrthoPerspectiveCamera,
OBC.SimpleRenderer
>();
world.scene = new OBC.SimpleScene(components);
world.scene.setup();
world.renderer = new OBC.SimpleRenderer(components, container);
world.camera = new OBC.OrthoPerspectiveCamera(components);
components.init();
Navigation Modes
Three built-in modes control how the user interacts with the 3D viewport.
Switch modes with camera.set(mode).
Mode Comparison Table
| Property | Orbit | FirstPerson | Plan |
|---|
| ID | "Orbit" | "FirstPerson" | "Plan" |
| Default | Yes (active at start) | No | No |
| Rotation | Full orbit around target | Look around from position | Disabled (locked) |
| Pan | Truck (right-drag) | Truck (high speed) | Truck (left-drag) |
| Zoom | Dolly (scroll) | Dolly (scroll) | Zoom (scroll) |
| Projection | Any | Perspective ONLY | Any (ortho preferred) |
| Use case | 3D model inspection | Walkthroughs | Floor plan views |
Orbit Mode (Default)
Standard 3D orbit navigation. Rotates around a target point.
Camera-controls settings applied:
minDistance: 1
maxDistance: 300
truckSpeed: 2
- Orbit target calculated from camera direction and distance
world.camera.set("Orbit");
FirstPerson Mode
FPS-style navigation. Camera stays at a fixed position; user looks around
and moves through the scene.
Camera-controls settings applied:
minDistance: 1, maxDistance: 1, distance: 1
truckSpeed: 50
- Mouse wheel mapped to dolly action
- Two-finger touch mapped to zoom-truck
CRITICAL: FirstPerson mode ALWAYS requires perspective projection. It
CANNOT run with orthographic projection. ALWAYS validate before switching:
const { current } = world.camera.projection;
if (current === "Orthographic") {
await world.camera.projection.set("Perspective");
}
world.camera.set("FirstPerson");
If you call set("FirstPerson") while in orthographic mode, the camera
falls back to Orbit mode instead.
Plan Mode
2D floor plan navigation. Rotation is disabled; only pan and zoom work.
Ideal for architectural floor plans combined with clipping planes.
Camera-controls settings applied:
- Azimuth rotation speed: 0 (disabled)
- Polar rotation speed: 0 (disabled)
- Left mouse mapped to TRUCK (pan)
- Single-touch mapped to TOUCH_TRUCK
- Dual-touch mapped to TOUCH_ZOOM
When Plan mode is deactivated, original rotation speeds and mouse/touch
mappings are restored.
world.camera.set("Plan");
await world.camera.projection.set("Orthographic");
Camera Controls Interaction Table
| Input | Orbit | FirstPerson | Plan |
|---|
| Left mouse drag | Rotate | Look around | Pan |
| Right mouse drag | Pan (truck) | Pan (truck) | Pan (truck) |
| Scroll wheel | Zoom (dolly) | Move (dolly) | Zoom |
| Single-finger touch | Rotate | Look around | Pan |
| Two-finger touch | Zoom + Pan | Zoom + Truck | Zoom |
Projection System
ProjectionManager handles switching between perspective and orthographic
cameras. Access it via camera.projection.
Switching Projection
await world.camera.projection.set("Orthographic");
await world.camera.projection.set("Perspective");
await world.camera.projection.toggle();
const current: OBC.CameraProjection = world.camera.projection.current;
Projection Changed Event
Listen for projection changes to update UI or other components:
world.camera.projection.onChanged.add(() => {
const projection = world.camera.projection.current;
console.log("Projection changed to:", projection);
grid.fade = projection === "Perspective";
});
onChanged fires after the projection switch completes. The event emits
the new active THREE.Camera instance.
Projection Compatibility Matrix
| Projection | Orbit | FirstPerson | Plan |
|---|
| Perspective | Yes | Yes | Yes |
| Orthographic | Yes | NO | Yes |
NEVER combine FirstPerson mode with Orthographic projection. ALWAYS
validate before allowing user selection of mode/projection combinations.
Dual Camera Access
OrthoPerspectiveCamera maintains two Three.js camera instances:
const perspCam = world.camera.threePersp;
const orthoCam = world.camera.threeOrtho;
const activeCam = world.camera.three;
threePersp and threeOrtho ALWAYS exist, regardless of current mode.
camera.three returns whichever camera is currently active.
- The frustum size for orthographic projection is fixed at 50.
- Aspect ratio is automatically updated when the renderer resizes.
Fit / Frame Objects
fit() positions the camera to frame a set of meshes in the viewport.
await world.camera.fit(meshes);
await world.camera.fit(meshes, 2.0);
Parameters:
meshes: Iterable<THREE.Mesh> โ the meshes to frame
offset: number (default 1.5) โ padding multiplier around the
bounding box. Values > 1 add space around the objects.
There is also a convenience method fitToItems() that fits to all loaded
fragment models:
world.camera.fitToItems();
User Input Control
Temporarily disable or enable all camera controls:
world.camera.setUserInput(false);
world.camera.setUserInput(true);
When disabled, setUserInput(false) stores the current mouse button
mappings and then nullifies them. When re-enabled, mappings are restored.
Use this to lock the camera during animations, automated camera movements,
or guided tours.
Custom Navigation Modes
Register a custom navigation mode that follows the NavigationMode
interface:
interface NavigationMode {
id: string;
enabled: boolean;
set(active: boolean): void;
}
Register and use:
const flyMode: OBC.NavigationMode = {
id: "Fly",
enabled: false,
set(active: boolean) {
this.enabled = active;
if (active) {
const controls = world.camera.controls;
controls.minDistance = 0;
controls.maxDistance = Infinity;
controls.truckSpeed = 10;
}
},
};
world.camera.addCustomNavigationMode(flyMode);
world.camera.set("Fly");
Custom modes are stored in the internal _navigationModes map alongside
the built-in Orbit, FirstPerson, and Plan modes.
Camera Controls (Low-Level)
Direct access to the underlying camera-controls instance:
const controls = world.camera.controls;
await controls.setLookAt(x, y, z, targetX, targetY, targetZ);
await controls.setLookAt(x, y, z, tx, ty, tz, true);
controls.addEventListener("update", () => {
fragments.core.update();
});
ALWAYS use controls.setLookAt() for programmatic camera positioning.
NEVER set camera.three.position directly โ camera-controls will
overwrite it on the next frame.
Floor Plan Setup Pattern
Complete pattern for floor plan viewing with Plan mode:
world.camera.set("Plan");
await world.camera.projection.set("Orthographic");
await world.camera.controls.setLookAt(
centerX, height, centerZ,
centerX, 0, centerZ,
);
grid.fade = false;
Critical Rules
- NEVER combine FirstPerson mode with Orthographic projection. The
camera silently falls back to Orbit mode, causing confusing behavior.
- ALWAYS validate mode/projection compatibility before switching.
- NEVER set
camera.three.position directly. ALWAYS use
camera.controls.setLookAt() or camera.controls.moveTo().
- ALWAYS call
projection.set() with await โ it returns a Promise.
- NEVER forget to listen to
projection.onChanged when your UI or
components depend on the current projection type.
- ALWAYS use
setUserInput(false) during programmatic camera
animations to prevent user interference.
- ALWAYS call
camera.dispose() on cleanup (handled automatically
when the world is disposed via worlds.delete(world)).
- NEVER instantiate OrthoPerspectiveCamera with
new and forget to
assign it to world.camera. The camera needs a world context.
Reference Files
Source Verification
All API signatures verified against:
- GitHub:
ThatOpen/engine_components main branch
(packages/core/src/core/OrthoPerspectiveCamera/)
- Example:
OrthoPerspectiveCamera/example.ts
- Research:
docs/research/vooronderzoek-thatopen.md Section 2.5