Merge pull request #1098 from vangel76/feat-gap21-percent-guides-effect-labels

# Conflicts:
#	crates/engine/src/tests_menu_cmds.rs
#	docs/effects-parity.md
#	docs/gaps.md
#	docs/ui-parity.md
This commit is contained in:
Brandon Thomas committed 2026-10-13 04:29:06 -04:00
commit a985dfb36b
20 files changed
+301 -25

No files matched your search

+3 -1
View File
@@ -22,6 +22,7 @@ fn crop(s: &mut Session, cid: effectcraft_project::ItemId, r: [f64; 4], label: &
let (w, h) = (effectcraft_project::limits::size_f64(r[2]), effectcraft_project::limits::size_f64(r[3]));
s.edit(label, None, |proj, st| {
let c = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
let (old_w, old_h) = (c.width, c.height);
c.width = w;
c.height = h;
for l in c.layers.iter_mut().filter(|l| l.parent.is_none()) {
@@ -30,7 +31,8 @@ fn crop(s: &mut Session, cid: effectcraft_project::ItemId, r: [f64; 4], label: &
}
}
for g in &mut c.guides {
g.position -= if g.vertical { x } else { y };
let px = g.px(old_w, old_h) - if g.vertical { x } else { y };
g.set_px(px, w, h);
}
st.region_of_interest = None;
Ok(())
+20
View File
@@ -227,6 +227,25 @@ fn toggle(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!(r))
}
/// `effect.setLabel`: the effect's colour label (None clears it).
fn set_label(s: &mut Session, p: &Value) -> Result<Value> {
let c = "effect.setLabel";
let (cid, lid, uid) = find_fx(s, p, c)?;
let lab = match p.get("label") {
Some(Value::Number(n)) => n.as_u64().and_then(|i| effectcraft_color::Label::ALL.get(usize::try_from(i).ok()?).copied()),
Some(Value::String(name)) => s.prefs.label_from_name(name),
_ => None,
}
.ok_or_else(|| bad(c, "missing or unknown `label` (name or index 0-16; 0 / None clears it)"))?;
s.edit("Effect Label", None, |proj, _| {
let l = layer_mut(proj, cid, lid)?;
let g = l.props.find_group_mut(uid).ok_or_else(|| bad(c, "gone"))?;
g.label = (lab != effectcraft_color::Label::None).then_some(lab);
Ok(())
})?;
Ok(Value::Null)
}
fn reorder(s: &mut Session, p: &Value) -> Result<Value> {
let (cid, lid, uid) = find_fx(s, p, "effect.reorder")?;
let to = p.get("index").and_then(Value::as_u64).ok_or_else(|| bad("effect.reorder", "missing `index` (1-based)"))? as usize;
@@ -482,6 +501,7 @@ pub fn specs() -> Vec<CommandSpec> {
cmd!("effect.removeAll", "Remove All", ["Effect"], Some("Cmd+Shift+E"), "{layers?}", has_layers, remove_all),
cmd!("effect.remove", "Remove Effect", [], None, "{layer?, effect: index|uid|name}", has_layers, remove),
cmd!("effect.toggle", "Toggle Effect", [], None, "{layer?, effect, value?}", has_layers, toggle),
cmd!("effect.setLabel", "Effect Label", [], None, "{layer?, effect, label: name|index 0-16 (None clears)}", has_layers, set_label),
cmd!("effect.reorder", "Reorder Effect", [], None, "{layer?, effect, index}", has_layers, reorder),
cmd!("effect.duplicate", "Duplicate Effect", [], None, "{layer?, effect}", has_layers, duplicate),
cmd!("effect.copy", "Copy Effects", [], None, "{layer?, effect? | effects?: [uid|name|index]} (default: the selected effects)", has_layers, copy),
+65 -4
View File
@@ -20,15 +20,58 @@ fn add_guide(s: &mut Session, p: &Value) -> Result<Value> {
o => return Err(bad("view.addGuide", format!("orientation must be vertical or horizontal, not `{o}`"))),
};
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
let position = f_p(p, "position").unwrap_or(if vertical { comp.width as f64 / 2.0 } else { comp.height as f64 / 2.0 });
let n = s.edit("Add Guide", None, |proj, _| {
let percent = p.get("percent").and_then(Value::as_bool).unwrap_or(false);
let default = if percent {
50.0
} else if vertical {
comp.width as f64 / 2.0
} else {
comp.height as f64 / 2.0
};
let position = f_p(p, "position").filter(|v| v.is_finite()).unwrap_or(default);
let n = s.edit(if percent { "Add Percentage Guide" } else { "Add Guide" }, None, |proj, _| {
let c = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
c.guides.push(Guide { vertical, position });
c.guides.push(Guide { vertical, position, percent });
Ok(c.guides.len())
})?;
Ok(json!({"guides": n}))
}
/// View ▸ Convert Guides to Percentage / Pixels: every guide (or just `index`) changes unit
/// without moving on the comp.
fn convert_guides(s: &mut Session, p: &Value) -> Result<Value> {
let cid = comp_id(s, p)?;
let percent = match str_p(p, "to").unwrap_or("percent") {
"percent" | "percentage" => true,
"pixels" | "px" => false,
o => return Err(bad("view.convertGuides", format!("`to` must be percent or pixels, not `{o}`"))),
};
let only = p.get("index").and_then(Value::as_u64).map(|i| i as usize);
let n = s.project.comp(cid).ok_or(EngineError::NoComp)?.guides.len();
if only.is_some_and(|i| i >= n) {
return Err(bad("view.convertGuides", format!("no guide {} (the comp has {n})", only.unwrap_or(0))));
}
s.edit(if percent { "Convert Guides to Percentage" } else { "Convert Guides to Pixels" }, None, |proj, _| {
let c = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
let (w, h) = (c.width, c.height);
for (i, g) in c.guides.iter_mut().enumerate() {
if only.is_none_or(|o| o == i) {
g.set_percent(percent, w, h);
}
}
Ok(())
})?;
Ok(json!({"guides": n, "percent": percent}))
}
fn add_percent_guide(s: &mut Session, p: &Value) -> Result<Value> {
let mut q = p.clone();
if let Some(o) = q.as_object_mut() {
o.insert("percent".into(), json!(true));
}
add_guide(s, &q)
}
fn clear_guides(s: &mut Session, p: &Value) -> Result<Value> {
let cid = comp_id(s, p)?;
s.edit("Clear Guides", None, |proj, _| {
@@ -128,7 +171,25 @@ macro_rules! res {
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!("view.addGuide", "Add Guide...", ["View"], None, "{orientation?: vertical|horizontal, position? (comp px)}", has_comp, add_guide),
cmd!(
"view.addGuide",
"Add Guide...",
["View"],
None,
"{orientation?: vertical|horizontal, position? (comp px, or percent with percent: true), percent?: bool}",
has_comp,
add_guide
),
cmd!(
"view.addPercentGuide",
"Add Percentage Guide...",
["View"],
None,
"{orientation?: vertical|horizontal, position? (percent of the comp)}",
has_comp,
add_percent_guide
),
cmd!("view.convertGuides", "Convert Guides to Percentage", ["View"], None, "{comp?, to?: percent|pixels, index?}", has_guides, convert_guides),
cmd!("view.clearGuides", "Clear Guides", ["View"], None, "{comp?}", has_guides, clear_guides),
cmd!("view.importGuides", "Import Guides...", ["View"], None, "{path}", has_comp, import_guides),
cmd!("view.exportGuides", "Export Guides...", ["View"], None, "{path}", has_guides, export_guides),
+5 -1
View File
@@ -210,8 +210,12 @@ fn guide_index(s: &Session, p: &Value, cmd: &str) -> Result<(effectcraft_project
fn move_guide(s: &mut Session, p: &Value) -> Result<Value> {
let (cid, i) = guide_index(s, p, "view.moveGuide")?;
let pos = f_p(p, "position").ok_or_else(|| bad("view.moveGuide", "missing `position` (comp px)"))?;
let pos = if pos.is_finite() { pos } else { return Err(bad("view.moveGuide", "`position` must be a finite number")) };
s.edit("Move Guide", merge_p(p), |proj, _| {
proj.comp_mut(cid).ok_or(EngineError::NoComp)?.guides[i].position = pos;
let c = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
let (w, h) = (c.width, c.height);
let g = c.guides.get_mut(i).ok_or(EngineError::NoComp)?;
g.set_px(pos, w, h);
Ok(())
})?;
Ok(json!(pos))
+3
View File
@@ -1117,6 +1117,9 @@ View
Snap to Guides | view.snapToGuides
Lock Guides | view.lockGuides
Add Guide... | view.addGuide
Add Percentage Guide... | view.addPercentGuide
Convert Guides to Percentage | view.convertGuides {"to":"percent"}
Convert Guides to Pixels | view.convertGuides {"to":"pixels"}
Clear Guides | view.clearGuides
Import Guides... | view.importGuides
Export Guides... | view.exportGuides
+32
View File
@@ -369,3 +369,35 @@ fn ocio_effect_warns_about_unsupported_config_transforms() {
set(&mut s, "configFile", json!("/no/such/config.ocio"));
assert!(warning(&mut s).as_str().is_some_and(|w| w.starts_with("Can't read the OCIO config")));
}
#[test]
fn effect_colour_labels_set_clear_undo_and_round_trip() {
let (mut s, a, _) = comp_with_two_solids();
let blur = apply(&mut s, a, "Gaussian Blur");
let label = |s: &Session| layer(s, a).effects().unwrap().groups().next().unwrap().label;
assert_eq!(label(&s), None);
s.execute("effect.setLabel", json!({"layer": a, "effect": blur, "label": "Blue"})).unwrap();
assert_eq!(label(&s), Some(effectcraft_color::Label::Blue));
// Duplicates keep the label.
s.execute("effect.duplicate", json!({"layer": a, "effect": blur})).unwrap();
assert_eq!(layer(&s, a).effects().unwrap().groups().filter(|g| g.label.is_some()).count(), 2);
s.undo();
// It saves with the project and survives a reload.
let json = s.project.to_file_json().unwrap();
let back = effectcraft_project::Project::from_json(&json).unwrap();
let l = back.comp(s.active_comp_id().unwrap()).unwrap().layer(LayerId(a)).unwrap().clone();
assert_eq!(l.effects().unwrap().groups().next().unwrap().label, Some(effectcraft_color::Label::Blue));
// None (or index 0) clears it; the change undoes.
s.execute("effect.setLabel", json!({"layer": a, "effect": blur, "label": 0})).unwrap();
assert_eq!(label(&s), None);
s.undo();
assert_eq!(label(&s), Some(effectcraft_color::Label::Blue));
assert!(s.execute("effect.setLabel", json!({"layer": a, "effect": blur, "label": "Chartreuse"})).is_err());
assert!(s.execute("effect.setLabel", json!({"layer": a, "effect": blur, "label": 99})).is_err());
// A project saved before labels existed has none.
let old = json.replace("\"label\":\"Blue\"", "\"x\":0").replace("\"label\": \"Blue\"", "\"x\": 0");
assert_ne!(old, json, "the replace removed the effect's label");
let old = effectcraft_project::Project::from_json(&old).unwrap();
let l = old.comp(s.active_comp_id().unwrap()).unwrap().layer(LayerId(a)).unwrap().clone();
assert_eq!(l.effects().unwrap().groups().next().unwrap().label, None);
}
+50
View File
@@ -1555,3 +1555,53 @@ fn interpret_footage_loop_extends_video_layers() {
assert_eq!(out(&s, before), 6.0, "a layer that ran to the clip's end still does");
assert_eq!(s.project.item(effectcraft_project::ItemId(p)).unwrap().duration(), Some(Tick::from_seconds_f64(6.0)));
}
#[test]
fn percentage_guides_follow_the_comp_size() {
let mut s = comp();
s.execute("view.addGuide", json!({"orientation": "vertical", "position": 25, "percent": true})).unwrap();
s.execute("view.addPercentGuide", json!({"orientation": "horizontal"})).unwrap();
s.execute("view.addGuide", json!({"orientation": "vertical", "position": 100})).unwrap();
let c = s.active_comp().unwrap().clone();
assert!(c.guides[0].percent && c.guides[1].percent && !c.guides[2].percent);
assert_eq!((c.guides[0].px(c.width, c.height), c.guides[1].px(c.width, c.height)), (160.0, 180.0));
// A comp resize moves percentage guides with it and leaves pixel guides alone.
s.execute("comp.settings", json!({"width": 1280, "height": 720})).unwrap();
let c = s.active_comp().unwrap().clone();
assert_eq!((c.guides[0].px(c.width, c.height), c.guides[1].px(c.width, c.height), c.guides[2].px(c.width, c.height)), (320.0, 360.0, 100.0));
// Dragging keeps the unit: the position arrives in comp pixels.
s.execute("view.moveGuide", json!({"index": 0, "position": 640})).unwrap();
assert_eq!(s.active_comp().unwrap().guides[0].position, 50.0);
// Snapping sees the pixel position.
let c = s.active_comp().unwrap().clone();
assert!(c.guides.iter().any(|g| g.percent && g.vertical && g.px(c.width, c.height) == 640.0));
// Converting keeps the guide where it is, and undoes in one step.
s.execute("view.convertGuides", json!({"to": "pixels"})).unwrap();
let g = s.active_comp().unwrap().guides.clone();
assert!(g.iter().all(|g| !g.percent));
assert_eq!((g[0].position, g[1].position), (640.0, 360.0));
s.execute("view.convertGuides", json!({"to": "percent", "index": 2})).unwrap();
assert_eq!(s.active_comp().unwrap().guides[2].position, 100.0 / 1280.0 * 100.0);
s.undo();
assert!(!s.active_comp().unwrap().guides[2].percent);
assert!(s.execute("view.convertGuides", json!({"to": "furlongs"})).is_err());
assert!(s.execute("view.convertGuides", json!({"index": 9})).is_err());
// A crop keeps percentage guides where they were on the image.
s.execute("view.convertGuides", json!({"to": "percent"})).unwrap();
s.execute("view.setRegionOfInterest", json!({"rect": [320, 0, 640, 720]})).unwrap();
s.execute("comp.cropToRegionOfInterest", json!({})).unwrap();
let c = s.active_comp().unwrap();
assert!((c.guides[0].px(c.width, c.height) - 320.0).abs() < 1e-9, "640 - 320 crop offset");
}
#[test]
fn old_guides_load_as_pixel_guides_and_percent_round_trips() {
let old: effectcraft_project::Guide = serde_json::from_str(r#"{"vertical":true,"position":12.5}"#).unwrap();
assert!(!old.percent);
assert!(!serde_json::to_string(&old).unwrap().contains("percent"), "pixel guides stay in the old shape");
let g = effectcraft_project::Guide { vertical: false, position: 10.0, percent: true };
let back: effectcraft_project::Guide = serde_json::from_str(&serde_json::to_string(&g).unwrap()).unwrap();
assert_eq!(back, g);
// A zero-sized comp never divides by zero.
assert!(g.px(0, 0).is_finite());
}
+2 -5
View File
@@ -335,11 +335,8 @@ fn targets_in(ctx: &EvalCtx, exclude: &[LayerId], opts: SnapOptions) -> Vec<Snap
}
if opts.guides {
for g in &comp.guides {
out.push(if g.vertical {
SnapTarget::vline(g.position, PRI_GUIDE, SnapSource::Guide)
} else {
SnapTarget::hline(g.position, PRI_GUIDE, SnapSource::Guide)
});
let pos = g.px(comp.width, comp.height);
out.push(if g.vertical { SnapTarget::vline(pos, PRI_GUIDE, SnapSource::Guide) } else { SnapTarget::hline(pos, PRI_GUIDE, SnapSource::Guide) });
}
}
if opts.grid && opts.grid_spacing >= 1.0 {
+9 -1
View File
@@ -487,7 +487,15 @@ fn build_group(project: &Project, comp: ItemId, eg: &EssentialGraphics, old: Opt
}
}
}
PropGroup { uid, match_id: GROUP.into(), name: "Essential Properties".into(), kind: GroupKind::Essential { overridden: keep }, enabled: true, children }
PropGroup {
uid,
match_id: GROUP.into(),
name: "Essential Properties".into(),
kind: GroupKind::Essential { overridden: keep },
enabled: true,
label: None,
children,
}
}
fn find_prop<'a>(nodes: &'a [Node], m: &str) -> Option<&'a Property> {
+30
View File
@@ -467,7 +467,37 @@ pub struct Comp {
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct Guide {
pub vertical: bool,
/// Comp pixels, or percent of the comp's width (vertical) / height (horizontal) when
/// [`Guide::percent`] is set.
pub position: f64,
/// A percentage guide (After Effects 26.5): `position` is a percent of the comp's size, so the
/// guide keeps its relative place when the comp is resized. Absent in older projects.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub percent: bool,
}
impl Guide {
/// The comp's extent along this guide's axis, never zero.
fn extent(&self, width: u32, height: u32) -> f64 {
f64::from(if self.vertical { width } else { height }).max(1.0)
}
/// The guide's position in comp pixels for a comp of this size.
pub fn px(&self, width: u32, height: u32) -> f64 {
if self.percent { self.position / 100.0 * self.extent(width, height) } else { self.position }
}
/// Place the guide at `px` comp pixels, keeping its unit.
pub fn set_px(&mut self, px: f64, width: u32, height: u32) {
self.position = if self.percent { px / self.extent(width, height) * 100.0 } else { px };
}
/// Switch between pixels and percent without moving the guide on the comp.
pub fn set_percent(&mut self, percent: bool, width: u32, height: u32) {
let px = self.px(width, height);
self.percent = percent;
self.set_px(px, width, height);
}
}
fn yes() -> bool {
+5 -1
View File
@@ -255,6 +255,10 @@ pub struct PropGroup {
/// fx switch for effects, eye for shape groups, etc.
#[serde(default = "yes")]
pub enabled: bool,
/// An effect's colour label (After Effects 26.5), shown in Effect Controls and the Timeline;
/// `None` is unlabelled. Absent in older projects.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<effectcraft_color::Label>,
#[serde(default)]
pub children: Vec<Node>,
}
@@ -344,7 +348,7 @@ impl PropGroup {
}
pub fn new(uid: Uid, match_id: &str, name: &str) -> PropGroup {
PropGroup { uid, match_id: match_id.into(), name: name.into(), kind: GroupKind::Plain, enabled: true, children: vec![] }
PropGroup { uid, match_id: match_id.into(), name: name.into(), kind: GroupKind::Plain, enabled: true, label: None, children: vec![] }
}
pub fn with(mut self, n: impl Into<Node>) -> PropGroup {
self.children.push(n.into());
+30
View File
@@ -778,6 +778,9 @@ const SIMPLIFIED_CHINESE: &[(&str, &str, &str)] = &[
("view.snapToGuides", "Snap to Guides", "对齐到参考线"),
("view.lockGuides", "Lock Guides", "锁定参考线"),
("view.addGuide", "Add Guide...", "添加参考线..."),
("view.addPercentGuide", "Add Percentage Guide...", "添加百分比参考线..."),
("view.convertGuides", "Convert Guides to Percentage", "将参考线转换为百分比"),
("view.convertGuides", "Convert Guides to Pixels", "将参考线转换为像素"),
("view.clearGuides", "Clear Guides", "清除参考线"),
("view.importGuides", "Import Guides...", "导入参考..."),
("view.exportGuides", "Export Guides...", "导出参考..."),
@@ -1409,6 +1412,9 @@ const TRADITIONAL_CHINESE: &[(&str, &str, &str)] = &[
("view.snapToGuides", "Snap to Guides", "靠齊參考線"),
("view.lockGuides", "Lock Guides", "鎖定參考線"),
("view.addGuide", "Add Guide...", "新增參考線..."),
("view.addPercentGuide", "Add Percentage Guide...", "新增百分比參考線..."),
("view.convertGuides", "Convert Guides to Percentage", "將參考線轉換為百分比"),
("view.convertGuides", "Convert Guides to Pixels", "將參考線轉換為像素"),
("view.clearGuides", "Clear Guides", "清除參考線"),
("view.importGuides", "Import Guides...", "匯入參考線..."),
("view.exportGuides", "Export Guides...", "匯出參考線..."),
@@ -2037,6 +2043,9 @@ const JAPANESE: &[(&str, &str, &str)] = &[
("view.snapToGuides", "Snap to Guides", "ガイドにスナップ"),
("view.lockGuides", "Lock Guides", "ガイドをロック"),
("view.addGuide", "Add Guide...", "ガイドを追加..."),
("view.addPercentGuide", "Add Percentage Guide...", "パーセントガイドを追加..."),
("view.convertGuides", "Convert Guides to Percentage", "ガイドをパーセントに変換"),
("view.convertGuides", "Convert Guides to Pixels", "ガイドをピクセルに変換"),
("view.clearGuides", "Clear Guides", "ガイドを消去"),
("view.importGuides", "Import Guides...", "ガイドを読み込み..."),
("view.exportGuides", "Export Guides...", "ガイドを書き出し..."),
@@ -2668,6 +2677,9 @@ const UKRAINIAN: &[(&str, &str, &str)] = &[
("view.snapToGuides", "Snap to Guides", "Прив’язувати до напрямних"),
("view.lockGuides", "Lock Guides", "Заблокувати напрямні"),
("view.addGuide", "Add Guide...", "Додати напрямну..."),
("view.addPercentGuide", "Add Percentage Guide...", "Додати відсоткову напрямну..."),
("view.convertGuides", "Convert Guides to Percentage", "Перетворити напрямні на відсотки"),
("view.convertGuides", "Convert Guides to Pixels", "Перетворити напрямні на пікселі"),
("view.clearGuides", "Clear Guides", "Очистити напрямні"),
("view.importGuides", "Import Guides...", "Імпортувати напрямні..."),
("view.exportGuides", "Export Guides...", "Експортувати напрямні..."),
@@ -3300,6 +3312,9 @@ const SPANISH: &[(&str, &str, &str)] = &[
("view.snapToGuides", "Snap to Guides", "Ajustar a las guías"),
("view.lockGuides", "Lock Guides", "Bloquear guías"),
("view.addGuide", "Add Guide...", "Añadir guía..."),
("view.addPercentGuide", "Add Percentage Guide...", "Añadir guía porcentual..."),
("view.convertGuides", "Convert Guides to Percentage", "Convertir guías a porcentaje"),
("view.convertGuides", "Convert Guides to Pixels", "Convertir guías a píxeles"),
("view.clearGuides", "Clear Guides", "Borrar guías"),
("view.importGuides", "Import Guides...", "Importar guías..."),
("view.exportGuides", "Export Guides...", "Exportar guías..."),
@@ -3930,6 +3945,9 @@ const PORTUGUESE: &[(&str, &str, &str)] = &[
("view.snapToGuides", "Snap to Guides", "Encaixar nas guias"),
("view.lockGuides", "Lock Guides", "Bloquear guias"),
("view.addGuide", "Add Guide...", "Adicionar guia..."),
("view.addPercentGuide", "Add Percentage Guide...", "Adicionar guia percentual..."),
("view.convertGuides", "Convert Guides to Percentage", "Converter guias em porcentagem"),
("view.convertGuides", "Convert Guides to Pixels", "Converter guias em pixels"),
("view.clearGuides", "Clear Guides", "Limpar guias"),
("view.importGuides", "Import Guides...", "Importar guias..."),
("view.exportGuides", "Export Guides...", "Exportar guias..."),
@@ -4562,6 +4580,9 @@ const RUSSIAN: &[(&str, &str, &str)] = &[
("view.snapToGuides", "Snap to Guides", "Привязывать к направляющим"),
("view.lockGuides", "Lock Guides", "Закрепить направляющие"),
("view.addGuide", "Add Guide...", "Добавить направляющую..."),
("view.addPercentGuide", "Add Percentage Guide...", "Добавить процентную направляющую..."),
("view.convertGuides", "Convert Guides to Percentage", "Преобразовать направляющие в проценты"),
("view.convertGuides", "Convert Guides to Pixels", "Преобразовать направляющие в пиксели"),
("view.clearGuides", "Clear Guides", "Очистить направляющие"),
("view.importGuides", "Import Guides...", "Импортировать направляющие..."),
("view.exportGuides", "Export Guides...", "Экспортировать направляющие..."),
@@ -5192,6 +5213,9 @@ const FRENCH: &[(&str, &str, &str)] = &[
("view.snapToGuides", "Snap to Guides", "Aimanter sur les repères"),
("view.lockGuides", "Lock Guides", "Verrouiller les repères"),
("view.addGuide", "Add Guide...", "Ajouter un repère..."),
("view.addPercentGuide", "Add Percentage Guide...", "Ajouter un repère en pourcentage..."),
("view.convertGuides", "Convert Guides to Percentage", "Convertir les repères en pourcentage"),
("view.convertGuides", "Convert Guides to Pixels", "Convertir les repères en pixels"),
("view.clearGuides", "Clear Guides", "Effacer les repères"),
("view.importGuides", "Import Guides...", "Importer des repères..."),
("view.exportGuides", "Export Guides...", "Exporter des repères..."),
@@ -5822,6 +5846,9 @@ const GERMAN: &[(&str, &str, &str)] = &[
("view.snapToGuides", "Snap to Guides", "An Hilfslinien einrasten"),
("view.lockGuides", "Lock Guides", "Hilfslinien sperren"),
("view.addGuide", "Add Guide...", "Hilfslinie hinzufügen..."),
("view.addPercentGuide", "Add Percentage Guide...", "Prozentuale Hilfslinie hinzufügen..."),
("view.convertGuides", "Convert Guides to Percentage", "Hilfslinien in Prozent umwandeln"),
("view.convertGuides", "Convert Guides to Pixels", "Hilfslinien in Pixel umwandeln"),
("view.clearGuides", "Clear Guides", "Hilfslinien entfernen"),
("view.importGuides", "Import Guides...", "Hilfslinien importieren..."),
("view.exportGuides", "Export Guides...", "Hilfslinien exportieren..."),
@@ -6453,6 +6480,9 @@ const CZECH: &[(&str, &str, &str)] = &[
("view.snapToGuides", "Snap to Guides", "Přichytávat k vodítkům"),
("view.lockGuides", "Lock Guides", "Zamknout vodítka"),
("view.addGuide", "Add Guide...", "Přidat vodítko..."),
("view.addPercentGuide", "Add Percentage Guide...", "Přidat procentuální vodítko..."),
("view.convertGuides", "Convert Guides to Percentage", "Převést vodítka na procenta"),
("view.convertGuides", "Convert Guides to Pixels", "Převést vodítka na pixely"),
("view.clearGuides", "Clear Guides", "Vymazat vodítka"),
("view.importGuides", "Import Guides...", "Importovat vodítka..."),
("view.exportGuides", "Export Guides...", "Exportovat vodítka..."),
@@ -1132,6 +1132,16 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let selected = app.session.state.selected_props.iter().any(|(l, u)| *l == layer.id && *u == g.uid);
let active = effect_active(g, layer.id, &app.session.state.selected_props);
bp.rect_filled(r, 0.0, if active { t.fx_header_active } else { t.fx_header });
// The effect's colour label (26.5): a bar on the header's left edge.
if let Some(lab) = g.label.filter(|l| *l != effectcraft_engine::color::Label::None) {
let [lr, lg, lb] = app.session.prefs.label_rgb(lab);
bp.rect_filled(Rect::from_min_size(r.min, vec2(4.0, r.height())), 0.0, Color32::from_rgb(lr, lg, lb));
app.auto.add(
&format!("effectControls.effect.{}.label", g.uid),
Rect::from_min_size(r.min, vec2(4.0, r.height())),
&app.session.prefs.label_name(lab),
);
}
let fxr = Rect::from_center_size(pos2(r.min.x + 14.0, r.center().y), vec2(16.0, 16.0));
if widgets::icon_toggle(ui, fxr, Icon::Fx, g.enabled, &t, egui::Id::new(("ec-fx", g.uid)), Sense::click()).clicked() {
actions.push(("effect.toggle".into(), json!({"layer": layer.id.0, "effect": g.uid})));
@@ -1221,6 +1231,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
crate::widgets::context_menu(&hresp, |ui| {
let lid = layer.id.0;
let mut picked_label: Option<usize> = None;
let mut item = |ui: &mut egui::Ui, label: &str, enabled: bool, cmd: &str, params: serde_json::Value| {
if ui.add_enabled(enabled, egui::Button::new(label)).clicked() {
actions.push((cmd.into(), params));
@@ -1236,11 +1247,23 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
ui.close();
}
ui.separator();
ui.menu_button(crate::i18n::tr("Label"), |ui| {
let cur = g.label.unwrap_or(effectcraft_engine::color::Label::None);
for (i, l) in effectcraft_engine::color::Label::ALL.into_iter().enumerate() {
if widgets::label_entry(ui, l, &app.session.prefs.label_name(l), l == cur, &t).clicked() {
picked_label = Some(i);
ui.close();
}
}
});
item(ui, "Reset", true, "effect.reset", json!({"layer": lid, "effect": g.uid}));
item(ui, "Remove", true, "effect.remove", json!({"layer": lid, "effect": g.uid}));
ui.separator();
item(ui, "Move Up", fi > 0, "effect.reorder", json!({"layer": lid, "effect": g.uid, "index": fi.max(1)}));
item(ui, "Move Down", fi + 1 < fx.len(), "effect.reorder", json!({"layer": lid, "effect": g.uid, "index": fi + 2}));
if let Some(i) = picked_label {
actions.push(("effect.setLabel".into(), json!({"layer": lid, "effect": g.uid, "label": i})));
}
});
// What the effect can't render as asked (an OCIO config it can't read, transforms it
// passes through), under its name.
+7
View File
@@ -357,6 +357,13 @@ pub fn open_form(app: &mut EffectcraftApp, id: &str, p: &Value) -> bool {
],
)
}
"view.addPercentGuide" if !has(p, &["position"]) => (
"Add Percentage Guide".into(),
vec![
Field::choice("orientation", "Orientation", &[("Vertical", json!("vertical")), ("Horizontal", json!("horizontal"))], 0),
Field::num("position", "Position (percent of the composition)", 50.0),
],
),
// Several items selected: After Effects' New Composition from Selection dialog.
"file.newCompFromSelection"
if s.state.project_selection.len() > 1 && !has(p, &["single", "dimensionsFrom", "duration", "sequence", "addToRenderQueue"]) =>
+2 -2
View File
@@ -950,10 +950,10 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let style = app.session.prefs.grids.guide_style.clone();
for g in &comp.guides {
if g.vertical {
let x = comp_rect.min.x + g.position as f32 * zoom;
let x = comp_rect.min.x + g.px(comp.width, comp.height) as f32 * zoom;
styled_line(&painter, [pos2(x, area.min.y), pos2(x, area.max.y)], stroke, &style);
} else {
let y = comp_rect.min.y + g.position as f32 * zoom;
let y = comp_rect.min.y + g.px(comp.width, comp.height) as f32 * zoom;
styled_line(&painter, [pos2(area.min.x, y), pos2(area.max.x, y)], stroke, &style);
}
}
+2 -1
View File
@@ -289,7 +289,8 @@ pub(crate) fn guide_at(app: &EffectcraftApp, comp: &Comp, map: &ViewerMap, pos:
return None;
}
comp.guides.iter().enumerate().find_map(|(i, g)| {
let s = map.to_screen([g.position, g.position]);
let px = g.px(comp.width, comp.height);
let s = map.to_screen([px, px]);
let d = if g.vertical { (s.x - pos.x).abs() } else { (s.y - pos.y).abs() };
(d < 4.0).then_some((i, g.vertical))
})