mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-01 14:08:38 -06:00
Merge main into #1739; keep both footage re-exports (Cineon + VIDEO_EXTENSIONS) and main's gaps.md localization row with the PR's AVI row
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -144,6 +144,9 @@ impl EffectcraftApp {
|
||||
// commands handled here use the right catalog already.
|
||||
let language = crate::i18n::language(self);
|
||||
crate::i18n::set_current(language);
|
||||
// #1089: a font-menu rescan runs off the UI thread. Poll it on the logic tick so the
|
||||
// status and the menu update even when the window is not painting.
|
||||
crate::panels::text_panels::poll_font_rescan(self, ctx);
|
||||
if self.styled_language != language {
|
||||
self.want_paint = true;
|
||||
}
|
||||
@@ -384,6 +387,11 @@ impl EffectcraftApp {
|
||||
let full = ui.max_rect();
|
||||
if self.dialog.is_some() {
|
||||
ui.disable();
|
||||
// A dialog that edits what is behind it live (Preview) leaves the window at full
|
||||
// brightness: clicks still don't reach it, but the canvas is not greyed out.
|
||||
if self.dialog == Some(Dialog::GradientEditor) || panels::live_preview::active(self) {
|
||||
ui.set_opacity(1.0);
|
||||
}
|
||||
}
|
||||
ui.painter().rect_filled(full, 0.0, t.app_bg);
|
||||
let mut top = full.min.y;
|
||||
|
||||
@@ -56,6 +56,18 @@ impl EffectcraftApp {
|
||||
&mut self.dialog_state.gradient
|
||||
}
|
||||
|
||||
/// The open settings dialog's draft and Preview state (`panels::dialogs`): Solid, Camera,
|
||||
/// Light and Composition Settings keep what OK would apply here.
|
||||
pub fn dialog_state(&self) -> &panels::dialogs::DialogState {
|
||||
&self.dialog_state
|
||||
}
|
||||
|
||||
/// The open settings dialog's draft, to edit (tests and automation; the dialog's own fields
|
||||
/// do the same).
|
||||
pub fn dialog_state_mut(&mut self) -> &mut panels::dialogs::DialogState {
|
||||
&mut self.dialog_state
|
||||
}
|
||||
|
||||
/// Show panel `p` (opening it in its usual place if it is closed), bring it to the front and
|
||||
/// give it the focus: Window ▸ `<panel>`.
|
||||
pub fn show_panel(&mut self, p: PanelKind) {
|
||||
|
||||
@@ -532,7 +532,7 @@ mod tests {
|
||||
let item = p.add_item("long.flac", Label::SeaFoam, None, ItemKind::Footage(footage));
|
||||
let mut comp = Comp::new(64, 36, FrameRate::new(30, 1), Tick::from_seconds_f64(10.0));
|
||||
let layer = build::layer(&mut p, &comp, "long.flac", LayerSource::Footage { item }, (0, 0), None);
|
||||
comp.layers = vec![layer];
|
||||
comp.layers = vec![layer].into();
|
||||
let cid = p.add_item("Comp", Label::Sandstone, None, ItemKind::Comp(comp.into()));
|
||||
let src = RenderSource {
|
||||
project: Arc::new(p),
|
||||
|
||||
@@ -246,7 +246,7 @@ pub fn contributors_ui(ui: &mut egui::Ui, auto: &mut Registry) {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(tr("Show"));
|
||||
for m in NameMode::ALL {
|
||||
let r = ui.selectable_label(v.names == m, m.label());
|
||||
let r = ui.selectable_label(v.names == m, tr(m.label()));
|
||||
auto.add(&format!("about.credits.show.{}", m.id()), r.rect, m.label());
|
||||
if r.clicked() {
|
||||
v.names = m;
|
||||
@@ -254,9 +254,9 @@ pub fn contributors_ui(ui: &mut egui::Ui, auto: &mut Registry) {
|
||||
}
|
||||
ui.separator();
|
||||
ui.label(tr("Sort"));
|
||||
let combo = craft_ui::ComboBox::from_id_salt("credits_sort").selected_text(v.key.label().0).show_ui(ui, |ui| {
|
||||
let combo = craft_ui::ComboBox::from_id_salt("credits_sort").selected_text(tr(v.key.label().0)).show_ui(ui, |ui| {
|
||||
for k in SortKey::ALL {
|
||||
let r = ui.selectable_label(v.key == k, k.label().0);
|
||||
let r = ui.selectable_label(v.key == k, tr(k.label().0));
|
||||
auto.add(&format!("about.credits.sort.{}", k.id()), r.rect, k.label().0);
|
||||
if r.clicked() {
|
||||
v.key = k;
|
||||
@@ -309,7 +309,7 @@ fn table(ui: &mut egui::Ui, list: &[&Contributor], v: &mut View, auto: &mut Regi
|
||||
egui::Grid::new("credits_table").striped(true).num_columns(SortKey::ALL.len()).show(ui, |ui| {
|
||||
for k in SortKey::ALL {
|
||||
let arrow = if v.key == k { if v.ascending { " ▲" } else { " ▼" } } else { "" };
|
||||
let r = ui.button(RichText::new(format!("{}{arrow}", k.label().1)).strong()).on_hover_text(k.label().0);
|
||||
let r = ui.button(RichText::new(format!("{}{arrow}", k.label().1)).strong()).on_hover_text(tr(k.label().0));
|
||||
auto.add(&format!("about.credits.header.{}", k.id()), r.rect, k.label().0);
|
||||
if r.clicked() {
|
||||
if v.key == k {
|
||||
|
||||
@@ -799,7 +799,11 @@ impl DockContent<PanelKind> for EffectContent<'_> {
|
||||
}
|
||||
|
||||
fn logical_label(&self, panel: &PanelKind) -> String {
|
||||
crate::i18n::command_label("window.panel", panel.title()).to_string()
|
||||
let title = panel.title();
|
||||
// The Window menu names most panels; `Markers` is only in the panel catalog, and the two
|
||||
// panels in neither (`History`, `ScriptUI Panel`) keep their English names.
|
||||
let listed = crate::i18n::command_label("window.panel", title);
|
||||
if listed == title { crate::i18n::tr(title).to_string() } else { listed.to_string() }
|
||||
}
|
||||
|
||||
/// The tab names what the panel shows ("Composition Intro"), after the label swatch of its
|
||||
|
||||
+775
-2
@@ -15,7 +15,7 @@ pub(crate) use ui::catalog_characters;
|
||||
pub(crate) use ui::{set_current, tr, tr_args};
|
||||
|
||||
/// Every language with a catalog, in the order Settings ▸ General ▸ Language lists them.
|
||||
const CATALOG_CODES: [&str; 11] = ["cs", "de", "es", "fr", "pt-br", "ru", "uk", "ka", "ja", "zh-hans", "zh-hant"];
|
||||
const CATALOG_CODES: [&str; 12] = ["cs", "de", "es", "fr", "pl", "pt-br", "ru", "uk", "ka", "ja", "zh-hans", "zh-hant"];
|
||||
|
||||
/// The active interface language: the stored preference when it names a catalog, otherwise `en`
|
||||
/// (English is the source of every label and needs no lookup).
|
||||
@@ -66,6 +66,11 @@ pub(crate) fn czech(app: &EffectcraftApp) -> bool {
|
||||
language(app) == "cs"
|
||||
}
|
||||
|
||||
/// The UI is in Polish.
|
||||
pub(crate) fn polish(app: &EffectcraftApp) -> bool {
|
||||
language(app) == "pl"
|
||||
}
|
||||
|
||||
/// The UI is in Georgian.
|
||||
pub(crate) fn georgian(app: &EffectcraftApp) -> bool {
|
||||
language(app) == "ka"
|
||||
@@ -134,6 +139,7 @@ fn supported(locale: Option<&str>) -> &'static str {
|
||||
"uk" => "uk",
|
||||
"es" => "es",
|
||||
"cs" => "cs",
|
||||
"pl" => "pl",
|
||||
"ka" => "ka",
|
||||
"ru" => "ru",
|
||||
"de" => "de",
|
||||
@@ -173,6 +179,13 @@ pub(crate) fn label<'a>(app: &EffectcraftApp, command: &str, source: &'a str) ->
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if language == "pl" && matches!(command, "file.revealInFinder" | "layer.revealInFinder") {
|
||||
match source {
|
||||
"Reveal in Explorer" => return "Pokaż w Eksploratorze",
|
||||
"Reveal in File Manager" => return "Pokaż w menedżerze plików",
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if language == "ka" && matches!(command, "file.revealInFinder" | "layer.revealInFinder") {
|
||||
match source {
|
||||
"Reveal in Explorer" => return "Explorer-ში ჩვენება",
|
||||
@@ -196,6 +209,7 @@ fn menu_catalog(language: &str) -> Option<&'static [(&'static str, &'static str,
|
||||
"fr" => FRENCH,
|
||||
"de" => GERMAN,
|
||||
"cs" => CZECH,
|
||||
"pl" => POLISH,
|
||||
"ka" => GEORGIAN,
|
||||
_ => return None,
|
||||
})
|
||||
@@ -266,6 +280,9 @@ pub(crate) fn entry(app: &EffectcraftApp, e: &MenuEntry, shown: String) -> Strin
|
||||
"es" => Some(("Deshacer", "No se puede deshacer", "Rehacer", "No se puede rehacer")),
|
||||
"pt-br" => Some(("Desfazer", "Não há nada para desfazer", "Refazer", "Não há nada para refazer")),
|
||||
"cs" => Some(("Zpět", "Není co vrátit", "Znovu", "Není co zopakovat")),
|
||||
"fr" => Some(("Annuler", "Impossible d'annuler", "Rétablir", "Impossible de rétablir")),
|
||||
"de" => Some(("Rückgängig", "Rückgängig nicht möglich", "Wiederholen", "Wiederholen nicht möglich")),
|
||||
"pl" => Some(("Cofnij", "Nie można cofnąć", "Ponów", "Nie można ponowić")),
|
||||
// A colon before the history's (untranslated) action name: "დაბრუნება: Comp1".
|
||||
"ka" => Some(("დაბრუნება:", "დაბრუნება შეუძლებელია", "ხელახლა შესრულება:", "ხელახლა შესრულება შეუძლებელია")),
|
||||
// A colon before the history's (untranslated) action name: "Отменить: Comp1".
|
||||
@@ -306,8 +323,23 @@ pub(crate) fn effect_category(children: &[MenuNode], source: &str) -> Option<Str
|
||||
(!children.is_empty() && children.iter().all(|c| matches!(c, MenuNode::Item(e) if e.command == "effect.apply"))).then(|| effects::category(source))
|
||||
}
|
||||
|
||||
/// Polish names for the Assign Shortcut submenus that carry a workspace's or 3D view's name.
|
||||
fn polish_assign_shortcut(source: &str, shown: &str) -> Option<String> {
|
||||
let named = shown.strip_prefix("Assign Shortcut to “")?;
|
||||
match source {
|
||||
"Assign Shortcut to Workspace" => named.strip_suffix("” Workspace").map(|name| format!("Przypisz skrót do obszaru roboczego „{name}”")),
|
||||
"Assign Shortcut to 3D View" => named.strip_suffix('”').map(|name| format!("Przypisz skrót do widoku 3D „{name}”")),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn submenu(app: &EffectcraftApp, source: &str, shown: String) -> String {
|
||||
match language(app) {
|
||||
"pl" => {
|
||||
if let Some(label) = polish_assign_shortcut(source, &shown) {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
"cs" => {
|
||||
if source == "Assign Shortcut to Workspace" {
|
||||
if let Some(name) = shown.strip_prefix("Assign Shortcut to “").and_then(|s| s.strip_suffix("” Workspace")) {
|
||||
@@ -836,6 +868,8 @@ const SIMPLIFIED_CHINESE: &[(&str, &str, &str)] = &[
|
||||
("layer.applyTextPreset", "Drop In Lines", "行落下"),
|
||||
("anim.addKeyframe", "Add Keyframe", "添加关键帧"),
|
||||
("keys.toggleHold", "Toggle Hold Keyframe", "切换定格关键帧"),
|
||||
("keys.setMuted", "Mute Keyframes", "静音关键帧"),
|
||||
("keys.setMuted", "Unmute Keyframes", "取消静音关键帧"),
|
||||
("keys.interpolation", "Keyframe Interpolation...", "关键帧插值..."),
|
||||
("keys.velocity", "Keyframe Velocity...", "关键帧速度..."),
|
||||
("", "Keyframe Assistant", "关键帧辅助"),
|
||||
@@ -1472,6 +1506,8 @@ const TRADITIONAL_CHINESE: &[(&str, &str, &str)] = &[
|
||||
("layer.applyTextPreset", "Drop In Lines", "行落下"),
|
||||
("anim.addKeyframe", "Add Keyframe", "新增關鍵影格"),
|
||||
("keys.toggleHold", "Toggle Hold Keyframe", "切換定格關鍵影格"),
|
||||
("keys.setMuted", "Mute Keyframes", "靜音關鍵影格"),
|
||||
("keys.setMuted", "Unmute Keyframes", "取消靜音關鍵影格"),
|
||||
("keys.interpolation", "Keyframe Interpolation...", "關鍵影格插補..."),
|
||||
("keys.velocity", "Keyframe Velocity...", "關鍵影格速度..."),
|
||||
("", "Keyframe Assistant", "關鍵影格輔助"),
|
||||
@@ -2105,6 +2141,8 @@ const JAPANESE: &[(&str, &str, &str)] = &[
|
||||
("layer.applyTextPreset", "Drop In Lines", "行をドロップイン"),
|
||||
("anim.addKeyframe", "Add Keyframe", "キーフレームを追加"),
|
||||
("keys.toggleHold", "Toggle Hold Keyframe", "停止したキーフレームの切り替え"),
|
||||
("keys.setMuted", "Mute Keyframes", "キーフレームをミュート"),
|
||||
("keys.setMuted", "Unmute Keyframes", "キーフレームのミュートを解除"),
|
||||
("keys.interpolation", "Keyframe Interpolation...", "キーフレーム補間法..."),
|
||||
("keys.velocity", "Keyframe Velocity...", "キーフレーム速度..."),
|
||||
("", "Keyframe Assistant", "キーフレーム補助"),
|
||||
@@ -2741,6 +2779,8 @@ const UKRAINIAN: &[(&str, &str, &str)] = &[
|
||||
("layer.applyTextPreset", "Drop In Lines", "Поява рядків згори"),
|
||||
("anim.addKeyframe", "Add Keyframe", "Додати ключовий кадр"),
|
||||
("keys.toggleHold", "Toggle Hold Keyframe", "Перемкнути утримання ключового кадру"),
|
||||
("keys.setMuted", "Mute Keyframes", "Заглушити ключові кадри"),
|
||||
("keys.setMuted", "Unmute Keyframes", "Зняти заглушення ключових кадрів"),
|
||||
("keys.interpolation", "Keyframe Interpolation...", "Інтерполяція ключових кадрів..."),
|
||||
("keys.velocity", "Keyframe Velocity...", "Швидкість ключових кадрів..."),
|
||||
("", "Keyframe Assistant", "Помічник ключових кадрів"),
|
||||
@@ -3378,6 +3418,8 @@ const SPANISH: &[(&str, &str, &str)] = &[
|
||||
("layer.applyTextPreset", "Drop In Lines", "Entrada de líneas cayendo"),
|
||||
("anim.addKeyframe", "Add Keyframe", "Añadir fotograma clave"),
|
||||
("keys.toggleHold", "Toggle Hold Keyframe", "Conmutar mantener fotograma clave"),
|
||||
("keys.setMuted", "Mute Keyframes", "Silenciar fotogramas clave"),
|
||||
("keys.setMuted", "Unmute Keyframes", "Reactivar fotogramas clave"),
|
||||
("keys.interpolation", "Keyframe Interpolation...", "Interpolación de fotogramas clave..."),
|
||||
("keys.velocity", "Keyframe Velocity...", "Velocidad de fotogramas clave..."),
|
||||
("", "Keyframe Assistant", "Asistente de fotogramas clave"),
|
||||
@@ -4013,6 +4055,8 @@ const PORTUGUESE: &[(&str, &str, &str)] = &[
|
||||
("layer.applyTextPreset", "Drop In Lines", "Entrar com queda de linhas"),
|
||||
("anim.addKeyframe", "Add Keyframe", "Adicionar quadro-chave"),
|
||||
("keys.toggleHold", "Toggle Hold Keyframe", "Alternar quadro-chave de retenção"),
|
||||
("keys.setMuted", "Mute Keyframes", "Silenciar quadros-chave"),
|
||||
("keys.setMuted", "Unmute Keyframes", "Reativar quadros-chave"),
|
||||
("keys.interpolation", "Keyframe Interpolation...", "Interpolação de quadro-chave..."),
|
||||
("keys.velocity", "Keyframe Velocity...", "Velocidade do quadro-chave..."),
|
||||
("", "Keyframe Assistant", "Assistente de quadro-chave"),
|
||||
@@ -4655,6 +4699,8 @@ const RUSSIAN: &[(&str, &str, &str)] = &[
|
||||
("layer.applyTextPreset", "Drop In Lines", "Появление строк сверху"),
|
||||
("anim.addKeyframe", "Add Keyframe", "Добавить ключевой кадр"),
|
||||
("keys.toggleHold", "Toggle Hold Keyframe", "Переключить фиксацию ключевого кадра"),
|
||||
("keys.setMuted", "Mute Keyframes", "Отключить ключевые кадры"),
|
||||
("keys.setMuted", "Unmute Keyframes", "Включить ключевые кадры"),
|
||||
("keys.interpolation", "Keyframe Interpolation...", "Интерполяция ключевых кадров..."),
|
||||
("keys.velocity", "Keyframe Velocity...", "Скорость ключевых кадров..."),
|
||||
("", "Keyframe Assistant", "Помощник по ключевым кадрам"),
|
||||
@@ -5290,6 +5336,8 @@ const FRENCH: &[(&str, &str, &str)] = &[
|
||||
("layer.applyTextPreset", "Drop In Lines", "Chute des lignes depuis le haut"),
|
||||
("anim.addKeyframe", "Add Keyframe", "Ajouter une image clé"),
|
||||
("keys.toggleHold", "Toggle Hold Keyframe", "Activer/Désactiver l'image clé figée"),
|
||||
("keys.setMuted", "Mute Keyframes", "Désactiver les images clés"),
|
||||
("keys.setMuted", "Unmute Keyframes", "Réactiver les images clés"),
|
||||
("keys.interpolation", "Keyframe Interpolation...", "Interpolation des images clés..."),
|
||||
("keys.velocity", "Keyframe Velocity...", "Vitesse des images clés..."),
|
||||
("", "Keyframe Assistant", "Assistant d'images clés"),
|
||||
@@ -5925,6 +5973,8 @@ const GERMAN: &[(&str, &str, &str)] = &[
|
||||
("layer.applyTextPreset", "Drop In Lines", "Zeilen fallen ein"),
|
||||
("anim.addKeyframe", "Add Keyframe", "Keyframe hinzufügen"),
|
||||
("keys.toggleHold", "Toggle Hold Keyframe", "Halte-Keyframe umschalten"),
|
||||
("keys.setMuted", "Mute Keyframes", "Keyframes stummschalten"),
|
||||
("keys.setMuted", "Unmute Keyframes", "Keyframes wieder aktivieren"),
|
||||
("keys.interpolation", "Keyframe Interpolation...", "Keyframe-Interpolation..."),
|
||||
("keys.velocity", "Keyframe Velocity...", "Keyframe-Geschwindigkeit..."),
|
||||
("", "Keyframe Assistant", "Keyframe-Assistent"),
|
||||
@@ -6124,6 +6174,640 @@ const GERMAN: &[(&str, &str, &str)] = &[
|
||||
|
||||
// <<< generated i18n catalogs
|
||||
|
||||
/// Polish (pl): original phrase translations of every fixed menu entry.
|
||||
const POLISH: &[(&str, &str, &str)] = &[
|
||||
("", "Front", "Przód"),
|
||||
("", "Bevel", "Fazowanie"),
|
||||
("", "Side", "Bok"),
|
||||
("", "Back", "Tył"),
|
||||
("layer.addTextAnimator", "Color", "Kolor"),
|
||||
("", "EffectCraft", "EffectCraft"),
|
||||
("app.about", "About EffectCraft...", "O programie EffectCraft..."),
|
||||
("", "Settings...", "Ustawienia..."),
|
||||
("app.settings", "General...", "Ogólne..."),
|
||||
("app.settings", "Startup & Repair...", "Uruchamianie i naprawa..."),
|
||||
("app.settings", "Project...", "Projekt..."),
|
||||
("app.settings", "Composition...", "Kompozycja..."),
|
||||
("app.settings", "Previews...", "Podglądy..."),
|
||||
("app.settings", "Appearance...", "Wygląd..."),
|
||||
("app.settings", "Grids & Guides...", "Siatki i linie pomocnicze..."),
|
||||
("app.settings", "Labels...", "Etykiety..."),
|
||||
("app.settings", "Type...", "Tekst..."),
|
||||
("app.settings", "Import...", "Import..."),
|
||||
("app.settings", "Export...", "Eksport..."),
|
||||
("app.settings", "Audio...", "Audio..."),
|
||||
("app.settings", "Disk...", "Dysk..."),
|
||||
("app.settings", "Memory & CPU...", "Pamięć i procesor..."),
|
||||
("app.settings", "Video...", "Wideo..."),
|
||||
("app.settings", "3D...", "3D..."),
|
||||
("app.settings", "Scripting & Expressions...", "Skrypty i wyrażenia..."),
|
||||
("app.hide", "Hide EffectCraft", "Ukryj EffectCraft"),
|
||||
("app.hideOthers", "Hide Others", "Ukryj pozostałe"),
|
||||
("app.showAll", "Show All", "Pokaż wszystko"),
|
||||
("app.quit", "Quit EffectCraft", "Zakończ EffectCraft"),
|
||||
("", "File", "Plik"),
|
||||
("", "New", "Nowy"),
|
||||
("file.newProject", "New Project", "Nowy projekt"),
|
||||
("file.newFromTemplate", "New Project from Template...", "Nowy projekt z szablonu..."),
|
||||
("project.newFolder", "New Folder", "Nowy folder"),
|
||||
("file.open", "Open Project...", "Otwórz projekt..."),
|
||||
("", "Open Recent", "Otwórz ostatnie"),
|
||||
("file.clearRecent", "Clear Recent Projects", "Wyczyść ostatnie projekty"),
|
||||
("file.close", "Close", "Zamknij"),
|
||||
("file.closeProject", "Close Project", "Zamknij projekt"),
|
||||
("file.save", "Save", "Zapisz"),
|
||||
("", "Save As", "Zapisz jako"),
|
||||
("file.saveAs", "Save As...", "Zapisz jako..."),
|
||||
("file.saveCopy", "Save a Copy...", "Zapisz kopię..."),
|
||||
("file.saveCopyAsXml", "Save a Copy As XML...", "Zapisz kopię jako XML..."),
|
||||
("templates.saveAs", "Save as Template...", "Zapisz jako szablon..."),
|
||||
("file.incrementAndSave", "Increment and Save", "Zapisz z kolejnym numerem"),
|
||||
("file.revert", "Revert", "Przywróć"),
|
||||
("", "Import", "Importuj"),
|
||||
("file.import", "File...", "Plik..."),
|
||||
("file.importMultiple", "Multiple Files...", "Wiele plików..."),
|
||||
("file.importTimeline", "Adobe Premiere Pro Project...", "Projekt Adobe Premiere Pro..."),
|
||||
("file.importProject", "EffectCraft Project...", "Projekt EffectCraft..."),
|
||||
("file.importPlaceholder", "Placeholder...", "Element zastępczy..."),
|
||||
("file.importSolid", "Solid...", "Plansza..."),
|
||||
("file.importLottie", "Lottie...", "Lottie..."),
|
||||
("file.importVanishingPoint", "Vanishing Point (.vpe)...", "Punkt zbiegu (.vpe)..."),
|
||||
("essential.importTemplate", "Essential Graphics Template...", "Szablon Essential Graphics..."),
|
||||
("", "Import Recent Footage", "Importuj ostatnie materiały"),
|
||||
("file.clearRecentFootage", "Clear Recent Footage", "Wyczyść ostatnie materiały"),
|
||||
("", "Export", "Eksportuj"),
|
||||
("renderQueue.add", "Add to Render Queue", "Dodaj do kolejki renderowania"),
|
||||
("file.exportTimeline", "Adobe Premiere Pro Project...", "Projekt Adobe Premiere Pro..."),
|
||||
("file.exportLottie", "Lottie JSON...", "Lottie JSON..."),
|
||||
("essential.exportTemplate", "Essential Graphics Template...", "Szablon Essential Graphics..."),
|
||||
("app.find", "Find", "Znajdź"),
|
||||
("layer.addItem", "Add Footage to Comp", "Dodaj materiał do kompozycji"),
|
||||
("file.newCompFromSelection", "New Comp from Selection...", "Nowa kompozycja z zaznaczenia..."),
|
||||
("", "Dependencies", "Zależności"),
|
||||
("file.collectFiles", "Collect Files...", "Zbierz pliki..."),
|
||||
("file.consolidateFootage", "Consolidate All Footage", "Scal wszystkie materiały"),
|
||||
("file.removeUnusedFootage", "Remove Unused Footage", "Usuń nieużywane materiały"),
|
||||
("file.reduceProject", "Reduce Project", "Zredukuj projekt"),
|
||||
("file.findMissing", "Find Missing Effects", "Znajdź brakujące efekty"),
|
||||
("file.findMissing", "Find Missing Fonts", "Znajdź brakujące czcionki"),
|
||||
("file.findMissing", "Find Missing Footage", "Znajdź brakujące materiały"),
|
||||
("file.relinkFootage", "Relink Missing Footage...", "Połącz ponownie brakujące materiały..."),
|
||||
("file.watchFolder", "Watch Folder...", "Folder obserwowany..."),
|
||||
("", "Scripts", "Skrypty"),
|
||||
("file.installScript", "Install Script File...", "Zainstaluj plik skryptu..."),
|
||||
("file.installScriptUIPanel", "Install ScriptUI Panel...", "Zainstaluj panel ScriptUI..."),
|
||||
("file.runScript", "Run Script File...", "Uruchom plik skryptu..."),
|
||||
("", "Create Proxy", "Utwórz proxy"),
|
||||
("file.createProxy", "Still...", "Klatka..."),
|
||||
("file.createProxy", "Movie...", "Film..."),
|
||||
("", "Set Proxy", "Ustaw proxy"),
|
||||
("file.setProxy", "File...", "Plik..."),
|
||||
("file.setProxyNone", "None", "Brak"),
|
||||
("", "Interpret Footage", "Interpretuj materiał"),
|
||||
("file.interpretFootage", "Main...", "Główny..."),
|
||||
("file.interpretProxy", "Proxy...", "Proxy..."),
|
||||
("file.rememberInterpretation", "Remember Interpretation", "Zapamiętaj interpretację"),
|
||||
("file.applyInterpretation", "Apply Interpretation", "Zastosuj interpretację"),
|
||||
("", "Replace Footage", "Zastąp materiał"),
|
||||
("file.replaceFootage", "File...", "Plik..."),
|
||||
("file.replaceWithPlaceholder", "Placeholder...", "Element zastępczy..."),
|
||||
("file.replaceWithSolid", "Solid...", "Plansza..."),
|
||||
("file.replaceWithLayeredComp", "With Layered Comp", "Z warstwową kompozycją"),
|
||||
("file.reloadFootage", "Reload Footage", "Wczytaj ponownie materiał"),
|
||||
("file.revealInFinder", "Reveal in Finder", "Pokaż w Finderze"),
|
||||
("file.projectSettings", "Project Settings...", "Ustawienia projektu..."),
|
||||
("app.quit", "Exit", "Zakończ"),
|
||||
("", "Edit", "Edycja"),
|
||||
("edit.undo", "Undo", "Cofnij"),
|
||||
("edit.redo", "Redo", "Ponów"),
|
||||
("", "History", "Historia"),
|
||||
("app.commandPalette", "Quick Apply...", "Szybkie stosowanie..."),
|
||||
("edit.cut", "Cut", "Wytnij"),
|
||||
("edit.copy", "Copy", "Kopiuj"),
|
||||
("edit.copyWithPropertyLinks", "Copy with Property Links", "Kopiuj z łączami właściwości"),
|
||||
("edit.copyWithRelativePropertyLinks", "Copy with Relative Property Links", "Kopiuj ze względnymi łączami właściwości"),
|
||||
("edit.copyExpressionOnly", "Copy Expression Only", "Kopiuj tylko wyrażenie"),
|
||||
("edit.paste", "Paste", "Wklej"),
|
||||
("edit.pasteReversedKeyframes", "Paste Reversed Keyframes", "Wklej odwrócone klatki kluczowe"),
|
||||
("edit.pasteTextMatchFormatting", "Paste Text and Match Formatting", "Wklej tekst i dopasuj formatowanie"),
|
||||
("edit.pasteTextFormattingOnly", "Paste Text Formatting Only", "Wklej tylko formatowanie tekstu"),
|
||||
("edit.clear", "Clear", "Wyczyść"),
|
||||
("edit.duplicate", "Duplicate", "Powiel"),
|
||||
("edit.splitLayer", "Split Layer", "Podziel warstwę"),
|
||||
("edit.liftWorkArea", "Lift Work Area", "Wytnij obszar roboczy"),
|
||||
("edit.extractWorkArea", "Extract Work Area", "Wyodrębnij obszar roboczy"),
|
||||
("edit.selectAll", "Select All", "Zaznacz wszystko"),
|
||||
("edit.deselectAll", "Deselect All", "Usuń zaznaczenie"),
|
||||
("", "Label", "Etykieta"),
|
||||
("edit.selectLabelGroup", "Select Label Group", "Zaznacz grupę etykiet"),
|
||||
("edit.label", "None", "Brak"),
|
||||
("edit.label", "Red", "Czerwony"),
|
||||
("edit.label", "Yellow", "Żółty"),
|
||||
("edit.label", "Aqua", "Akwamaryna"),
|
||||
("edit.label", "Pink", "Różowy"),
|
||||
("edit.label", "Lavender", "Lawendowy"),
|
||||
("edit.label", "Peach", "Brzoskwiniowy"),
|
||||
("edit.label", "Sea Foam", "Morska piana"),
|
||||
("edit.label", "Blue", "Niebieski"),
|
||||
("edit.label", "Green", "Zielony"),
|
||||
("edit.label", "Purple", "Fioletowy"),
|
||||
("edit.label", "Orange", "Pomarańczowy"),
|
||||
("edit.label", "Brown", "Brązowy"),
|
||||
("edit.label", "Fuchsia", "Fuksja"),
|
||||
("edit.label", "Cyan", "Cyjan"),
|
||||
("edit.label", "Sandstone", "Piaskowiec"),
|
||||
("edit.label", "Dark Green", "Ciemnozielony"),
|
||||
("app.settings", "Edit Label Colors...", "Edytuj kolory etykiet..."),
|
||||
("", "Select Keyframe Label Group", "Zaznacz grupę etykiet klatek kluczowych"),
|
||||
("keys.selectLabelGroup", "On Selected Layers", "Na zaznaczonych warstwach"),
|
||||
("keys.selectLabelGroup", "On All Layers", "Na wszystkich warstwach"),
|
||||
("keys.selectLabelGroup", "Visible Keyframes on Selected Layers", "Widoczne klatki kluczowe na zaznaczonych warstwach"),
|
||||
("keys.selectLabelGroup", "Visible Keyframes on All Layers", "Widoczne klatki kluczowe na wszystkich warstwach"),
|
||||
("", "Purge", "Wyczyść pamięć"),
|
||||
("edit.purge", "All Cache...", "Całą pamięć podręczną..."),
|
||||
("edit.purge", "All Memory & Disk Cache...", "Całą pamięć i pamięć dyskową..."),
|
||||
("edit.purge", "All Memory", "Całą pamięć"),
|
||||
("edit.purge", "All Disk Cache...", "Całą pamięć dyskową..."),
|
||||
("edit.purge", "All 3D Cache...", "Całą pamięć 3D..."),
|
||||
("edit.purgeUndo", "Undo", "Cofanie"),
|
||||
("edit.purge", "Image Cache Memory", "Pamięć podręczna obrazów"),
|
||||
("edit.purge", "Snapshot", "Migawka"),
|
||||
("edit.editOriginal", "Edit Original...", "Edytuj oryginał..."),
|
||||
("", "Templates", "Szablony"),
|
||||
("app.templates", "Render Settings...", "Ustawienia renderowania..."),
|
||||
("app.templates", "Output Module...", "Moduł wyjściowy..."),
|
||||
("app.keyboardShortcuts", "Keyboard Shortcuts", "Skróty klawiaturowe"),
|
||||
("", "Preferences", "Preferencje"),
|
||||
("", "Composition", "Kompozycja"),
|
||||
("comp.new", "New Composition...", "Nowa kompozycja..."),
|
||||
("comp.settings", "Composition Settings...", "Ustawienia kompozycji..."),
|
||||
("comp.setPosterTime", "Set Poster Time", "Ustaw czas plakatu"),
|
||||
("comp.trimToWorkArea", "Trim Comp to Work Area", "Przytnij kompozycję do obszaru roboczego"),
|
||||
("comp.cropToRegionOfInterest", "Crop Comp to Region of Interest", "Kadruj kompozycję do obszaru zainteresowania"),
|
||||
("comp.cropToLayerBounds", "Crop Comp to Selected Layer(s) Bounds", "Kadruj kompozycję do granic zaznaczonych warstw (warstwy)"),
|
||||
("render.addOutputModule", "Add Output Module", "Dodaj moduł wyjściowy"),
|
||||
("", "Preview", "Podgląd"),
|
||||
("playback.toggle", "Play Current Preview", "Odtwórz bieżący podgląd"),
|
||||
("playback.cacheWhenIdle", "Cache Frames When Idle", "Buforuj klatki w bezczynności"),
|
||||
("playback.audio", "Audio", "Audio"),
|
||||
("", "Save Frame As", "Zapisz klatkę jako"),
|
||||
("comp.saveFrameAs", "File...", "Plik..."),
|
||||
("comp.saveFrameAsPsd", "Photoshop Layers...", "Warstwy Photoshopa..."),
|
||||
("comp.saveFrameAsExr", "ProEXR...", "ProEXR..."),
|
||||
("comp.copyFrame", "Copy Frame to Clipboard", "Kopiuj klatkę do schowka"),
|
||||
("render.preRender", "Pre-render...", "Wstępne renderowanie..."),
|
||||
("render.saveCurrentPreview", "Save Current Preview...", "Zapisz bieżący podgląd..."),
|
||||
("comp.openInEssentialGraphics", "Open in Essential Graphics", "Otwórz w Essential Graphics"),
|
||||
("", "Responsive Design — Time", "Projekt responsywny — czas"),
|
||||
("comp.responsiveTime", "Create Intro", "Utwórz intro"),
|
||||
("comp.responsiveTime", "Create Outro", "Utwórz outro"),
|
||||
("comp.responsiveTime", "Create Protected Region from Work Area", "Utwórz chroniony obszar z obszaru roboczego"),
|
||||
("comp.flowchart", "Composition Flowchart", "Schemat kompozycji"),
|
||||
("comp.miniFlowchart", "Composition Mini-Flowchart", "Miniaturowy schemat kompozycji"),
|
||||
("", "VR", "VR"),
|
||||
("comp.vr.createEnvironment", "Create VR Environment...", "Utwórz środowisko VR..."),
|
||||
("comp.vr.extractCubemap", "Extract Cubemap...", "Wyodrębnij cubemapę..."),
|
||||
("", "Layer", "Warstwa"),
|
||||
("layer.newText", "Text", "Tekst"),
|
||||
("layer.newSolid", "Solid...", "Plansza..."),
|
||||
("layer.newLight", "Light...", "Światło..."),
|
||||
("layer.newCamera", "Camera...", "Kamera..."),
|
||||
("layer.newNull", "Null Object", "Obiekt zerowy"),
|
||||
("layer.newShape", "Shape Layer", "Warstwa kształtu"),
|
||||
("layer.newAdjustment", "Adjustment Layer", "Warstwa korekcyjna"),
|
||||
("layer.newContentAwareFill", "Content-Aware Fill Layer...", "Warstwa wypełnienia z uwzględnieniem zawartości..."),
|
||||
("layer.new3dPrimitive", "Cube", "Sześcian"),
|
||||
("layer.new3dPrimitive", "Sphere", "Kula"),
|
||||
("layer.new3dPrimitive", "Plane", "Płaszczyzna"),
|
||||
("layer.new3dPrimitive", "Torus", "Torus"),
|
||||
("layer.new3dPrimitive", "Cone", "Stożek"),
|
||||
("layer.new3dPrimitive", "Cylinder", "Walec"),
|
||||
("layer.settings", "Layer Settings...", "Ustawienia warstwy..."),
|
||||
("layer.openLayer", "Open Layer", "Otwórz warstwę"),
|
||||
("layer.openSource", "Open Layer Source", "Otwórz źródło warstwy"),
|
||||
("layer.revealInFinder", "Reveal in Finder", "Pokaż w Finderze"),
|
||||
("", "Mask", "Maska"),
|
||||
("layer.addMask", "New Mask", "Nowa maska"),
|
||||
("layer.mask.shape", "Mask Shape...", "Kształt maski..."),
|
||||
("layer.mask.set", "Mask Feather...", "Wtapianie maski..."),
|
||||
("layer.mask.set", "Mask Opacity...", "Krycie maski..."),
|
||||
("layer.mask.set", "Mask Expansion...", "Rozszerzenie maski..."),
|
||||
("layer.mask.reset", "Reset Mask", "Resetuj maskę"),
|
||||
("layer.mask.remove", "Remove Mask", "Usuń maskę"),
|
||||
("layer.mask.removeAll", "Remove All Masks", "Usuń wszystkie maski"),
|
||||
("track.mask", "Track Mask", "Śledź maskę"),
|
||||
("", "Mode", "Tryb"),
|
||||
("layer.mask.mode", "None", "Brak"),
|
||||
("layer.mask.mode", "Add", "Dodaj"),
|
||||
("layer.mask.mode", "Subtract", "Odejmij"),
|
||||
("layer.mask.mode", "Intersect", "Część wspólna"),
|
||||
("layer.mask.mode", "Lighten", "Rozjaśnij"),
|
||||
("layer.mask.mode", "Darken", "Przyciemnij"),
|
||||
("layer.mask.mode", "Difference", "Różnica"),
|
||||
("layer.mask.invert", "Inverted", "Odwrócona"),
|
||||
("layer.mask.lock", "Locked", "Zablokowana"),
|
||||
("", "Motion Blur", "Rozmycie ruchu"),
|
||||
("layer.mask.motionBlur", "Same As Layer", "Jak warstwa"),
|
||||
("layer.mask.motionBlur", "On", "Wł."),
|
||||
("layer.mask.motionBlur", "Off", "Wył."),
|
||||
("", "Feather Falloff", "Zanikanie wtapiania"),
|
||||
("layer.mask.featherFalloff", "Smooth", "Gładkie"),
|
||||
("layer.mask.featherFalloff", "Linear", "Liniowe"),
|
||||
("layer.mask.unlockAll", "Unlock All Masks", "Odblokuj wszystkie maski"),
|
||||
("layer.mask.lockOthers", "Lock Other Masks", "Zablokuj pozostałe maski"),
|
||||
("layer.mask.hideLocked", "Hide Locked Masks", "Ukryj zablokowane maski"),
|
||||
("", "Mask and Shape Path", "Ścieżka maski i kształtu"),
|
||||
("path.rotoBezier", "RotoBezier", "RotoBezier"),
|
||||
("mask.setClosed", "Closed", "Zamknięta"),
|
||||
("path.convertToBezier", "Convert To Bezier Path", "Konwertuj na ścieżkę Béziera"),
|
||||
("path.setFirstVertex", "Set First Vertex", "Ustaw pierwszy wierzchołek"),
|
||||
("path.freeTransform", "Free Transform Points", "Swobodne przekształcanie punktów"),
|
||||
("", "Quality", "Jakość"),
|
||||
("layer.quality", "Best", "Najlepsza"),
|
||||
("layer.quality", "Draft", "Robocza"),
|
||||
("layer.quality", "Wireframe", "Szkielet"),
|
||||
("layer.sampling", "Bilinear", "Dwuliniowa"),
|
||||
("layer.sampling", "Bicubic", "Dwusześcienna"),
|
||||
("", "Switches", "Przełączniki"),
|
||||
("layer.hideOtherVideo", "Hide Other Video", "Ukryj pozostałe wideo"),
|
||||
("layer.showAllVideo", "Show All Video", "Pokaż całe wideo"),
|
||||
("layer.unlockAll", "Unlock All Layers", "Odblokuj wszystkie warstwy"),
|
||||
("layer.expressions", "Enable Expressions", "Włącz wyrażenia"),
|
||||
("layer.expressions", "Disable Expressions", "Wyłącz wyrażenia"),
|
||||
("layer.setSwitch", "Shy", "Nieśmiała"),
|
||||
("layer.setSwitch", "Lock", "Zablokuj"),
|
||||
("layer.setSwitch", "Audio", "Audio"),
|
||||
("layer.setSwitch", "Video", "Wideo"),
|
||||
("layer.setSwitch", "Solo", "Solo"),
|
||||
("layer.setSwitch", "Effect", "Efekt"),
|
||||
("layer.setSwitch", "Collapse", "Zwiń"),
|
||||
("layer.setSwitch", "Motion Blur", "Rozmycie ruchu"),
|
||||
("layer.setSwitch", "Adjustment Layer", "Warstwa korekcyjna"),
|
||||
("", "Transform", "Przekształć"),
|
||||
("layer.transform", "Reset", "Resetuj"),
|
||||
("layer.setTransform", "Anchor Point...", "Punkt zakotwiczenia..."),
|
||||
("layer.setTransform", "Position...", "Położenie..."),
|
||||
("layer.setTransform", "Scale...", "Skala..."),
|
||||
("layer.setTransform", "Orientation...", "Orientacja..."),
|
||||
("layer.setTransform", "Rotation...", "Obrót..."),
|
||||
("layer.setTransform", "Opacity...", "Krycie..."),
|
||||
("layer.transform", "Flip Horizontal", "Odbij w poziomie"),
|
||||
("layer.transform", "Flip Vertical", "Odbij w pionie"),
|
||||
("layer.transform", "Center In View", "Wyśrodkuj w widoku"),
|
||||
("layer.centerAnchor", "Center Anchor Point in Layer Content", "Wyśrodkuj punkt zakotwiczenia w zawartości warstwy"),
|
||||
("layer.transform", "Fit to Comp", "Dopasuj do kompozycji"),
|
||||
("layer.transform", "Fit to Comp Width", "Dopasuj do szerokości kompozycji"),
|
||||
("layer.transform", "Fit to Comp Height", "Dopasuj do wysokości kompozycji"),
|
||||
("layer.autoOrient", "Auto-Orient...", "Automatyczna orientacja..."),
|
||||
("", "Time", "Czas"),
|
||||
("layer.enableTimeRemap", "Enable Time Remapping", "Włącz przemapowanie czasu"),
|
||||
("layer.timeReverse", "Time-Reverse Layer", "Odwróć czas warstwy"),
|
||||
("layer.timeStretch", "Time Stretch...", "Rozciągnięcie czasu..."),
|
||||
("layer.freezeFrame", "Freeze Frame", "Zamrożenie klatki"),
|
||||
("layer.freezeOnLastFrame", "Freeze On Last Frame", "Zamroź na ostatniej klatce"),
|
||||
("layer.alignVideoToData", "Align Video to Data", "Wyrównaj wideo do danych"),
|
||||
("", "Frame Blending", "Mieszanie klatek"),
|
||||
("layer.frameBlending", "Off", "Wył."),
|
||||
("layer.frameBlending", "Frame Mix", "Miks klatek"),
|
||||
("layer.frameBlending", "Pixel Motion", "Ruch pikseli"),
|
||||
("layer.setSwitch", "3D Layer", "Warstwa 3D"),
|
||||
("layer.setSwitch", "Guide Layer", "Warstwa pomocnicza"),
|
||||
("layer.environment", "Environment Layer", "Warstwa środowiska"),
|
||||
("", "Markers", "Znaczniki"),
|
||||
("layer.addMarker", "Add Marker", "Dodaj znacznik"),
|
||||
("layer.updateMarkersFromSource", "Update Markers From Source", "Aktualizuj znaczniki ze źródła"),
|
||||
("layer.markersLock", "Lock Markers", "Zablokuj znaczniki"),
|
||||
("layer.deleteAllMarkers", "Delete All Markers", "Usuń wszystkie znaczniki"),
|
||||
("layer.setSwitch", "Preserve Transparency", "Zachowaj przezroczystość"),
|
||||
("", "Blending Mode", "Tryb mieszania"),
|
||||
("layer.setBlendMode", "Normal", "Normalny"),
|
||||
("layer.setBlendMode", "Dissolve", "Rozpuszczenie"),
|
||||
("layer.setBlendMode", "Dancing Dissolve", "Tańczące rozpuszczenie"),
|
||||
("layer.setBlendMode", "Darken", "Ciemniej"),
|
||||
("layer.setBlendMode", "Multiply", "Mnożenie"),
|
||||
("layer.setBlendMode", "Color Burn", "Ściemnianie koloru"),
|
||||
("layer.setBlendMode", "Classic Color Burn", "Klasyczne ściemnianie koloru"),
|
||||
("layer.setBlendMode", "Linear Burn", "Ściemnianie liniowe"),
|
||||
("layer.setBlendMode", "Darker Color", "Ciemniejszy kolor"),
|
||||
("layer.setBlendMode", "Add", "Dodaj"),
|
||||
("layer.setBlendMode", "Lighten", "Jaśniej"),
|
||||
("layer.setBlendMode", "Screen", "Mnożenie odwrotne"),
|
||||
("layer.setBlendMode", "Color Dodge", "Rozjaśnianie koloru"),
|
||||
("layer.setBlendMode", "Classic Color Dodge", "Klasyczne rozjaśnianie koloru"),
|
||||
("layer.setBlendMode", "Linear Dodge", "Rozjaśnianie liniowe"),
|
||||
("layer.setBlendMode", "Lighter Color", "Jaśniejszy kolor"),
|
||||
("layer.setBlendMode", "Overlay", "Nakładka"),
|
||||
("layer.setBlendMode", "Soft Light", "Łagodne światło"),
|
||||
("layer.setBlendMode", "Hard Light", "Ostre światło"),
|
||||
("layer.setBlendMode", "Linear Light", "Światło liniowe"),
|
||||
("layer.setBlendMode", "Vivid Light", "Jaskrawe światło"),
|
||||
("layer.setBlendMode", "Pin Light", "Światło punktowe"),
|
||||
("layer.setBlendMode", "Hard Mix", "Twarde mieszanie"),
|
||||
("layer.setBlendMode", "Difference", "Różnica"),
|
||||
("layer.setBlendMode", "Classic Difference", "Klasyczna różnica"),
|
||||
("layer.setBlendMode", "Exclusion", "Wykluczenie"),
|
||||
("layer.setBlendMode", "Subtract", "Odejmowanie"),
|
||||
("layer.setBlendMode", "Divide", "Dzielenie"),
|
||||
("layer.setBlendMode", "Hue", "Barwa"),
|
||||
("layer.setBlendMode", "Saturation", "Nasycenie"),
|
||||
("layer.setBlendMode", "Color", "Kolor"),
|
||||
("layer.setBlendMode", "Luminosity", "Jasność"),
|
||||
("layer.setBlendMode", "Stencil Alpha", "Szablon alfa"),
|
||||
("layer.setBlendMode", "Stencil Luma", "Szablon luma"),
|
||||
("layer.setBlendMode", "Silhouette Alpha", "Sylweta alfa"),
|
||||
("layer.setBlendMode", "Silhouette Luma", "Sylweta luma"),
|
||||
("layer.setBlendMode", "Alpha Add", "Dodawanie alfa"),
|
||||
("layer.setBlendMode", "Luminescent Premul", "Luminescencja premul"),
|
||||
("layer.setBlendMode", "Next Blending Mode", "Następny tryb mieszania"),
|
||||
("layer.setBlendMode", "Previous Blending Mode", "Poprzedni tryb mieszania"),
|
||||
("", "Track Matte", "Matowa ścieżka"),
|
||||
("layer.trackMatte", "No Track Matte", "Bez matowej ścieżki"),
|
||||
("layer.trackMatte", "Alpha Matte", "Matowa alfa"),
|
||||
("layer.trackMatte", "Alpha Inverted Matte", "Odwrócona matowa alfa"),
|
||||
("layer.trackMatte", "Luma Matte", "Matowa luma"),
|
||||
("layer.trackMatte", "Luma Inverted Matte", "Odwrócona matowa luma"),
|
||||
("layer.trackMatte", "Matte with Layer Above", "Matowa z warstwą powyżej"),
|
||||
("layer.trackMatte", "Matte with Layer Below", "Matowa z warstwą poniżej"),
|
||||
("", "Layer Styles", "Style warstwy"),
|
||||
("layer.style.options", "Layer Style Options...", "Opcje stylu warstwy..."),
|
||||
("layer.style.convertToEditable", "Convert to Editable Styles", "Konwertuj na edytowalne style"),
|
||||
("layer.style.showAll", "Show All", "Pokaż wszystkie"),
|
||||
("layer.style.removeAll", "Remove All", "Usuń wszystkie"),
|
||||
("layer.style.dropShadow", "Drop Shadow", "Cień"),
|
||||
("layer.style.innerShadow", "Inner Shadow", "Cień wewnętrzny"),
|
||||
("layer.style.outerGlow", "Outer Glow", "Blask zewnętrzny"),
|
||||
("layer.style.innerGlow", "Inner Glow", "Blask wewnętrzny"),
|
||||
("layer.style.bevelEmboss", "Bevel and Emboss", "Faza i płaskorzeźba"),
|
||||
("layer.style.satin", "Satin", "Satyna"),
|
||||
("layer.style.colorOverlay", "Color Overlay", "Nakładka koloru"),
|
||||
("layer.style.gradientOverlay", "Gradient Overlay", "Nakładka gradientu"),
|
||||
("layer.style.stroke", "Stroke", "Obrys"),
|
||||
("path.groupShapes", "Group Shapes", "Grupuj kształty"),
|
||||
("path.ungroupShapes", "Ungroup Shapes", "Rozgrupuj kształty"),
|
||||
("", "Arrange", "Kolejność"),
|
||||
("layer.arrange", "Bring Layer to Front", "Przesuń warstwę na wierzch"),
|
||||
("layer.arrange", "Bring Layer Forward", "Przesuń warstwę do przodu"),
|
||||
("layer.arrange", "Send Layer Backward", "Przesuń warstwę do tyłu"),
|
||||
("layer.arrange", "Send Layer to Back", "Przesuń warstwę na spód"),
|
||||
("", "Reveal", "Pokaż"),
|
||||
("layer.revealSource", "Reveal Layer Source in Project", "Pokaż źródło warstwy w projekcie"),
|
||||
("comp.flowchart", "Reveal Layer in Project Flowchart", "Pokaż warstwę w schemacie projektu"),
|
||||
("comp.revealInProject", "Reveal Composition in Project", "Pokaż kompozycję w projekcie"),
|
||||
("layer.revealExpressionErrors", "Reveal Expression Errors", "Pokaż błędy wyrażeń"),
|
||||
("", "Create", "Utwórz"),
|
||||
("layer.create", "Convert to Editable Text", "Konwertuj na edytowalny tekst"),
|
||||
("layer.create", "Create Shapes from Text", "Utwórz kształty z tekstu"),
|
||||
("layer.create", "Create Masks from Text", "Utwórz maski z tekstu"),
|
||||
("layer.create", "Create Shapes from Vector Layer", "Utwórz kształty z warstwy wektorowej"),
|
||||
("layer.create", "Create Keyframes from Data", "Utwórz klatki kluczowe z danych"),
|
||||
("layer.create", "Null Controllers for Positional Points", "Kontrolery zerowe dla punktów położenia"),
|
||||
("layer.create", "Null Controllers for Path Points", "Kontrolery zerowe dla punktów ścieżki"),
|
||||
("layer.create", "Nulls Following Path Points", "Obiekty zerowe podążające za punktami ścieżki"),
|
||||
("layer.create", "Null Tracing Along Path", "Obiekt zerowy wzdłuż ścieżki"),
|
||||
("layer.create", "Create 3D Layer Instance", "Utwórz instancję warstwy 3D"),
|
||||
("", "Camera", "Kamera"),
|
||||
("camera.fromView", "Create Camera from 3D View", "Utwórz kamerę z widoku 3D"),
|
||||
("camera.stereoRig", "Create Stereo 3D Rig", "Utwórz zestaw stereo 3D"),
|
||||
("camera.orbitNull", "Create Orbit Null", "Utwórz obiekt zerowy orbity"),
|
||||
("camera.fromModel", "Create Cameras from 3D Model", "Utwórz kamery z modelu 3D"),
|
||||
("camera.linkFocusToPoi", "Link Focus Distance to Point of Interest", "Powiąż odległość ostrości z punktem zainteresowania"),
|
||||
("camera.linkFocusToLayer", "Link Focus Distance to Layer", "Powiąż odległość ostrości z warstwą"),
|
||||
("camera.setFocusToLayer", "Set Focus Distance to Layer", "Ustaw odległość ostrości na warstwę"),
|
||||
("layer.cameraSettings", "Camera Settings...", "Ustawienia kamery..."),
|
||||
("view.reset3DView", "Reset 3D View", "Resetuj widok 3D"),
|
||||
("", "Light", "Światło"),
|
||||
("light.fromModel", "Create Lights from 3D Model", "Utwórz światła z modelu 3D"),
|
||||
("light.controlWithCamera", "Control Light with Camera", "Steruj światłem za pomocą kamery"),
|
||||
("light.environmentBackground", "Create Environment Light Background Layer", "Utwórz warstwę tła światła otoczenia"),
|
||||
("", "Material", "Materiał"),
|
||||
("material.revealSource", "Reveal Material Source in Project", "Pokaż źródło materiału w projekcie"),
|
||||
("material.reset", "Reset Material", "Resetuj materiał"),
|
||||
("material.duplicateAssign", "Duplicate and Assign Material", "Powiel i przypisz materiał"),
|
||||
("layer.autoTrace", "Auto-trace...", "Automatyczne obrysowanie..."),
|
||||
("layer.precompose", "Pre-compose...", "Utwórz kompozycję wstępną..."),
|
||||
("layer.sceneEditDetection", "Scene Edit Detection...", "Wykrywanie zmian scen..."),
|
||||
("", "Effect", "Efekt"),
|
||||
("window.panel", "Effect Controls", "Ustawienia efektów"),
|
||||
("effect.applyLast", "Last Effect", "Ostatni efekt"),
|
||||
("effect.removeAll", "Remove All", "Usuń wszystkie"),
|
||||
("effect.manage", "Manage Effects...", "Zarządzaj efektami..."),
|
||||
("effect.plugins.load", "Load Effect Plug-in...", "Wczytaj wtyczkę efektu..."),
|
||||
("", "Animation", "Animacja"),
|
||||
("anim.savePreset", "Save Animation Preset...", "Zapisz ustawienie animacji..."),
|
||||
("anim.applyPreset", "Apply Animation Preset...", "Zastosuj ustawienie animacji..."),
|
||||
("", "Recent Animation Presets", "Ostatnie ustawienia animacji"),
|
||||
("anim.clearRecentPresets", "Clear Recent Presets", "Wyczyść ostatnie ustawienia"),
|
||||
("anim.browsePresets", "Browse Presets...", "Przeglądaj ustawienia..."),
|
||||
("", "Text Animation Presets", "Ustawienia animacji tekstu"),
|
||||
("layer.applyTextPreset", "Typewriter", "Maszyna do pisania"),
|
||||
("layer.applyTextPreset", "Fade Up Characters", "Pojawianie się znaków"),
|
||||
("layer.applyTextPreset", "Bounce In Words", "Odbijające się słowa"),
|
||||
("layer.applyTextPreset", "Tracking In", "Zbliżanie liter"),
|
||||
("layer.applyTextPreset", "Scramble", "Mieszanie znaków"),
|
||||
("layer.applyTextPreset", "Blur In", "Wyostrzanie z rozmycia"),
|
||||
("layer.applyTextPreset", "Jitter", "Drganie"),
|
||||
("layer.applyTextPreset", "Drop In Lines", "Wpadające wiersze"),
|
||||
("anim.addKeyframe", "Add Keyframe", "Dodaj klatkę kluczową"),
|
||||
("keys.toggleHold", "Toggle Hold Keyframe", "Przełącz klatkę zatrzymania"),
|
||||
("keys.interpolation", "Keyframe Interpolation...", "Interpolacja klatek kluczowych..."),
|
||||
("keys.velocity", "Keyframe Velocity...", "Prędkość klatek kluczowych..."),
|
||||
("", "Keyframe Assistant", "Asystent klatek kluczowych"),
|
||||
("keys.audioToKeyframes", "Convert Audio to Keyframes", "Konwertuj audio na klatki kluczowe"),
|
||||
("prop.convertExpressionToKeyframes", "Convert Expression to Keyframes", "Konwertuj wyrażenie na klatki kluczowe"),
|
||||
("keys.easyEase", "Easy Ease", "Łagodne przejście"),
|
||||
("keys.easyEaseIn", "Easy Ease In", "Łagodne wejście"),
|
||||
("keys.easyEaseOut", "Easy Ease Out", "Łagodne wyjście"),
|
||||
("keys.exponentialScale", "Exponential Scale", "Skalowanie wykładnicze"),
|
||||
("keys.rpfCameraImport", "RPF Camera Import", "Import kamery RPF"),
|
||||
("layer.sequence", "Sequence Layers...", "Ułóż warstwy w sekwencję..."),
|
||||
("keys.timeReverse", "Time-Reverse Keyframes", "Odwróć czas klatek kluczowych"),
|
||||
("", "Animate Text", "Animuj tekst"),
|
||||
("layer.enablePerChar3D", "Enable Per-character 3D", "Włącz 3D dla każdego znaku"),
|
||||
("layer.addTextAnimator", "Anchor Point", "Punkt zakotwiczenia"),
|
||||
("layer.addTextAnimator", "Position", "Położenie"),
|
||||
("layer.addTextAnimator", "Scale", "Skala"),
|
||||
("layer.addTextAnimator", "Skew", "Pochylenie"),
|
||||
("layer.addTextAnimator", "Rotation", "Obrót"),
|
||||
("layer.addTextAnimator", "Opacity", "Krycie"),
|
||||
("layer.addTextAnimator", "All Transform Properties", "Wszystkie właściwości przekształcenia"),
|
||||
("", "Fill Color", "Kolor wypełnienia"),
|
||||
("layer.addTextAnimator", "RGB", "RGB"),
|
||||
("layer.addTextAnimator", "Hue", "Barwa"),
|
||||
("layer.addTextAnimator", "Saturation", "Nasycenie"),
|
||||
("layer.addTextAnimator", "Brightness", "Jasność"),
|
||||
("", "Stroke Color", "Kolor obrysu"),
|
||||
("layer.addTextAnimator", "Stroke Width", "Szerokość obrysu"),
|
||||
("layer.addTextAnimator", "Tracking", "Światło"),
|
||||
("layer.addTextAnimator", "Line Anchor", "Zakotwiczenie wiersza"),
|
||||
("layer.addTextAnimator", "Line Spacing", "Odstęp między wierszami"),
|
||||
("layer.addTextAnimator", "Character Offset", "Przesunięcie znaku"),
|
||||
("layer.addTextAnimator", "Character Value", "Wartość znaku"),
|
||||
("layer.addTextAnimator", "Blur", "Rozmycie"),
|
||||
("text.animatorFontAxes", "Variable Font Axes", "Osie czcionki zmiennej"),
|
||||
("", "Add Text Selector", "Dodaj selektor tekstu"),
|
||||
("text.addSelector", "Range", "Zakres"),
|
||||
("text.addSelector", "Wiggly", "Falowanie"),
|
||||
("text.addSelector", "Expression", "Wyrażenie"),
|
||||
("text.removeAllAnimators", "Remove All Text Animators", "Usuń wszystkie animatory tekstu"),
|
||||
("prop.setExpression", "Add Expression", "Dodaj wyrażenie"),
|
||||
("essential.addProperty", "Add Property to Essential Graphics", "Dodaj właściwość do Essential Graphics"),
|
||||
("prop.separateDimensions", "Separate Dimensions", "Rozdziel wymiary"),
|
||||
("track.camera", "Track Camera", "Śledź kamerę"),
|
||||
("track.warpStabilizer", "Warp Stabilizer VFX", "Warp Stabilizer VFX"),
|
||||
("track.motion", "Track Motion", "Śledź ruch"),
|
||||
("track.stabilize", "Stabilize Motion", "Stabilizuj ruch"),
|
||||
("track.property", "Track this Property", "Śledź tę właściwość"),
|
||||
("anim.reveal", "Reveal Properties with Keyframes", "Pokaż właściwości z klatkami kluczowymi"),
|
||||
("anim.reveal", "Reveal Properties with Animation", "Pokaż właściwości z animacją"),
|
||||
("anim.reveal", "Reveal All Modified Properties", "Pokaż wszystkie zmienione właściwości"),
|
||||
("", "View", "Widok"),
|
||||
("view.newViewer", "New Viewer", "Nowa przeglądarka"),
|
||||
("view.splitLockedViewer", "Split with New Locked Viewer", "Podziel z nową zablokowaną przeglądarką"),
|
||||
("view.zoomIn", "Zoom In", "Powiększ"),
|
||||
("view.zoomOut", "Zoom Out", "Pomniejsz"),
|
||||
("", "Resolution", "Rozdzielczość"),
|
||||
("view.res.full", "Full", "Pełna"),
|
||||
("view.res.half", "Half", "Połowa"),
|
||||
("view.res.third", "Third", "Jedna trzecia"),
|
||||
("view.res.quarter", "Quarter", "Jedna czwarta"),
|
||||
("view.res.custom", "Custom...", "Niestandardowa..."),
|
||||
("view.displayColorManagement", "Use Display Color Management", "Używaj zarządzania kolorem wyświetlacza"),
|
||||
("", "Simulate Output", "Symuluj wyjście"),
|
||||
("view.simulateOutput", "No Output Simulation", "Bez symulacji wyjścia"),
|
||||
("view.simulateOutput", "HDTV (Rec. 709)", "HDTV (Rec. 709)"),
|
||||
("view.simulateOutput", "SDTV NTSC", "SDTV NTSC"),
|
||||
("view.simulateOutput", "SDTV PAL", "SDTV PAL"),
|
||||
("view.simulateOutput", "Legacy Macintosh RGB (Gamma 1.8)", "Starsze RGB komputerów Macintosh (gamma 1,8)"),
|
||||
("view.simulateOutput", "Internet Standard RGB (sRGB)", "Standardowe internetowe RGB (sRGB)"),
|
||||
("view.simulateOutput", "UHDTV (Rec. 2020)", "UHDTV (Rec. 2020)"),
|
||||
("view.simulateOutput", "Display P3", "Display P3"),
|
||||
("view.simulateOutput", "Linear (1.0 Gamma)", "Liniowe (gamma 1,0)"),
|
||||
("view.customRgb", "My Custom RGB...", "Moje niestandardowe RGB..."),
|
||||
("view.simulateOutput", "Custom...", "Niestandardowe..."),
|
||||
("view.rulers", "Show Rulers", "Pokaż linijki"),
|
||||
("", "Panel Background Color", "Kolor tła panelu"),
|
||||
("view.panelBackground", "Black", "Czarny"),
|
||||
("view.panelBackground", "Dark Gray", "Ciemnoszary"),
|
||||
("view.panelBackground", "Medium Gray (Default)", "Średnioszary (domyślny)"),
|
||||
("view.panelBackground", "Light Gray", "Jasnoszary"),
|
||||
("view.panelBackground", "White", "Biały"),
|
||||
("view.panelBackground", "Custom", "Niestandardowy"),
|
||||
("view.panelBackground", "Select Custom Background Color...", "Wybierz niestandardowy kolor tła..."),
|
||||
("view.guides", "Show Guides", "Pokaż linie pomocnicze"),
|
||||
("view.snapToGuides", "Snap to Guides", "Przyciągaj do linii pomocniczych"),
|
||||
("view.lockGuides", "Lock Guides", "Zablokuj linie pomocnicze"),
|
||||
("view.addGuide", "Add Guide...", "Dodaj linię pomocniczą..."),
|
||||
("view.addPercentGuide", "Add Percentage Guide...", "Dodaj procentową linię pomocniczą..."),
|
||||
("view.convertGuides", "Convert Guides to Percentage", "Konwertuj linie pomocnicze na procenty"),
|
||||
("view.convertGuides", "Convert Guides to Pixels", "Konwertuj linie pomocnicze na piksele"),
|
||||
("view.clearGuides", "Clear Guides", "Wyczyść linie pomocnicze"),
|
||||
("view.importGuides", "Import Guides...", "Importuj linie pomocnicze..."),
|
||||
("view.exportGuides", "Export Guides...", "Eksportuj linie pomocnicze..."),
|
||||
("view.grid", "Show Grid", "Pokaż siatkę"),
|
||||
("view.snapToGrid", "Snap to Grid", "Przyciągaj do siatki"),
|
||||
("view.snapping", "Snapping", "Przyciąganie"),
|
||||
("view.options", "View Options...", "Opcje widoku..."),
|
||||
("view.layerControls", "Show Layer Controls", "Pokaż elementy sterujące warstwy"),
|
||||
("", "Switch View Layout", "Przełącz układ widoków"),
|
||||
("view.layout", "1 View", "1 widok"),
|
||||
("view.layout", "2 Views", "2 widoki"),
|
||||
("view.layout", "4 Views", "4 widoki"),
|
||||
("view.shareViewOptions", "Share View Options", "Wspólne opcje widoku"),
|
||||
("", "Switch 3D View", "Przełącz widok 3D"),
|
||||
("view.3d.activeCamera", "Active Camera", "Aktywna kamera"),
|
||||
("view.3d.default", "Default", "Domyślny"),
|
||||
("view.3d.front", "Front", "Przód"),
|
||||
("view.3d.left", "Left", "Lewo"),
|
||||
("view.3d.top", "Top", "Góra"),
|
||||
("view.3d.back", "Back", "Tył"),
|
||||
("view.3d.right", "Right", "Prawo"),
|
||||
("view.3d.bottom", "Bottom", "Dół"),
|
||||
("view.3d.custom1", "Custom View 1", "Widok niestandardowy 1"),
|
||||
("view.3d.custom2", "Custom View 2", "Widok niestandardowy 2"),
|
||||
("view.3d.custom3", "Custom View 3", "Widok niestandardowy 3"),
|
||||
("", "Assign Shortcut to 3D View", "Przypisz skrót do widoku 3D"),
|
||||
("view.3d.last", "Switch to Last 3D View", "Przełącz na ostatni widok 3D"),
|
||||
("view.lookAtSelected", "Look at Selected Layers", "Patrz na zaznaczone warstwy"),
|
||||
("view.lookAtAll", "Look at All Layers", "Patrz na wszystkie warstwy"),
|
||||
("time.set", "Go to Time...", "Przejdź do czasu..."),
|
||||
("view.fullScreen", "Enter Full Screen", "Pełny ekran"),
|
||||
("", "Window", "Okno"),
|
||||
("", "Workspace", "Obszar roboczy"),
|
||||
("window.workspace", "Default", "Domyślny"),
|
||||
("window.workspace", "Review", "Przegląd"),
|
||||
("window.workspace", "Learn", "Nauka"),
|
||||
("window.workspace", "Small Screen", "Mały ekran"),
|
||||
("window.workspace", "Standard", "Standardowy"),
|
||||
("window.workspace", "All Panels", "Wszystkie panele"),
|
||||
("window.workspace", "Animation", "Animacja"),
|
||||
("window.workspace", "Color", "Kolor"),
|
||||
("window.workspace", "Effects", "Efekty"),
|
||||
("window.workspace", "Essential Graphics", "Essential Graphics"),
|
||||
("window.workspace", "Minimal", "Minimalny"),
|
||||
("window.workspace", "Motion Tracking", "Śledzenie ruchu"),
|
||||
("window.workspace", "Paint", "Malowanie"),
|
||||
("window.workspace", "Text", "Tekst"),
|
||||
("window.workspace", "Undocked Panels", "Panele odpięte"),
|
||||
("window.resetWorkspace", "Reset to Saved Layout", "Przywróć zapisany układ"),
|
||||
("window.saveWorkspace", "Save Changes to this Workspace", "Zapisz zmiany w tym obszarze roboczym"),
|
||||
("window.saveWorkspaceAs", "Save as New Workspace...", "Zapisz jako nowy obszar roboczy..."),
|
||||
("window.editWorkspaces", "Edit Workspaces...", "Edytuj obszary robocze..."),
|
||||
("", "Assign Shortcut to Workspace", "Przypisz skrót do obszaru roboczego"),
|
||||
("window.panel", "Align", "Wyrównanie"),
|
||||
("window.panel", "Audio", "Audio"),
|
||||
("window.panel", "Brushes", "Pędzle"),
|
||||
("window.panel", "Character", "Znak"),
|
||||
("window.panel", "Content-Aware Fill", "Wypełnienie z uwzględnieniem zawartości"),
|
||||
("window.panel", "Effects & Presets", "Efekty i ustawienia"),
|
||||
("window.panel", "Essential Graphics", "Essential Graphics"),
|
||||
("window.panel", "Info", "Informacje"),
|
||||
("help.inAppTutorials", "Learn", "Nauka"),
|
||||
("window.panel", "Lumetri Scopes", "Zakresy Lumetri"),
|
||||
("window.panel", "Mask Interpolation", "Interpolacja maski"),
|
||||
("window.panel", "Media Browser", "Przeglądarka multimediów"),
|
||||
("window.panel", "Metadata", "Metadane"),
|
||||
("window.panel", "Motion Sketch", "Szkic ruchu"),
|
||||
("window.panel", "Paint", "Malowanie"),
|
||||
("window.panel", "Paragraph", "Akapit"),
|
||||
("window.panel", "Preview", "Podgląd"),
|
||||
("window.panel", "Progress", "Postęp"),
|
||||
("window.panel", "Properties", "Właściwości"),
|
||||
("window.panel", "Script Console", "Konsola skryptów"),
|
||||
("window.panel", "Smoother", "Wygładzanie"),
|
||||
("window.panel", "Tools", "Narzędzia"),
|
||||
("window.panel", "Tracker", "Śledzenie"),
|
||||
("window.panel", "Wiggler", "Wahanie"),
|
||||
("window.panel", "Composition", "Kompozycja"),
|
||||
("window.panel", "Flowchart", "Schemat"),
|
||||
("window.panel", "Footage", "Materiał"),
|
||||
("window.panel", "Layer", "Warstwa"),
|
||||
("window.panel", "Project", "Projekt"),
|
||||
("window.panel", "Render Queue", "Kolejka renderowania"),
|
||||
("window.panel", "Timeline", "Oś czasu"),
|
||||
("window.panel", "Create Nulls From Paths", "Utwórz obiekty zerowe ze ścieżek"),
|
||||
("window.panel", "VR Comp Editor", "Edytor kompozycji VR"),
|
||||
("", "Help", "Pomoc"),
|
||||
("help.docs", "EffectCraft Help...", "Pomoc EffectCraft..."),
|
||||
("help.docs", "Scripting Help...", "Pomoc dotycząca skryptów..."),
|
||||
("help.docs", "Expression Reference...", "Dokumentacja wyrażeń..."),
|
||||
("help.docs", "Effect Reference...", "Dokumentacja efektów..."),
|
||||
("anim.browsePresets", "Animation Presets...", "Ustawienia animacji..."),
|
||||
("app.keyboardShortcuts", "Keyboard Shortcuts...", "Skróty klawiaturowe..."),
|
||||
("help.inAppTutorials", "In-App Tutorials...", "Samouczki w aplikacji..."),
|
||||
("help.onlineTutorials", "Online Tutorials...", "Samouczki online..."),
|
||||
("help.systemReport", "System Compatibility Report...", "Raport zgodności systemu..."),
|
||||
("help.enableLogging", "Enable Logging", "Włącz rejestrowanie"),
|
||||
("help.revealLogFile", "Reveal Logging File", "Pokaż plik dziennika"),
|
||||
("help.discord", "Join the ArtCraft Discord...", "Dołącz do Discorda ArtCraft..."),
|
||||
("help.reportIssue", "Provide Feedback...", "Prześlij opinię..."),
|
||||
("help.website", "ArtCraft Website", "Witryna ArtCraft"),
|
||||
("help.appPage", "EffectCraft Home Page", "Strona główna EffectCraft"),
|
||||
("help.github", "EffectCraft on GitHub", "EffectCraft w serwisie GitHub"),
|
||||
("file.openDemoProject", "Open Demo Project", "Otwórz projekt demonstracyjny"),
|
||||
("", "Recent Script Files", "Ostatnie pliki skryptów"),
|
||||
("layer.newPrimitive", "3D Primitive...", "Bryła podstawowa 3D..."),
|
||||
];
|
||||
|
||||
/// Czech (cs): original phrase translations of every fixed menu entry.
|
||||
const CZECH: &[(&str, &str, &str)] = &[
|
||||
("", "Front", "Přední strana"),
|
||||
@@ -6561,6 +7245,8 @@ const CZECH: &[(&str, &str, &str)] = &[
|
||||
("layer.applyTextPreset", "Drop In Lines", "Spustit řádky shora"),
|
||||
("anim.addKeyframe", "Add Keyframe", "Přidat klíčový snímek"),
|
||||
("keys.toggleHold", "Toggle Hold Keyframe", "Přepnout podržení klíčového snímku"),
|
||||
("keys.setMuted", "Mute Keyframes", "Ztlumit klíčové snímky"),
|
||||
("keys.setMuted", "Unmute Keyframes", "Obnovit klíčové snímky"),
|
||||
("keys.interpolation", "Keyframe Interpolation...", "Interpolace klíčových snímků..."),
|
||||
("keys.velocity", "Keyframe Velocity...", "Rychlost klíčového snímku..."),
|
||||
("", "Keyframe Assistant", "Pomocník klíčových snímků"),
|
||||
@@ -7192,6 +7878,8 @@ const GEORGIAN: &[(&str, &str, &str)] = &[
|
||||
("layer.applyTextPreset", "Drop In Lines", "სტრიქონების ჩამოვარდნა"),
|
||||
("anim.addKeyframe", "Add Keyframe", "საკვანძო კადრის დამატება"),
|
||||
("keys.toggleHold", "Toggle Hold Keyframe", "ფიქსირებული საკვანძო კადრის გადართვა"),
|
||||
("keys.setMuted", "Mute Keyframes", "საკვანძო კადრების დადუმება"),
|
||||
("keys.setMuted", "Unmute Keyframes", "საკვანძო კადრების დადუმების მოხსნა"),
|
||||
("keys.interpolation", "Keyframe Interpolation...", "საკვანძო კადრის ინტერპოლაცია..."),
|
||||
("keys.velocity", "Keyframe Velocity...", "საკვანძო კადრის სიჩქარე..."),
|
||||
("", "Keyframe Assistant", "საკვანძო კადრების ასისტენტი"),
|
||||
@@ -7512,6 +8200,19 @@ mod tests {
|
||||
println!("total: {total}");
|
||||
}
|
||||
|
||||
/// Prints the translated-row counts per language, the numbers `docs/localization-parity.md`
|
||||
/// quotes. `cargo xtask i18n-report` runs it with `--nocapture`.
|
||||
#[test]
|
||||
fn catalog_row_counts_report() {
|
||||
println!("Localization catalog rows:");
|
||||
for code in CATALOG_CODES {
|
||||
let menus = menu_catalog(code).map_or(0, |catalog| catalog.len());
|
||||
let panels = ui::translated_rows(code);
|
||||
let effect_names = effects::translated_rows(code);
|
||||
println!("{code}: menus {menus}, panels {panels}, effect names {effect_names}, total {}", menus + panels + effect_names);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn menu_catalogs_have_no_stale_or_duplicate_rows() {
|
||||
let mut actual = BTreeSet::new();
|
||||
@@ -7550,6 +8251,43 @@ mod tests {
|
||||
app.session.execute("prefs.set", json!({"key":"general.language", "value":"en"})).unwrap();
|
||||
assert_eq!(label(&app, "", "Composition"), "Composition");
|
||||
}
|
||||
/// Match System in the browser also picks the Russian, French and German catalogs, in the
|
||||
/// order of the browser's preferences (#1700).
|
||||
#[test]
|
||||
fn browser_preferences_choose_russian_french_and_german() {
|
||||
let tags = |t: &[&str]| t.iter().map(|s| s.to_string()).collect::<Vec<_>>();
|
||||
for (locales, expected) in
|
||||
[(vec!["de-DE", "es-ES"], "de"), (vec!["xx-XX", "fr-CA"], "fr"), (vec!["ru-RU", "cs"], "ru"), (vec!["en-US", "de-DE"], "en"), (vec!["xx-XX"], "en")]
|
||||
{
|
||||
assert_eq!(super::first_supported(None, &tags(&locales), super::web_draws), expected, "{locales:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The Edit menu's Undo and Redo name the history's action in the interface language, also in
|
||||
/// the languages added with #1700.
|
||||
#[test]
|
||||
fn undo_and_redo_are_localized_for_russian_french_and_german() {
|
||||
for (language, undo, no_undo, redo, no_redo) in [
|
||||
("ru", "Отменить:", "Нечего отменять", "Повторить:", "Нечего повторять"),
|
||||
("fr", "Annuler", "Impossible d'annuler", "Rétablir", "Impossible de rétablir"),
|
||||
("de", "Rückgängig", "Rückgängig nicht möglich", "Wiederholen", "Wiederholen nicht möglich"),
|
||||
] {
|
||||
let mut app = EffectcraftApp::new(effectcraft_engine::Session::default());
|
||||
app.session.execute("prefs.set", json!({"key": "general.language", "value": language})).unwrap();
|
||||
let entry_of = |app: &EffectcraftApp, command: &str| {
|
||||
let (_, e) = effectcraft_engine::menus::entries().into_iter().find(|(_, e)| e.command == command).unwrap();
|
||||
entry(app, e, e.label.clone())
|
||||
};
|
||||
assert_eq!(entry_of(&app, "edit.undo"), no_undo, "{language}");
|
||||
assert_eq!(entry_of(&app, "edit.redo"), no_redo, "{language}");
|
||||
app.session.execute("comp.new", json!({"name": "A", "duration": 1})).unwrap();
|
||||
let action = app.session.history.undo.last().map(|u| u.0.clone()).unwrap();
|
||||
assert_eq!(entry_of(&app, "edit.undo"), format!("{undo} {action}"), "{language}");
|
||||
app.session.execute("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(entry_of(&app, "edit.redo"), format!("{redo} {action}"), "{language}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_system_uses_the_system_language_where_there_is_a_catalog() {
|
||||
for (locale, language) in [
|
||||
@@ -7579,6 +8317,10 @@ mod tests {
|
||||
(Some("cs-CZ"), "cs"),
|
||||
(Some("cs_CZ.UTF-8"), "cs"),
|
||||
(Some("CS_cz.UTF-8"), "cs"),
|
||||
(Some("pl"), "pl"),
|
||||
(Some("pl-PL"), "pl"),
|
||||
(Some("pl_PL.UTF-8"), "pl"),
|
||||
(Some("PL_pl.UTF-8"), "pl"),
|
||||
(Some("ka"), "ka"),
|
||||
(Some("ka-GE"), "ka"),
|
||||
(Some("ka_GE.UTF-8"), "ka"),
|
||||
@@ -7606,6 +8348,7 @@ mod tests {
|
||||
assert_eq!(traditional(&app), system_language() == "zh-hant");
|
||||
assert_eq!(spanish(&app), system_language() == "es");
|
||||
assert_eq!(czech(&app), system_language() == "cs");
|
||||
assert_eq!(polish(&app), system_language() == "pl");
|
||||
app.session.execute("prefs.set", json!({"key":"general.language", "value":"ja"})).unwrap();
|
||||
assert!(japanese(&app));
|
||||
app.session.execute("prefs.set", json!({"key":"general.language", "value":"zh-hans"})).unwrap();
|
||||
@@ -7632,6 +8375,7 @@ mod tests {
|
||||
("es", SPANISH),
|
||||
("pt-br", PORTUGUESE),
|
||||
("cs", CZECH),
|
||||
("pl", POLISH),
|
||||
("ka", GEORGIAN),
|
||||
] {
|
||||
for (command, en, translated) in catalog {
|
||||
@@ -7643,6 +8387,35 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The dock's tab titles are `PanelKind::title()`; the panels the Window menu lists are drawn in
|
||||
/// the interface language through their `(window.panel, title)` rows. `History` and `ScriptUI
|
||||
/// Panel` are in no menu and keep their English names, which is what this pins.
|
||||
#[test]
|
||||
fn the_dock_draws_every_panel_it_can_in_the_interface_language() {
|
||||
use crate::dock::PanelKind;
|
||||
const WITHOUT_A_ROW: &[&str] = &["History", "ScriptUI Panel"];
|
||||
for (name, catalog, code) in [("ja", JAPANESE, "ja"), ("zh-hans", SIMPLIFIED_CHINESE, "zh-hans"), ("zh-hant", TRADITIONAL_CHINESE, "zh-hant")] {
|
||||
// The dock asks both catalogs, so a title may be named by either.
|
||||
ui::set_current(code);
|
||||
for p in PanelKind::ALL {
|
||||
let title = p.title();
|
||||
let in_menu = catalog.iter().any(|(c, en, _)| *c == "window.panel" && *en == title);
|
||||
let in_panel = ui::tr(title) != title;
|
||||
assert_eq!(
|
||||
in_menu || in_panel,
|
||||
!WITHOUT_A_ROW.contains(&title),
|
||||
"{name}: {title} is {}named by a catalog and {}in the exception list",
|
||||
if in_menu || in_panel { "" } else { "not " },
|
||||
if WITHOUT_A_ROW.contains(&title) { "" } else { "not " }
|
||||
);
|
||||
}
|
||||
}
|
||||
ui::set_current("en");
|
||||
// The dock asks the catalog rather than drawing the English name.
|
||||
let dock_ui = include_str!("dock_ui.rs");
|
||||
assert!(dock_ui.contains(r#"crate::i18n::command_label("window.panel", title)"#), "dock_ui.rs no longer asks the catalog for the panel titles");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ukrainian_catalog_matches_the_settings_registry() {
|
||||
let codes: Vec<_> = effectcraft_engine::prefs::LANGUAGES.iter().map(|(_, code)| *code).filter(|code| !matches!(*code, "system" | "en")).collect();
|
||||
@@ -7676,7 +8449,7 @@ mod tests {
|
||||
app.session.execute("prefs.set", json!({"key":"general.language", "value":"ru"})).unwrap();
|
||||
assert_eq!(entry(&app, &undo, "Can't Undo".into()), "Нечего отменять");
|
||||
app.session.execute("prefs.set", json!({"key":"general.language", "value":"de"})).unwrap();
|
||||
assert_eq!(entry(&app, &undo, "Can't Undo".into()), "Can't Undo", "no German undo wording yet: the shown label");
|
||||
assert_eq!(entry(&app, &undo, "Can't Undo".into()), "Rückgängig nicht möglich");
|
||||
app.session.execute("prefs.set", json!({"key":"general.language", "value":"en"})).unwrap();
|
||||
assert_eq!(entry(&app, &new_comp, new_comp.label.clone()), "New Composition...");
|
||||
}
|
||||
|
||||
@@ -87,6 +87,13 @@ pub(crate) fn name(id: &str, english: &str) -> String {
|
||||
catalog(current()).and_then(|c| c.row(id)).map_or_else(|| english.to_string(), |(_, translated)| translated.to_string())
|
||||
}
|
||||
|
||||
/// The number of effect-name rows `language` translates, for the row-count report; 0 when it has
|
||||
/// no catalog or when it is English.
|
||||
#[cfg(test)]
|
||||
pub(super) fn translated_rows(language: &str) -> usize {
|
||||
catalog(language).map_or(0, |c| c.rows.len())
|
||||
}
|
||||
|
||||
/// The name to draw for an effect group the property tree stores as `stored`: the catalog name,
|
||||
/// plus the instance index the engine appends to a repeat (`Gaussian Blur 2` → `高斯模糊 2`).
|
||||
///
|
||||
|
||||
+2030
-1
File diff suppressed because it is too large
Load Diff
@@ -179,7 +179,12 @@ fn menu_nodes(
|
||||
"recentScripts" => app.session.prefs.recent_scripts.get(i).map(|r| r.text().to_string()),
|
||||
_ => None,
|
||||
};
|
||||
let r = ui.add_enabled(entry_enabled(app, e), egui::Button::new((gutter(entry_checked(app, e) == Some(true)), e.label.as_str())));
|
||||
let mut b = egui::Button::new((gutter(entry_checked(app, e) == Some(true)), e.label.as_str()));
|
||||
// File ▸ Scripts entries show the key a script was given.
|
||||
if let Some(s) = entry_shortcut(app, e) {
|
||||
b = b.shortcut_text(shortcut_text(&s));
|
||||
}
|
||||
let r = ui.add_enabled(entry_enabled(app, e), b);
|
||||
let r = match &tip {
|
||||
Some(t) => r.on_hover_text(t),
|
||||
None => r,
|
||||
|
||||
@@ -210,6 +210,9 @@ fn escape_dialog(app: &mut EffectcraftApp, ctx: &egui::Context) {
|
||||
ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape));
|
||||
if app.dialog == Some(crate::Dialog::LayerStyles) {
|
||||
crate::panels::layer_styles_dialog::finish(app, false);
|
||||
} else if app.dialog == Some(crate::Dialog::GradientEditor) {
|
||||
// Escape is Cancel: the preview rolls back.
|
||||
crate::panels::gradient_editor::finish(app, false, ctx);
|
||||
} else if app.dialog == Some(crate::Dialog::Info) {
|
||||
crate::panels::forms::close_info(app);
|
||||
} else {
|
||||
@@ -222,10 +225,37 @@ fn escape_dialog(app: &mut EffectcraftApp, ctx: &egui::Context) {
|
||||
if app.dialog == Some(crate::Dialog::UnsavedChanges) {
|
||||
app.dialog_state.unsaved.command = None;
|
||||
}
|
||||
// Escape is Cancel: a Preview that edited the project rolls back.
|
||||
crate::panels::live_preview::cancel(app);
|
||||
app.dialog = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Escape with a dialog open: it is Cancel. Escape while recording a shortcut cancels the
|
||||
/// recording, not the editor; so does Escape in its Delete Preset prompt.
|
||||
fn cancel_dialog(app: &mut EffectcraftApp, ctx: &egui::Context) {
|
||||
if crate::panels::shortcut_editor::recording(app) || crate::panels::shortcut_editor::confirming(app) {
|
||||
return;
|
||||
}
|
||||
if app.dialog == Some(crate::Dialog::Info) {
|
||||
crate::panels::forms::close_info(app);
|
||||
return;
|
||||
}
|
||||
if app.dialog == Some(crate::Dialog::GradientEditor) {
|
||||
crate::panels::gradient_editor::finish(app, false, ctx);
|
||||
return;
|
||||
}
|
||||
// Escape in Settings is Cancel: restore the settings from when it opened.
|
||||
if app.dialog == Some(crate::Dialog::Settings)
|
||||
&& let Some(p) = app.dialog_state.prefs_snapshot.take()
|
||||
{
|
||||
app.session.prefs = p;
|
||||
app.session.prefs_changed();
|
||||
}
|
||||
crate::panels::live_preview::cancel(app);
|
||||
app.dialog = None;
|
||||
}
|
||||
|
||||
type KeyEvent = (egui::Key, egui::Modifiers, bool, Option<egui::Key>);
|
||||
|
||||
/// The frame's key presses, with clipboard events mapped back to Ctrl+C / X / V.
|
||||
@@ -341,23 +371,7 @@ fn handle_shortcuts_impl(app: &mut EffectcraftApp, ctx: &egui::Context, tab_only
|
||||
}
|
||||
// Escape closes dialogs.
|
||||
if key == egui::Key::Escape && app.dialog.is_some() {
|
||||
// Escape while recording a shortcut cancels the recording, not the editor; so does
|
||||
// Escape in its Delete Preset prompt.
|
||||
if crate::panels::shortcut_editor::recording(app) || crate::panels::shortcut_editor::confirming(app) {
|
||||
continue;
|
||||
}
|
||||
if app.dialog == Some(crate::Dialog::Info) {
|
||||
crate::panels::forms::close_info(app);
|
||||
continue;
|
||||
}
|
||||
// Escape in Settings is Cancel: restore the settings from when it opened.
|
||||
if app.dialog == Some(crate::Dialog::Settings)
|
||||
&& let Some(p) = app.dialog_state.prefs_snapshot.take()
|
||||
{
|
||||
app.session.prefs = p;
|
||||
app.session.prefs_changed();
|
||||
}
|
||||
app.dialog = None;
|
||||
cancel_dialog(app, ctx);
|
||||
continue;
|
||||
}
|
||||
if app.dialog.is_some() {
|
||||
|
||||
@@ -196,8 +196,9 @@ pub fn build(app: &EffectcraftApp) -> NativeMenu {
|
||||
label: e.label.clone(),
|
||||
command: e.command.clone(),
|
||||
params: e.params.clone(),
|
||||
shortcut: None,
|
||||
accelerator: None,
|
||||
// File ▸ Scripts entries show the key a script was given.
|
||||
shortcut: crate::menus::entry_shortcut(app, e),
|
||||
accelerator: crate::menus::entry_shortcut(app, e).as_deref().and_then(accelerator),
|
||||
enabled: crate::menus::entry_enabled(app, e),
|
||||
checked: crate::menus::entry_checked(app, e),
|
||||
}));
|
||||
@@ -243,6 +244,8 @@ pub fn build(app: &EffectcraftApp) -> NativeMenu {
|
||||
"Serviços"
|
||||
} else if crate::i18n::czech(app) {
|
||||
"Služby"
|
||||
} else if crate::i18n::polish(app) {
|
||||
"Usługi"
|
||||
} else if crate::i18n::georgian(app) {
|
||||
"სერვისები"
|
||||
} else {
|
||||
@@ -360,7 +363,12 @@ pub fn state_key(app: &EffectcraftApp) -> u64 {
|
||||
s.history.undo.iter().for_each(|(l, _)| l.hash(&mut h));
|
||||
s.state.open_comps.hash(&mut h);
|
||||
s.active_comp_id().and_then(|c| s.state.views3d.get(&c)).map(|v| format!("{:?}", v.current)).hash(&mut h);
|
||||
s.shortcuts().keys.len().hash(&mut h);
|
||||
// Each binding's keys: a key changed to another one gives the item a new accelerator.
|
||||
let shortcuts = s.shortcuts();
|
||||
for b in &shortcuts.bindables {
|
||||
b.key.hash(&mut h);
|
||||
shortcuts.keys.get(&b.key).hash(&mut h);
|
||||
}
|
||||
s.keymaps.active.hash(&mut h);
|
||||
let v = &app.ui.viewer;
|
||||
(v.rulers, v.guides, v.snap_guides, v.lock_guides, v.grid, v.snap_grid, v.show_layer_controls, v.pasteboard).hash(&mut h);
|
||||
@@ -578,6 +586,27 @@ mod tests {
|
||||
assert_eq!(translated, ["Služby", "Skrýt EffectCraft", "Skrýt ostatní", "Zobrazit vše"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polish_translates_macos_predefined_items() {
|
||||
if !cfg!(target_os = "macos") {
|
||||
return;
|
||||
}
|
||||
let mut app = EffectcraftApp::new(effectcraft_engine::Session::default());
|
||||
app.session.execute("prefs.set", json!({"key": "general.language", "value": "pl"})).unwrap();
|
||||
fn labels(nodes: &[NativeNode], out: &mut Vec<String>) {
|
||||
for node in nodes {
|
||||
match node {
|
||||
NativeNode::Predefined { label, .. } => out.push(label.clone()),
|
||||
NativeNode::Submenu { children, .. } => labels(children, out),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut translated = vec![];
|
||||
labels(&build(&app).menus, &mut translated);
|
||||
assert_eq!(translated, ["Usługi", "Ukryj EffectCraft", "Ukryj pozostałe", "Pokaż wszystko"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn georgian_translates_macos_predefined_items() {
|
||||
if !cfg!(target_os = "macos") {
|
||||
@@ -722,6 +751,24 @@ mod tests {
|
||||
assert!(spec2.items().iter().any(|i| i.command == "file.openRecent" && i.label == "Recent Test.ecproj"));
|
||||
}
|
||||
|
||||
/// A script's key changed to another key changes the key, so the File ▸ Scripts entry gets
|
||||
/// the new accelerator even though the number of bindings stays the same.
|
||||
#[test]
|
||||
fn a_changed_shortcut_changes_the_state_key() {
|
||||
let mut app = app();
|
||||
let script = json!({"name": "Rename Layers.jsx"});
|
||||
let b = app.session.shortcuts().find(&effectcraft_engine::shortcuts::binding_key("file.runScript", &script)).cloned().unwrap();
|
||||
app.session.keymaps.set(&b, &["Cmd+Alt+Shift+F7".to_string()]).unwrap();
|
||||
app.session.shortcuts_changed();
|
||||
let k0 = state_key(&app);
|
||||
app.session.keymaps.set(&b, &["Cmd+Alt+Shift+F8".to_string()]).unwrap();
|
||||
app.session.shortcuts_changed();
|
||||
assert_ne!(state_key(&app), k0);
|
||||
let spec = build(&app);
|
||||
let entry = spec.items().into_iter().find(|i| i.command == "file.runScript" && i.params == script).unwrap();
|
||||
assert_eq!(entry.shortcut.as_deref(), Some("Cmd+Alt+Shift+F8"));
|
||||
}
|
||||
|
||||
/// #1630: a checked Window menu entry closes its panel; an unchecked one opens it.
|
||||
#[test]
|
||||
fn window_menu_entry_toggles_its_panel() {
|
||||
|
||||
@@ -27,6 +27,7 @@ pub mod key_dialogs;
|
||||
pub mod layer_panel;
|
||||
pub mod layer_styles_dialog;
|
||||
pub mod learn;
|
||||
pub mod live_preview;
|
||||
pub mod markers_ui;
|
||||
pub mod media_panels;
|
||||
pub mod misc;
|
||||
@@ -35,6 +36,7 @@ pub mod paint_panels;
|
||||
pub mod panel_kit;
|
||||
pub mod path_vr_panels;
|
||||
pub mod precomp;
|
||||
pub(crate) mod preview_merge;
|
||||
pub mod primitive_dialog;
|
||||
pub mod project;
|
||||
pub mod properties;
|
||||
@@ -331,7 +333,7 @@ pub fn panel_menu_popup(app: &mut EffectcraftApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
ui.label(egui::RichText::new(panel.title()).weak());
|
||||
ui.label(egui::RichText::new(crate::i18n::label(app, "window.panel", panel.title())).weak());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -127,7 +127,7 @@ pub fn wiggler(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
app.ui.anim_tools = st.clone();
|
||||
let b = Rect::from_min_size(pos2(rect.max.x - 92.0, y), vec2(80.0, 24.0));
|
||||
let enabled = app.session.state.selected_keys.len() >= 2;
|
||||
if widgets::text_button(ui, b, "Apply", enabled, &t, egui::Id::unique("wg-apply")).clicked() {
|
||||
if widgets::text_button(ui, b, crate::i18n::tr("Apply"), enabled, &t, egui::Id::unique("wg-apply")).clicked() {
|
||||
let params = json!({
|
||||
"apply": if st.wiggle_spatial { "spatial" } else { "temporal" },
|
||||
"noise": if st.wiggle_smooth { "smooth" } else { "jagged" },
|
||||
@@ -175,7 +175,7 @@ pub fn smoother(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
y += ROW + 8.0;
|
||||
let b = Rect::from_min_size(pos2(rect.max.x - 92.0, y), vec2(80.0, 24.0));
|
||||
let enabled = app.session.state.selected_keys.len() >= 3;
|
||||
if widgets::text_button(ui, b, "Apply", enabled, &t, egui::Id::unique("sm-apply")).clicked() {
|
||||
if widgets::text_button(ui, b, crate::i18n::tr("Apply"), enabled, &t, egui::Id::unique("sm-apply")).clicked() {
|
||||
apply(app, "keys.smooth", json!({"tolerance": tol}));
|
||||
}
|
||||
app.auto.add("smoother.apply", b, "Apply");
|
||||
@@ -375,7 +375,7 @@ pub fn mask_interpolation(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rec
|
||||
changes.insert("keyframeRate".into(), json!(nr));
|
||||
}
|
||||
} else {
|
||||
p.text(pos2(xv + 78.0, y + 9.0), Align2::LEFT_CENTER, "per second", Tokens::ui(12.0), t.text_dim);
|
||||
p.text(pos2(xv + 78.0, y + 9.0), Align2::LEFT_CENTER, crate::i18n::tr("per second"), Tokens::ui(12.0), t.text_dim);
|
||||
}
|
||||
y += ROW;
|
||||
let mut check = |app: &mut EffectcraftApp, y: f32, key: &str, text: &str, on: bool| {
|
||||
@@ -454,7 +454,7 @@ pub fn mask_interpolation(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rec
|
||||
}
|
||||
let b = Rect::from_min_size(pos2(rect.max.x - 92.0, y), vec2(80.0, 24.0));
|
||||
let enabled = app.session.state.selected_keys.len() >= 2;
|
||||
if widgets::text_button(ui, b, "Apply", enabled, &t, egui::Id::unique("mi-apply")).clicked() {
|
||||
if widgets::text_button(ui, b, crate::i18n::tr("Apply"), enabled, &t, egui::Id::unique("mi-apply")).clicked() {
|
||||
match app.session.execute("mask.interpolate", json!({})) {
|
||||
Ok(r) => app.ui.status = format!("Mask Interpolation: {} keyframes added ({} vertices)", r["keys"], r["vertices"]),
|
||||
Err(e) => app.ui.status = e.to_string(),
|
||||
|
||||
@@ -413,12 +413,12 @@ pub fn editor(
|
||||
let b1 = Rect::from_min_size(pos2(r.min.x + 24.0, r.min.y + 4.0), vec2(84.0, 22.0));
|
||||
let b2 = Rect::from_min_size(pos2(b1.max.x + 8.0, b1.min.y), vec2(84.0, 22.0));
|
||||
let b3 = Rect::from_min_size(pos2(b2.max.x + 8.0, b1.min.y), vec2(110.0, 22.0));
|
||||
if widgets::text_button(ui, b1, "Analyze", false, &t, egui::Id::unique(("ct-analyze", g.uid))).clicked() && !busy {
|
||||
if widgets::text_button(ui, b1, crate::i18n::tr("Analyze"), false, &t, egui::Id::unique(("ct-analyze", g.uid))).clicked() && !busy {
|
||||
actions.push(("camera.analyze".into(), me.clone()));
|
||||
}
|
||||
app.auto.add(&format!("effectControls.cameraTracker.{}.analyze", g.uid), b1, "Analyze");
|
||||
if running {
|
||||
if widgets::text_button(ui, b2, "Cancel", false, &t, egui::Id::unique(("ct-cancel", g.uid))).clicked() {
|
||||
if widgets::text_button(ui, b2, crate::i18n::tr("Cancel"), false, &t, egui::Id::unique(("ct-cancel", g.uid))).clicked() {
|
||||
actions.push(("camera.cancel".into(), json!({})));
|
||||
}
|
||||
app.auto.add(&format!("effectControls.cameraTracker.{}.cancel", g.uid), b2, "Cancel");
|
||||
@@ -426,7 +426,7 @@ pub fn editor(
|
||||
let params = effectcraft_engine::camera_track::static_params(g);
|
||||
let solve = ct::solve(¶ms);
|
||||
if solve.is_some() {
|
||||
if widgets::text_button(ui, b3, "Create Camera", false, &t, egui::Id::unique(("ct-camera", g.uid))).clicked() {
|
||||
if widgets::text_button(ui, b3, crate::i18n::tr("Create Camera"), false, &t, egui::Id::unique(("ct-camera", g.uid))).clicked() {
|
||||
actions.push(("camera.create".into(), me.clone()));
|
||||
}
|
||||
app.auto.add(&format!("effectControls.cameraTracker.{}.createCamera", g.uid), b3, "Create Camera");
|
||||
|
||||
@@ -330,11 +330,11 @@ fn tab_body(
|
||||
let cur = PIXEL_ASPECTS
|
||||
.iter()
|
||||
.find(|(_, v)| (v - d.pixel_aspect).abs() < 1e-3)
|
||||
.map(|p| p.0.to_string())
|
||||
.map(|p| tr(p.0).to_string())
|
||||
.unwrap_or_else(|| format!("{:.2}", d.pixel_aspect));
|
||||
let r = craft_ui::ComboBox::from_id_salt("comp-par").width(220.0).selected_text(cur).show_ui(ui, |ui| {
|
||||
for (name, v) in PIXEL_ASPECTS {
|
||||
if ui.selectable_label((v - d.pixel_aspect).abs() < 1e-3, *name).clicked() {
|
||||
if ui.selectable_label((v - d.pixel_aspect).abs() < 1e-3, tr(name)).clicked() {
|
||||
d.pixel_aspect = *v;
|
||||
}
|
||||
}
|
||||
@@ -402,7 +402,7 @@ fn tab_body(
|
||||
ui.horizontal(|ui| {
|
||||
let r = crate::widgets::srgb_color_button(ui, &mut d.bg);
|
||||
auto.add("dialog.comp.background", r.rect, "Background Color");
|
||||
ui.label(color_name(d.bg));
|
||||
ui.label(tr(color_name(d.bg)));
|
||||
});
|
||||
ui.end_row();
|
||||
});
|
||||
@@ -487,6 +487,7 @@ pub fn show(
|
||||
auto: &mut Registry,
|
||||
custom: &[CompPreset],
|
||||
cmds: &mut Vec<(&'static str, serde_json::Value)>,
|
||||
preview: Option<&mut bool>,
|
||||
) -> Option<serde_json::Value> {
|
||||
let mut out = None;
|
||||
ui.horizontal(|ui| {
|
||||
@@ -498,7 +499,7 @@ pub fn show(
|
||||
ui.add_space(8.0);
|
||||
ui.horizontal(|ui| {
|
||||
for (tab, label, key) in [(Tab::Basic, "Basic", "basic"), (Tab::Advanced, "Advanced", "advanced"), (Tab::Renderer, "3D Renderer", "renderer")] {
|
||||
let r = ui.selectable_label(d.tab == tab, label);
|
||||
let r = ui.selectable_label(d.tab == tab, tr(label));
|
||||
auto.add(&format!("dialog.comp.tab.{key}"), r.rect, label);
|
||||
if r.clicked() {
|
||||
d.tab = tab;
|
||||
@@ -528,6 +529,9 @@ pub fn show(
|
||||
if r.clicked() {
|
||||
out = Some(serde_json::Value::Null);
|
||||
}
|
||||
if let Some(on) = preview {
|
||||
super::live_preview::checkbox(ui, auto, on, "dialog.comp.preview");
|
||||
}
|
||||
});
|
||||
});
|
||||
out
|
||||
|
||||
@@ -66,6 +66,8 @@ pub struct DialogState {
|
||||
pub delete_items: super::delete_items::Pending,
|
||||
/// The Gradient Editor's draft and the property it edits.
|
||||
pub gradient: super::gradient_editor::Draft,
|
||||
/// Preview of the settings dialog being edited (Solid, Camera, Light, Composition Settings).
|
||||
pub live: super::live_preview::LivePreview,
|
||||
/// The user template the Delete Template prompt asks about: its id and name.
|
||||
pub delete_template: Option<(String, String)>,
|
||||
}
|
||||
@@ -76,6 +78,7 @@ pub fn open_new_comp(app: &mut EffectcraftApp) {
|
||||
app.dialog_state.comp =
|
||||
CompDraft { name: format!("Comp {n}"), tab: super::comp_settings::Tab::Basic, start_tc: None, dur_tc: None, preset_name: None, ..last };
|
||||
app.dialog_state.editing_existing = false;
|
||||
app.dialog_state.live = Default::default();
|
||||
app.dialog = Some(Dialog::NewComp);
|
||||
}
|
||||
|
||||
@@ -109,6 +112,7 @@ pub fn open_comp_settings_for(app: &mut EffectcraftApp, cid: ItemId) -> Result<(
|
||||
app.dialog_state.editing_existing = true;
|
||||
app.dialog_state.editing_comp = Some(cid);
|
||||
app.dialog = Some(Dialog::CompSettings);
|
||||
super::live_preview::begin(app, Dialog::CompSettings);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -123,6 +127,7 @@ pub fn open_new_solid(app: &mut EffectcraftApp) -> Result<(), String> {
|
||||
if d.solid_color == [0.0; 3] {
|
||||
d.solid_color = [0.2, 0.45, 0.85];
|
||||
}
|
||||
d.live = Default::default();
|
||||
app.dialog = Some(Dialog::SolidSettings);
|
||||
Ok(())
|
||||
}
|
||||
@@ -138,6 +143,7 @@ fn open_solid_settings(app: &mut EffectcraftApp, layer: u64, item: ItemId) -> Re
|
||||
(d.solid_name, d.solid_color, d.solid_size, d.solid_pixel_aspect) = (name, so.color, [so.width, so.height], so.pixel_aspect);
|
||||
(d.solid_layer, d.solid_shared, d.solid_affect_all) = (Some(layer), users > 1, false);
|
||||
app.dialog = Some(Dialog::SolidSettings);
|
||||
super::live_preview::begin(app, Dialog::SolidSettings);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -190,6 +196,31 @@ pub(crate) fn modal_vertical(ctx: &egui::Context, title: &str, size: egui::Vec2,
|
||||
modal_with_scroll(ctx, title, size, t, true, body);
|
||||
}
|
||||
|
||||
/// Where a modal dialog opens and whether it dims the window behind it. The default is the middle
|
||||
/// of the window over a dimmed backdrop.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Placement {
|
||||
/// The opening position as a fraction of the room the dialog can move in, from the middle
|
||||
/// `(0, 0)` to the bottom right `(1, 1)`.
|
||||
pub start: egui::Vec2,
|
||||
/// Dim the window behind the dialog.
|
||||
pub dim: bool,
|
||||
}
|
||||
|
||||
impl Default for Placement {
|
||||
fn default() -> Self {
|
||||
Placement { start: vec2(0.0, 0.0), dim: true }
|
||||
}
|
||||
}
|
||||
|
||||
/// [`modal`] at `place`: a dialog that edits what is behind it keeps the canvas in view.
|
||||
pub(crate) fn modal_placed(ctx: &egui::Context, title: &str, size: egui::Vec2, t: &Tokens, place: Placement, body: impl FnOnce(&mut egui::Ui)) {
|
||||
let max_height = (modal_space(ctx).y - 84.0).max(40.0);
|
||||
modal_fit_placed(ctx, title, size, t, place, |ui, _| {
|
||||
egui::ScrollArea::both().max_height(max_height).auto_shrink([false, true]).show(ui, body);
|
||||
});
|
||||
}
|
||||
|
||||
fn modal_with_scroll(ctx: &egui::Context, title: &str, size: egui::Vec2, t: &Tokens, vertical_only: bool, body: impl FnOnce(&mut egui::Ui)) {
|
||||
let max_height = (modal_space(ctx).y - 84.0).max(40.0);
|
||||
modal_fit(ctx, title, size, t, |ui, _| {
|
||||
@@ -202,6 +233,11 @@ fn modal_with_scroll(ctx: &egui::Context, title: &str, size: egui::Vec2, t: &Tok
|
||||
/// only its content: `body` gets the height left under the title, with `size` kept within the
|
||||
/// window.
|
||||
pub(crate) fn modal_fit(ctx: &egui::Context, title: &str, size: egui::Vec2, t: &Tokens, body: impl FnOnce(&mut egui::Ui, f32)) {
|
||||
modal_fit_placed(ctx, title, size, t, Placement::default(), body);
|
||||
}
|
||||
|
||||
/// [`modal_fit`] at `place`.
|
||||
pub(crate) fn modal_fit_placed(ctx: &egui::Context, title: &str, size: egui::Vec2, t: &Tokens, place: Placement, body: impl FnOnce(&mut egui::Ui, f32)) {
|
||||
let pass = ctx.cumulative_pass_nr_for(egui::ViewportId::ROOT);
|
||||
ctx.data_mut(|d| d.insert_temp(modal_pass_id(), pass));
|
||||
let size = size.min(modal_space(ctx));
|
||||
@@ -210,10 +246,10 @@ pub(crate) fn modal_fit(ctx: &egui::Context, title: &str, size: egui::Vec2, t: &
|
||||
// window, so dragging back never has to undo a part the dialog could not follow.
|
||||
let offset_id = id.with("offset");
|
||||
let room = ((ctx.content_rect().size() - size) * 0.5).max(vec2(0.0, 0.0));
|
||||
let offset = ctx.data(|d| d.get_temp::<egui::Vec2>(offset_id)).unwrap_or_default().clamp(-room, room);
|
||||
let offset = ctx.data(|d| d.get_temp::<egui::Vec2>(offset_id)).unwrap_or(room * place.start).clamp(-room, room);
|
||||
egui::Modal::new(id)
|
||||
.area(egui::Modal::default_area(id).anchor(Align2::CENTER_CENTER, offset))
|
||||
.backdrop_color(Color32::from_black_alpha(120))
|
||||
.backdrop_color(Color32::from_black_alpha(if place.dim { 120 } else { 0 }))
|
||||
.frame(egui::Frame::window(&ctx.style_of(ctx.theme())).fill(t.panel_bg).inner_margin(egui::Margin::same(18)))
|
||||
.show(ctx, |ui| {
|
||||
ui.set_width(size.x - 36.0);
|
||||
@@ -403,9 +439,15 @@ fn comp_settings(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens, exis
|
||||
let mut result = None;
|
||||
let custom = app.session.prefs.comp_presets.clone();
|
||||
let mut cmds = Vec::new();
|
||||
modal(ctx, "Composition Settings", vec2(600.0, 470.0), t, |ui| {
|
||||
result = super::comp_settings::show(ui, &mut d, t, &mut app.auto, &custom, &mut cmds);
|
||||
// The title keys the modal id and its drag offset; `modal_fit` translates the heading itself,
|
||||
// so passing the source keeps the dialog in place when the interface language changes.
|
||||
let live = existing && super::live_preview::active(app);
|
||||
let place = super::live_preview::place(app);
|
||||
let mut preview = app.dialog_state.live.on;
|
||||
modal_placed(ctx, "Composition Settings", vec2(600.0, 470.0), t, place, |ui| {
|
||||
result = super::comp_settings::show(ui, &mut d, t, &mut app.auto, &custom, &mut cmds, live.then_some(&mut preview));
|
||||
});
|
||||
app.dialog_state.live.on = preview;
|
||||
if d.preset_name.is_some() {
|
||||
// Save Preset asks for the name in its own window over the dialog, as After Effects does.
|
||||
result = None;
|
||||
@@ -423,11 +465,25 @@ fn comp_settings(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens, exis
|
||||
}
|
||||
app.dialog_state.comp = d;
|
||||
match result {
|
||||
Some(serde_json::Value::Null) => app.dialog = None,
|
||||
Some(serde_json::Value::Null) => {
|
||||
super::live_preview::cancel(app);
|
||||
app.dialog = None;
|
||||
}
|
||||
Some(mut params) => {
|
||||
if existing && let (Some(cid), Some(m)) = (app.dialog_state.editing_comp, params.as_object_mut()) {
|
||||
m.insert("comp".into(), json!(cid.0));
|
||||
}
|
||||
// A preview already made the edit (one undo step): OK keeps it.
|
||||
if existing && super::live_preview::keep(app, "comp.settings", params.clone()) {
|
||||
// The Resolution is not part of the command, so the preview never applied it.
|
||||
if let Some(cid) = app.dialog_state.editing_comp
|
||||
&& let Err(e) = set_resolution(app, cid.0, app.dialog_state.comp.resolution)
|
||||
{
|
||||
app.ui.status = e.to_string();
|
||||
}
|
||||
app.dialog = None;
|
||||
return;
|
||||
}
|
||||
let r = if existing { app.session.execute("comp.settings", params) } else { app.session.execute("comp.new", params) };
|
||||
let cid = if existing { app.dialog_state.editing_comp.map(|c| c.0) } else { r.as_ref().ok().and_then(|v| v["comp"].as_u64()) };
|
||||
let r = match cid {
|
||||
@@ -442,7 +498,15 @@ fn comp_settings(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens, exis
|
||||
}
|
||||
app.dialog = None;
|
||||
}
|
||||
None => {}
|
||||
None => {
|
||||
if existing && let Some(cid) = app.dialog_state.editing_comp {
|
||||
let mut params = super::comp_settings::params(&app.dialog_state.comp);
|
||||
if let Some(m) = params.as_object_mut() {
|
||||
m.insert("comp".into(), json!(cid.0));
|
||||
}
|
||||
super::live_preview::sync(app, "comp.settings", params);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -499,8 +563,11 @@ fn solid(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
let comp_size = app.session.active_comp().map(|c| (c.width, c.height)).unwrap_or((1920, 1080));
|
||||
let lock_id = egui::Id::unique("solid-lock-aspect");
|
||||
let mut lock = ctx.data(|m| m.get_temp::<bool>(lock_id)).unwrap_or(true);
|
||||
let live = super::live_preview::active(app);
|
||||
let place = super::live_preview::place(app);
|
||||
let mut preview = app.dialog_state.live.on;
|
||||
let auto = &mut app.auto;
|
||||
modal(ctx, "Solid Settings", vec2(480.0, if editing.is_some() { 430.0 } else { 390.0 }), t, |ui| {
|
||||
modal_placed(ctx, "Solid Settings", vec2(480.0, if editing.is_some() { 430.0 } else { 390.0 }), t, place, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tr("Name:"));
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut name).desired_width(320.0));
|
||||
@@ -586,16 +653,28 @@ fn solid(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
let r = ui.button(tr("Cancel"));
|
||||
auto.add("dialog.solid.cancel", r.rect, "Cancel");
|
||||
close = r.clicked();
|
||||
if live {
|
||||
super::live_preview::checkbox(ui, auto, &mut preview, "dialog.solid.preview");
|
||||
}
|
||||
});
|
||||
});
|
||||
ctx.data_mut(|m| m.insert_temp(lock_id, lock));
|
||||
let ds = &mut app.dialog_state;
|
||||
ds.live.on = preview;
|
||||
(ds.solid_name, ds.solid_color, ds.solid_size, ds.solid_pixel_aspect, ds.solid_affect_all) = (name, color, size, par, affect_all);
|
||||
let (command, params) = (solid_command(ds), solid_params(ds));
|
||||
if ok || ui_enter(ctx) {
|
||||
if let Err(e) = app.session.execute(solid_command(ds), solid_params(ds)) {
|
||||
// A preview already made the edit (one undo step): OK keeps it.
|
||||
if !super::live_preview::keep(app, command, params.clone())
|
||||
&& let Err(e) = app.session.execute(command, params)
|
||||
{
|
||||
app.ui.status = e.to_string();
|
||||
}
|
||||
close = true;
|
||||
} else if close {
|
||||
super::live_preview::cancel(app);
|
||||
} else {
|
||||
super::live_preview::sync(app, command, params);
|
||||
}
|
||||
if close {
|
||||
app.dialog = None;
|
||||
|
||||
@@ -44,12 +44,19 @@ pub struct LightDraft {
|
||||
|
||||
const FALLOFFS: [&str; 3] = ["None", "Smooth", "Inverse Square Clamped"];
|
||||
|
||||
/// Whether a light type has Falloff and Shadows options. Ambient and Environment lights come
|
||||
/// from no one place, so they have neither.
|
||||
/// Whether a light type has Falloff options (distance attenuation): image-based Environment
|
||||
/// lights and Ambient come from no one place, so they have none.
|
||||
fn falls_off(kind: LightKind) -> bool {
|
||||
!matches!(kind, LightKind::Ambient | LightKind::Environment)
|
||||
}
|
||||
|
||||
/// Whether a light type has Shadows options in the dialog. After Effects' Light Settings shows
|
||||
/// Cast Shadows (with Shadow Darkness and Diffusion) for every light but Ambient — an
|
||||
/// Environment light's shadow comes from its image, not a falloff.
|
||||
fn casts_shadows(kind: LightKind) -> bool {
|
||||
kind != LightKind::Ambient
|
||||
}
|
||||
|
||||
fn comp_w(app: &EffectcraftApp) -> f64 {
|
||||
app.session.active_comp().map(|c| c.width as f64).unwrap_or(1920.0)
|
||||
}
|
||||
@@ -92,8 +99,14 @@ pub fn open_camera(app: &mut EffectcraftApp, layer: Option<u64>) -> Result<(), S
|
||||
}
|
||||
}
|
||||
};
|
||||
let editing = d.layer.is_some();
|
||||
app.dialog_state.camera = d;
|
||||
app.dialog = Some(Dialog::CameraSettings);
|
||||
if editing {
|
||||
super::live_preview::begin(app, Dialog::CameraSettings);
|
||||
} else {
|
||||
app.dialog_state.live = Default::default();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -142,13 +155,19 @@ pub fn open_light(app: &mut EffectcraftApp, layer: Option<u64>) -> Result<(), St
|
||||
}
|
||||
}
|
||||
};
|
||||
let editing = d.layer.is_some();
|
||||
app.dialog_state.light = d;
|
||||
app.dialog = Some(Dialog::LightSettings);
|
||||
if editing {
|
||||
super::live_preview::begin(app, Dialog::LightSettings);
|
||||
} else {
|
||||
app.dialog_state.live = Default::default();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn buttons(ui: &mut egui::Ui, t: &Tokens, ok: &mut bool, close: &mut bool) -> (egui::Rect, egui::Rect) {
|
||||
let mut r = (egui::Rect::NOTHING, egui::Rect::NOTHING);
|
||||
fn buttons(ui: &mut egui::Ui, t: &Tokens, ok: &mut bool, close: &mut bool, preview: Option<&mut bool>) -> (egui::Rect, egui::Rect, egui::Rect) {
|
||||
let mut r = (egui::Rect::NOTHING, egui::Rect::NOTHING, egui::Rect::NOTHING);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let b = ui.add(egui::Button::new(egui::RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent));
|
||||
r.0 = b.rect;
|
||||
@@ -160,6 +179,9 @@ fn buttons(ui: &mut egui::Ui, t: &Tokens, ok: &mut bool, close: &mut bool) -> (e
|
||||
if c.clicked() {
|
||||
*close = true;
|
||||
}
|
||||
if let Some(on) = preview {
|
||||
r.2 = ui.checkbox(on, tr("Preview")).rect;
|
||||
}
|
||||
});
|
||||
r
|
||||
}
|
||||
@@ -168,8 +190,11 @@ pub fn camera(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
let w = comp_w(app);
|
||||
let mut d = app.dialog_state.camera.clone();
|
||||
let (mut ok, mut close) = (false, false);
|
||||
let mut rects = (egui::Rect::NOTHING, egui::Rect::NOTHING);
|
||||
super::dialogs::modal(ctx, "Camera Settings", vec2(520.0, 420.0), t, |ui| {
|
||||
let mut rects = (egui::Rect::NOTHING, egui::Rect::NOTHING, egui::Rect::NOTHING);
|
||||
let live = super::live_preview::active(app);
|
||||
let place = super::live_preview::place(app);
|
||||
let mut preview = app.dialog_state.live.on;
|
||||
super::dialogs::modal_placed(ctx, "Camera Settings", vec2(520.0, 420.0), t, place, |ui| {
|
||||
egui::Grid::new("cam-grid").num_columns(2).spacing([14.0, 9.0]).show(ui, |ui| {
|
||||
ui.label(tr("Type"));
|
||||
craft_ui::ComboBox::from_id_salt("cam-type").selected_text(if d.two_node { "Two-Node Camera" } else { "One-Node Camera" }).show_ui(ui, |ui| {
|
||||
@@ -222,46 +247,66 @@ pub fn camera(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
ui.end_row();
|
||||
});
|
||||
ui.add_space(14.0);
|
||||
rects = buttons(ui, t, &mut ok, &mut close);
|
||||
rects = buttons(ui, t, &mut ok, &mut close, live.then_some(&mut preview));
|
||||
});
|
||||
app.dialog_state.live.on = preview;
|
||||
if d.lock_to_zoom {
|
||||
d.focus = d.zoom;
|
||||
}
|
||||
app.auto.add("dialog.camera.ok", rects.0, "OK");
|
||||
app.auto.add("dialog.camera.cancel", rects.1, "Cancel");
|
||||
if live {
|
||||
app.auto.add("dialog.camera.preview", rects.2, "Preview");
|
||||
}
|
||||
app.dialog_state.camera = d.clone();
|
||||
let (id, p) = camera_command(&d);
|
||||
if ok || super::dialogs::ui_enter(ctx) {
|
||||
let mut p = json!({
|
||||
"name": d.name,
|
||||
"type": if d.two_node { "twoNode" } else { "oneNode" },
|
||||
"zoom": d.zoom,
|
||||
"dof": d.dof,
|
||||
"focusDistance": d.focus,
|
||||
"aperture": d.aperture,
|
||||
"blurLevel": d.blur,
|
||||
});
|
||||
let id = match d.layer {
|
||||
Some(l) => {
|
||||
p["layer"] = json!(l);
|
||||
"layer.cameraSettings"
|
||||
}
|
||||
None => "layer.newCamera",
|
||||
};
|
||||
if let Err(e) = app.session.execute(id, p) {
|
||||
// A preview already made the edit (one undo step): OK keeps it.
|
||||
if !super::live_preview::keep(app, id, p.clone())
|
||||
&& let Err(e) = app.session.execute(id, p)
|
||||
{
|
||||
app.ui.status = e.to_string();
|
||||
}
|
||||
close = true;
|
||||
} else if close {
|
||||
super::live_preview::cancel(app);
|
||||
} else {
|
||||
super::live_preview::sync(app, id, p);
|
||||
}
|
||||
if close {
|
||||
app.dialog = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// The command Camera Settings' OK runs, and its parameters.
|
||||
fn camera_command(d: &CameraDraft) -> (&'static str, Value) {
|
||||
let mut p = json!({
|
||||
"name": d.name,
|
||||
"type": if d.two_node { "twoNode" } else { "oneNode" },
|
||||
"zoom": d.zoom,
|
||||
"dof": d.dof,
|
||||
"focusDistance": d.focus,
|
||||
"aperture": d.aperture,
|
||||
"blurLevel": d.blur,
|
||||
});
|
||||
let id = match d.layer {
|
||||
Some(l) => {
|
||||
p["layer"] = json!(l);
|
||||
"layer.cameraSettings"
|
||||
}
|
||||
None => "layer.newCamera",
|
||||
};
|
||||
(id, p)
|
||||
}
|
||||
|
||||
pub fn light(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
let mut d = app.dialog_state.light.clone();
|
||||
let (mut ok, mut close) = (false, false);
|
||||
let mut rects = (egui::Rect::NOTHING, egui::Rect::NOTHING);
|
||||
super::dialogs::modal(ctx, "Light Settings", vec2(480.0, 470.0), t, |ui| {
|
||||
let mut rects = (egui::Rect::NOTHING, egui::Rect::NOTHING, egui::Rect::NOTHING);
|
||||
let live = super::live_preview::active(app);
|
||||
let place = super::live_preview::place(app);
|
||||
let mut preview = app.dialog_state.live.on;
|
||||
super::dialogs::modal_placed(ctx, "Light Settings", vec2(480.0, 470.0), t, place, |ui| {
|
||||
egui::Grid::new("light-grid").num_columns(2).spacing([14.0, 9.0]).show(ui, |ui| {
|
||||
ui.label(tr("Name"));
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut d.name).desired_width(220.0));
|
||||
@@ -306,8 +351,11 @@ pub fn light(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut d.falloff_distance).range(0.0..=100000.0).suffix(" px")));
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
if casts_shadows(d.kind) {
|
||||
ui.label(tr("Shadows"));
|
||||
ui.checkbox(&mut d.casts_shadows, tr("Casts Shadows"));
|
||||
let cb = ui.checkbox(&mut d.casts_shadows, tr("Casts Shadows"));
|
||||
app.auto.add_response("dialog.light.casts", &cb, "Casts Shadows");
|
||||
ui.end_row();
|
||||
ui.label(tr("Shadow Darkness"));
|
||||
ui.add_enabled(d.casts_shadows, crate::widgets::drag_value(egui::DragValue::new(&mut d.shadow_darkness).range(0.0..=100.0).suffix(" %")));
|
||||
@@ -318,52 +366,71 @@ pub fn light(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
}
|
||||
});
|
||||
ui.add_space(14.0);
|
||||
if falls_off(d.kind) {
|
||||
if casts_shadows(d.kind) {
|
||||
ui.label(egui::RichText::new(tr("Note: Shadows are only cast from layers with Cast Shadows enabled.")).font(Tokens::ui(11.0)).color(t.text_dim));
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
rects = buttons(ui, t, &mut ok, &mut close);
|
||||
rects = buttons(ui, t, &mut ok, &mut close, live.then_some(&mut preview));
|
||||
});
|
||||
app.dialog_state.live.on = preview;
|
||||
app.auto.add("dialog.light.ok", rects.0, "OK");
|
||||
app.auto.add("dialog.light.cancel", rects.1, "Cancel");
|
||||
if live {
|
||||
app.auto.add("dialog.light.preview", rects.2, "Preview");
|
||||
}
|
||||
app.dialog_state.light = d.clone();
|
||||
let (id, p) = light_command(&d);
|
||||
if ok || super::dialogs::ui_enter(ctx) {
|
||||
let kind = d.kind;
|
||||
let mut p = json!({
|
||||
"name": d.name,
|
||||
"kind": kind.label(),
|
||||
"color": [d.color[0], d.color[1], d.color[2]],
|
||||
"intensity": d.intensity,
|
||||
});
|
||||
if kind == LightKind::Spot {
|
||||
p["coneAngle"] = json!(d.cone_angle);
|
||||
p["coneFeather"] = json!(d.cone_feather);
|
||||
}
|
||||
if falls_off(kind) {
|
||||
p["falloff"] = json!(d.falloff);
|
||||
p["radius"] = json!(d.radius);
|
||||
p["falloffDistance"] = json!(d.falloff_distance);
|
||||
p["castsShadows"] = json!(d.casts_shadows);
|
||||
p["shadowDarkness"] = json!(d.shadow_darkness);
|
||||
p["shadowDiffusion"] = json!(d.shadow_diffusion);
|
||||
}
|
||||
let id = match d.layer {
|
||||
Some(l) => {
|
||||
p["layer"] = json!(l);
|
||||
"layer.lightSettings"
|
||||
}
|
||||
None => "layer.newLight",
|
||||
};
|
||||
if let Err(e) = app.session.execute(id, p) {
|
||||
// A preview already made the edit (one undo step): OK keeps it.
|
||||
if !super::live_preview::keep(app, id, p.clone())
|
||||
&& let Err(e) = app.session.execute(id, p)
|
||||
{
|
||||
app.ui.status = e.to_string();
|
||||
}
|
||||
close = true;
|
||||
} else if close {
|
||||
super::live_preview::cancel(app);
|
||||
} else {
|
||||
super::live_preview::sync(app, id, p);
|
||||
}
|
||||
if close {
|
||||
app.dialog = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// The command Light Settings' OK runs, and its parameters.
|
||||
fn light_command(d: &LightDraft) -> (&'static str, Value) {
|
||||
let kind = d.kind;
|
||||
let mut p = json!({
|
||||
"name": d.name,
|
||||
"kind": kind.label(),
|
||||
"color": [d.color[0], d.color[1], d.color[2]],
|
||||
"intensity": d.intensity,
|
||||
});
|
||||
if kind == LightKind::Spot {
|
||||
p["coneAngle"] = json!(d.cone_angle);
|
||||
p["coneFeather"] = json!(d.cone_feather);
|
||||
}
|
||||
if falls_off(kind) {
|
||||
p["falloff"] = json!(d.falloff);
|
||||
p["radius"] = json!(d.radius);
|
||||
p["falloffDistance"] = json!(d.falloff_distance);
|
||||
}
|
||||
if casts_shadows(kind) {
|
||||
p["castsShadows"] = json!(d.casts_shadows);
|
||||
p["shadowDarkness"] = json!(d.shadow_darkness);
|
||||
p["shadowDiffusion"] = json!(d.shadow_diffusion);
|
||||
}
|
||||
let id = match d.layer {
|
||||
Some(l) => {
|
||||
p["layer"] = json!(l);
|
||||
"layer.lightSettings"
|
||||
}
|
||||
None => "layer.newLight",
|
||||
};
|
||||
(id, p)
|
||||
}
|
||||
|
||||
/// Menu/shortcut routing: commands that open a 3D settings dialog when invoked without params.
|
||||
/// Returns true when a dialog was opened.
|
||||
pub fn route(app: &mut EffectcraftApp, id: &str, params: &Value) -> Result<bool, String> {
|
||||
|
||||
@@ -8,12 +8,12 @@ pub(super) fn warp_editor(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui:
|
||||
let busy = app.session.is_warp_analyzing();
|
||||
let b1 = Rect::from_min_size(pos2(r.min.x + 24.0, r.min.y + 4.0), vec2(84.0, 22.0));
|
||||
let b2 = Rect::from_min_size(pos2(b1.max.x + 8.0, b1.min.y), vec2(84.0, 22.0));
|
||||
if widgets::text_button(ui, b1, "Analyze", false, &t, egui::Id::unique(("warp-analyze", g.uid))).clicked() && !busy {
|
||||
if widgets::text_button(ui, b1, crate::i18n::tr("Analyze"), false, &t, egui::Id::unique(("warp-analyze", g.uid))).clicked() && !busy {
|
||||
actions.push(("warp.analyze".into(), json!({"layer": layer.id.0, "effect": g.uid})));
|
||||
}
|
||||
app.auto.add(&format!("effectControls.warp.{}.analyze", g.uid), b1, "Analyze");
|
||||
if running {
|
||||
if widgets::text_button(ui, b2, "Cancel", false, &t, egui::Id::unique(("warp-cancel", g.uid))).clicked() {
|
||||
if widgets::text_button(ui, b2, crate::i18n::tr("Cancel"), false, &t, egui::Id::unique(("warp-cancel", g.uid))).clicked() {
|
||||
actions.push(("warp.cancel".into(), json!({})));
|
||||
}
|
||||
app.auto.add(&format!("effectControls.warp.{}.cancel", g.uid), b2, "Cancel");
|
||||
|
||||
@@ -192,7 +192,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
bp.text(
|
||||
pos2(body.center().x, body.min.y + 40.0),
|
||||
Align2::CENTER_CENTER,
|
||||
"No effects. Drag one from Effects & Presets.",
|
||||
crate::i18n::tr("No effects. Drag one from Effects & Presets."),
|
||||
Tokens::ui(12.0),
|
||||
t.text_faint,
|
||||
);
|
||||
|
||||
@@ -376,7 +376,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
}
|
||||
let sr = Rect::from_min_size(rect.min + vec2(8.0, 8.0), vec2(rect.width() - 44.0, 24.0));
|
||||
let mut q = app.ui.effects_search.clone();
|
||||
widgets::search_field(ui, sr, &mut q, "Search effects and presets", &t);
|
||||
widgets::search_field(ui, sr, &mut q, crate::i18n::tr("Search effects and presets"), &t);
|
||||
app.ui.effects_search = q.clone();
|
||||
app.auto.add("effects.search", sr, "Search");
|
||||
let force_scan = contents_menu(app, ui, &ctx, rect, sr, &t);
|
||||
|
||||
@@ -250,7 +250,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let p = ui.painter().with_clip_rect(rect);
|
||||
p.rect_filled(rect, 0.0, t.tl_bg);
|
||||
let Some(rid) = root(app) else {
|
||||
p.text(rect.center(), Align2::CENTER_CENTER, "(no composition)", Tokens::ui(12.0), t.text_faint);
|
||||
p.text(rect.center(), Align2::CENTER_CENTER, crate::i18n::tr("(no composition)"), Tokens::ui(12.0), t.text_faint);
|
||||
return;
|
||||
};
|
||||
// ---- toolbar: switches and direction.
|
||||
|
||||
@@ -68,7 +68,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let cached: Option<((u64, i64, u64), egui::TextureHandle, [u32; 2])> = ctx.data(|d| d.get_temp(tid));
|
||||
let tex = match cached {
|
||||
Some((k, tex, size)) if k == key => Some((tex, size)),
|
||||
_ => app.session.footage.frame(view.item, f, view.time).map(|img| {
|
||||
_ => effectcraft_engine::commands::footage_panel::frame(&app.session, view.item, f, view.time).map(|img| {
|
||||
let ci = egui::ColorImage::from_rgba_unmultiplied([img.width as usize, img.height as usize], &img.to_rgba8());
|
||||
let tex = crate::frames::load_fitted(&ctx, "footage-panel", ci, egui::TextureOptions::LINEAR);
|
||||
ctx.data_mut(|d| d.insert_temp(tid, (key, tex.clone(), [img.width, img.height])));
|
||||
|
||||
@@ -233,7 +233,7 @@ fn new_comp_from_selection_form(s: &effectcraft_engine::Session) -> (String, Vec
|
||||
}
|
||||
|
||||
/// The Interpret Footage dialog for the selected footage (or its proxy), if there is one.
|
||||
fn interpret_footage_form(s: &effectcraft_engine::Session, proxy: bool) -> Option<(String, Vec<Field>)> {
|
||||
fn interpret_footage_form(s: &effectcraft_engine::Session, proxy: bool, base: &mut Value) -> Option<(String, Vec<Field>)> {
|
||||
let f = s.state.project_selection.first().and_then(|i| s.project.item(*i)).and_then(|it| match (&it.kind, &it.proxy) {
|
||||
(_, Some(px)) if proxy => Some(px.footage.clone()),
|
||||
(effectcraft_engine::project::ItemKind::Footage(f), _) if !proxy => Some(f.clone()),
|
||||
@@ -286,6 +286,30 @@ fn interpret_footage_form(s: &effectcraft_engine::Session, proxy: bool) -> Optio
|
||||
Field::bool("preserveRgb", "Preserve RGB", f.preserve_rgb),
|
||||
Field::bool("linearLight", "Interpret As Linear Light", f.linear_light),
|
||||
];
|
||||
// Color ▸ Cineon Settings (DPX and Cineon files). The converted points show in the
|
||||
// footage's Units, and `pointUnits` reads them back in those units when Units changes.
|
||||
if f.is_cineon_file() {
|
||||
use effectcraft_engine::project::CineonUnits;
|
||||
let c = f.cineon;
|
||||
let units: Vec<(&str, Value)> = CineonUnits::ALL.iter().map(|u| (u.label(), json!(u.id()))).collect();
|
||||
fields.extend([
|
||||
Field::note("cineonSettings", "Cineon Settings"),
|
||||
Field::bool("logConversion", "Logarithmic Conversion", c.log_conversion),
|
||||
Field::num("convertedBlack", "Converted Black Point", c.units.from_ten_bit(f64::from(c.converted_black))),
|
||||
Field::num("convertedWhite", "Converted White Point", c.units.from_ten_bit(f64::from(c.converted_white))),
|
||||
Field::num("tenBitBlack", "10 Bit Black Point", f64::from(c.black)),
|
||||
Field::num("tenBitWhite", "10 Bit White Point", f64::from(c.white)),
|
||||
Field::num("currentGamma", "Current Gamma", f64::from(c.gamma)),
|
||||
Field::num("highlightRolloff", "Highlight Rolloff", f64::from(c.rolloff)),
|
||||
Field::choice("cineonUnits", "Units", &units, CineonUnits::ALL.iter().position(|u| *u == c.units).unwrap_or(0)),
|
||||
]);
|
||||
if !base.is_object() {
|
||||
*base = json!({});
|
||||
}
|
||||
if let Some(m) = base.as_object_mut() {
|
||||
m.insert("pointUnits".into(), json!(c.units.id()));
|
||||
}
|
||||
}
|
||||
// After Frame Rate: an image sequence's frames (missing ones show colour bars, as in
|
||||
// After Effects), then Start Timecode.
|
||||
let mut timing = Vec::new();
|
||||
@@ -487,7 +511,7 @@ pub fn open_form(app: &mut EffectcraftApp, id: &str, p: &Value) -> bool {
|
||||
let layer = comp.and_then(|c| s.state.selected_layers.first().and_then(|l| c.layer(*l)));
|
||||
let t = s.time();
|
||||
let lt = layer.map(|l| l.layer_time(t)).unwrap_or(t);
|
||||
let base = p.clone();
|
||||
let mut base = p.clone();
|
||||
let (title, fields): (String, Vec<Field>) = match id {
|
||||
// Photoshop files: Import Kind (Footage / Composition / – Retain Layer Sizes). Dropped
|
||||
// files start at Settings ▸ Import ▸ Default Drag Import As.
|
||||
@@ -651,10 +675,33 @@ pub fn open_form(app: &mut EffectcraftApp, id: &str, p: &Value) -> bool {
|
||||
],
|
||||
),
|
||||
"file.interpretFootage" | "file.interpretProxy"
|
||||
if !has(p, &["frameRate", "alpha", "loop", "pixelAspect", "colorProfile", "fields", "invertAlpha", "matteColor", "linearLight", "startFrame"]) =>
|
||||
if !has(
|
||||
p,
|
||||
&[
|
||||
"frameRate",
|
||||
"alpha",
|
||||
"loop",
|
||||
"pixelAspect",
|
||||
"colorProfile",
|
||||
"fields",
|
||||
"invertAlpha",
|
||||
"matteColor",
|
||||
"linearLight",
|
||||
"startFrame",
|
||||
"logConversion",
|
||||
"cineonUnits",
|
||||
"pointUnits",
|
||||
"convertedBlack",
|
||||
"convertedWhite",
|
||||
"tenBitBlack",
|
||||
"tenBitWhite",
|
||||
"currentGamma",
|
||||
"highlightRolloff",
|
||||
],
|
||||
) =>
|
||||
{
|
||||
let proxy = id == "file.interpretProxy";
|
||||
let Some(form) = interpret_footage_form(s, proxy) else { return false };
|
||||
let Some(form) = interpret_footage_form(s, proxy, &mut base) else { return false };
|
||||
form
|
||||
}
|
||||
"file.projectSettings" if p.as_object().is_none_or(|m| m.is_empty()) => project_settings_form(s),
|
||||
@@ -1074,4 +1121,83 @@ mod tests {
|
||||
assert_eq!(r["path"], path.as_str());
|
||||
assert!(std::fs::read_to_string(&path).unwrap().contains("<xmeml"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interpretation_menu_applies_explicit_cineon_parameters_without_a_dialog() {
|
||||
use effectcraft_engine::project::{Footage, FootageKind, ItemKind, Proxy};
|
||||
for command in ["file.interpretFootage", "file.interpretProxy"] {
|
||||
let mut app = EffectcraftApp::new(effectcraft_engine::Session::default());
|
||||
let footage = Footage { path: "/m/scan.dpx".into(), kind: FootageKind::Still, width: 4, height: 4, has_video: true, ..Default::default() };
|
||||
let project = std::sync::Arc::make_mut(&mut app.session.project);
|
||||
let item = project.add_item("scan.dpx", Default::default(), None, ItemKind::Footage(footage.clone()));
|
||||
project.item_mut(item).unwrap().proxy = Some(Box::new(Proxy { footage, enabled: true }));
|
||||
app.session.state.project_selection = vec![item];
|
||||
let ctx = egui::Context::default();
|
||||
for params in [
|
||||
json!({"logConversion": true}),
|
||||
json!({"cineonUnits": "10bit"}),
|
||||
json!({"pointUnits": "10bit"}),
|
||||
json!({"convertedBlack": 10.0}),
|
||||
json!({"convertedWhite": 900.0}),
|
||||
json!({"tenBitBlack": 90.0}),
|
||||
json!({"tenBitWhite": 690.0}),
|
||||
json!({"currentGamma": 2.2}),
|
||||
json!({"highlightRolloff": 30.0}),
|
||||
] {
|
||||
crate::menus::invoke(&mut app, &ctx, command, params.clone()).unwrap();
|
||||
assert!(app.dialog.is_none(), "{command}: {params}");
|
||||
}
|
||||
let item = app.session.project.item(item).unwrap();
|
||||
let footage = if command == "file.interpretProxy" {
|
||||
&item.proxy.as_ref().unwrap().footage
|
||||
} else {
|
||||
let ItemKind::Footage(footage) = &item.kind else { panic!("not footage") };
|
||||
footage
|
||||
};
|
||||
assert!(footage.cineon.log_conversion);
|
||||
assert_eq!(footage.cineon.gamma, 2.2);
|
||||
assert_eq!(footage.cineon.converted_white, 900.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Interpret Footage shows Cineon Settings for DPX footage only. The converted points show in
|
||||
/// the footage's Units, and choosing other Units reads the shown values in the old ones.
|
||||
#[test]
|
||||
fn interpret_footage_shows_cineon_settings_for_dpx() {
|
||||
use effectcraft_engine::project::{CineonUnits, Footage, FootageKind, ItemKind};
|
||||
let mut app = EffectcraftApp::new(effectcraft_engine::Session::default());
|
||||
let still =
|
||||
|path: &str| ItemKind::Footage(Footage { path: path.into(), kind: FootageKind::Still, width: 4, height: 4, has_video: true, ..Default::default() });
|
||||
let project = std::sync::Arc::make_mut(&mut app.session.project);
|
||||
let dpx = project.add_item("scan.dpx", Default::default(), None, still("/m/scan.dpx"));
|
||||
let png = project.add_item("still.png", Default::default(), None, still("/m/still.png"));
|
||||
let keys = |app: &EffectcraftApp| app.dialog_state.form.fields.iter().map(|f| f.key.clone()).collect::<Vec<_>>();
|
||||
app.session.state.project_selection = vec![png];
|
||||
assert!(open_form(&mut app, "file.interpretFootage", &json!({})));
|
||||
assert!(!keys(&app).iter().any(|k| k == "logConversion"), "{:?}", keys(&app));
|
||||
app.session.state.project_selection = vec![dpx];
|
||||
assert!(open_form(&mut app, "file.interpretFootage", &json!({})));
|
||||
for k in ["logConversion", "convertedBlack", "convertedWhite", "tenBitBlack", "tenBitWhite", "currentGamma", "highlightRolloff", "cineonUnits"] {
|
||||
assert!(keys(&app).iter().any(|x| x == k), "{k}: {:?}", keys(&app));
|
||||
}
|
||||
// Units ▸ 8-bit with Converted White Point still showing 1023 (10-bit).
|
||||
let form = &mut app.dialog_state.form;
|
||||
for f in form.fields.iter_mut() {
|
||||
match (f.key.as_str(), &mut f.kind) {
|
||||
("cineonUnits", FieldKind::Choice { sel, .. }) => *sel = 1,
|
||||
("logConversion", FieldKind::Bool(b)) => *b = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let params = form.params();
|
||||
assert_eq!((params["pointUnits"].as_str(), params["cineonUnits"].as_str()), (Some("10bit"), Some("8bit")), "{params}");
|
||||
app.session.execute("file.interpretFootage", params).unwrap();
|
||||
let Some(ItemKind::Footage(f)) = app.session.project.item(dpx).map(|i| &i.kind) else { panic!("not footage") };
|
||||
assert!(f.cineon.log_conversion && f.cineon.units == CineonUnits::EightBit, "{:?}", f.cineon);
|
||||
assert!((f.cineon.converted_white - 1023.0).abs() < 1e-3, "{:?}", f.cineon);
|
||||
// Opened again, the point shows in 8-bit units.
|
||||
assert!(open_form(&mut app, "file.interpretFootage", &json!({})));
|
||||
let white = app.dialog_state.form.fields.iter().find(|f| f.key == "convertedWhite").map(|f| f.kind.clone());
|
||||
assert!(matches!(white, Some(FieldKind::Number { value, .. }) if (value - 255.0).abs() < 1e-6), "{white:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -600,7 +600,7 @@ fn colorama_wheel(
|
||||
p.text(
|
||||
pos2(sr.min.x, sr.max.y + 9.0),
|
||||
Align2::LEFT_CENTER,
|
||||
"Click the ring to add a colour; Alt-click a stop (or drag it off) to remove it",
|
||||
crate::i18n::tr("Click the ring to add a colour; Alt-click a stop (or drag it off) to remove it"),
|
||||
Tokens::ui(10.5),
|
||||
t.text_faint,
|
||||
);
|
||||
@@ -801,19 +801,19 @@ fn reshape_buttons(
|
||||
let b1 = Rect::from_min_size(pos2(r.min.x + 24.0, r.min.y + 4.0), vec2(84.0, 22.0));
|
||||
let b2 = Rect::from_min_size(pos2(b1.max.x + 8.0, b1.min.y), vec2(96.0, 22.0));
|
||||
let b3 = Rect::from_min_size(pos2(b2.max.x + 8.0, b1.min.y), vec2(56.0, 22.0));
|
||||
if widgets::text_button(ui, b1, "Add Point", false, &t, egui::Id::unique(("reshape-add", euid))).clicked() {
|
||||
if widgets::text_button(ui, b1, crate::i18n::tr("Add Point"), false, &t, egui::Id::unique(("reshape-add", euid))).clicked() {
|
||||
let mut v = pairs.clone();
|
||||
v.push(add_pair(&pairs));
|
||||
set_str(actions, layer, g, "correspondencePoints", format_pairs(&v), None);
|
||||
}
|
||||
app.auto.add(&format!("effectControls.effect.{euid}.reshape.add"), b1, "Add Point");
|
||||
if widgets::text_button(ui, b2, "Remove Point", false, &t, egui::Id::unique(("reshape-remove", euid))).clicked() && !pairs.is_empty() {
|
||||
if widgets::text_button(ui, b2, crate::i18n::tr("Remove Point"), false, &t, egui::Id::unique(("reshape-remove", euid))).clicked() && !pairs.is_empty() {
|
||||
let mut v = pairs.clone();
|
||||
v.pop();
|
||||
set_str(actions, layer, g, "correspondencePoints", format_pairs(&v), None);
|
||||
}
|
||||
app.auto.add(&format!("effectControls.effect.{euid}.reshape.remove"), b2, "Remove Point");
|
||||
if widgets::text_button(ui, b3, "Clear", false, &t, egui::Id::unique(("reshape-clear", euid))).clicked() && !pairs.is_empty() {
|
||||
if widgets::text_button(ui, b3, crate::i18n::tr("Clear"), false, &t, egui::Id::unique(("reshape-clear", euid))).clicked() && !pairs.is_empty() {
|
||||
set_str(actions, layer, g, "correspondencePoints", String::new(), None);
|
||||
}
|
||||
app.auto.add(&format!("effectControls.effect.{euid}.reshape.clear"), b3, "Clear");
|
||||
|
||||
@@ -3,14 +3,18 @@
|
||||
//! Opacity stops sit above the preview bar and colour stops below it. Click above or below the
|
||||
//! bar to add a stop (it takes that track's value there), drag a stop to move it, edit the
|
||||
//! selected stop's location, opacity or colour, and Delete removes it (never the last stop of a
|
||||
//! track). The editor works on a draft: OK sets the property once with `prop.set` at the time it
|
||||
//! opened (one undo step, nothing at all when nothing changed); Cancel and Escape leave it as it
|
||||
//! was.
|
||||
//! track). The editor works on a draft. With **Preview** on (the default, as in After Effects) each
|
||||
//! edit is applied to the property at once, with `prop.set` at the time the editor opened and one
|
||||
//! merged undo step, so the canvas shows the gradient as it is edited; OK keeps it, Cancel and
|
||||
//! Escape roll it back to the checkpoint taken when the editor opened. With Preview off, OK sets
|
||||
//! the property once (one undo step, nothing at all when nothing changed). The dialog opens at
|
||||
//! the lower right of the window and does not dim it, so the canvas stays in view.
|
||||
//!
|
||||
//! Automation ids: `dialog.gradient.bar`, `dialog.gradient.addOpacity`, `dialog.gradient.addColor`,
|
||||
//! `dialog.gradient.opacityStop.<i>`, `dialog.gradient.colorStop.<i>` (in order along the bar),
|
||||
//! `dialog.gradient.location`, `dialog.gradient.opacity`, `dialog.gradient.color`,
|
||||
//! `dialog.gradient.delete`, `dialog.gradient.ok`, `dialog.gradient.cancel`.
|
||||
//! `dialog.gradient.delete`, `dialog.gradient.preview`, `dialog.gradient.ok`,
|
||||
//! `dialog.gradient.cancel`.
|
||||
|
||||
use effectcraft_engine::keyframe::{Gradient, Value};
|
||||
use effectcraft_engine::project::{ItemId, LayerId};
|
||||
@@ -18,6 +22,8 @@ use egui::{Color32, Rect, Sense, Stroke, StrokeKind, pos2, vec2};
|
||||
use serde_json::{Value as Json, json};
|
||||
|
||||
use crate::i18n::tr;
|
||||
use crate::panels::layer_styles_dialog::{Checkpoint, checkpoint};
|
||||
use crate::panels::preview_merge::{self, Applied};
|
||||
use crate::theme::Tokens;
|
||||
use crate::{Dialog, EffectcraftApp};
|
||||
|
||||
@@ -58,6 +64,14 @@ pub struct Draft {
|
||||
pub baseline: Gradient,
|
||||
/// Stop counts (colour, opacity) of the keyframes just before and after the edit time.
|
||||
pub neighbors: Vec<(usize, usize)>,
|
||||
/// Preview: edits are applied to the property as they are made.
|
||||
pub preview: bool,
|
||||
/// The gradient the property was last given by the preview, if it has been.
|
||||
pub applied: Option<Gradient>,
|
||||
/// The project as the preview left it, to tell edits made since from the preview's.
|
||||
applied_at: Option<Applied>,
|
||||
/// The project and undo history as the editor opened, for Cancel.
|
||||
checkpoint: Option<Checkpoint>,
|
||||
next_id: u64,
|
||||
}
|
||||
|
||||
@@ -220,7 +234,10 @@ pub fn open(app: &mut EffectcraftApp, p: &Json) -> Result<Json, String> {
|
||||
let before = prop.keys.iter().rev().find(|k| k.time < lt).and_then(|k| counts(&k.value));
|
||||
let after = prop.keys.iter().find(|k| k.time > lt).and_then(|k| counts(&k.value));
|
||||
let target = Target { comp: cid.0, layer: lid, prop: uid, time: ct.seconds() };
|
||||
app.dialog_state.gradient = Draft::new(&g, Some(target), before.into_iter().chain(after).collect());
|
||||
let mut draft = Draft::new(&g, Some(target), before.into_iter().chain(after).collect());
|
||||
draft.preview = true;
|
||||
draft.checkpoint = Some(checkpoint(app));
|
||||
app.dialog_state.gradient = draft;
|
||||
app.dialog = Some(Dialog::GradientEditor);
|
||||
Ok(json!({"dialog": "gradientEditor"}))
|
||||
}
|
||||
@@ -231,6 +248,11 @@ fn commit(app: &mut EffectcraftApp, ctx: &egui::Context) {
|
||||
app.dialog = None;
|
||||
let d = std::mem::take(&mut app.dialog_state.gradient);
|
||||
let g = d.gradient();
|
||||
// The preview already set the property to this draft, in one merged undo step: OK keeps it.
|
||||
if d.applied.as_ref() == Some(&g) {
|
||||
app.session.history.merge_key = None;
|
||||
return;
|
||||
}
|
||||
let Some(tg) = d.target else { return };
|
||||
if g == d.baseline {
|
||||
return;
|
||||
@@ -241,6 +263,55 @@ fn commit(app: &mut EffectcraftApp, ctx: &egui::Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Close the editor without keeping the preview: the project and undo history go back to how they
|
||||
/// were when it opened (nothing happens when the preview never changed the property).
|
||||
pub fn finish(app: &mut EffectcraftApp, ok: bool, ctx: &egui::Context) {
|
||||
if ok {
|
||||
// An edit made in the same frame as OK is not in the last preview yet.
|
||||
sync_preview(app, ctx);
|
||||
commit(app, ctx);
|
||||
return;
|
||||
}
|
||||
let d = std::mem::take(&mut app.dialog_state.gradient);
|
||||
if d.applied.is_some()
|
||||
&& let Some(cp) = d.checkpoint
|
||||
{
|
||||
preview_merge::rollback(app, cp, d.applied_at.as_ref());
|
||||
}
|
||||
app.session.history.merge_key = None;
|
||||
app.dialog = None;
|
||||
}
|
||||
|
||||
/// Keep the property in step with the draft while Preview is on: apply a changed draft with one
|
||||
/// merged undo step, and roll back to the checkpoint when Preview goes off or the draft is back to
|
||||
/// how it opened.
|
||||
fn sync_preview(app: &mut EffectcraftApp, ctx: &egui::Context) {
|
||||
let d = &app.dialog_state.gradient;
|
||||
let Some(tg) = d.target.clone() else { return };
|
||||
let g = d.gradient();
|
||||
let want = d.preview && g != d.baseline;
|
||||
if want && d.applied.as_ref() != Some(&g) {
|
||||
let params = json!({
|
||||
"comp": tg.comp, "layer": tg.layer, "prop": tg.prop, "time": tg.time, "timeBase": "comp",
|
||||
"value": Value::Gradient(g.clone()).to_json(), "merge": format!("gradient-editor-{}", tg.prop),
|
||||
});
|
||||
match crate::menus::invoke(app, ctx, "prop.set", params) {
|
||||
Ok(_) => {
|
||||
app.dialog_state.gradient.applied = Some(g);
|
||||
app.dialog_state.gradient.applied_at = Some(preview_merge::applied(app));
|
||||
}
|
||||
Err(e) => app.ui.status = e,
|
||||
}
|
||||
} else if !want
|
||||
&& d.applied.is_some()
|
||||
&& let Some(cp) = d.checkpoint.clone()
|
||||
{
|
||||
let at = app.dialog_state.gradient.applied_at.take();
|
||||
preview_merge::rollback(app, cp, at.as_ref());
|
||||
app.dialog_state.gradient.applied = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn to_color32(c: [f32; 4]) -> Color32 {
|
||||
let b = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8;
|
||||
Color32::from_rgba_unmultiplied(b(c[0]), b(c[1]), b(c[2]), b(c[3]))
|
||||
@@ -258,7 +329,8 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
return;
|
||||
}
|
||||
let mut answer: Option<bool> = None;
|
||||
super::dialogs::modal(ctx, tr("Gradient Editor"), vec2(520.0, 270.0), t, |ui| {
|
||||
// At the lower right, undimmed: the canvas it edits stays in view (After Effects).
|
||||
super::dialogs::modal_placed(ctx, tr("Gradient Editor"), vec2(520.0, 270.0), t, super::live_preview::PLACE, |ui| {
|
||||
let w = ui.available_width().max(200.0);
|
||||
let (area, _) = ui.allocate_exact_size(vec2(w, 72.0), Sense::hover());
|
||||
let bar = Rect::from_min_max(pos2(area.min.x + 8.0, area.min.y + 22.0), pos2(area.max.x - 8.0, area.min.y + 48.0));
|
||||
@@ -416,6 +488,10 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
if r.clicked() {
|
||||
answer = Some(true);
|
||||
}
|
||||
let mut preview = app.dialog_state.gradient.preview;
|
||||
let r = ui.checkbox(&mut preview, tr("Preview"));
|
||||
app.auto.add("dialog.gradient.preview", r.rect, "Preview");
|
||||
app.dialog_state.gradient.preview = preview;
|
||||
});
|
||||
});
|
||||
// Delete / Backspace remove the selected stop, but not while typing in a field.
|
||||
@@ -427,12 +503,8 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
app.dialog_state.gradient.remove(track, id);
|
||||
}
|
||||
match answer {
|
||||
Some(true) => commit(app, ctx),
|
||||
Some(false) => {
|
||||
app.dialog = None;
|
||||
app.dialog_state.gradient = Draft::default();
|
||||
}
|
||||
None => {}
|
||||
Some(ok) => finish(app, ok, ctx),
|
||||
None => sync_preview(app, ctx),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,9 +48,21 @@ struct KeyPt {
|
||||
t: f64,
|
||||
v: f64,
|
||||
sel: bool,
|
||||
muted: bool,
|
||||
handles: Vec<Handle>,
|
||||
}
|
||||
|
||||
/// A graph key's fill and edge colours: muted keys stay visible and selectable, dimmed.
|
||||
fn key_colors(t: &Tokens, edge: Color32, k: &KeyPt) -> (Color32, Color32) {
|
||||
let fill = match (k.muted, k.sel) {
|
||||
(true, true) => t.keyframe_selected.gamma_multiply(0.5),
|
||||
(true, false) => Color32::from_gray(0x70),
|
||||
(false, true) => t.keyframe_selected,
|
||||
(false, false) => Color32::from_gray(0xe0),
|
||||
};
|
||||
(fill, if k.muted { edge.gamma_multiply(0.4) } else { edge })
|
||||
}
|
||||
|
||||
struct Curve<'a> {
|
||||
layer: &'a Layer,
|
||||
prop: &'a Property,
|
||||
@@ -153,7 +165,7 @@ fn curves<'a>(app: &EffectcraftApp, comp: &'a Comp, ectx: &EvalCtx, tm: TMap, pl
|
||||
let dt = e.influence * dur * if out_side { 1.0 } else { -1.0 };
|
||||
let (hv, show) = if speed { (e.speed.abs(), true) } else { (k.value.components().get(d).copied().unwrap_or(0.0) + e.speed * dt, !spatial) };
|
||||
let bezier = if out_side { k.out_interp == Interp::Bezier } else { k.in_interp == Interp::Bezier };
|
||||
if ks && show && (bezier || speed) {
|
||||
if ks && !k.muted && show && (bezier || speed) {
|
||||
handles.push(Handle { out: out_side, t: kt + dt, v: hv, speed: e.speed, dur });
|
||||
}
|
||||
}
|
||||
@@ -162,7 +174,7 @@ fn curves<'a>(app: &EffectcraftApp, comp: &'a Comp, ectx: &EvalCtx, tm: TMap, pl
|
||||
} else {
|
||||
k.value.components().get(d).copied().unwrap_or(0.0)
|
||||
};
|
||||
keys.push(KeyPt { idx: i, time: k.time, t: kt, v, sel: ks, handles });
|
||||
keys.push(KeyPt { idx: i, time: k.time, t: kt, v, sel: ks, muted: k.muted, handles });
|
||||
}
|
||||
out.push(Curve { layer: l, prop: pr, dim: d, color, pts, keys, editable: !spatial });
|
||||
}
|
||||
@@ -221,7 +233,7 @@ impl ButtonBar {
|
||||
let chosen = widgets::popup_list(ui, pid, pos, r, rows, 0, |ui| {
|
||||
let mut chosen = None;
|
||||
for (id, label, on, tip) in &self.hidden {
|
||||
let item = ui.selectable_label(*on, label.as_str()).on_hover_text(tip.as_str());
|
||||
let item = ui.selectable_label(*on, crate::i18n::tr(label)).on_hover_text(crate::i18n::tr(tip));
|
||||
app.auto.add(&format!("timeline.graph.more.{id}"), item.rect, tip);
|
||||
if item.clicked() {
|
||||
chosen = Some(id.clone());
|
||||
@@ -251,14 +263,16 @@ struct BarBtn<'a> {
|
||||
fn text_button(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, bar: &mut ButtonBar, y: f32, btn: BarBtn) -> bool {
|
||||
let BarBtn { label, on, id, tip } = btn;
|
||||
let t = app.tokens;
|
||||
let w = p.layout_no_wrap(label.to_string(), Tokens::ui(11.0), t.text).size().x + 14.0;
|
||||
let (shown, shown_tip) = (crate::i18n::tr(label), crate::i18n::tr(tip));
|
||||
let w = p.layout_no_wrap(shown.to_string(), Tokens::ui(11.0), t.text).size().x + 14.0;
|
||||
// Once one button overflows, the rest follow it into the menu (they keep their order).
|
||||
if !bar.hidden.is_empty() || bar.x + w > bar.limit {
|
||||
// The menu translates these English labels when it draws them; automation keeps them.
|
||||
bar.hidden.push((id.to_string(), label.to_string(), on, tip.to_string()));
|
||||
return bar.chosen.as_deref() == Some(id);
|
||||
}
|
||||
let r = Rect::from_min_size(pos2(bar.x, y - 9.0), vec2(w, 18.0));
|
||||
let resp = ui.interact(r, egui::Id::unique(("graph-btn", id)), Sense::click()).on_hover_text(tip);
|
||||
let resp = ui.interact(r, egui::Id::unique(("graph-btn", id)), Sense::click()).on_hover_text(shown_tip);
|
||||
p.rect_filled(
|
||||
r,
|
||||
3.0,
|
||||
@@ -270,7 +284,7 @@ fn text_button(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, b
|
||||
Color32::from_rgb(0x2c, 0x2c, 0x2c)
|
||||
},
|
||||
);
|
||||
p.text(r.center(), Align2::CENTER_CENTER, label, Tokens::ui(11.0), if on { Color32::WHITE } else { t.text });
|
||||
p.text(r.center(), Align2::CENTER_CENTER, shown, Tokens::ui(11.0), if on { Color32::WHITE } else { t.text });
|
||||
app.auto.add(&format!("timeline.graph.{id}"), r, tip);
|
||||
bar.x += w + 4.0;
|
||||
resp.clicked()
|
||||
@@ -522,9 +536,9 @@ pub(crate) fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painte
|
||||
}
|
||||
}
|
||||
// The key itself.
|
||||
let col = if k.sel { t.keyframe_selected } else { Color32::from_gray(0xe0) };
|
||||
let (col, edge) = key_colors(&t, c.color, k);
|
||||
pc.rect_filled(Rect::from_center_size(kp, vec2(7.0, 7.0)), 0.0, col);
|
||||
pc.rect_stroke(Rect::from_center_size(kp, vec2(7.0, 7.0)), 0.0, Stroke::new(1.0, c.color), StrokeKind::Outside);
|
||||
pc.rect_stroke(Rect::from_center_size(kp, vec2(7.0, 7.0)), 0.0, Stroke::new(1.0, edge), StrokeKind::Outside);
|
||||
let kr = Rect::from_center_size(kp, vec2(11.0, 11.0));
|
||||
let kresp = ui.interact(kr, egui::Id::unique(("gkey", uid, d, k.idx)), Sense::click_and_drag());
|
||||
app.auto.add(&format!("timeline.graph.key.{uid}.{d}.{}", k.idx), kr, &c.prop.name);
|
||||
|
||||
@@ -24,10 +24,11 @@ pub struct LayerStyleState {
|
||||
checkpoint: Option<Checkpoint>,
|
||||
}
|
||||
|
||||
/// The project and undo history as a dialog found them, for a Cancel that rolls its live edits back.
|
||||
#[derive(Clone)]
|
||||
struct Checkpoint {
|
||||
project: std::sync::Arc<effectcraft_engine::project::Project>,
|
||||
history: effectcraft_engine::history::History,
|
||||
pub(crate) struct Checkpoint {
|
||||
pub(crate) project: std::sync::Arc<effectcraft_engine::project::Project>,
|
||||
pub(crate) history: effectcraft_engine::history::History,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Checkpoint {
|
||||
@@ -36,7 +37,7 @@ impl std::fmt::Debug for Checkpoint {
|
||||
}
|
||||
}
|
||||
|
||||
fn checkpoint(app: &mut EffectcraftApp) -> Checkpoint {
|
||||
pub(crate) fn checkpoint(app: &mut EffectcraftApp) -> Checkpoint {
|
||||
app.session.history.merge_key = None;
|
||||
Checkpoint { project: app.session.project.clone(), history: app.session.history.clone() }
|
||||
}
|
||||
@@ -258,25 +259,32 @@ pub fn finish(app: &mut EffectcraftApp, ok: bool) {
|
||||
if let Some(checkpoint) = app.dialog_state.layer_style.checkpoint.take()
|
||||
&& !ok
|
||||
{
|
||||
let changed = !std::sync::Arc::ptr_eq(&app.session.project, &checkpoint.project);
|
||||
if changed {
|
||||
// Snapshot only when Cancel has something to restore: opening and cancelling without
|
||||
// edits must not copy the whole TextDoc.
|
||||
let edited = active_editor_snapshot(&app.session);
|
||||
app.session.project = checkpoint.project;
|
||||
app.session.history = checkpoint.history;
|
||||
app.session.sanitize_after_replacement(edited.as_ref());
|
||||
app.session.bump();
|
||||
} else {
|
||||
// No edit was committed, so nothing can have become invalid. In particular, do not
|
||||
// call sanitize_state here: its text-edit cleanup clones the whole TextDoc.
|
||||
app.session.history = checkpoint.history;
|
||||
}
|
||||
restore(app, checkpoint);
|
||||
}
|
||||
app.session.history.merge_key = None;
|
||||
app.dialog = None;
|
||||
}
|
||||
|
||||
/// Put the project and undo history back as `checkpoint` has them (nothing happens when nothing
|
||||
/// changed since).
|
||||
pub(crate) fn restore(app: &mut EffectcraftApp, checkpoint: Checkpoint) {
|
||||
let changed = !std::sync::Arc::ptr_eq(&app.session.project, &checkpoint.project);
|
||||
if changed {
|
||||
// Snapshot only when Cancel has something to restore: opening and cancelling without
|
||||
// edits must not copy the whole TextDoc.
|
||||
let edited = active_editor_snapshot(&app.session);
|
||||
app.session.project = checkpoint.project;
|
||||
app.session.history = checkpoint.history;
|
||||
app.session.sanitize_after_replacement(edited.as_ref());
|
||||
app.session.bump();
|
||||
} else {
|
||||
// No edit was committed, so nothing can have become invalid. In particular, do not
|
||||
// call sanitize_state here: its text-edit cleanup clones the whole TextDoc.
|
||||
app.session.history = checkpoint.history;
|
||||
}
|
||||
app.session.history.merge_key = None;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -18,7 +18,7 @@ pub fn home_tab(app: &mut EffectcraftApp, ui: &mut egui::Ui, area: Rect) {
|
||||
p.text(
|
||||
pos2(x, y + 26.0),
|
||||
Align2::LEFT_CENTER,
|
||||
"Hands-on tutorials that run inside EffectCraft. Do each step yourself, or press Show me.",
|
||||
crate::i18n::tr("Hands-on tutorials that run inside EffectCraft. Do each step yourself, or press Show me."),
|
||||
Tokens::ui(11.5),
|
||||
t.text_faint,
|
||||
);
|
||||
@@ -38,7 +38,7 @@ pub fn home_tab(app: &mut EffectcraftApp, ui: &mut egui::Ui, area: Rect) {
|
||||
p.circle_filled(badge.center(), 22.0, t.accent.gamma_multiply(0.25));
|
||||
p.circle_stroke(badge.center(), 22.0, Stroke::new(1.5, t.accent));
|
||||
p.text(badge.center() - vec2(0.0, 5.0), Align2::CENTER_CENTER, tut.steps.len().to_string(), Tokens::semibold(15.0), Color32::WHITE);
|
||||
p.text(badge.center() + vec2(0.0, 10.0), Align2::CENTER_CENTER, "steps", Tokens::ui(9.0), t.text_dim);
|
||||
p.text(badge.center() + vec2(0.0, 10.0), Align2::CENTER_CENTER, crate::i18n::tr("steps"), Tokens::ui(9.0), t.text_dim);
|
||||
let tx = badge.max.x + 16.0;
|
||||
p.text(pos2(tx, r.min.y + 22.0), Align2::LEFT_CENTER, &tut.title, Tokens::semibold(14.0), Color32::WHITE);
|
||||
p.text(pos2(tx, r.min.y + 40.0), Align2::LEFT_CENTER, format!("{} min", tut.minutes), Tokens::ui(11.0), t.text_faint);
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
//! Preview for the settings dialogs that edit something that already exists (Solid Settings on a
|
||||
//! solid layer, Camera Settings, Light Settings, Composition Settings): After Effects' Preview
|
||||
//! checkbox. Each change of the draft is applied to the project at once, so the canvas shows it
|
||||
//! while it is made; OK keeps it and Cancel and Escape take it back.
|
||||
//!
|
||||
//! Every step goes back to the checkpoint taken when the dialog opened (the project and its undo
|
||||
//! history) and then runs the dialog's command once with the draft's parameters. So the project is
|
||||
//! always "as it opened, plus one command": OK leaves exactly one undo step, whatever was tried
|
||||
//! meanwhile, and Cancel only has to put the checkpoint back. With Preview off nothing is applied
|
||||
//! until OK, and switching it off takes back what was applied.
|
||||
//!
|
||||
//! While a preview is active the dialog opens at the lower right of the window, without the dark
|
||||
//! backdrop, and the window behind it is not greyed out, so the canvas stays in view.
|
||||
|
||||
use egui::vec2;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::panels::dialogs::Placement;
|
||||
use crate::panels::layer_styles_dialog::{Checkpoint, checkpoint};
|
||||
use crate::panels::preview_merge::{self, Applied};
|
||||
use crate::{Dialog, EffectcraftApp};
|
||||
|
||||
/// Where a previewing dialog opens: the lower right, undimmed.
|
||||
pub const PLACE: Placement = Placement { start: vec2(0.75, 0.8), dim: false };
|
||||
|
||||
/// The state of the one dialog being previewed (only one dialog is open at a time).
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct LivePreview {
|
||||
/// The dialog this belongs to; a preview left over from another one is ignored.
|
||||
dialog: Option<Dialog>,
|
||||
/// The Preview checkbox.
|
||||
pub on: bool,
|
||||
checkpoint: Option<Checkpoint>,
|
||||
/// The draft's parameters when the dialog opened: nothing is applied while it equals them.
|
||||
baseline: Option<Value>,
|
||||
/// The command and parameters the preview last applied.
|
||||
applied: Option<(&'static str, Value)>,
|
||||
/// The project as that command left it, to tell edits made since from the preview's.
|
||||
applied_at: Option<Applied>,
|
||||
}
|
||||
|
||||
impl LivePreview {
|
||||
fn active_for(&self, dialog: Option<Dialog>) -> bool {
|
||||
self.checkpoint.is_some() && self.dialog.is_some() && self.dialog == dialog
|
||||
}
|
||||
}
|
||||
|
||||
/// Start previewing for `dialog`, which has just opened on an existing item (Preview on).
|
||||
pub fn begin(app: &mut EffectcraftApp, dialog: Dialog) {
|
||||
app.dialog_state.live = LivePreview { dialog: Some(dialog), on: true, checkpoint: Some(checkpoint(app)), ..Default::default() };
|
||||
}
|
||||
|
||||
/// The dialog that is open is previewing.
|
||||
pub fn active(app: &EffectcraftApp) -> bool {
|
||||
app.dialog_state.live.active_for(app.dialog)
|
||||
}
|
||||
|
||||
/// Where the open dialog goes: the lower right, undimmed, while it previews; else the middle.
|
||||
pub fn place(app: &EffectcraftApp) -> Placement {
|
||||
if active(app) { PLACE } else { Placement::default() }
|
||||
}
|
||||
|
||||
/// Keep the project in step with the draft: `command` with `params` is what OK would run. The
|
||||
/// first call (the draft as it opened) only notes it.
|
||||
pub fn sync(app: &mut EffectcraftApp, command: &'static str, params: Value) {
|
||||
if !active(app) {
|
||||
return;
|
||||
}
|
||||
let live = &app.dialog_state.live;
|
||||
if live.baseline.is_none() {
|
||||
app.dialog_state.live.baseline = Some(params);
|
||||
return;
|
||||
}
|
||||
let want = live.on && live.baseline.as_ref() != Some(¶ms);
|
||||
let is_applied = live.applied.is_some();
|
||||
if want {
|
||||
if live.applied.as_ref().is_some_and(|(c, p)| *c == command && *p == params) {
|
||||
return;
|
||||
}
|
||||
if is_applied {
|
||||
put_back(app);
|
||||
}
|
||||
match app.session.execute(command, params.clone()) {
|
||||
Ok(_) => {
|
||||
app.dialog_state.live.applied = Some((command, params));
|
||||
app.dialog_state.live.applied_at = Some(preview_merge::applied(app));
|
||||
}
|
||||
Err(e) => app.ui.status = e.to_string(),
|
||||
}
|
||||
} else if is_applied {
|
||||
put_back(app);
|
||||
}
|
||||
}
|
||||
|
||||
/// The project as the dialog found it.
|
||||
fn put_back(app: &mut EffectcraftApp) {
|
||||
if let Some(cp) = app.dialog_state.live.checkpoint.clone() {
|
||||
let at = app.dialog_state.live.applied_at.take();
|
||||
// Only what the preview did is taken back: an edit made while the dialog was open stays.
|
||||
preview_merge::rollback(app, cp, at.as_ref());
|
||||
}
|
||||
app.dialog_state.live.applied = None;
|
||||
app.dialog_state.live.applied_at = None;
|
||||
}
|
||||
|
||||
/// OK: the preview's result is the edit. The preview is first brought up to `command` with
|
||||
/// `params`, the draft as OK found it (an edit made in the same frame as OK is not in the last
|
||||
/// preview). Returns whether the project then already has the edit (the caller must not run the
|
||||
/// command again); otherwise the caller runs it, as without a preview.
|
||||
pub fn keep(app: &mut EffectcraftApp, command: &'static str, params: Value) -> bool {
|
||||
sync(app, command, params);
|
||||
let kept = active(app) && app.dialog_state.live.applied.is_some();
|
||||
app.dialog_state.live = LivePreview::default();
|
||||
kept
|
||||
}
|
||||
|
||||
/// Cancel and Escape: take back whatever the preview applied.
|
||||
pub fn cancel(app: &mut EffectcraftApp) {
|
||||
if active(app) {
|
||||
put_back(app);
|
||||
}
|
||||
app.dialog_state.live = LivePreview::default();
|
||||
}
|
||||
|
||||
/// The Preview checkbox, when the open dialog previews; `id` is its automation id.
|
||||
pub fn checkbox(ui: &mut egui::Ui, auto: &mut crate::automation::Registry, on: &mut bool, id: &str) {
|
||||
let r = ui.checkbox(on, crate::i18n::tr("Preview"));
|
||||
auto.add(id, r.rect, "Preview");
|
||||
}
|
||||
@@ -11,7 +11,8 @@ use crate::{EffectcraftApp, widgets};
|
||||
|
||||
pub fn placeholder(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect, p: PanelKind) {
|
||||
let t = app.tokens;
|
||||
ui.painter().text(rect.center(), Align2::CENTER_CENTER, tr_args("{} — coming soon", &[&(p.title())]), Tokens::ui(12.0), t.text_faint);
|
||||
let name = crate::i18n::label(app, "window.panel", p.title());
|
||||
ui.painter().text(rect.center(), Align2::CENTER_CENTER, tr_args("{} — coming soon", &[&name]), Tokens::ui(12.0), t.text_faint);
|
||||
}
|
||||
|
||||
/// Preview panel: transport controls, the shortcut whose options are shown, and that
|
||||
@@ -42,7 +43,10 @@ pub fn preview(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
for (icon, cmd, tip) in buttons {
|
||||
let r = Rect::from_min_size(pos2(x, y), vec2(bw, 28.0));
|
||||
let icon = if cmd == "playback.toggle" && app.playback.playing { Icon::Pause } else { icon };
|
||||
if widgets::icon_button(ui, r, icon, cmd == "playback.toggle" && app.playback.playing, &t, egui::Id::unique(("pv", cmd))).on_hover_text(tip).clicked() {
|
||||
if widgets::icon_button(ui, r, icon, cmd == "playback.toggle" && app.playback.playing, &t, egui::Id::unique(("pv", cmd)))
|
||||
.on_hover_text(tr(tip))
|
||||
.clicked()
|
||||
{
|
||||
// The play button plays with the options of the shortcut shown below.
|
||||
let params = if cmd == "playback.toggle" { json!({"shortcut": current.id()}) } else { json!({}) };
|
||||
let _ = crate::menus::invoke(app, &ctx, cmd, params);
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
//! Taking a dialog's Preview back without taking other edits with it.
|
||||
//!
|
||||
//! A previewing dialog (Camera, Light, Solid, Composition Settings, the Gradient Editor) puts the
|
||||
//! project and undo history back to a checkpoint when it is cancelled or Preview goes off. If the
|
||||
//! project changed in the meantime for another reason (a camera analysis finishing, a script, the
|
||||
//! control channel), swapping the checkpoint in would throw that edit away. This module undoes
|
||||
//! only what the preview did: a three-way merge of the checkpoint (before the preview), the project
|
||||
//! right after the preview (`Applied`) and the project now. Whatever the preview did not touch
|
||||
//! stays as it is now; what only the preview touched goes back to the checkpoint; what both touched
|
||||
//! keeps the other edit.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use effectcraft_engine::commands::text_edit::active_editor_snapshot;
|
||||
use effectcraft_engine::project::{Comp, Item, ItemId, ItemKind, Layer, LayerId, Project};
|
||||
|
||||
use crate::EffectcraftApp;
|
||||
use crate::panels::layer_styles_dialog::{Checkpoint, restore};
|
||||
|
||||
/// The project right after a preview was applied, and the undo depth then.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Applied {
|
||||
project: Arc<Project>,
|
||||
undo_len: usize,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Applied {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Applied").field("undo_len", &self.undo_len).finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
/// Note the project as the preview that has just been applied left it.
|
||||
pub(crate) fn applied(app: &EffectcraftApp) -> Applied {
|
||||
Applied { project: app.session.project.clone(), undo_len: app.session.history.undo.len() }
|
||||
}
|
||||
|
||||
/// Put the project back as `checkpoint` has it, minus nothing but the preview: when the project is
|
||||
/// still what the preview made (or nothing says what that was) the checkpoint is simply restored.
|
||||
pub(crate) fn rollback(app: &mut EffectcraftApp, checkpoint: Checkpoint, applied: Option<&Applied>) {
|
||||
let Some(applied) = applied.filter(|a| !Arc::ptr_eq(&a.project, &app.session.project)) else {
|
||||
restore(app, checkpoint);
|
||||
return;
|
||||
};
|
||||
let merged = merge(&checkpoint.project, &applied.project, &app.session.project);
|
||||
// One step for what was done meanwhile, from the project before the dialog: Undo takes it back.
|
||||
let label = app.session.history.undo.get(applied.undo_len).map(|(label, _)| label.clone()).unwrap_or_else(|| "Edit".to_string());
|
||||
let mut history = checkpoint.history;
|
||||
history.undo.push((label, checkpoint.project));
|
||||
history.redo.clear();
|
||||
history.merge_key = None;
|
||||
let edited = active_editor_snapshot(&app.session);
|
||||
app.session.project = Arc::new(merged);
|
||||
app.session.history = history;
|
||||
app.session.sanitize_after_replacement(edited.as_ref());
|
||||
app.session.bump();
|
||||
}
|
||||
|
||||
/// `now` without the changes between `before` and `after`, keeping any other change.
|
||||
fn merge(before: &Project, after: &Project, now: &Project) -> Project {
|
||||
let mut merged = now.clone();
|
||||
let ids: BTreeSet<ItemId> = before.items.keys().chain(after.items.keys()).chain(now.items.keys()).copied().collect();
|
||||
for id in ids {
|
||||
match item(before.items.get(&id), after.items.get(&id), now.items.get(&id)) {
|
||||
Some(it) => {
|
||||
merged.items.insert(id, it);
|
||||
}
|
||||
None => {
|
||||
merged.items.remove(&id);
|
||||
}
|
||||
}
|
||||
}
|
||||
merged
|
||||
}
|
||||
|
||||
/// The three-way choice for one value: untouched by the preview keeps `now`, touched only by the
|
||||
/// preview goes back to `before`, touched by both is `both`.
|
||||
fn pick<T: PartialEq + Clone>(before: Option<&T>, after: Option<&T>, now: Option<&T>, both: impl FnOnce() -> Option<T>) -> Option<T> {
|
||||
if after == before {
|
||||
now.cloned()
|
||||
} else if now == after {
|
||||
before.cloned()
|
||||
} else {
|
||||
both()
|
||||
}
|
||||
}
|
||||
|
||||
fn item(before: Option<&Item>, after: Option<&Item>, now: Option<&Item>) -> Option<Item> {
|
||||
pick(before, after, now, || {
|
||||
if let (Some(b), Some(a), Some(n)) = (before, after, now)
|
||||
&& let (ItemKind::Comp(bc), ItemKind::Comp(ac), ItemKind::Comp(nc)) = (&b.kind, &a.kind, &n.kind)
|
||||
{
|
||||
let mut merged = n.clone();
|
||||
// Name, folder and the like: only a preview that changed the item itself and nothing else
|
||||
// is taken back; the comp's own contents are merged below.
|
||||
merged.kind = ItemKind::Comp(Arc::new(comp(bc, ac, nc)));
|
||||
return Some(merged);
|
||||
}
|
||||
now.cloned()
|
||||
})
|
||||
}
|
||||
|
||||
fn bare(c: &Comp) -> Comp {
|
||||
let mut b = c.clone();
|
||||
b.layers.clear();
|
||||
b
|
||||
}
|
||||
|
||||
fn layer_of(c: &Comp, id: LayerId) -> Option<&Layer> {
|
||||
c.layers.iter().find(|l| l.id == id)
|
||||
}
|
||||
|
||||
fn comp(before: &Comp, after: &Comp, now: &Comp) -> Comp {
|
||||
// The comp's own settings, apart from its layers.
|
||||
let mut merged = pick(Some(&bare(before)), Some(&bare(after)), Some(&bare(now)), || Some(bare(now))).unwrap_or_else(|| bare(now));
|
||||
let merge_layer = |id: LayerId| pick(layer_of(before, id), layer_of(after, id), layer_of(now, id), || layer_of(now, id).cloned());
|
||||
// Layers as they stand now, in their order, then the ones the preview removed put back.
|
||||
let mut layers: Vec<Layer> = now.layers.iter().filter_map(|l| merge_layer(l.id)).collect();
|
||||
for (at, l) in before.layers.iter().enumerate() {
|
||||
if layer_of(now, l.id).is_none()
|
||||
&& let Some(back) = merge_layer(l.id)
|
||||
{
|
||||
layers.insert(at.min(layers.len()), back);
|
||||
}
|
||||
}
|
||||
merged.layers = layers.into();
|
||||
merged
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Dialog;
|
||||
use crate::panels::live_preview;
|
||||
use effectcraft_engine::Session;
|
||||
use serde_json::json;
|
||||
|
||||
fn app_with_light() -> (EffectcraftApp, u64) {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "Scene", "width": 320, "height": 180, "duration": 1})).unwrap();
|
||||
let light = s.execute("layer.newLight", json!({"kind": "Point"})).unwrap()["layer"].as_u64().unwrap();
|
||||
(EffectcraftApp::new(s), light)
|
||||
}
|
||||
|
||||
fn name_of(app: &EffectcraftApp, layer: u64) -> String {
|
||||
app.session.active_comp().unwrap().layer(LayerId(layer)).unwrap().name.clone()
|
||||
}
|
||||
|
||||
fn layer_count(app: &EffectcraftApp) -> usize {
|
||||
app.session.active_comp().unwrap().layers.len()
|
||||
}
|
||||
|
||||
/// The preview renames the light; a null is added meanwhile (what a finished background job
|
||||
/// would do). Cancel takes the rename back and keeps the null, with one undo step for it.
|
||||
#[test]
|
||||
fn cancelling_a_preview_keeps_an_edit_made_while_it_was_open() {
|
||||
let (mut app, light) = app_with_light();
|
||||
let original = name_of(&app, light);
|
||||
app.dialog = Some(Dialog::LightSettings);
|
||||
live_preview::begin(&mut app, Dialog::LightSettings);
|
||||
let rename = |name: &str| json!({"layer": light, "name": name});
|
||||
live_preview::sync(&mut app, "layer.rename", rename(&original));
|
||||
live_preview::sync(&mut app, "layer.rename", rename("Previewed"));
|
||||
assert_eq!(name_of(&app, light), "Previewed");
|
||||
let layers = layer_count(&app);
|
||||
app.session.execute("layer.newNull", json!({})).unwrap();
|
||||
live_preview::cancel(&mut app);
|
||||
assert_eq!(name_of(&app, light), original, "the preview is taken back");
|
||||
assert_eq!(layer_count(&app), layers + 1, "the other edit stays");
|
||||
assert!(app.session.undo());
|
||||
assert_eq!(layer_count(&app), layers, "and has its own undo step");
|
||||
assert_eq!(name_of(&app, light), original);
|
||||
}
|
||||
|
||||
/// With nothing else done meanwhile the checkpoint is restored as before.
|
||||
#[test]
|
||||
fn cancelling_an_undisturbed_preview_restores_the_checkpoint() {
|
||||
let (mut app, light) = app_with_light();
|
||||
let original = name_of(&app, light);
|
||||
let steps = app.session.history.undo.len();
|
||||
app.dialog = Some(Dialog::LightSettings);
|
||||
live_preview::begin(&mut app, Dialog::LightSettings);
|
||||
live_preview::sync(&mut app, "layer.rename", json!({"layer": light, "name": original}));
|
||||
live_preview::sync(&mut app, "layer.rename", json!({"layer": light, "name": "Previewed"}));
|
||||
live_preview::cancel(&mut app);
|
||||
assert_eq!(name_of(&app, light), original);
|
||||
assert_eq!(app.session.history.undo.len(), steps);
|
||||
}
|
||||
|
||||
/// An item both the preview and another edit changed keeps the other edit.
|
||||
#[test]
|
||||
fn a_layer_both_edits_changed_keeps_the_other_edit() {
|
||||
let (mut app, light) = app_with_light();
|
||||
let original = name_of(&app, light);
|
||||
app.dialog = Some(Dialog::LightSettings);
|
||||
live_preview::begin(&mut app, Dialog::LightSettings);
|
||||
live_preview::sync(&mut app, "layer.rename", json!({"layer": light, "name": original}));
|
||||
live_preview::sync(&mut app, "layer.rename", json!({"layer": light, "name": "Previewed"}));
|
||||
app.session.execute("layer.rename", json!({"layer": light, "name": "Renamed elsewhere"})).unwrap();
|
||||
live_preview::cancel(&mut app);
|
||||
assert_eq!(name_of(&app, light), "Renamed elsewhere");
|
||||
}
|
||||
}
|
||||
@@ -812,7 +812,7 @@ fn footer(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, foot:
|
||||
(Icon::NewComp, "newComp", "Create a new Composition"),
|
||||
] {
|
||||
let r = Rect::from_min_size(pos2(x, foot.min.y + 3.0), vec2(22.0, 22.0));
|
||||
let resp = widgets::icon_button(ui, r, icon, false, &t, egui::Id::unique(("pfoot", id))).on_hover_text(tip);
|
||||
let resp = widgets::icon_button(ui, r, icon, false, &t, egui::Id::unique(("pfoot", id))).on_hover_text(crate::i18n::tr(tip));
|
||||
app.auto.add(&format!("project.{id}"), r, tip);
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, crate::i18n::tr(tip)));
|
||||
if resp.clicked() {
|
||||
|
||||
@@ -451,6 +451,22 @@ fn queue_drop(app: &EffectcraftApp, ui: &egui::Ui, rect: Rect, p: &egui::Painter
|
||||
}
|
||||
}
|
||||
|
||||
/// Elapsed and estimated remaining seconds at UI time `now`. The job reports them as frames
|
||||
/// finish; between reports they count on from the last one, as a clock does, so a slow frame
|
||||
/// doesn't read as a stalled render (#2593).
|
||||
fn live_times(ctx: &egui::Context, pr: &effectcraft_engine::render_queue::JobState, now: f64) -> (f64, Option<f64>) {
|
||||
let report = (pr.current, pr.done, pr.item_elapsed.to_bits());
|
||||
let since = ctx.data_mut(|d| {
|
||||
let seen = d.get_temp_mut_or_insert_with(egui::Id::unique("rq-progress-seen"), || (report, now));
|
||||
if seen.0 != report {
|
||||
*seen = (report, now);
|
||||
}
|
||||
seen.1
|
||||
});
|
||||
let dt = if pr.finished { 0.0 } else { (now - since).max(0.0) };
|
||||
(pr.item_elapsed + dt, pr.remaining.map(|r| (r - dt).max(0.0)))
|
||||
}
|
||||
|
||||
/// The Current Render strip inside `top`: title, elapsed / remaining time, Render, Stop, Notify
|
||||
/// and the progress bar.
|
||||
fn current_render_strip(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, top: Rect, rq: &RqView, actions: &mut Vec<Action>) {
|
||||
@@ -461,11 +477,15 @@ fn current_render_strip(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::P
|
||||
let cur_item = progress.as_ref().and_then(|p| p.current).and_then(|id| queue.iter().find(|i| i.id == id));
|
||||
let cur_name = cur_item.and_then(|i| app.session.project.item(i.comp)).map(|i| i.name.clone());
|
||||
if let Some(pr) = &progress {
|
||||
let rem = pr.remaining.map(hms).unwrap_or_else(|| "–".into());
|
||||
let (elapsed, remaining) = live_times(ui.ctx(), pr, ui.input(|i| i.time));
|
||||
if rendering {
|
||||
ui.ctx().request_repaint_after(std::time::Duration::from_millis(500));
|
||||
}
|
||||
let rem = remaining.map(hms).unwrap_or_else(|| "–".into());
|
||||
p.text(
|
||||
pos2(top.max.x - 12.0, top.min.y + 16.0),
|
||||
Align2::RIGHT_CENTER,
|
||||
format!("Elapsed: {} Est. Remain: {rem}", hms(pr.item_elapsed)),
|
||||
format!("Elapsed: {} Est. Remain: {rem}", hms(elapsed)),
|
||||
Tokens::ui(11.5),
|
||||
t.text_dim,
|
||||
);
|
||||
@@ -474,7 +494,7 @@ fn current_render_strip(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::P
|
||||
let render_b = Rect::from_min_size(pos2(top.max.x - 12.0 - bw, top.min.y + 34.0), vec2(bw, 26.0));
|
||||
let stop_b = Rect::from_min_size(pos2(render_b.min.x - 8.0 - bw, render_b.min.y), vec2(bw, 26.0));
|
||||
let can_render = !rendering && queue.iter().any(RenderQueueItem::is_queued) && !available.is_empty();
|
||||
if widgets::text_button(ui, render_b, "Render", can_render, &t, egui::Id::unique("rq-render")).clicked() && can_render {
|
||||
if widgets::text_button(ui, render_b, crate::i18n::tr("Render"), can_render, &t, egui::Id::unique("rq-render")).clicked() && can_render {
|
||||
actions.push(("renderQueue.render", json!({"wait": false})));
|
||||
}
|
||||
app.auto.add("renderQueue.render", render_b, "Render");
|
||||
@@ -487,7 +507,7 @@ fn current_render_strip(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::P
|
||||
p.text(pos2(nb.max.x + 5.0, nb.center().y), Align2::LEFT_CENTER, tr("Notify"), Tokens::ui(11.5), t.text_dim);
|
||||
app.auto.add("renderQueue.notify", nb, if notify { "Notify: on" } else { "Notify: off" });
|
||||
if rendering {
|
||||
if widgets::text_button(ui, stop_b, "Stop", false, &t, egui::Id::unique("rq-stop")).clicked() {
|
||||
if widgets::text_button(ui, stop_b, crate::i18n::tr("Stop"), false, &t, egui::Id::unique("rq-stop")).clicked() {
|
||||
actions.push(("renderQueue.stop", json!({})));
|
||||
}
|
||||
app.auto.add("renderQueue.stop", stop_b, "Stop");
|
||||
@@ -903,4 +923,26 @@ mod tests {
|
||||
assert_eq!(super::utc_stamp(u64::MAX), "584554051223-11-09 07:00:15");
|
||||
assert_eq!(super::hms(3725.4), "1:02:05");
|
||||
}
|
||||
|
||||
/// #2593: between progress reports the times count on from the last report, and a new
|
||||
/// report starts the count again.
|
||||
#[test]
|
||||
fn render_times_tick_between_reports() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut pr = effectcraft_engine::render_queue::JobState {
|
||||
current: Some(1),
|
||||
done: 2,
|
||||
total: 10,
|
||||
item_elapsed: 20.0,
|
||||
remaining: Some(80.0),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(super::live_times(&ctx, &pr, 100.0), (20.0, Some(80.0)));
|
||||
assert_eq!(super::live_times(&ctx, &pr, 103.0), (23.0, Some(77.0)));
|
||||
assert_eq!(super::live_times(&ctx, &pr, 300.0).1, Some(0.0), "never below zero");
|
||||
(pr.done, pr.item_elapsed, pr.remaining) = (3, 31.0, Some(72.0));
|
||||
assert_eq!(super::live_times(&ctx, &pr, 301.0), (31.0, Some(72.0)));
|
||||
pr.finished = true;
|
||||
assert_eq!(super::live_times(&ctx, &pr, 400.0), (31.0, Some(72.0)), "a finished job stands still");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
p.text(
|
||||
out_rect.center(),
|
||||
Align2::CENTER_CENTER,
|
||||
"JavaScript with the After Effects scripting object model — try app.project.numItems",
|
||||
crate::i18n::tr("JavaScript with the After Effects scripting object model — try app.project.numItems"),
|
||||
Tokens::ui(12.0),
|
||||
t.text_faint,
|
||||
);
|
||||
|
||||
@@ -395,7 +395,11 @@ fn mouse_events(ui: &egui::Ui, origin: egui::Pos2, area: Rect, win: u32, root: &
|
||||
return;
|
||||
}
|
||||
let mut st: MouseTrack = ui.data(|d| d.get_temp(id)).unwrap_or_default();
|
||||
let (latest, events, mods, time) = ui.input(|i| (i.pointer.latest_pos(), i.events.clone(), i.modifiers, i.time));
|
||||
let (latest, events, mods, time, held) = ui.input(|i| {
|
||||
let held = |b: egui::PointerButton, bit: u8| if i.pointer.button_down(b) { bit } else { 0 };
|
||||
let held = held(egui::PointerButton::Primary, 1) | held(egui::PointerButton::Secondary, 2) | held(egui::PointerButton::Middle, 4);
|
||||
(i.pointer.latest_pos(), i.events.clone(), i.modifiers, i.time, held)
|
||||
});
|
||||
let ctx = ui.ctx();
|
||||
let layer = ui.layer_id();
|
||||
let target = |p: egui::Pos2| -> Option<&Widget> {
|
||||
@@ -414,7 +418,7 @@ fn mouse_events(ui: &egui::Ui, origin: egui::Pos2, area: Rect, win: u32, root: &
|
||||
w.id,
|
||||
json!({
|
||||
"type": ty, "x": p.x - at.x, "y": p.y - at.y, "screenX": p.x, "screenY": p.y,
|
||||
"button": button, "detail": detail,
|
||||
"button": button, "buttons": held, "detail": detail,
|
||||
"altKey": mods.alt, "ctrlKey": mods.ctrl, "shiftKey": mods.shift, "metaKey": mods.mac_cmd,
|
||||
}),
|
||||
));
|
||||
@@ -431,10 +435,15 @@ fn mouse_events(ui: &egui::Ui, origin: egui::Pos2, area: Rect, win: u32, root: &
|
||||
}
|
||||
st.hover = now_id;
|
||||
}
|
||||
if let Some((w, p)) = now
|
||||
&& st.at != Some(p)
|
||||
{
|
||||
send(w, "mousemove", p, 0, 0, acts);
|
||||
// While a button is held, the control it was pressed on keeps getting the moves, inside or
|
||||
// outside the window, as in After Effects (#2584).
|
||||
let captured = st.down.and_then(|(c, _)| root.find(c));
|
||||
if let Some(p) = latest.filter(|p| st.at != Some(*p)) {
|
||||
match (captured, now) {
|
||||
(Some(c), _) => send(c, "mousemove", p, 0, 0, acts),
|
||||
(None, Some((w, _))) => send(w, "mousemove", p, 0, 0, acts),
|
||||
(None, None) => {}
|
||||
}
|
||||
}
|
||||
st.at = latest;
|
||||
// Presses and releases.
|
||||
@@ -446,7 +455,9 @@ fn mouse_events(ui: &egui::Ui, origin: egui::Pos2, area: Rect, win: u32, root: &
|
||||
egui::PointerButton::Secondary => 2,
|
||||
_ => continue,
|
||||
};
|
||||
let Some(w) = target(*pos) else {
|
||||
// A release goes to the control the button was pressed on, wherever the pointer is.
|
||||
let pressed_on = st.down.filter(|(_, db)| !*pressed && *db == b).and_then(|(c, _)| root.find(c));
|
||||
let Some(w) = pressed_on.or_else(|| target(*pos)) else {
|
||||
if !pressed {
|
||||
st.down = None;
|
||||
}
|
||||
@@ -460,7 +471,8 @@ fn mouse_events(ui: &egui::Ui, origin: egui::Pos2, area: Rect, win: u32, root: &
|
||||
send(w, "mousedown", *pos, b, st.clicks, acts);
|
||||
} else {
|
||||
send(w, "mouseup", *pos, b, st.clicks, acts);
|
||||
if st.down == Some((w.id, b)) && b == 0 && w.enabled && w.mouse.iter().any(|m| m == "click") {
|
||||
let over = target(*pos).is_some_and(|t| t.id == w.id);
|
||||
if over && st.down == Some((w.id, b)) && b == 0 && w.enabled && w.mouse.iter().any(|m| m == "click") {
|
||||
// The click with the pointer's details, after mousedown / mouseup, in place of
|
||||
// the plain click the control reported.
|
||||
acts.retain(|a| !matches!(a, Act::Click(aw, ac) if *aw == win && *ac == w.id));
|
||||
|
||||
@@ -49,6 +49,8 @@ const PANEL_COLOR: Color32 = Color32::from_rgb(0x3f, 0xa8, 0x6c);
|
||||
|
||||
pub fn open(app: &mut EffectcraftApp) {
|
||||
app.dialog_state.shortcuts = EditorState::default();
|
||||
// Scripts copied into the Scripts folder since the table was built are listed too.
|
||||
app.session.scripts_changed();
|
||||
app.dialog = Some(Dialog::Shortcuts);
|
||||
}
|
||||
|
||||
|
||||
@@ -62,14 +62,14 @@ pub fn home_tab(app: &mut EffectcraftApp, ui: &mut egui::Ui, area: Rect) {
|
||||
p.text(
|
||||
pos2(x, y + 26.0),
|
||||
Align2::LEFT_CENTER,
|
||||
"Start from a ready-made project. Opening a template makes an untitled copy.",
|
||||
crate::i18n::tr("Start from a ready-made project. Opening a template makes an untitled copy."),
|
||||
Tokens::ui(11.5),
|
||||
t.text_faint,
|
||||
);
|
||||
// Save the open project as a user template.
|
||||
let can_save = app.session.project.comps().next().is_some();
|
||||
let sr = Rect::from_min_size(pos2(area.max.x - 210.0, y - 4.0), vec2(210.0, 28.0));
|
||||
let save = widgets::text_button(ui, sr, "Save Current as Template…", false, &t, egui::Id::unique("home-templates-save")).clicked();
|
||||
let save = widgets::text_button(ui, sr, crate::i18n::tr("Save Current as Template…"), false, &t, egui::Id::unique("home-templates-save")).clicked();
|
||||
app.auto.add("home.templates.saveCurrent", sr, "Save Current as Template…");
|
||||
if save {
|
||||
if can_save {
|
||||
|
||||
@@ -654,6 +654,17 @@ struct FontSearch {
|
||||
}
|
||||
|
||||
/// Shared font menu for Character and Properties: search, recent fonts and previews.
|
||||
/// Finish a font rescan started from the font menu and report how many faces it added.
|
||||
/// While the scan is still walking font folders, ask for another frame so the menu updates.
|
||||
pub(crate) fn poll_font_rescan(app: &mut EffectcraftApp, ctx: &egui::Context) {
|
||||
if effectcraft_engine::text::fonts::rescan_busy() {
|
||||
ctx.request_repaint_after(std::time::Duration::from_millis(100));
|
||||
}
|
||||
if let Some(n) = effectcraft_engine::text::fonts::poll_rescan() {
|
||||
app.ui.status = format!("{} +{n}", tr("Rescan Fonts"));
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn font_picker(app: &mut EffectcraftApp, ui: &mut egui::Ui, id: egui::Id, rect: Rect, current: &str, auto: &str, enabled: bool) -> Option<String> {
|
||||
let state_id = id.with("search-state");
|
||||
app.auto.add(auto, rect, "Font family");
|
||||
@@ -724,6 +735,15 @@ pub(super) fn font_picker(app: &mut EffectcraftApp, ui: &mut egui::Ui, id: egui:
|
||||
let boxed = widgets::checkbox(ui, box_rect, only_variable, &t, id.with("variable-check"));
|
||||
ui.painter().text(label_rect.left_center(), Align2::LEFT_CENTER, tr("Variable fonts only"), Tokens::ui(11.5), t.text);
|
||||
app_ref.auto.add(&format!("{auto}.variable"), row.rect, "Variable fonts only");
|
||||
// #1089: fonts installed after launch appear without restarting. The button is its own
|
||||
// row: the popup is only 180 px wide, so a control pinned to the filter row covers
|
||||
// "Variable fonts only". The scan itself runs off the UI thread (`poll_font_rescan`).
|
||||
let rescan_h = 22.0;
|
||||
let (resc, _) = ui.allocate_exact_size(vec2(ui.available_width(), rescan_h), egui::Sense::hover());
|
||||
if widgets::text_button(ui, resc, tr("Rescan Fonts"), false, &t, id.with("rescan")).clicked() && effectcraft_engine::text::fonts::start_rescan() {
|
||||
app_ref.ui.status = format!("{}…", tr("Rescan Fonts"));
|
||||
}
|
||||
app_ref.auto.add(&format!("{auto}.rescan"), resc, "Rescan Fonts");
|
||||
let toggled = row.clicked() || boxed.clicked();
|
||||
if toggled {
|
||||
only_variable = !only_variable;
|
||||
|
||||
@@ -1528,8 +1528,8 @@ fn shift_range(ctx: &egui::Context, cid: ItemId, comp: &Comp, rows: &[Row], sele
|
||||
let anchor = ctx.data(|d| d.get_temp::<LayerId>(anchor_id(cid))).or_else(|| selected.first().copied())?;
|
||||
let a = comp.layers.iter().position(|l| l.id == anchor)?;
|
||||
let b = comp.layers.iter().position(|l| l.id == layer)?;
|
||||
let span = comp.layers.get(a.min(b)..=a.max(b))?;
|
||||
Some(span.iter().filter(|l| !l.switches.locked && rows.iter().any(|r| r.layer == l.id)).map(|l| l.id.0).collect())
|
||||
let span = comp.layers.iter().skip(a.min(b)).take(a.max(b) - a.min(b) + 1);
|
||||
Some(span.filter(|l| !l.switches.locked && rows.iter().any(|r| r.layer == l.id)).map(|l| l.id.0).collect())
|
||||
}
|
||||
|
||||
/// Set on the frame the reorder drag is released.
|
||||
@@ -1772,7 +1772,7 @@ fn layout(app: &mut EffectcraftApp, ui: &egui::Ui, rect: Rect, cid: ItemId, comp
|
||||
fn empty_timeline(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let t = app.tokens;
|
||||
let ctx = ui.ctx().clone();
|
||||
ui.painter().text(rect.center(), Align2::CENTER_CENTER, "(no composition)", Tokens::ui(12.0), t.text_faint);
|
||||
ui.painter().text(rect.center(), Align2::CENTER_CENTER, crate::i18n::tr("(no composition)"), Tokens::ui(12.0), t.text_faint);
|
||||
// Dropping footage into an empty Timeline should create its first composition,
|
||||
// just like dropping onto the Project panel's New Composition button (#649).
|
||||
if let Some(crate::panels::DragPayload::Item(dragged)) = egui::DragAndDrop::payload::<crate::panels::DragPayload>(&ctx).as_deref()
|
||||
@@ -2213,7 +2213,7 @@ fn header_search_and_switches(app: &mut EffectcraftApp, ui: &mut egui::Ui, g: &T
|
||||
let comp = g.comp.clone();
|
||||
let sr = Rect::from_min_size(pos2(tc_rect.max.x + 14.0, top + 12.0), vec2((left_w - 420.0).clamp(120.0, 220.0), 22.0));
|
||||
let mut search = app.ui.timeline.search.clone();
|
||||
widgets::search_field(ui, sr, &mut search, "Search", &t);
|
||||
widgets::search_field(ui, sr, &mut search, crate::i18n::tr("Search"), &t);
|
||||
app.ui.timeline.search = search;
|
||||
app.auto.add("timeline.search", sr, "Search");
|
||||
let mut bx = rect.min.x + left_w - 10.0;
|
||||
@@ -3622,7 +3622,7 @@ fn waveform_row(app: &mut EffectcraftApp, g: &Rw, rc: &RowCtx<'_>) {
|
||||
let RowKind::Waveform { item, .. } = &rc.row.kind else { return };
|
||||
lp.rect_filled(left, 0.0, if ri.is_multiple_of(2) { t.row } else { t.row_alt });
|
||||
let indent = cw.name + 6.0 + 14.0 * row.depth as f32;
|
||||
lp.text(pos2(indent + 10.0, cy), Align2::LEFT_CENTER, "Waveform", Tokens::ui(12.0), t.text);
|
||||
lp.text(pos2(indent + 10.0, cy), Align2::LEFT_CENTER, crate::i18n::tr("Waveform"), Tokens::ui(12.0), t.text);
|
||||
let wr = Rect::from_min_max(pos2(graph_x0, r.min.y), r.max);
|
||||
gp.rect_filled(wr, 0.0, t.tl_bg);
|
||||
app.auto.add(&format!("timeline.layer.{}.waveform", layer.id.0), wr, "Waveform");
|
||||
@@ -3632,7 +3632,7 @@ fn waveform_row(app: &mut EffectcraftApp, g: &Rw, rc: &RowCtx<'_>) {
|
||||
gp.text(
|
||||
pos2(tm.x(layer.in_point.seconds()).max(wr.min.x) + 6.0, wr.center().y),
|
||||
Align2::LEFT_CENTER,
|
||||
"Building waveform…",
|
||||
crate::i18n::tr("Building waveform…"),
|
||||
Tokens::ui(11.0),
|
||||
t.text_faint,
|
||||
);
|
||||
@@ -4111,6 +4111,7 @@ fn key_context_menu(app: &mut EffectcraftApp, g: &mut Rw, rc: &RowCtx<'_>, prop:
|
||||
("Keyframe Interpolation…", "keys.interpolation", json!({})),
|
||||
("Keyframe Velocity…", "keys.velocity", json!({})),
|
||||
("Toggle Hold Keyframe", "keys.toggleHold", json!({})),
|
||||
(if k.muted { "Unmute Keyframes" } else { "Mute Keyframes" }, "keys.setMuted", json!({"muted": !k.muted})),
|
||||
("Rove Across Time", "keys.interpolation", json!({"roving": !k.roving})),
|
||||
("-", "", json!({})),
|
||||
("Easy Ease", "keys.easyEase", json!({})),
|
||||
@@ -4203,6 +4204,8 @@ fn key_paint(g: &Rw, rc: &RowCtx<'_>, k: &effectcraft_engine::keyframe::Keyframe
|
||||
None if ks => t.keyframe_selected,
|
||||
None => t.keyframe,
|
||||
};
|
||||
// Muted keys stay visible and editable but no longer drive the value.
|
||||
let kcol = if k.muted { kcol.gamma_multiply(0.35) } else { kcol };
|
||||
if k.roving {
|
||||
// Roving keys: a small dot (their time follows their neighbours).
|
||||
gp.circle_filled(c, 2.5, kcol);
|
||||
@@ -5128,6 +5131,7 @@ const SHAPE_ADD_ITEMS: &[(&str, &str)] = &[
|
||||
("trim", "Trim Paths"),
|
||||
("twist", "Twist"),
|
||||
("wiggle", "Wiggle Paths"),
|
||||
("wiggler", "Wiggle Transform"),
|
||||
("zigzag", "Zig Zag"),
|
||||
];
|
||||
|
||||
|
||||
@@ -626,7 +626,7 @@ pub fn apply_dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
super::dialogs::modal(ctx, "Motion Tracker Apply Options", vec2(380.0, 170.0), t, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(crate::i18n::tr("Apply Dimensions:"));
|
||||
let labels = ["X and Y", "X only", "Y only"];
|
||||
let labels = [crate::i18n::tr("X and Y"), crate::i18n::tr("X only"), crate::i18n::tr("Y only")];
|
||||
let r = craft_ui::ComboBox::from_id_salt("track-apply-dims")
|
||||
.selected_text(labels[d.dims.min(2)])
|
||||
.show_ui(ui, |ui| {
|
||||
|
||||
@@ -16,7 +16,16 @@ use crate::theme::Tokens;
|
||||
use crate::{Dialog, EffectcraftApp};
|
||||
|
||||
/// Commands that replace or close the open project (Quit is asked when the window closes).
|
||||
const CLOSING: &[&str] = &["file.newProject", "file.open", "file.openRecent", "file.closeProject", "file.revert", "file.openDemoProject", "templates.create"];
|
||||
const CLOSING: &[&str] = &[
|
||||
"file.newProject",
|
||||
"file.open",
|
||||
"file.openRecent",
|
||||
"file.closeProject",
|
||||
"file.revert",
|
||||
"file.openDemoProject",
|
||||
"templates.create",
|
||||
"file.runShutdownScripts",
|
||||
];
|
||||
|
||||
/// The command waiting on the prompt, and whether the user already answered it.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -239,6 +248,23 @@ mod tests {
|
||||
assert!(app.session.is_dirty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shutdown_scripts_prompt_before_discarding_unsaved_work() {
|
||||
let (mut app, ctx) = app();
|
||||
modified(&mut app);
|
||||
let result = crate::menus::invoke(&mut app, &ctx, "file.runShutdownScripts", json!({})).unwrap();
|
||||
assert_eq!(result, json!({"dialog": "unsavedChanges"}));
|
||||
assert_eq!(app.session.project.comps().count(), 1);
|
||||
assert!(app.session.is_dirty());
|
||||
answer_with(&mut app, &ctx, Answer::Cancel);
|
||||
assert_eq!(app.session.project.comps().count(), 1);
|
||||
assert!(app.session.is_dirty());
|
||||
crate::menus::invoke(&mut app, &ctx, "file.runShutdownScripts", json!({})).unwrap();
|
||||
answer_with(&mut app, &ctx, Answer::Discard);
|
||||
assert_eq!(app.session.project.comps().count(), 0);
|
||||
assert!(!app.session.is_dirty() && app.dialog.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closing_a_modified_project_asks_and_cancel_keeps_it() {
|
||||
let (mut app, ctx) = app();
|
||||
|
||||
@@ -9,13 +9,13 @@ pub(super) fn empty_state(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rec
|
||||
let c = rect.center();
|
||||
p.text(c - vec2(0.0, 40.0), Align2::CENTER_CENTER, crate::i18n::tr("Composition"), Tokens::semibold(16.0), t.text);
|
||||
let b1 = Rect::from_center_size(c + vec2(0.0, 0.0), vec2(220.0, 30.0));
|
||||
if widgets::text_button(ui, b1, "New Composition", true, &t, egui::Id::unique("empty-newcomp")).clicked() {
|
||||
if widgets::text_button(ui, b1, crate::i18n::tr("New Composition"), true, &t, egui::Id::unique("empty-newcomp")).clicked() {
|
||||
crate::panels::dialogs::open_new_comp(app);
|
||||
}
|
||||
app.auto.add("viewer.empty.newComp", b1, "New Composition");
|
||||
let b2 = Rect::from_center_size(c + vec2(0.0, 40.0), vec2(220.0, 30.0));
|
||||
if widgets::text_button(ui, b2, "New Composition From Footage", false, &t, egui::Id::unique("empty-fromfootage")).clicked() {
|
||||
let _ = crate::menus::invoke(app, &ui.ctx().clone(), "file.import", json!({}));
|
||||
if widgets::text_button(ui, b2, crate::i18n::tr("New Composition From Footage"), false, &t, egui::Id::unique("empty-fromfootage")).clicked() {
|
||||
let _ = crate::menus::invoke(app, &ui.ctx().clone(), "file.import", json!({"newComp": true}));
|
||||
}
|
||||
app.auto.add("viewer.empty.import", b2, "New Composition From Footage");
|
||||
// A Project item dropped on the empty viewer creates the first composition,
|
||||
|
||||
@@ -360,31 +360,36 @@ pub(crate) fn motion_path(
|
||||
}
|
||||
let v = k.value.as_vec3();
|
||||
let s = to_scr(v);
|
||||
// Auto-Bezier keys show handles on a side the path leaves straight too (a two-key path's
|
||||
// ends), to drag it into a curve; Linear keys (no tangents) show none.
|
||||
let (tin, tout) = if k.spatial_auto {
|
||||
effectcraft_engine::keyframe::spatial_handles(&pos.keys, i)
|
||||
} else {
|
||||
effectcraft_engine::keyframe::spatial_tangents(&pos.keys, i)
|
||||
};
|
||||
for (out, tg) in [(false, tin), (true, tout)] {
|
||||
let has = (out && i + 1 < pos.keys.len()) || (!out && i > 0);
|
||||
if !has || (tg[0] == 0.0 && tg[1] == 0.0) {
|
||||
continue;
|
||||
let selected = sel.contains(&KeyRef { layer: l.id, prop: pos.uid, time: k.time });
|
||||
// A muted position key is off the motion path: no tangents, and a dimmed point.
|
||||
let kc = if k.muted { col.gamma_multiply(if selected { 0.55 } else { 0.35 }) } else { col };
|
||||
if !k.muted {
|
||||
// Auto-Bezier keys show handles on a side the path leaves straight too (a two-key path's
|
||||
// ends), to drag it into a curve; Linear keys (no tangents) show none.
|
||||
let (tin, tout) = if k.spatial_auto {
|
||||
effectcraft_engine::keyframe::spatial_handles(&pos.keys, i)
|
||||
} else {
|
||||
effectcraft_engine::keyframe::spatial_tangents(&pos.keys, i)
|
||||
};
|
||||
for (out, tg) in [(false, tin), (true, tout)] {
|
||||
let has = (out && i + 1 < pos.keys.len()) || (!out && i > 0);
|
||||
if !has || (tg[0] == 0.0 && tg[1] == 0.0) {
|
||||
continue;
|
||||
}
|
||||
let h = to_scr([v[0] + tg[0], v[1] + tg[1], v[2] + tg[2]]);
|
||||
painter.line_segment([s, h], Stroke::new(1.0, kc));
|
||||
painter.circle_filled(h, 3.0, kc);
|
||||
let side = if out { "out" } else { "in" };
|
||||
app.auto.add(&format!("viewer.motionPath.{}.{i}.{side}", l.id.0), Rect::from_center_size(h, vec2(8.0, 8.0)), "Spatial tangent");
|
||||
hits.push(KeyHit { layer: l.id, prop: pos.uid, time: k.time, pos: h, tangent: Some(out), value: v, p2c });
|
||||
}
|
||||
let h = to_scr([v[0] + tg[0], v[1] + tg[1], v[2] + tg[2]]);
|
||||
painter.line_segment([s, h], Stroke::new(1.0, col));
|
||||
painter.circle_filled(h, 3.0, col);
|
||||
let side = if out { "out" } else { "in" };
|
||||
app.auto.add(&format!("viewer.motionPath.{}.{i}.{side}", l.id.0), Rect::from_center_size(h, vec2(8.0, 8.0)), "Spatial tangent");
|
||||
hits.push(KeyHit { layer: l.id, prop: pos.uid, time: k.time, pos: h, tangent: Some(out), value: v, p2c });
|
||||
}
|
||||
let r = Rect::from_center_size(s, vec2(7.0, 7.0));
|
||||
if sel.contains(&KeyRef { layer: l.id, prop: pos.uid, time: k.time }) {
|
||||
painter.rect_filled(r, 0.0, col);
|
||||
if selected {
|
||||
painter.rect_filled(r, 0.0, kc);
|
||||
} else {
|
||||
painter.rect_filled(r, 0.0, Color32::from_black_alpha(140));
|
||||
painter.rect_stroke(r, 0.0, Stroke::new(1.0, col), StrokeKind::Inside);
|
||||
painter.rect_stroke(r, 0.0, Stroke::new(1.0, kc), StrokeKind::Inside);
|
||||
}
|
||||
app.auto.add(&format!("viewer.motionPath.{}.{i}", l.id.0), r.expand(2.0), "Position keyframe");
|
||||
hits.push(KeyHit { layer: l.id, prop: pos.uid, time: k.time, pos: s, tangent: None, value: v, p2c });
|
||||
|
||||
@@ -602,7 +602,7 @@ pub fn popup_menu(ui: &mut Ui, id: egui::Id, pos: egui::Pos2, options: &[String]
|
||||
pub fn layer_picker(
|
||||
ui: &mut Ui,
|
||||
rect: Rect,
|
||||
layers: &[effectcraft_engine::project::Layer],
|
||||
layers: &effectcraft_engine::project::Layers,
|
||||
current: Option<u64>,
|
||||
t: &Tokens,
|
||||
id: egui::Id,
|
||||
|
||||
Reference in New Issue
Block a user