monkeystudio / scripts /modules /timeline.gd
geter62's picture
Upload folder using huggingface_hub
84e8204 verified
Raw History Blame Contribute Delete
4.95 kB
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)