monkeystudio / scripts /studio /touch_camera.gd
geter62's picture
Upload folder using huggingface_hub
84e8204 verified
Raw History Blame Contribute Delete
5.55 kB
extends Camera3D
##
## TouchCamera - lag-free mobile navigation for the 3D viewport (Step 3.1).
##
## Gesture map (configurable):
## one finger drag -> orbit (rotate around the focus point)
## two finger pinch -> dolly zoom
## two finger drag -> pan (move the focus point in screen space)
## three finger drag -> pan (alternative, larger screens)
##
## Design: raw InputEvent handling (not _input echo), one gesture re-evaluated
## per frame, no allocations in the hot path. Values are smoothed with an
## exponential filter so motion feels fluid even when touch sampling is jittery.
##
@export var orbit_speed: float = 0.4
@export var pan_speed: float = 0.6
@export var zoom_speed: float = 1.2
@export var min_distance: float = 0.5
@export var max_distance: float = 200.0
@export var smoothing: float = 18.0 # higher = snappier, lower = floatier
var focus: Vector3 = Vector3.ZERO
var _yaw: float = 0.6
var _pitch: float = -0.5
var _distance: float = 8.0
## When false, a single finger is reserved for sculpting/painting and only
## multi-finger gestures navigate. The UI flips this when a sculpt tool is on.
var one_finger_orbit: bool = true
var _painting := false
var _last_paint: Vector2 = Vector2.ZERO
# Smoothed targets
var _t_yaw: float = 0.6
var _t_pitch: float = -0.5
var _t_distance: float = 8.0
var _t_focus: Vector3 = Vector3.ZERO
# Touch bookkeeping
var _touches: Dictionary = {} # index -> position
var _prev_centroid: Vector2 = Vector2.ZERO
var _prev_spread: float = 0.0
var _gesture_active := false
func _ready() -> void:
_t_yaw = _yaw
_t_pitch = _pitch
_t_distance = _distance
_t_focus = focus
_apply_transform()
func _input(event: InputEvent) -> void:
if event is InputEventScreenTouch:
_handle_touch(event)
elif event is InputEventScreenDrag:
_handle_drag(event)
func _handle_touch(e: InputEventScreenTouch) -> void:
if e.pressed:
_touches[e.index] = e.position
if e.index == 0 and not one_finger_orbit and _touches.size() == 1:
_painting = true
_last_paint = e.position
StudioBus.stroke_begin.emit(_ray_center(e.position))
else:
_touches.erase(e.index)
if e.index == 0 and _painting:
_painting = false
StudioBus.stroke_end.emit()
if _touches.size() >= 2:
_recompute_gesture_baseline()
func _handle_drag(e: InputEventScreenDrag) -> void:
_touches[e.index] = e.position
if _painting and e.index == 0 and not one_finger_orbit:
var world := _ray_center(e.position)
var prev_world := _ray_center(_last_paint)
StudioBus.stroke_apply.emit(world, world - prev_world)
_last_paint = e.position
return
var n := _touches.size()
if n == 1:
# Orbit
var d := e.relative
_t_yaw -= d.x * orbit_speed * 0.01
_t_pitch = clampf(_t_pitch - d.y * orbit_speed * 0.01, -1.5, 1.5)
elif n == 2:
var pts := _touches.values()
var centroid: Vector2 = (pts[0] + pts[1]) * 0.5
var spread: float = (pts[0] as Vector2).distance_to(pts[1] as Vector2)
if _prev_spread > 0.0:
var dzoom := (spread - _prev_spread) * zoom_speed * 0.01
_t_distance = clampf(_t_distance - dzoom, min_distance, max_distance)
var dpan := centroid - _prev_centroid
_apply_pan(dpan)
_prev_centroid = centroid
_prev_spread = spread
elif n >= 3:
var pts := _touches.values()
var centroid := Vector2.ZERO
for p in pts:
centroid += p
centroid /= float(pts.size())
var dpan := centroid - _prev_centroid
_apply_pan(dpan)
_prev_centroid = centroid
func _recompute_gesture_baseline() -> void:
var pts := _touches.values()
if pts.size() >= 2:
_prev_centroid = (pts[0] + pts[1]) * 0.5
_prev_spread = (pts[0] as Vector2).distance_to(pts[1] as Vector2)
func _apply_pan(screen_delta: Vector2) -> void:
# Pan along camera right/up, scaled by distance so it feels consistent at
# any zoom level.
var scale := _distance * pan_speed * 0.002
var right := global_transform.basis.x
var up := global_transform.basis.y
_t_focus -= right * screen_delta.x * scale
_t_focus += up * screen_delta.y * scale
focus = _t_focus
func _process(delta: float) -> void:
var t := 1.0 - exp(-smoothing * delta)
_yaw = lerpf(_yaw, _t_yaw, t)
_pitch = lerpf(_pitch, _t_pitch, t)
_distance = lerpf(_distance, _t_distance, t)
focus = focus.lerp(_t_focus, t)
_apply_transform()
func _apply_transform() -> void:
var offset := Vector3(
cos(_pitch) * sin(_yaw),
sin(_pitch),
cos(_pitch) * cos(_yaw)
) * _distance
global_position = focus + offset
look_at(focus, Vector3.UP)
## --- Public helpers ---------------------------------------------------------
func frame_aabb(box: AABB, margin: float = 1.6) -> void:
_t_focus = box.get_center()
focus = _t_focus
var radius := maxf(0.5, box.size.length() * 0.5)
_t_distance = clampf(radius * margin * 2.0, min_distance, max_distance)
_distance = _t_distance
func set_focus(p: Vector3) -> void:
_t_focus = p
focus = p
func screen_to_ray(screen_pos: Vector2) -> Dictionary:
return {
"origin": project_ray_origin(screen_pos),
"dir": project_ray_normal(screen_pos),
}
## Ray-cast the viewport at a screen point against physics; returns the world
## hit (or a point in front of the camera if nothing is hit).
func _ray_center(screen_pos: Vector2) -> Vector3:
var origin := project_ray_origin(screen_pos)
var dir := project_ray_normal(screen_pos)
var space := get_world_3d().direct_space_state
var q := PhysicsRayQueryParameters3D.create(origin, origin + dir * 1000.0)
var res := space.intersect_ray(q)
if not res.is_empty():
return res["position"]
return origin + dir * _distance