class_name StudioViewportCamera extends Camera3D ## Professional 3D Studio Camera controller supporting: ## - Freecam flying (WASD + Q/E) ## - Turntable Orbit & Panning (RMB/MMB drag) ## - Selection framing/focus (F key) ## - Orthographic view presets (Top, Front, Side, Perspective) signal view_preset_changed(preset_name: String) enum ViewPreset { PERSPECTIVE, TOP, FRONT, SIDE } var current_preset: ViewPreset = ViewPreset.PERSPECTIVE # Camera movement parameters var move_speed: float = 12.0 var fast_multiplier: float = 2.5 var slow_multiplier: float = 0.3 var mouse_sensitivity: float = 0.003 var zoom_speed: float = 1.5 # Orbit / focus state var focal_point: Vector3 = Vector3.ZERO var orbit_distance: float = 15.0 var _yaw: float = -0.75 var _pitch: float = -0.5 var _rmb_down: bool = false var _mmb_down: bool = false var _move_input: Vector3 = Vector3.ZERO func _ready() -> void: current = true _update_orbit_transform() func _unhandled_input(event: InputEvent) -> void: if event is InputEventMouseButton: var mb := event as InputEventMouseButton if mb.button_index == MOUSE_BUTTON_RIGHT: _rmb_down = mb.pressed elif mb.button_index == MOUSE_BUTTON_MIDDLE: _mmb_down = mb.pressed elif mb.button_index == MOUSE_BUTTON_WHEEL_UP: zoom_camera(-zoom_speed) elif mb.button_index == MOUSE_BUTTON_WHEEL_DOWN: zoom_camera(zoom_speed) elif event is InputEventMouseMotion: var mm := event as InputEventMouseMotion if _rmb_down: # Freecam / FPS mouse look _yaw -= mm.relative.x * mouse_sensitivity _pitch = clampf(_pitch - mm.relative.y * mouse_sensitivity, -PI * 0.49, PI * 0.49) rotation = Vector3(_pitch, _yaw, 0.0) elif _mmb_down: # Pan camera parallel to viewport var pan_speed := orbit_distance * 0.0015 var right := global_transform.basis.x var up := global_transform.basis.y var delta := (-right * mm.relative.x + up * mm.relative.y) * pan_speed global_position += delta focal_point += delta elif event is InputEventKey: var k := event as InputEventKey if k.pressed and not k.echo: if k.keycode == KEY_F: focus_on(focal_point, 5.0) elif k.keycode == KEY_1: set_view_preset(ViewPreset.FRONT) elif k.keycode == KEY_3: set_view_preset(ViewPreset.SIDE) elif k.keycode == KEY_7: set_view_preset(ViewPreset.TOP) elif k.keycode == KEY_5: toggle_perspective_ortho() func _process(delta: float) -> void: if _rmb_down: _process_keyboard_flight(delta) func _process_keyboard_flight(delta: float) -> void: var dir := Vector3.ZERO if Input.is_physical_key_pressed(KEY_W): dir -= global_transform.basis.z if Input.is_physical_key_pressed(KEY_S): dir += global_transform.basis.z if Input.is_physical_key_pressed(KEY_A): dir -= global_transform.basis.x if Input.is_physical_key_pressed(KEY_D): dir += global_transform.basis.x if Input.is_physical_key_pressed(KEY_E): dir += Vector3.UP if Input.is_physical_key_pressed(KEY_Q): dir -= Vector3.UP if dir.length_squared() > 0.01: var speed := move_speed if Input.is_physical_key_pressed(KEY_SHIFT): speed *= fast_multiplier elif Input.is_physical_key_pressed(KEY_CTRL): speed *= slow_multiplier global_position += dir.normalized() * speed * delta focal_point = global_position - global_transform.basis.z * orbit_distance func zoom_camera(amount: float) -> void: orbit_distance = clampf(orbit_distance + amount, 1.0, 300.0) if projection == PROJECTION_ORTHOGONAL: size = clampf(size + amount, 1.0, 200.0) else: _update_orbit_transform() func focus_on(target_pos: Vector3, radius: float = 3.0) -> void: focal_point = target_pos orbit_distance = clampf(radius * 2.5, 3.0, 50.0) _update_orbit_transform() func _update_orbit_transform() -> void: var rot_basis := Basis.from_euler(Vector3(_pitch, _yaw, 0.0)) var offset := rot_basis * Vector3(0.0, 0.0, orbit_distance) _set_camera_pos(focal_point + offset) rotation = Vector3(_pitch, _yaw, 0.0) func _set_camera_pos(pos: Vector3) -> void: if is_inside_tree(): global_position = pos else: position = pos func set_view_preset(preset: ViewPreset) -> void: current_preset = preset match preset: ViewPreset.TOP: projection = PROJECTION_ORTHOGONAL size = orbit_distance rotation_degrees = Vector3(-90, 0, 0) _set_camera_pos(focal_point + Vector3(0, orbit_distance, 0)) view_preset_changed.emit("Top") ViewPreset.FRONT: projection = PROJECTION_ORTHOGONAL size = orbit_distance rotation_degrees = Vector3(0, 0, 0) _set_camera_pos(focal_point + Vector3(0, 0, orbit_distance)) view_preset_changed.emit("Front") ViewPreset.SIDE: projection = PROJECTION_ORTHOGONAL size = orbit_distance rotation_degrees = Vector3(0, 90, 0) _set_camera_pos(focal_point + Vector3(orbit_distance, 0, 0)) view_preset_changed.emit("Side") ViewPreset.PERSPECTIVE: projection = PROJECTION_PERSPECTIVE _pitch = -0.5 _yaw = -0.75 _update_orbit_transform() view_preset_changed.emit("Perspective") func toggle_perspective_ortho() -> void: if projection == PROJECTION_PERSPECTIVE: projection = PROJECTION_ORTHOGONAL size = orbit_distance else: projection = PROJECTION_PERSPECTIVE