File size: 5,554 Bytes
84e8204
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
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