mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
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:
commit
a985dfb36b
20 files changed
+301
-25
No files matched your search
@@ -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(())
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"]) =>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user