@@ -23,9 +23,9 @@ use cap_media::{
2323 sources:: { CaptureScreen , CaptureWindow } ,
2424} ;
2525use cap_project:: {
26- Platform , ProjectConfiguration , RecordingMeta , RecordingMetaInner , SharingMeta ,
27- StudioRecordingMeta , TimelineConfiguration , TimelineSegment , ZoomMode , ZoomSegment ,
28- cursor:: CursorEvents ,
26+ CursorClickEvent , Platform , ProjectConfiguration , RecordingMeta , RecordingMetaInner ,
27+ SharingMeta , StudioRecordingMeta , TimelineConfiguration , TimelineSegment , ZoomMode ,
28+ ZoomSegment , cursor:: CursorEvents ,
2929} ;
3030use cap_recording:: {
3131 CompletedStudioRecording , RecordingError , RecordingMode , StudioRecordingHandle ,
@@ -831,48 +831,22 @@ async fn handle_recording_finish(
831831 Ok ( ( ) )
832832}
833833
834- /// Generates zoom segments based on mouse click events during recording .
834+ /// Core logic for generating zoom segments based on mouse click events.
835835/// This is an experimental feature that automatically creates zoom effects
836836/// around user interactions to highlight important moments.
837- fn generate_zoom_segments_from_clicks (
838- recording : & CompletedStudioRecording ,
837+ fn generate_zoom_segments_from_clicks_impl (
838+ mut clicks : Vec < CursorClickEvent > ,
839839 recordings : & ProjectRecordingsMeta ,
840840) -> Vec < ZoomSegment > {
841841 const ZOOM_SEGMENT_AFTER_CLICK_PADDING : f64 = 1.5 ;
842+ const ZOOM_SEGMENT_BEFORE_CLICK_PADDING : f64 = 0.8 ;
842843 const ZOOM_DURATION : f64 = 1.0 ;
843844 const CLICK_GROUP_THRESHOLD : f64 = 0.6 ; // seconds
845+ const MIN_SEGMENT_PADDING : f64 = 2.0 ; // minimum gap between segments
844846
845847 let max_duration = recordings. duration ( ) ;
846848
847- // Build a temporary RecordingMeta so we can load cursor events from disk
848- let recording_meta = RecordingMeta {
849- platform : None ,
850- project_path : recording. project_path . clone ( ) ,
851- pretty_name : String :: new ( ) ,
852- sharing : None ,
853- inner : RecordingMetaInner :: Studio ( recording. meta . clone ( ) ) ,
854- } ;
855-
856- let mut all_events = CursorEvents :: default ( ) ;
857-
858- match & recording. meta {
859- StudioRecordingMeta :: SingleSegment { segment } => {
860- if let Some ( cursor_path) = & segment. cursor {
861- if let Ok ( mut ev) = CursorEvents :: load_from_file ( & recording_meta. path ( cursor_path) )
862- {
863- all_events. clicks . append ( & mut ev. clicks ) ;
864- }
865- }
866- }
867- StudioRecordingMeta :: MultipleSegments { inner, .. } => {
868- for seg in & inner. segments {
869- let mut ev = seg. cursor_events ( & recording_meta) ;
870- all_events. clicks . append ( & mut ev. clicks ) ;
871- }
872- }
873- }
874-
875- all_events. clicks . sort_by ( |a, b| {
849+ clicks. sort_by ( |a, b| {
876850 a. time_ms
877851 . partial_cmp ( & b. time_ms )
878852 . unwrap_or ( std:: cmp:: Ordering :: Equal )
@@ -881,24 +855,30 @@ fn generate_zoom_segments_from_clicks(
881855 let mut segments = Vec :: < ZoomSegment > :: new ( ) ;
882856
883857 // Generate segments around mouse clicks
884- for click in & all_events . clicks {
858+ for click in & clicks {
885859 if !click. down {
886860 continue ;
887861 }
888862
889863 let time = click. time_ms / 1000.0 ;
890864
865+ let proposed_start = ( time - ZOOM_SEGMENT_BEFORE_CLICK_PADDING ) . max ( 0.0 ) ;
866+ let proposed_end = ( time + ZOOM_SEGMENT_AFTER_CLICK_PADDING ) . min ( max_duration) ;
867+
891868 if let Some ( last) = segments. last_mut ( ) {
892- if time <= last. end + CLICK_GROUP_THRESHOLD {
893- last. end = ( time + ZOOM_SEGMENT_AFTER_CLICK_PADDING ) . min ( max_duration) ;
869+ // Merge if within group threshold OR if segments would be too close together
870+ if time <= last. end + CLICK_GROUP_THRESHOLD
871+ || proposed_start <= last. end + MIN_SEGMENT_PADDING
872+ {
873+ last. end = proposed_end;
894874 continue ;
895875 }
896876 }
897877
898878 if time < max_duration - ZOOM_DURATION {
899879 segments. push ( ZoomSegment {
900- start : ( time - 0.8 ) . max ( 0.0 ) ,
901- end : ( time + ZOOM_SEGMENT_AFTER_CLICK_PADDING ) . min ( max_duration ) ,
880+ start : proposed_start ,
881+ end : proposed_end ,
902882 amount : 2.0 ,
903883 mode : ZoomMode :: Auto ,
904884 } ) ;
@@ -908,6 +888,54 @@ fn generate_zoom_segments_from_clicks(
908888 segments
909889}
910890
891+ /// Generates zoom segments based on mouse click events during recording.
892+ /// Used during the recording completion process.
893+ pub fn generate_zoom_segments_from_clicks (
894+ recording : & CompletedStudioRecording ,
895+ recordings : & ProjectRecordingsMeta ,
896+ ) -> Vec < ZoomSegment > {
897+ // Build a temporary RecordingMeta so we can use the common implementation
898+ let recording_meta = RecordingMeta {
899+ platform : None ,
900+ project_path : recording. project_path . clone ( ) ,
901+ pretty_name : String :: new ( ) ,
902+ sharing : None ,
903+ inner : RecordingMetaInner :: Studio ( recording. meta . clone ( ) ) ,
904+ } ;
905+
906+ generate_zoom_segments_for_project ( & recording_meta, recordings)
907+ }
908+
909+ /// Generates zoom segments from clicks for an existing project.
910+ /// Used in the editor context where we have RecordingMeta.
911+ pub fn generate_zoom_segments_for_project (
912+ recording_meta : & RecordingMeta ,
913+ recordings : & ProjectRecordingsMeta ,
914+ ) -> Vec < ZoomSegment > {
915+ let RecordingMetaInner :: Studio ( studio_meta) = & recording_meta. inner else {
916+ return Vec :: new ( ) ;
917+ } ;
918+
919+ let all_events = match studio_meta {
920+ StudioRecordingMeta :: SingleSegment { segment } => {
921+ if let Some ( cursor_path) = & segment. cursor {
922+ CursorEvents :: load_from_file ( & recording_meta. path ( cursor_path) )
923+ . unwrap_or_default ( )
924+ . clicks
925+ } else {
926+ vec ! [ ]
927+ }
928+ }
929+ StudioRecordingMeta :: MultipleSegments { inner, .. } => inner
930+ . segments
931+ . iter ( )
932+ . flat_map ( |s| s. cursor_events ( recording_meta) . clicks )
933+ . collect ( ) ,
934+ } ;
935+
936+ generate_zoom_segments_from_clicks_impl ( all_events, recordings)
937+ }
938+
911939fn project_config_from_recording (
912940 app : & AppHandle ,
913941 completed_recording : & CompletedStudioRecording ,
@@ -940,3 +968,9 @@ fn project_config_from_recording(
940968 ..default_config. unwrap_or_default ( )
941969 }
942970}
971+
972+ #[ cfg( test) ]
973+ mod test {
974+ #[ test]
975+ fn zoom_segment_gaps ( ) { }
976+ }
0 commit comments