Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/opentake-agent/src/mcp/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -721,7 +721,7 @@ impl Dispatcher {
let Some(transform_patch) = a.transform else {
let res = self.apply(EditCommand::SetClipProperties {
clip_ids,
properties,
properties: Box::new(properties),
})?;
return Ok(ToolResult::ok(res.summary));
};
Expand All @@ -746,7 +746,7 @@ impl Dispatcher {
for (clip_id, clip_properties) in per_clip {
let res = self.apply(EditCommand::SetClipProperties {
clip_ids: vec![clip_id],
properties: clip_properties,
properties: Box::new(clip_properties),
})?;
summaries.push(res.summary);
}
Expand Down
105 changes: 103 additions & 2 deletions crates/opentake-ops/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ pub struct ClipProperties {
pub opacity: Option<f64>,
pub transform: Option<Transform>,
pub text_content: Option<String>,
/// Text style for a text clip (font / size / color / alignment / shadow /
/// background / border). Replaces the clip's whole `text_style`.
pub text_style: Option<opentake_domain::TextStyle>,
/// Per-clip crop insets (normalized 0–1). Setting this clears `crop_track`.
pub crop: Option<Crop>,
/// Fade-in length in frames. Setting this clamps to the clip duration.
Expand Down Expand Up @@ -204,9 +207,12 @@ pub enum EditCommand {
/// Overwrite-style trim: resize clips in place from new source-frame trims.
TrimClips { edits: Vec<TrimEdit> },
/// Assign clip properties (timing changes propagate to linked partners).
/// `properties` is boxed: it carries a full `TextStyle`, which would
/// otherwise make this the dominant `EditCommand` variant (the enum is
/// `Clone`d on every undo snapshot path).
SetClipProperties {
clip_ids: Vec<String>,
properties: ClipProperties,
properties: Box<ClipProperties>,
},
/// Replace (or clear) a clip's keyframe track for one property.
SetKeyframes {
Expand Down Expand Up @@ -394,7 +400,7 @@ pub fn apply(
EditCommand::SetClipProperties {
clip_ids,
properties,
} => set_clip_properties(state, clip_ids, properties),
} => set_clip_properties(state, clip_ids, *properties),
EditCommand::SetKeyframes {
clip_id,
property,
Expand Down Expand Up @@ -1056,6 +1062,9 @@ fn apply_property_changes(
if let Some(c) = &props.text_content {
clip.text_content = Some(c.clone());
}
if let Some(s) = &props.text_style {
clip.text_style = Some(s.clone());
}
}

fn set_keyframes(
Expand Down Expand Up @@ -2842,3 +2851,95 @@ mod duplicate_clips_tests {
assert_eq!(state.version(), version_before + 2); // commit + undo
}
}

#[cfg(test)]
mod text_style_property_tests {
use super::*;
use crate::id::SeqIdGen;
use opentake_domain::{Clip, ClipType, TextAlignment, TextStyle, Track};

fn state_with_text_clip() -> EditorState {
let mut tl = Timeline::new();
let mut t = Track::new("v1", ClipType::Video);
let mut clip = Clip::new("c1", "", 0, 30);
clip.media_type = ClipType::Text;
clip.source_clip_type = ClipType::Text;
clip.text_content = Some("Hi".into());
clip.text_style = Some(TextStyle::default());
t.clips.push(clip);
tl.tracks.push(t);
EditorState::from_timeline(tl)
}

#[test]
fn set_text_style_replaces_clip_style_and_is_undoable() {
let mut state = state_with_text_clip();
let ids = SeqIdGen::default();
let version_before = state.version();

let style = TextStyle {
font_name: "Times-Bold".into(),
font_size: 48.0,
alignment: TextAlignment::Left,
..Default::default()
};
let res = apply(
&mut state,
EditCommand::SetClipProperties {
clip_ids: vec!["c1".into()],
properties: Box::new(ClipProperties {
text_style: Some(style.clone()),
..Default::default()
}),
},
&ids,
)
.unwrap();

assert!(res.changed);
let applied = state.timeline.tracks[0].clips[0]
.text_style
.as_ref()
.expect("text_style present");
assert_eq!(applied.font_name, "Times-Bold");
assert_eq!(applied.font_size, 48.0);
assert_eq!(applied.alignment, TextAlignment::Left);

// Undo restores the original default style.
apply(&mut state, EditCommand::Undo, &ids).unwrap();
let restored = state.timeline.tracks[0].clips[0]
.text_style
.as_ref()
.expect("text_style present");
assert_eq!(restored.font_name, "Helvetica-Bold");
assert_eq!(state.version(), version_before + 2); // commit + undo
}

#[test]
fn set_text_style_alongside_text_content() {
let mut state = state_with_text_clip();
let ids = SeqIdGen::default();

let style = TextStyle {
font_size: 120.0,
..Default::default()
};
apply(
&mut state,
EditCommand::SetClipProperties {
clip_ids: vec!["c1".into()],
properties: Box::new(ClipProperties {
text_content: Some("Updated".into()),
text_style: Some(style),
..Default::default()
}),
},
&ids,
)
.unwrap();

let clip = &state.timeline.tracks[0].clips[0];
assert_eq!(clip.text_content.as_deref(), Some("Updated"));
assert_eq!(clip.text_style.as_ref().unwrap().font_size, 120.0);
}
}
4 changes: 2 additions & 2 deletions crates/opentake-ops/src/intent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,11 @@ pub fn plan_smart_reframe(
"smart_reframe",
vec![EditCommand::SetClipProperties {
clip_ids: clip_ids.to_vec(),
properties: ClipProperties {
properties: Box::new(ClipProperties {
crop: Some(crop),
transform,
..Default::default()
},
}),
}],
))
}
Expand Down
28 changes: 14 additions & 14 deletions crates/opentake-ops/tests/command_apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -562,10 +562,10 @@ fn set_clip_properties_propagates_timing_to_linked_partner() {
&mut st,
EditCommand::SetClipProperties {
clip_ids: vec!["v1".into()],
properties: ClipProperties {
properties: Box::new(ClipProperties {
duration_frames: Some(40),
..Default::default()
},
}),
},
&g,
)
Expand Down Expand Up @@ -593,10 +593,10 @@ fn set_clip_properties_scalar_clears_keyframe_track() {
&mut st,
EditCommand::SetClipProperties {
clip_ids: vec!["c".into()],
properties: ClipProperties {
properties: Box::new(ClipProperties {
opacity: Some(0.5),
..Default::default()
},
}),
},
&g,
)
Expand Down Expand Up @@ -627,15 +627,15 @@ fn set_clip_properties_crop_sets_and_clears_track() {
&mut st,
EditCommand::SetClipProperties {
clip_ids: vec!["c".into()],
properties: ClipProperties {
properties: Box::new(ClipProperties {
crop: Some(opentake_domain::Crop {
left: 0.2,
top: 0.1,
right: 0.0,
bottom: 0.0,
}),
..Default::default()
},
}),
},
&g,
)
Expand All @@ -655,13 +655,13 @@ fn set_clip_properties_fade_sets_frames_and_interpolation() {
&mut st,
EditCommand::SetClipProperties {
clip_ids: vec!["c".into()],
properties: ClipProperties {
properties: Box::new(ClipProperties {
fade_in_frames: Some(10),
fade_out_frames: Some(15),
fade_in_interpolation: Some(Interpolation::Smooth),
fade_out_interpolation: Some(Interpolation::Hold),
..Default::default()
},
}),
},
&g,
)
Expand All @@ -683,10 +683,10 @@ fn set_clip_properties_fade_clamps_to_duration() {
&mut st,
EditCommand::SetClipProperties {
clip_ids: vec!["c".into()],
properties: ClipProperties {
properties: Box::new(ClipProperties {
fade_in_frames: Some(100),
..Default::default()
},
}),
},
&g,
)
Expand All @@ -704,11 +704,11 @@ fn set_clip_properties_flip_writes_to_transform() {
&mut st,
EditCommand::SetClipProperties {
clip_ids: vec!["c".into()],
properties: ClipProperties {
properties: Box::new(ClipProperties {
flip_horizontal: Some(true),
flip_vertical: Some(true),
..Default::default()
},
}),
},
&g,
)
Expand All @@ -726,7 +726,7 @@ fn set_clip_properties_multiple_fields_at_once() {
&mut st,
EditCommand::SetClipProperties {
clip_ids: vec!["c".into()],
properties: ClipProperties {
properties: Box::new(ClipProperties {
crop: Some(opentake_domain::Crop {
left: 0.1,
top: 0.2,
Expand All @@ -738,7 +738,7 @@ fn set_clip_properties_multiple_fields_at_once() {
flip_horizontal: Some(true),
opacity: Some(0.8),
..Default::default()
},
}),
},
&g,
)
Expand Down
33 changes: 31 additions & 2 deletions src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,9 @@ pub enum EditRequest {
#[serde(rename_all = "camelCase")]
SetClipProperties {
clip_ids: Vec<String>,
properties: ClipPropertiesDto,
// Boxed to keep `EditRequest` small: `ClipPropertiesDto` carries a full
// `TextStyle`, which would otherwise dominate the enum size.
properties: Box<ClipPropertiesDto>,
},
#[serde(rename_all = "camelCase")]
SetKeyframes {
Expand Down Expand Up @@ -304,7 +306,7 @@ impl EditRequest {
properties,
} => EditCommand::SetClipProperties {
clip_ids,
properties: properties.into_properties(),
properties: Box::new((*properties).into_properties()),
},
EditRequest::SetKeyframes {
clip_id,
Expand Down Expand Up @@ -540,6 +542,8 @@ pub struct ClipPropertiesDto {
#[serde(default)]
pub text_content: Option<String>,
#[serde(default)]
pub text_style: Option<TextStyle>,
#[serde(default)]
pub crop: Option<Crop>,
#[serde(default)]
pub fade_in_frames: Option<i32>,
Expand All @@ -566,6 +570,7 @@ impl ClipPropertiesDto {
opacity: self.opacity,
transform: self.transform,
text_content: self.text_content,
text_style: self.text_style,
crop: self.crop,
fade_in_frames: self.fade_in_frames,
fade_out_frames: self.fade_out_frames,
Expand Down Expand Up @@ -739,6 +744,30 @@ mod edit_request_serde_tests {
.expect("rippleDeleteClips camelCase");
}

#[test]
fn deserializes_set_clip_properties_with_text_style() {
// The Inspector sends camelCase `textStyle` with nested camelCase fields
// (fontName/fontSize/…). It must deserialize and map onto the command's
// ClipProperties.text_style.
let request = serde_json::from_str::<EditRequest>(
r#"{"type":"setClipProperties","clipIds":["c1"],"properties":{"textStyle":{"fontName":"Times-Bold","fontSize":48,"alignment":"left"}}}"#,
)
.expect("setClipProperties with textStyle camelCase");

match request.into_command().expect("setClipProperties command") {
EditCommand::SetClipProperties {
clip_ids,
properties,
} => {
assert_eq!(clip_ids, vec!["c1"]);
let style = properties.text_style.expect("text_style present");
assert_eq!(style.font_name, "Times-Bold");
assert_eq!(style.font_size, 48.0);
}
other => panic!("expected SetClipProperties, got {other:?}"),
}
}

#[test]
fn deserializes_swap_media_and_maps_to_command() {
let request = serde_json::from_str::<EditRequest>(
Expand Down
Loading
Loading