Editor Camera
In Edit mode the editor needs its own view, separate from whatever camera the game uses. SafiEditorCamera is a dedicated ECS entity that carries {SafiTransform, SafiCamera, SafiEditorCamera} and runs a fly-cam controller on EcsOnUpdate. When the editor is in Edit, the fly-cam owns the SafiActiveCamera tag; switching to Play or Paused restores the tag to whichever entity held it before.
The editor cam is installed automatically by safi_app_init when enable_debug_ui is set. It is not serialized — it rebuilds from defaults every launch.
Controls (Edit mode only)
Pitch is clamped to ±89° to avoid gimbal lock. A new right-mouse-drag is only accepted if the cursor is over the viewport — but once dragging, the cursor can sweep over any MicroUI panel without breaking the gesture (SDL relative-mouse mode captures the cursor for the duration).
WASD / QE only translate while RMB is held. Without RMB the keys are free for the Q/W/E/R tool shortcuts; pressing W to switch to Translate doesn't also fly the camera forward. This matches the Unreal scene-view UX.
Struct
Defaults: move_speed = 5.0, look_speed = 0.003. Mutable at runtime — bump them in the Inspector once a "+ Add Component" workflow lands, or set them directly from user code.
API
safi_editor_camera_installcreates the editor-cam entity, registers the fly-cam system onEcsOnUpdate, and primes active-camera arbitration. Idempotent — calling it twice is a no-op. The engine calls it automatically insafi_app_initwhen the debug UI is enabled.safi_editor_camera_entityreturns the editor-cam id (0 if not installed).
Active-camera arbitration
The fly-cam system checks SafiEditorState.mode at the top of every tick and shuffles the SafiActiveCamera tag accordingly:
- Entering Edit. If the editor cam isn't already active, remember whichever entity is (stash its id in
_prev_active_cam), remove the tag from it, and attach it to the editor cam. - Leaving Edit. Remove the tag from the editor cam and restore it on
_prev_active_cam(if still alive). Also drops any in-flight RMB drag so SDL relative-mouse mode doesn't linger into gameplay.
The result: the renderer's (SafiCamera, SafiActiveCamera) query always finds exactly one viewpoint — the gameplay camera during Play, the fly-cam during Edit. Users don't have to plumb anything.
Input guards
The fly-cam cooperates with the debug UI:
- WASD/QE are skipped when
safi_debug_ui_wants_input()is true — i.e. the user is typing into an Inspector field. Without this, typingwin a property would fly the camera. - New RMB drags are rejected when
safi_debug_ui_mouse_over_viewport()is false — clicks that land on the Scene or Inspector panel never start a camera rotation.
Both helpers live in safi/ui/debug_ui.h.
Camera pose fields
SafiCamera now carries an explicit pose (eye, forward, up). The fly-cam writes to those fields each frame; the render system reads them directly. Legacy scenes that only store target still work — the deserializer synthesises a pose from the old eye = target + (0,0,3) convention. See Camera System for the struct.
Demo keybind
The gltf_viewer demo maps F1 to toggle Edit ↔ Play. In Edit, right-click-drag the viewport to fly around the scene; press F1 to return to Play and watch the gameplay camera snap back.
See also
- Editor State — the mode flag the fly-cam gates on
- Camera System — the
SafiCamerapose fields the fly-cam writes - Scheduler — why the fly-cam runs on
EcsOnUpdateand not the game phase - Debug UI —
safi_debug_ui_mouse_over_viewport