mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Roto Brush: Cmd/Ctrl-drag resizes the brush, and the tool options show its Diameter (#509)
Cause: the Roto Brush and Refine Edge diameters live in the editor state and the roto.options command sets them, but the UI never did: the Layer panel only painted strokes and the bottom bar only set the view mode, so the brush size could not be changed. Fix, as in After Effects: Cmd-drag (Ctrl-drag on Windows and Linux) in the Layer panel resizes the active brush (Roto Brush or Refine Edge) without painting. The circle stays at the press point, its edge follows the pointer, and the size is shown while dragging. The tools bar shows a Diameter field (header.roto.diameter) while a Roto Brush tool is active. Both go through roto.options, and the next stroke uses the new radius. Diameter is in all four interface catalogs. Test: roto_brush_ctrl_drag_resizes_the_brush (a 20 px Cmd/Ctrl-drag makes a 40 px brush and paints nothing, the next stroke's radius is half of it, and the Diameter field is registered). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f (cherry picked from commit b4183c28eea34ba097d7a16153b84fcf7aeea045)
This commit is contained in:
4 files changed
+90
No files matched your search
@@ -164,6 +164,9 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
if app.ui.tool.puppet_kind().is_some() {
|
||||
x = puppet_options(app, ui, &p, x, cy);
|
||||
}
|
||||
if app.ui.tool.roto_kind(false).is_some() {
|
||||
x = roto_options(app, ui, &p, x, cy);
|
||||
}
|
||||
let snap = Rect::from_min_size(pos2(x, cy - 10.0), vec2(20.0, 20.0));
|
||||
// The engine owns snapping (View ▸ Snapping); the checkbox mirrors it.
|
||||
app.ui.snapping = app.session.state.snapping;
|
||||
@@ -481,6 +484,26 @@ fn paint_swatch(p: &egui::Painter, r: Rect, paint: &ToolPaint, hovered: bool, t:
|
||||
p.rect_stroke(r, 2.0, Stroke::new(1.0, if hovered { t.text } else { t.field_border }), StrokeKind::Inside);
|
||||
}
|
||||
|
||||
/// Roto Brush / Refine Edge tool options: the brush Diameter (⌘/Ctrl-drag in the Layer panel
|
||||
/// sets it too, as in After Effects). Returns the next x.
|
||||
fn roto_options(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, mut x: f32, cy: f32) -> f32 {
|
||||
let t = app.tokens;
|
||||
let refine = app.ui.tool.roto_kind(false).is_some_and(|k| k.starts_with("refine"));
|
||||
let o = &app.session.state.roto;
|
||||
let (key, v) = if refine { ("refineDiameter", o.refine_diameter) } else { ("diameter", o.diameter) };
|
||||
let label = tr("Diameter:");
|
||||
let lr = p.text(pos2(x, cy), Align2::LEFT_CENTER, label, Tokens::ui(12.0), t.text_dim);
|
||||
x = lr.max.x + 6.0;
|
||||
let (r, nv, _) = widgets::hot_number_at(ui, pos2(x, cy - 9.0), egui::Id::new(("roto-opt", key)), v, 0.5, (1.0, 5000.0), 0, " px", &t);
|
||||
app.auto.add("header.roto.diameter", r, label);
|
||||
if let Some(nv) = nv
|
||||
&& let Err(e) = app.session.execute("roto.options", json!({key: nv}))
|
||||
{
|
||||
app.ui.status = e.to_string();
|
||||
}
|
||||
r.max.x + 12.0
|
||||
}
|
||||
|
||||
/// Puppet tool options: Mesh: Show, Expansion, Density (for new meshes and the selected
|
||||
/// layer's meshes). Returns the next x.
|
||||
fn puppet_options(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, mut x: f32, cy: f32) -> f32 {
|
||||
|
||||
@@ -286,6 +286,7 @@ const JAPANESE: &[(&str, &str)] = &[
|
||||
("Maximize Panel", "パネルを最大化"),
|
||||
("Media Replacement", "メディアの置き換え"),
|
||||
("Mesh:", "メッシュ:"),
|
||||
("Diameter:", "直径:"),
|
||||
("Method Used: {} Average Error: {} pixels", "使用した手法:{} 平均誤差:{}ピクセル"),
|
||||
("Models folder: {}", "モデルフォルダー:{}"),
|
||||
("Modifiers:", "修飾キー:"),
|
||||
@@ -721,6 +722,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
|
||||
("Maximize Panel", "最大化面板"),
|
||||
("Media Replacement", "媒体替换"),
|
||||
("Mesh:", "网格:"),
|
||||
("Diameter:", "直径:"),
|
||||
("Method Used: {} Average Error: {} pixels", "使用方法:{} 平均误差:{} 像素"),
|
||||
("Models folder: {}", "模型文件夹:{}"),
|
||||
("Modifiers:", "修饰键:"),
|
||||
@@ -1244,6 +1246,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
|
||||
("Maximize Panel", "最大化面板"),
|
||||
("Media Replacement", "媒體替換"),
|
||||
("Mesh:", "格點:"),
|
||||
("Diameter:", "直徑:"),
|
||||
("Method Used: {} Average Error: {} pixels", "使用方法:{} 平均誤差:{} 像素"),
|
||||
("Models folder: {}", "模型資料夾:{}"),
|
||||
("Modifiers:", "修飾鍵:"),
|
||||
@@ -1757,6 +1760,7 @@ const UKRAINIAN: &[(&str, &str)] = &[
|
||||
("Maximize Panel", "Розгорнути панель"),
|
||||
("Media Replacement", "Заміна матеріалу"),
|
||||
("Mesh:", "Сітка:"),
|
||||
("Diameter:", "Діаметр:"),
|
||||
("Method Used: {} Average Error: {} pixels", "Використаний метод: {} Середня похибка: {} пікселів"),
|
||||
("Models folder: {}", "Тека моделей: {}"),
|
||||
("Modifiers:", "Модифікатори:"),
|
||||
|
||||
@@ -168,6 +168,32 @@ pub fn interact(
|
||||
let sign = if kind == "bg" || kind == "refineErase" { "−" } else { "+" };
|
||||
painter.text(hp, Align2::CENTER_CENTER, sign, Tokens::semibold(12.0), col);
|
||||
}
|
||||
// ⌘/Ctrl-drag resizes the brush (After Effects): the circle stays at the press point and its
|
||||
// edge follows the pointer. No stroke is painted.
|
||||
let rid = egui::Id::new("layer-panel-roto-resize");
|
||||
if resp.drag_started()
|
||||
&& mods.command
|
||||
&& let Some(pos) = ui.input(|i| i.pointer.press_origin()).or(resp.interact_pointer_pos())
|
||||
{
|
||||
ctx.data_mut(|d| d.insert_temp(rid, pos));
|
||||
}
|
||||
if let Some(origin) = ctx.data(|d| d.get_temp::<Pos2>(rid)) {
|
||||
if let Some(pos) = resp.interact_pointer_pos()
|
||||
&& zoom > 0.0
|
||||
{
|
||||
let dia = (2.0 * f64::from(origin.distance(pos)) / zoom).clamp(1.0, 5000.0).round();
|
||||
let key = if refine { "refineDiameter" } else { "diameter" };
|
||||
if let Err(e) = app.session.execute("roto.options", json!({key: dia})) {
|
||||
app.ui.status = e.to_string();
|
||||
}
|
||||
painter.circle_stroke(origin, (dia * 0.5 * zoom) as f32, Stroke::new(1.5, col));
|
||||
painter.text(origin + vec2(0.0, -14.0), Align2::CENTER_BOTTOM, format!("{dia:.0} px"), Tokens::ui(11.0), col);
|
||||
}
|
||||
if resp.drag_stopped() || !resp.dragged() {
|
||||
ctx.data_mut(|d| d.remove::<Pos2>(rid));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
let did = egui::Id::new("layer-panel-roto-drag");
|
||||
if resp.drag_started()
|
||||
&& let Some(pos) = ui.input(|i| i.pointer.press_origin()).or(resp.interact_pointer_pos())
|
||||
|
||||
@@ -1837,3 +1837,40 @@ fn full_resolution_frames_wider_than_the_texture_limit_fit() {
|
||||
assert!(full(&mut h), "the full-size frame is shown (and stays readable): {:?}", h.state().ui.viewer.res);
|
||||
assert_eq!(h.state().viewer_texture_size(), Some([800, 134]), "2400×400 averaged by 3");
|
||||
}
|
||||
|
||||
/// #509: ⌘/Ctrl-drag in the Layer panel resizes the Roto Brush (as in After Effects) without
|
||||
/// painting, the next stroke uses the new size, and the tool options show the Diameter.
|
||||
#[test]
|
||||
fn roto_brush_ctrl_drag_resizes_the_brush() {
|
||||
let mut h = harness();
|
||||
let id = h.state().session.active_comp().unwrap().layers.iter().find(|l| l.name == "Box").unwrap().id.0;
|
||||
h.state_mut().session.execute("layer.select", json!({"layers": [id]})).unwrap();
|
||||
h.state_mut().ui.layer_panel = Some(id);
|
||||
h.state_mut().show_panel(effectcraft_ui_egui::dock::PanelKind::Layer);
|
||||
h.state_mut().ui.tool = Tool::RotoBrush;
|
||||
h.run_steps(3);
|
||||
assert!(h.state().auto.find("header.roto.diameter").is_some(), "the Diameter field");
|
||||
let canvas = rect(&h, "layerPanel.canvas");
|
||||
let zoom = canvas.width() / 80.0;
|
||||
let c = canvas.center();
|
||||
h.input_mut().events.push(Event::ModifiersChanged(egui::Modifiers::COMMAND));
|
||||
drag(&mut h, c, c + vec2(20.0 * zoom, 0.0));
|
||||
h.input_mut().events.push(Event::ModifiersChanged(egui::Modifiers::NONE));
|
||||
h.run_steps(2);
|
||||
let dia = h.state().session.state.roto.diameter;
|
||||
assert!((dia - 40.0).abs() <= 1.0, "a 20 px drag makes a 40 px brush: {dia}");
|
||||
let strokes = |h: &Harness<'_, EffectcraftApp>| {
|
||||
let l = h.state().session.active_comp().unwrap().layer(LayerId(id)).unwrap().clone();
|
||||
let roto = effectcraft_engine::effects::roto::ID;
|
||||
let g = l
|
||||
.effects()
|
||||
.and_then(|fx| fx.groups().find(|g| matches!(&g.kind, effectcraft_engine::project::GroupKind::Effect { effect } if effect == roto)).cloned());
|
||||
g.map(|g| effectcraft_engine::effects::roto::data(&effectcraft_engine::effects::flatten_params(&g, &mut |p| p.value.clone())).strokes.clone())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
assert!(strokes(&h).is_empty(), "resizing paints nothing");
|
||||
drag(&mut h, c - vec2(10.0 * zoom, 0.0), c + vec2(10.0 * zoom, 0.0));
|
||||
let s = strokes(&h);
|
||||
assert_eq!(s.len(), 1);
|
||||
assert!((s[0].radius - dia / 2.0).abs() < 1e-6, "{} vs {dia}", s[0].radius);
|
||||
}
|
||||
Reference in new issue
Block a user