@@ -378,18 +378,19 @@ pub(crate) fn build_clusters(
378378 . filter ( |m| m. time_ms >= start_ms && m. time_ms <= end_ms)
379379 . collect ( ) ;
380380
381+ let previous = moves_until_start
382+ . iter ( )
383+ . filter ( finite)
384+ . rev ( )
385+ . find ( |m| m. time_ms <= start_ms) ;
386+
381387 if events_in_range. is_empty ( ) {
382- let fallback = moves_until_start
383- . iter ( )
384- . filter ( finite)
385- . rev ( )
386- . find ( |m| m. time_ms <= start_ms)
387- . or_else ( || {
388- moves_from_start
389- . iter ( )
390- . filter ( finite)
391- . find ( |m| m. time_ms >= start_ms)
392- } ) ;
388+ let fallback = previous. or_else ( || {
389+ moves_from_start
390+ . iter ( )
391+ . filter ( finite)
392+ . find ( |m| m. time_ms >= start_ms)
393+ } ) ;
393394
394395 if let Some ( evt) = fallback {
395396 let ( x, y) = map_uv ( evt. x , evt. y ) ;
@@ -398,12 +399,16 @@ pub(crate) fn build_clusters(
398399 return vec ! [ ] ;
399400 }
400401
402+ let mut events = previous
403+ . filter ( |_| events_in_range[ 0 ] . time_ms > start_ms)
404+ . into_iter ( )
405+ . chain ( events_in_range) ;
401406 let mut clusters = Vec :: new ( ) ;
402- let first = events_in_range [ 0 ] ;
407+ let first = events . next ( ) . unwrap ( ) ;
403408 let ( first_x, first_y) = map_uv ( first. x , first. y ) ;
404409 let mut current = ClickCluster :: new ( first_x, first_y, first. time_ms ) ;
405410
406- for evt in & events_in_range [ 1 .. ] {
411+ for evt in events {
407412 let ( x, y) = map_uv ( evt. x , evt. y ) ;
408413 if current. can_add ( x, y, cluster_w, cluster_h) {
409414 current. add ( x, y, evt. time_ms ) ;
@@ -1836,6 +1841,65 @@ mod tests {
18361841 )
18371842 }
18381843
1844+ #[ test]
1845+ fn auto_zoom_uses_position_at_segment_start_over_earlier_movement ( ) {
1846+ let cursor = CursorEvents {
1847+ moves : vec ! [
1848+ move_event( 0.0 , 0.9 , 0.9 ) ,
1849+ move_event( 2000.0 , 0.1 , 0.1 ) ,
1850+ move_event( 6000.0 , 0.9 , 0.9 ) ,
1851+ ] ,
1852+ clicks : Vec :: new ( ) ,
1853+ } ;
1854+ for ordered in [ false , true ] {
1855+ let clusters = build_clusters ( & cursor, ordered, 2.0 , 10.0 , 2.0 , None ) ;
1856+ assert_eq ! ( cluster_center_at_time( & clusters, 2000.0 ) , Some ( ( 0.1 , 0.1 ) ) ) ;
1857+ }
1858+ }
1859+
1860+ #[ test]
1861+ fn auto_zoom_keeps_idle_cursor_visible_until_movement_resumes ( ) {
1862+ for ( recording_start, timescale) in [ ( 0.0 , 1.0 ) , ( 10.0 , 2.0 ) ] {
1863+ let segment = auto_segment ( 2.0 , 10.0 , 2.0 ) ;
1864+ let mut project = project_with_crop_styles ( vec ! [ segment] , Vec :: new ( ) ) ;
1865+ project. timeline . as_mut ( ) . unwrap ( ) . segments = vec ! [ TimelineSegment {
1866+ recording_clip: 0 ,
1867+ start: recording_start,
1868+ end: recording_start + 12.0 * timescale,
1869+ timescale,
1870+ name: None ,
1871+ speed_audio_mode: None ,
1872+ hide_cursor: None ,
1873+ volume: None ,
1874+ } ] ;
1875+ let cursor = CursorEvents {
1876+ moves : vec ! [
1877+ move_event( recording_start * 1000.0 , 0.1 , 0.1 ) ,
1878+ move_event( ( recording_start + 6.0 * timescale) * 1000.0 , 0.9 , 0.9 ) ,
1879+ ] ,
1880+ clicks : Vec :: new ( ) ,
1881+ } ;
1882+ let mut timeline = ZoomTransformTimeline :: from_project_for_clip (
1883+ & project,
1884+ & cursor,
1885+ 12.0 ,
1886+ XY :: new ( 1920 , 1080 ) ,
1887+ 0 ,
1888+ ) ;
1889+ timeline. precompute ( ) ;
1890+
1891+ for ( time, position) in [ ( 4.0 , 0.1 ) , ( 8.0 , 0.9 ) ] {
1892+ let zoom = timeline. sample ( time) ;
1893+ let ( left, top, size) = visible_viewport ( & zoom) ;
1894+ assert ! (
1895+ ( left..=left + size) . contains( & position)
1896+ && ( top..=top + size) . contains( & position) ,
1897+ "cursor {position} outside viewport at {time}s: {zoom:?}, source start={recording_start}, speed={timescale}"
1898+ ) ;
1899+ }
1900+ }
1901+ }
1902+
18391903 #[ test]
18401904 fn hovering_into_a_corner_re_aims_without_a_click ( ) {
18411905 // Regression for the real-recording report: clicks early near the
0 commit comments