draw thumbnails on group clips too
This commit is contained in:
parent
b3e1da3152
commit
78577babb1
|
|
@ -1679,6 +1679,213 @@ impl TimelinePane {
|
|||
}
|
||||
}
|
||||
|
||||
// Render video thumbnails from the top video child layer
|
||||
// and waveforms from audio child layers inside the collapsed group row
|
||||
{
|
||||
let span_y_min = y + 10.0;
|
||||
let span_y_max = y + LAYER_HEIGHT - 10.0;
|
||||
let span_height = span_y_max - span_y_min;
|
||||
let thumb_y_max = span_y_min + span_height * (2.0 / 3.0);
|
||||
let wave_y_min = thumb_y_max;
|
||||
|
||||
// Find the first (top) video child and draw thumbnails for its clips
|
||||
if let Some(video_child) = g.children.iter().find(|c| matches!(c, AnyLayer::Video(_))) {
|
||||
if let AnyLayer::Video(vl) = video_child {
|
||||
for ci in &vl.clip_instances {
|
||||
let clip_dur = document.get_clip_duration(&ci.clip_id)
|
||||
.unwrap_or_else(|| ci.trim_end.unwrap_or(1.0) - ci.trim_start);
|
||||
let mut ci_start = ci.effective_start();
|
||||
if is_move_drag && selection.contains_clip_instance(&ci.id) {
|
||||
ci_start = (ci_start + self.drag_offset).max(0.0);
|
||||
}
|
||||
let ci_duration = ci.total_duration(clip_dur);
|
||||
let ci_end = ci_start + ci_duration;
|
||||
|
||||
let sx = self.time_to_x(ci_start);
|
||||
let ex = self.time_to_x(ci_end);
|
||||
if ex < 0.0 || sx > rect.width() { continue; }
|
||||
|
||||
let ci_rect = egui::Rect::from_min_max(
|
||||
egui::pos2((rect.min.x + sx).max(rect.min.x), span_y_min),
|
||||
egui::pos2((rect.min.x + ex).min(rect.max.x), thumb_y_max),
|
||||
);
|
||||
|
||||
visible_video_clip_ids.insert(ci.clip_id);
|
||||
|
||||
// Collect for hover tooltip (use full span height as hover target)
|
||||
let hover_rect = egui::Rect::from_min_max(
|
||||
egui::pos2(ci_rect.min.x, span_y_min),
|
||||
egui::pos2(ci_rect.max.x, span_y_max),
|
||||
);
|
||||
video_clip_hovers.push((hover_rect, ci.clip_id, ci.trim_start, ci_start));
|
||||
|
||||
let thumb_display_height = (thumb_y_max - span_y_min) - 4.0;
|
||||
if thumb_display_height > 8.0 {
|
||||
let video_mgr = video_manager.lock().unwrap();
|
||||
if let Some((tw, th, _)) = video_mgr.get_thumbnail_at(&ci.clip_id, 0.0) {
|
||||
let aspect = tw as f32 / th as f32;
|
||||
let thumb_display_width = thumb_display_height * aspect;
|
||||
let ci_width = ci_rect.width();
|
||||
let num_thumbs = ((ci_width / thumb_display_width).ceil() as usize).max(1);
|
||||
|
||||
for ti in 0..num_thumbs {
|
||||
let x_offset = ti as f32 * thumb_display_width;
|
||||
if x_offset >= ci_width { break; }
|
||||
|
||||
let time_offset = (x_offset as f64 + thumb_display_width as f64 * 0.5)
|
||||
/ self.pixels_per_second as f64;
|
||||
let content_time = ci.trim_start + time_offset;
|
||||
|
||||
if let Some((tw, th, rgba_data)) = video_mgr.get_thumbnail_at(&ci.clip_id, content_time) {
|
||||
let ts_key = (content_time * 1000.0) as i64;
|
||||
let cache_key = (ci.clip_id, ts_key);
|
||||
|
||||
let texture = self.video_thumbnail_textures
|
||||
.entry(cache_key)
|
||||
.or_insert_with(|| {
|
||||
let image = egui::ColorImage::from_rgba_unmultiplied(
|
||||
[tw as usize, th as usize],
|
||||
&rgba_data,
|
||||
);
|
||||
ui.ctx().load_texture(
|
||||
format!("vthumb_{}_{}", ci.clip_id, ts_key),
|
||||
image,
|
||||
egui::TextureOptions::LINEAR,
|
||||
)
|
||||
});
|
||||
|
||||
let full_rect = egui::Rect::from_min_size(
|
||||
egui::pos2(ci_rect.min.x + x_offset, ci_rect.min.y + 2.0),
|
||||
egui::vec2(thumb_display_width, thumb_display_height),
|
||||
);
|
||||
let thumb_rect = full_rect.intersect(ci_rect);
|
||||
|
||||
if thumb_rect.width() > 2.0 && thumb_rect.height() > 2.0 {
|
||||
let uv_min = egui::pos2(
|
||||
(thumb_rect.min.x - full_rect.min.x) / full_rect.width(),
|
||||
(thumb_rect.min.y - full_rect.min.y) / full_rect.height(),
|
||||
);
|
||||
let uv_max = egui::pos2(
|
||||
(thumb_rect.max.x - full_rect.min.x) / full_rect.width(),
|
||||
(thumb_rect.max.y - full_rect.min.y) / full_rect.height(),
|
||||
);
|
||||
|
||||
painter.image(
|
||||
texture.id(),
|
||||
thumb_rect,
|
||||
egui::Rect::from_min_max(uv_min, uv_max),
|
||||
egui::Color32::WHITE,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw waveforms from audio child layers
|
||||
let screen_size = ui.ctx().content_rect().size();
|
||||
let waveform_tint = [
|
||||
bright_teal.r() as f32 / 255.0,
|
||||
bright_teal.g() as f32 / 255.0,
|
||||
bright_teal.b() as f32 / 255.0,
|
||||
bright_teal.a() as f32 / 255.0,
|
||||
];
|
||||
for child in &g.children {
|
||||
if let AnyLayer::Audio(al) = child {
|
||||
for ci in &al.clip_instances {
|
||||
let audio_clip = match document.get_audio_clip(&ci.clip_id) {
|
||||
Some(c) => c,
|
||||
None => continue,
|
||||
};
|
||||
let audio_pool_index = match audio_clip.audio_pool_index() {
|
||||
Some(idx) => idx,
|
||||
None => continue,
|
||||
};
|
||||
let (samples, sr, ch) = match raw_audio_cache.get(&audio_pool_index) {
|
||||
Some(v) => v,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let total_frames = samples.len() / (*ch).max(1) as usize;
|
||||
let audio_file_duration = total_frames as f64 / *sr as f64;
|
||||
|
||||
let clip_dur = audio_clip.duration;
|
||||
let mut ci_start = ci.effective_start();
|
||||
if is_move_drag && selection.contains_clip_instance(&ci.id) {
|
||||
ci_start = (ci_start + self.drag_offset).max(0.0);
|
||||
}
|
||||
let ci_duration = ci.total_duration(clip_dur);
|
||||
|
||||
let ci_screen_start = rect.min.x + self.time_to_x(ci_start);
|
||||
let ci_screen_end = ci_screen_start + (ci_duration * self.pixels_per_second as f64) as f32;
|
||||
|
||||
let waveform_rect = egui::Rect::from_min_max(
|
||||
egui::pos2(ci_screen_start.max(rect.min.x), wave_y_min),
|
||||
egui::pos2(ci_screen_end.min(rect.max.x), span_y_max),
|
||||
);
|
||||
|
||||
if waveform_rect.width() > 0.0 && waveform_rect.height() > 0.0 {
|
||||
let pending_upload = if waveform_gpu_dirty.contains(&audio_pool_index) {
|
||||
let chunk = crate::waveform_gpu::UPLOAD_CHUNK_FRAMES;
|
||||
let progress = self.waveform_upload_progress.get(&audio_pool_index).copied().unwrap_or(0);
|
||||
let next_end = (progress + chunk).min(total_frames);
|
||||
let frame_limit = Some(next_end);
|
||||
if next_end >= total_frames {
|
||||
waveform_gpu_dirty.remove(&audio_pool_index);
|
||||
self.waveform_upload_progress.remove(&audio_pool_index);
|
||||
} else {
|
||||
self.waveform_upload_progress.insert(audio_pool_index, next_end);
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
Some(crate::waveform_gpu::PendingUpload {
|
||||
samples: samples.clone(),
|
||||
sample_rate: *sr,
|
||||
channels: *ch,
|
||||
frame_limit,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let instance_id = ci.id.as_u128() as u64;
|
||||
let callback = crate::waveform_gpu::WaveformCallback {
|
||||
pool_index: audio_pool_index,
|
||||
segment_index: 0,
|
||||
params: crate::waveform_gpu::WaveformParams {
|
||||
clip_rect: [waveform_rect.min.x, waveform_rect.min.y, waveform_rect.max.x, waveform_rect.max.y],
|
||||
viewport_start_time: self.viewport_start_time as f32,
|
||||
pixels_per_second: self.pixels_per_second as f32,
|
||||
audio_duration: audio_file_duration as f32,
|
||||
sample_rate: *sr as f32,
|
||||
clip_start_time: ci_screen_start,
|
||||
trim_start: ci.trim_start as f32,
|
||||
tex_width: crate::waveform_gpu::tex_width() as f32,
|
||||
total_frames: total_frames as f32,
|
||||
segment_start_frame: 0.0,
|
||||
display_mode: if waveform_stereo { 1.0 } else { 0.0 },
|
||||
_pad1: [0.0, 0.0],
|
||||
tint_color: waveform_tint,
|
||||
screen_size: [screen_size.x, screen_size.y],
|
||||
_pad: [0.0, 0.0],
|
||||
},
|
||||
target_format,
|
||||
pending_upload,
|
||||
instance_id,
|
||||
};
|
||||
|
||||
ui.painter().add(egui_wgpu::Callback::new_paint_callback(
|
||||
waveform_rect,
|
||||
callback,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Separator line at bottom
|
||||
painter.line_segment(
|
||||
[
|
||||
|
|
|
|||
Loading…
Reference in New Issue