Download scripts/studio/touch_camera.gd from geter62/monkeystudio: direct link, hf CLI and curl.
- Browser
- Download file 5.55 kB
-
https://huggingface.co/geter62/monkeystudio/resolve/main/scripts/studio/touch_camera.gd
- Command line
-
hf download hf://geter62/monkeystudio/scripts/studio/touch_camera.gd
-
curl -L -o touch_camera.gd https://huggingface.co/geter62/monkeystudio/resolve/main/scripts/studio/touch_camera.gd
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 | |