File size: 4,949 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
extends Node
##
## TimelineModule - keyframe timeline for objects and bones (Features 81-100).
##
## Backed by a real Godot Animation resource + AnimationPlayer, so playback,
## interpolation and looping use engine-native code (fast, cached). The module
## exposes a touch-friendly API: scrub(), add_keyframe(), play(), pause().
##

@export var duration: float = 5.0
@export var fps: int = 30

var _host: Node = null
var _player: AnimationPlayer = null
var _anim: Animation = null
var _playhead: float = 0.0
var _playing := false
var _key_count := 0


func _activate(host: Node) -> void:
	_host = host
	_ensure_player()
	print("[Timeline] activated (%.1fs @ %d fps)" % [duration, fps])


func _deactivate() -> void:
	_playing = false
	_player = null
	_anim = null
	_host = null


func _ensure_player() -> void:
	if _player != null and is_instance_valid(_player):
		return
	_anim = Animation.new()
	_anim.length = duration
	_anim.step = 1.0 / float(fps)
	_player = AnimationPlayer.new()
	_player.name = "TimelinePlayer"
	if _host != null:
		_host.add_child(_player)
	var lib := AnimationLibrary.new()
	lib.add_animation("main", _anim)
	_player.add_animation_library("", lib)
	_player.animation_finished.connect(_on_finished)


func _process(delta: float) -> void:
	if not _playing or _player == null:
		return
	_playhead = _player.current_animation_position
	StudioBus.timeline_playhead.emit(_playhead)


## --- Playback ---------------------------------------------------------------

func play(from: float = -1.0) -> void:
	_ensure_player()
	if from >= 0.0:
		_player.seek(from, true)
	_player.play("main")
	_playing = true
	StudioBus.playback_state_changed.emit(true)


func pause() -> void:
	if _player != null:
		_player.pause()
	_playing = false
	StudioBus.playback_state_changed.emit(false)


func stop() -> void:
	if _player != null:
		_player.stop()
	_playhead = 0.0
	_playing = false
	StudioBus.playback_state_changed.emit(false)


## Jump the playhead to `time` seconds (scrubbing).
func scrub(time: float) -> void:
	_ensure_player()
	time = clampf(time, 0.0, duration)
	_playhead = time
	_player.seek(time, true)
	StudioBus.timeline_playhead.emit(time)


func is_playing() -> bool:
	return _playing


func playhead() -> float:
	return _playhead


## --- Keyframes --------------------------------------------------------------

## Add a keyframe for a `property` (NodePath-style) on an object at `time`.
## For bones use property like "bones/Head:position" (Skeleton3D pose tracks).
func add_keyframe(target_node: Node3D, property: String, time: float, value: Variant) -> void:
	_ensure_player()
	time = clampf(time, 0.0, duration)
	var path := _player.get_path_to(target_node)
	if path == NodePath():
		push_warning("Timeline: target is not under the player root")
		return
	var track := _find_or_create_track(String(path), property, value)
	var key_idx := _anim.track_insert_key(track, time, value, 0.0)
	if key_idx >= 0:
		_key_count += 1
		StudioBus.keyframe_added.emit(String(path), property, time)
		StudioBus.scene_modified.emit()


func _find_or_create_track(node_path: String, property: String, value: Variant) -> int:
	var full := NodePath(node_path + ":" + property)
	for i in range(_anim.get_track_count()):
		if _anim.track_get_path(i) == full:
			return i
	var type := Animation.TYPE_VALUE
	if value is Transform3D:
		type = Animation.TYPE_VALUE
	_anim.add_track(type)
	var idx := _anim.get_track_count() - 1
	_anim.track_set_path(idx, full)
	if value is Vector3:
		_anim.track_set_interpolation_type(idx, Animation.INTERPOLATION_CUBIC)
	return idx


## Convenience: key every transform channel of a bone of a Skeleton3D.
## Godot stores bone poses as 3D transforms, so we key position/rotation/scale.
func add_bone_keyframe(skeleton: Skeleton3D, bone_name: String, time: float) -> void:
	if skeleton == null:
		return
	var bone_idx := skeleton.find_bone(bone_name)
	if bone_idx < 0:
		push_warning("Timeline: bone '%s' not found" % bone_name)
		return
	var skin_path := "%s:%s" % [_player.get_path_to(skeleton), bone_name]
	var pose := skeleton.get_bone_pose(bone_idx)
	add_keyframe(skeleton, "bones/" + bone_name + "/position", time, pose.origin)
	# Store rotation as quaternion for clean interpolation.
	add_keyframe(skeleton, "bones/" + bone_name + "/rotation", time, pose.basis.get_rotation_quaternion())
	add_keyframe(skeleton, "bones/" + bone_name + "/scale", time, pose.basis.get_scale())


func delete_keyframe(track: int, key: int) -> void:
	if track >= 0 and track < _anim.get_track_count():
		_anim.track_remove_key(track, key)
		_key_count = maxi(0, _key_count - 1)


func track_count() -> int:
	return _anim.get_track_count() if _anim != null else 0


func key_count() -> int:
	return _key_count


func set_duration(seconds: float) -> void:
	duration = maxf(0.1, seconds)
	_ensure_player()
	_anim.length = duration


func _on_finished() -> void:
	_playing = false
	StudioBus.playback_state_changed.emit(false)