From 9253b5bdc62505d53e9e8e5ae22f9b387d40c80c Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 8 Nov 2026 19:47:28 -0800 Subject: [PATCH 1/5] Updates: take craft-update from craft-libs main at 1b34eba41 --- Cargo.lock | 15 +-------------- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f897737db..f45bf8414 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1184,7 +1184,7 @@ dependencies = [ [[package]] name = "craft-update" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7" +source = "git+https://github.com/storytold/craft-libs?rev=1b34eba41e62138fc2438d2734523b302840b45d#1b34eba41e62138fc2438d2734523b302840b45d" dependencies = [ "chrono", "dunce", @@ -7959,7 +7959,6 @@ dependencies = [ "nix", "typed-path 0.13.0", "windows-sys 0.61.2", - "zopfli", ] [[package]] @@ -7974,18 +7973,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" -[[package]] -name = "zopfli" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf7fc5d30c28483d93805c4a5e12b05bbb52407fa67c5f8bd552374cd01fb11" -dependencies = [ - "bumpalo", - "crc32fast", - "log", - "simd-adler32", -] - [[package]] name = "zune-core" version = "0.5.3" diff --git a/Cargo.toml b/Cargo.toml index abb2256c2..b85c3c6b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-l craft-quit = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" } craft-ecproj-format = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "ca1b5e2edb8568335c6c14f390307bd5beae8238" } # craft-update: app-specific pretend versions apply to startup and manual checks. -craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", features = ["egui"] } +craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "1b34eba41e62138fc2438d2734523b302840b45d", features = ["egui"] } # internal effectcraft-time = { path = "crates/time" } effectcraft-geom = { path = "crates/geom" } From 9893eb0d8d6e0fd317256a3dc1a185593530cee9 Mon Sep 17 00:00:00 2001 From: FredApps <153173854+FredApps@users.noreply.github.com> Date: Mon, 9 Nov 2026 04:51:10 +0100 Subject: [PATCH 2/5] Naming: descriptive names for the CC effects, Lumetri, Mocha, Keylight and Mercury (#3327) Related to #1365. Display labels only; effect, command, parameter, panel and serialized ids and the AEP/script match-name tables are unchanged. - 69 Cycore CC effects, Key Light, Mocha shape, Lumetri Color/Scopes and the Mercury renderer labels get descriptive names; the old names stay case-insensitive lookup and search aliases. - Backend::parse accepts the new renderer labels, so reported settings round-trip. - Display names are unique per language in every effect catalog (new test). - Warp Stabilizer and Roto Brush keep their names until RFC-0029 settles. - Mapping in docs/feature-naming.md. --- README.md | 10 +- apps/effectcraft-cli/src/main.rs | 8 +- crates/automation/tests/qa/compositing.rs | 2 +- crates/effects/src/blur2.rs | 16 +- crates/effects/src/blur3.rs | 8 +- crates/effects/src/channel2.rs | 8 +- crates/effects/src/color2.rs | 8 +- crates/effects/src/color3.rs | 14 +- crates/effects/src/display_names.rs | 123 ++ crates/effects/src/distort2.rs | 22 +- crates/effects/src/distort3.rs | 38 +- crates/effects/src/distort4.rs | 8 +- crates/effects/src/generate2.rs | 12 +- crates/effects/src/generate3/registry.rs | 4 +- crates/effects/src/keying2.rs | 4 +- crates/effects/src/keylight.rs | 10 +- crates/effects/src/lib.rs | 91 +- crates/effects/src/mocha_shape.rs | 4 +- crates/effects/src/perspective.rs | 12 +- crates/effects/src/perspective2.rs | 6 +- crates/effects/src/sim/registry.rs | 16 +- crates/effects/src/sim2/registry.rs | 6 +- crates/effects/src/sim3.rs | 10 +- crates/effects/src/stylize2.rs | 18 +- crates/effects/src/stylize3.rs | 28 +- crates/effects/src/time_fx.rs | 10 +- crates/effects/src/transition.rs | 10 +- crates/effects/src/transition2.rs | 28 +- crates/effects/src/utility.rs | 2 +- crates/engine/src/background/offload.rs | 2 +- crates/engine/src/cmd/file.rs | 8 +- crates/engine/src/cmd/panels_cmds.rs | 6 +- crates/engine/src/home/demo.rs | 2 +- crates/engine/src/home/learn.rs | 2 +- crates/engine/src/menus.rs | 2 +- crates/engine/src/menus/tests.rs | 19 + crates/engine/src/tests/rig3d.rs | 4 +- crates/engine/src/tests/settings.rs | 30 +- crates/render/src/accel.rs | 7 +- crates/render/src/tests.rs | 43 +- crates/ui-egui/src/dock.rs | 12 +- crates/ui-egui/src/i18n.rs | 41 +- crates/ui-egui/src/i18n/effects.rs | 1009 +++++++++-------- crates/ui-egui/src/i18n/ui.rs | 92 +- crates/ui-egui/src/panels/forms.rs | 20 +- crates/ui-egui/src/state.rs | 2 +- crates/ui-egui/tests/it/ui_angles.rs | 2 +- crates/ui-egui/tests/it/ui_color_hex.rs | 4 +- crates/ui-egui/tests/it/ui_effect_controls.rs | 6 +- .../tests/it/ui_effect_drop_highlight.rs | 2 +- crates/ui-egui/tests/it/ui_fx_editors.rs | 2 +- crates/ui-egui/tests/it/ui_live_preview.rs | 4 +- .../ui-egui/tests/it/ui_parameter_spacing.rs | 2 +- crates/ui-egui/tests/it/ui_viewer.rs | 2 +- docs/architecture.md | 26 +- docs/effects.md | 144 +-- docs/feature-naming.md | 108 ++ docs/web.md | 2 +- 58 files changed, 1321 insertions(+), 820 deletions(-) create mode 100644 crates/effects/src/display_names.rs create mode 100644 docs/feature-naming.md diff --git a/README.md b/README.md index 81b5a0ba5..913f8aa06 100644 --- a/README.md +++ b/README.md @@ -129,18 +129,14 @@ or into the Timeline, between layers and at the time you point to. 308 built-in effects across its categories, each with its parameter names, order and defaults: blur and sharpen, channel, color correction (Curves, Levels, Hue/Saturation, a full -color-grading panel…), distort (Warp, Bulge, Turbulent Displace, CC Power Pin…), generate (Fractal Noise, +color-grading panel…), distort (Warp, Bulge, Turbulent Displace, Perspective Pin…), generate (Fractal Noise, Gradient Ramp, Stroke, Write-on, Audio Spectrum…), keying, matte, noise and grain, perspective, -**simulation** (CC Particle World, CC Rainfall, Shatter, Card Dance, Caustics, Wave World…), -stylize (Glow, CC Glass…), **time** (Echo, Posterize Time, Timewarp, Time Displacement…), +**simulation** (Particle Emitter 3D, Rainfall, Shatter, Card Dance, Caustics, Wave World…), +stylize (Glow, Glass Relief…), **time** (Echo, Posterize Time, Timewarp, Time Displacement…), **audio** (Reverb, Parametric EQ, Delay, Stereo Mixer…), text, transitions, utility and expression controls. All nine **Layer Styles** (Drop Shadow, Inner/Outer Glow, Bevel and Emboss, Satin, overlays, Stroke) with Global Light. Preview plays audio in sync, with meters and waveforms. -

- A CC Particle World burst with Glow, and the Effect Controls panel listing the particle system's parameters -

- ## 3D Classic 3D compositing: 3D layers with orientation and material options, diff --git a/apps/effectcraft-cli/src/main.rs b/apps/effectcraft-cli/src/main.rs index 2981519e9..b8634178e 100644 --- a/apps/effectcraft-cli/src/main.rs +++ b/apps/effectcraft-cli/src/main.rs @@ -35,8 +35,8 @@ //! //! Project: --project F.ecproj (or a positional *.ecproj) | --demo | --empty (default: demo; mcp: empty) //! Saving: --save (back to --project) | --save-as F.ecproj -//! GPU: --gpu renders on the GPU compositor (Mercury GPU Acceleration) when an adapter exists; -//! the default is the CPU (Mercury Software Only) +//! GPU: --gpu renders on the GPU compositor (GPU Acceleration) when an adapter exists; +//! the default is the CPU (Software Rendering) //! Bridge: --bridge PORT drives a running `effectcraft --control PORT` instead of a headless session //! Output: --json for one compact JSON document on stdout (errors: {"error": …}, exit 1) //! ``` @@ -466,7 +466,7 @@ fn backend(args: &Args, default_demo: bool) -> Result { } /// A wired session; `--gpu` attaches the GPU compositor (renders then follow the project's -/// renderer setting, Mercury GPU Acceleration by default). +/// renderer setting, GPU Acceleration by default). fn session(args: &Args) -> Result { let mut s = effectcraft_host::session(); if args.flag("--autosave") { @@ -1443,7 +1443,7 @@ fn adv3d_bench_comp(p: &mut effectcraft_engine::project::Project) -> Option Result<(), Failure> { use effectcraft_render::Backend; diff --git a/crates/automation/tests/qa/compositing.rs b/crates/automation/tests/qa/compositing.rs index d5de8f687..3cf8356fb 100644 --- a/crates/automation/tests/qa/compositing.rs +++ b/crates/automation/tests/qa/compositing.rs @@ -106,7 +106,7 @@ fn track_matte(qa: &mut Qa) { fn adjustment_layer(qa: &mut Qa, normal: &Img) { // Adjustment layer: Lumetri exposure, inside a feathered, expanded elliptical mask; then Glow. qa.exec("layer.newAdjustment", json!({"name": "Grade"})); - let fx = qa.exec("effect.apply", json!({"effect": "Lumetri Color", "layers": ["Grade"]})); + let fx = qa.exec("effect.apply", json!({"effect": "Color Grading", "layers": ["Grade"]})); assert_eq!(fx["paths"], json!(["effects/#1"]), "effect.apply answers with the instance path: {fx}"); qa.tool("set_property", json!({"layer": "Grade", "path": "effects/#1/basicCorrection/tone/exposure", "value": 2})); let before = qa.frame(None, 0.5); diff --git a/crates/effects/src/blur2.rs b/crates/effects/src/blur2.rs index 929f41553..880ad16c5 100644 --- a/crates/effects/src/blur2.rs +++ b/crates/effects/src/blur2.rs @@ -1,6 +1,6 @@ //! Blur & Sharpen, batch 2: edge-preserving blurs (Bilateral, Smart), per-channel and map-driven //! blurs (Channel, Compound, CC Vector), bokeh (Camera Lens Blur), radial streaks and small -//! utilities (Reduce Interlace Flicker, CC Cross Blur). +//! utilities (Reduce Interlace Flicker, Cross Blur). use effectcraft_color::luminance; use effectcraft_keyframe::Value; @@ -507,9 +507,9 @@ fn camera_lens_blur(ctx: &EffectCtx, mut b: Buf) -> Buf { } // --------------------------------------------------------------------------------------------- -// CC Radial Fast Blur +// Fast Radial Blur -/// Transparent pixels CC Radial Fast Blur adds on every side of a `w` × `h` buffer with Center +/// Transparent pixels Fast Radial Blur adds on every side of a `w` × `h` buffer with Center /// at `c` (buffer pixels) and smear `s`: content `r` from Center streaks out to `r / (1 − s)`, /// so the farthest corner reaches `d · s / (1 − s)` past itself (at most 4096, as the other /// growing effects). Shared with the GPU path so both grow the same way. @@ -562,7 +562,7 @@ fn radial_fast_blur(ctx: &EffectCtx, mut b: Buf) -> Buf { } // --------------------------------------------------------------------------------------------- -// CC Vector Blur +// Vector Blur fn vector_blur(ctx: &EffectCtx, mut b: Buf) -> Buf { let amount = ctx.params.f("amount").max(0.0) * b.scale; @@ -616,7 +616,7 @@ fn vector_blur(ctx: &EffectCtx, mut b: Buf) -> Buf { } // --------------------------------------------------------------------------------------------- -// Reduce Interlace Flicker / CC Cross Blur +// Reduce Interlace Flicker / Cross Blur fn reduce_flicker(ctx: &EffectCtx, mut b: Buf) -> Buf { let s = ctx.params.f("softness").max(0.0) * b.scale; @@ -735,7 +735,7 @@ pub fn specs() -> Vec { ), spec( "ec.blur.ccradialfast", - "CC Radial Fast Blur", + "Fast Radial Blur", vec![ p("center", "Center", Value::Vec2([0.5, 0.5]), ParamUi::Point), p("amount", "Amount", num(50.0), slider(0.0, 100.0, 0.0, 100.0, 1)), @@ -745,7 +745,7 @@ pub fn specs() -> Vec { ), spec( "ec.blur.ccvector", - "CC Vector Blur", + "Vector Blur", vec![ p("type", "Type", Value::Enum(0), popup(&["Natural", "Constant Length", "Perpendicular", "Direction Center", "Direction Fading"])), p("amount", "Amount", num(10.0), slider(0.0, 500.0, 0.0, 100.0, 1)), @@ -765,7 +765,7 @@ pub fn specs() -> Vec { }, spec( "ec.blur.cccross", - "CC Cross Blur", + "Cross Blur", vec![ p("radiusX", "Radius X", num(10.0), slider(0.0, 500.0, 0.0, 100.0, 1)), p("radiusY", "Radius Y", num(10.0), slider(0.0, 500.0, 0.0, 100.0, 1)), diff --git a/crates/effects/src/blur3.rs b/crates/effects/src/blur3.rs index aee2a2713..6369a8ced 100644 --- a/crates/effects/src/blur3.rs +++ b/crates/effects/src/blur3.rs @@ -1,6 +1,6 @@ -//! Blur & Sharpen: CC Radial Blur and Camera-Shake Deblur. +//! Blur & Sharpen: Variable Radial Blur and Camera-Shake Deblur. //! -//! * **CC Radial Blur** — zoom (straight, fading, centred both ways) and rotational (plain or +//! * **Variable Radial Blur** — zoom (straight, fading, centred both ways) and rotational (plain or //! fading) blurs around a centre point, sampled along the ray / arc through each pixel with a //! sample count set by Quality. //! * **Camera-Shake Deblur** — frame-based restoration in the spirit of published video deblurring @@ -22,7 +22,7 @@ fn spec(id: &'static str, name: &'static str, params: Vec, ren EffectSpec { id, name, category: "Blur & Sharpen", params, render, gpu: false, float: true } } -// ---------------------------------------------------------------- CC Radial Blur +// ---------------------------------------------------------------- Variable Radial Blur pub const RADIAL_TYPES: [&str; 5] = ["Straight Zoom", "Fading Zoom", "Centered", "Rotate", "Rotate Fading"]; @@ -246,7 +246,7 @@ pub fn specs() -> Vec { vec![ spec( "ec.blur.ccradial", - "CC Radial Blur", + "Variable Radial Blur", vec![ p("type", "Type", Value::Enum(1), popup(&RADIAL_TYPES)), p("amount", "Amount", num(10.0), slider(-100.0, 100.0, -100.0, 100.0, 1)), diff --git a/crates/effects/src/channel2.rs b/crates/effects/src/channel2.rs index 5be3c94e7..ef391f6f1 100644 --- a/crates/effects/src/channel2.rs +++ b/crates/effects/src/channel2.rs @@ -1,5 +1,5 @@ //! Channel effects, batch 2: layer-to-layer combinations (Blend, Calculations, Compound -//! Arithmetic) and CC Composite. Second layers come from layer parameters through the effect +//! Arithmetic) and Source Composite. Second layers come from layer parameters through the effect //! host; with no layer chosen the effect uses the layer itself, as After Effects does. use effectcraft_color::{BlendMode, blend_pixel, hsl_to_rgb, luminance, rgb_to_hsl}; @@ -69,7 +69,7 @@ fn blend(ctx: &EffectCtx, mut b: Buf) -> Buf { // ---------------------------------------------------------------- Calculations -/// Blending modes offered by Calculations / CC Composite (menu order). +/// Blending modes offered by Calculations / Source Composite (menu order). const CALC_MODES: [BlendMode; 22] = [ BlendMode::Normal, BlendMode::Darken, @@ -150,7 +150,7 @@ fn calculations(ctx: &EffectCtx, mut b: Buf) -> Buf { b } -// ---------------------------------------------------------------- CC Composite +// ---------------------------------------------------------------- Source Composite const CC_COMPOSITE_MODES: [&str; 16] = [ "In Front", @@ -321,7 +321,7 @@ pub fn specs() -> Vec { ), spec( "ec.channel.cccomposite", - "CC Composite", + "Source Composite", vec![ p("opacity", "Opacity", num(100.0), pct()), p("compositeOriginal", "Composite Original", Value::Enum(0), popup(&CC_COMPOSITE_MODES)), diff --git a/crates/effects/src/color2.rs b/crates/effects/src/color2.rs index 25203cd7a..6639e649e 100644 --- a/crates/effects/src/color2.rs +++ b/crates/effects/src/color2.rs @@ -878,7 +878,7 @@ fn broadcast_colors(ctx: &EffectCtx, mut b: Buf) -> Buf { } // --------------------------------------------------------------------------------------------- -// CC Toner, CC Color Offset, CC Kernel +// Multi-Tone Tint, Channel Offset, Convolution Kernel fn cc_toner(ctx: &EffectCtx, mut b: Buf) -> Buf { let g = |id: &str| { @@ -1101,7 +1101,7 @@ pub fn specs() -> Vec { ), spec( "ec.color.cctoner", - "CC Toner", + "Multi-Tone Tint", vec![ p("tones", "Tones", Value::Enum(1), popup(&["Duotone", "Tritone", "Pentone"])), p("highlights", "Highlights", col(1.0, 1.0, 1.0), ParamUi::Color), @@ -1115,7 +1115,7 @@ pub fn specs() -> Vec { ), spec( "ec.color.cccoloroffset", - "CC Color Offset", + "Channel Offset", vec![ p("redPhase", "Red Phase", num(0.0), ParamUi::Angle), p("greenPhase", "Green Phase", num(0.0), ParamUi::Angle), @@ -1124,7 +1124,7 @@ pub fn specs() -> Vec { ], cc_color_offset, ), - spec("ec.color.cckernel", "CC Kernel", k_params, cc_kernel), + spec("ec.color.cckernel", "Convolution Kernel", k_params, cc_kernel), ] } diff --git a/crates/effects/src/color3.rs b/crates/effects/src/color3.rs index d4cdedd98..d0d398100 100644 --- a/crates/effects/src/color3.rs +++ b/crates/effects/src/color3.rs @@ -1,5 +1,5 @@ -//! Color Correction effects, batch 3: Change Color, Video Limiter, CC Color Neutralizer, -//! PS Arbitrary Map and Lumetri Color (Basic Correction, Creative, Curves, Color Wheels, +//! Color Correction effects, batch 3: Change Color, Video Limiter, Color Neutralizer, +//! PS Arbitrary Map and Color Grading (Basic Correction, Creative, Curves, Color Wheels, //! Vignette). use effectcraft_color::{hsl_to_rgb, luminance, rgb_to_hsl}; @@ -154,7 +154,7 @@ fn video_limiter(ctx: &EffectCtx, mut b: Buf) -> Buf { b } -// ---------------------------------------------------------------- CC Color Neutralizer +// ---------------------------------------------------------------- Color Neutralizer fn color_neutralizer(ctx: &EffectCtx, mut b: Buf) -> Buf { let pairs = [("shadowsUnbalance", "shadowsBalance"), ("midtonesUnbalance", "midtonesBalance"), ("highlightsUnbalance", "highlightsBalance")]; @@ -225,7 +225,7 @@ fn arbitrary_map(ctx: &EffectCtx, mut b: Buf) -> Buf { b } -// ---------------------------------------------------------------- Lumetri Color +// ---------------------------------------------------------------- Color Grading /// Smooth tone-curve adjustment: offsets for shadows/midtones/highlights (each -1..1), with the /// end points fixed. @@ -431,7 +431,7 @@ impl HslKey { } } -/// Lumetri Color: Basic Correction (input LUT, white balance, HDR range, tone, saturation), +/// Color Grading: Basic Correction (input LUT, white balance, HDR range, tone, saturation), /// Creative (look, adjustments), Curves (tone sliders, RGB curves, hue/saturation curves), /// Color Wheels, HSL Secondary (key, refine, correction), Vignette, in that order. fn lumetri(ctx: &EffectCtx, mut b: Buf) -> Buf { @@ -834,7 +834,7 @@ pub fn specs() -> Vec { ), spec( "ec.color.cccolorneutralizer", - "CC Color Neutralizer", + "Color Neutralizer", vec![ p("shadowsUnbalance", "Shadows Unbalance", col(0.0, 0.0, 0.0), ParamUi::Color), p("shadowsBalance", "Shadows Balance", col(0.0, 0.0, 0.0), ParamUi::Color), @@ -860,7 +860,7 @@ pub fn specs() -> Vec { ], arbitrary_map, ), - spec("ec.color.lumetri", "Lumetri Color", lum, lumetri), + spec("ec.color.lumetri", "Color Grading", lum, lumetri), ] } diff --git a/crates/effects/src/display_names.rs b/crates/effects/src/display_names.rs new file mode 100644 index 000000000..b30c00d46 --- /dev/null +++ b/crates/effects/src/display_names.rs @@ -0,0 +1,123 @@ +//! Display naming must not change file, command or script identities. +use crate::{find, instantiate, lookup, name_matches}; +use effectcraft_project::build::Ids; + +const RENAMED: &[(&str, &str, &str)] = &[ + ("ec.sim.ccballaction", "CC Ball Action", "Ball Grid"), + ("ec.distort.ccbendit", "CC Bend It", "Two-Point Bend"), + ("ec.distort.ccbender", "CC Bender", "Flexible Bend"), + ("ec.distort.ccblobbylize", "CC Blobbylize", "Organic Displacement"), + ("ec.stylize.ccblockload", "CC Block Load", "Block Reveal"), + ("ec.sim.ccbubbles", "CC Bubbles", "Bubbles"), + ("ec.stylize.ccburnfilm", "CC Burn Film", "Film Burn"), + ("ec.color.cccolorneutralizer", "CC Color Neutralizer", "Color Neutralizer"), + ("ec.color.cccoloroffset", "CC Color Offset", "Channel Offset"), + ("ec.channel.cccomposite", "CC Composite", "Source Composite"), + ("ec.blur.cccross", "CC Cross Blur", "Cross Blur"), + ("ec.perspective.cccylinder", "CC Cylinder", "Cylinder Wrap"), + ("ec.sim.ccdrizzle", "CC Drizzle", "Water Ripples"), + ("ec.perspective.ccenvironment", "CC Environment", "Environment Map"), + ("ec.distort.ccflomotion", "CC Flo Motion", "Dual Lens Warp"), + ("ec.time.ccforcemotionblur", "CC Force Motion Blur", "Temporal Motion Blur"), + ("ec.stylize.ccglass", "CC Glass", "Glass Relief"), + ("ec.transition.ccglasswipe", "CC Glass Wipe", "Glass Wipe"), + ("ec.generate.ccgluegun", "CC Glue Gun", "Paint Beads"), + ("ec.transition.ccgridwipe", "CC Grid Wipe", "Grid Wipe"), + ("ec.distort.ccgriddler", "CC Griddler", "Tile Grid"), + ("ec.sim.cchair", "CC Hair", "Surface Fibers"), + ("ec.stylize.cchextile", "CC HexTile", "Hexagonal Tiles"), + ("ec.transition.ccimagewipe", "CC Image Wipe", "Luminance Image Wipe"), + ("ec.transition.ccjaws", "CC Jaws", "Serrated Wipe"), + ("ec.stylize.cckaleida", "CC Kaleida", "Kaleidoscope"), + ("ec.color.cckernel", "CC Kernel", "Convolution Kernel"), + ("ec.distort.cclens", "CC Lens", "Lens Distortion"), + ("ec.generate.cclightburst", "CC Light Burst 2.5", "Radial Light Burst"), + ("ec.generate.cclightrays", "CC Light Rays", "Light Rays"), + ("ec.generate.cclightsweep", "CC Light Sweep", "Light Sweep"), + ("ec.transition.cclightwipe", "CC Light Wipe", "Light Wipe"), + ("ec.transition.cclinesweep", "CC Line Sweep", "Line Sweep"), + ("ec.sim.ccmrmercury", "CC Mr. Mercury", "Liquid Particles"), + ("ec.stylize.ccmrsmoothie", "CC Mr. Smoothie", "Palette Map"), + ("ec.utility.ccoverbrights", "CC Overbrights", "Out-of-Range Display"), + ("ec.distort.ccpageturn", "CC Page Turn", "Page Curl"), + ("ec.sim.ccparticlesystems2", "CC Particle Systems II", "Particle Emitter 2D"), + ("ec.sim.ccparticleworld", "CC Particle World", "Particle Emitter 3D"), + ("ec.sim.ccpixelpolly", "CC Pixel Polly", "Pixel Shatter"), + ("ec.stylize.ccplastic", "CC Plastic", "Plastic Shading"), + ("ec.distort.ccpowerpin", "CC Power Pin", "Perspective Pin"), + ("ec.blur.ccradial", "CC Radial Blur", "Variable Radial Blur"), + ("ec.blur.ccradialfast", "CC Radial Fast Blur", "Fast Radial Blur"), + ("ec.transition.ccradialscalewipe", "CC Radial ScaleWipe", "Radial Scale Wipe"), + ("ec.sim.ccrainfall", "CC Rainfall", "Rainfall"), + ("ec.stylize.ccrepetile", "CC RepeTile", "Edge Tile Repeat"), + ("ec.distort.ccripplepulse", "CC Ripple Pulse", "Pulsed Ripple"), + ("ec.transition.ccscalewipe", "CC Scale Wipe", "Scale Wipe"), + ("ec.sim.ccscatterize", "CC Scatterize", "Pixel Scatter"), + ("ec.key.ccsimplewireremoval", "CC Simple Wire Removal", "Line Removal"), + ("ec.distort.ccslant", "CC Slant", "Slant"), + ("ec.distort.ccsmear", "CC Smear", "Directional Smear"), + ("ec.sim.ccsnowfall", "CC Snowfall", "Snowfall"), + ("ec.perspective.ccsphere", "CC Sphere", "Sphere Wrap"), + ("ec.distort.ccsplit", "CC Split", "Split Warp"), + ("ec.distort.ccsplit2", "CC Split 2", "Dual Split Warp"), + ("ec.perspective.ccspotlight", "CC Spotlight", "Surface Spotlight"), + ("ec.sim.ccstarburst", "CC Star Burst", "Starfield"), + ("ec.generate.ccthreads", "CC Threads", "Woven Threads"), + ("ec.stylize.ccthreshold", "CC Threshold", "Channel Threshold"), + ("ec.stylize.ccthresholdrgb", "CC Threshold RGB", "RGB Threshold"), + ("ec.distort.cctiler", "CC Tiler", "Scaled Tiles"), + ("ec.color.cctoner", "CC Toner", "Multi-Tone Tint"), + ("ec.transition.cctwister", "CC Twister", "Twist Wipe"), + ("ec.blur.ccvector", "CC Vector Blur", "Vector Blur"), + ("ec.stylize.ccvignette", "CC Vignette", "Vignette"), + ("ec.transition.ccwarpomatic", "CC WarpoMatic", "Warp Dissolve"), + ("ec.time.ccwidetime", "CC Wide Time", "Temporal Blend"), + ("ec.color.lumetri", "Lumetri Color", "Color Grading"), + ("ec.obsolete.mochashape", "Mocha shape", "Tracked Shape Matte"), + ("ec.keying.keylight", "Key Light", "Screen Color Key"), +]; + +#[test] +fn descriptive_names_keep_every_legacy_lookup_and_search() { + for &(id, old, new) in RENAMED { + let spec = find(id).unwrap(); + assert_eq!(spec.name, new, "{id}"); + for query in [id.to_string(), old.to_string(), old.to_lowercase(), new.to_string()] { + assert_eq!(lookup(&query).map(|s| s.id), Some(id), "{query}"); + } + assert!(name_matches(spec, &old.to_lowercase()), "{old}"); + assert!(name_matches(spec, &new.to_lowercase()), "{new}"); + } + for alias in ["Keylight", "Keylight (1.2)", "Key Light"] { + assert_eq!(lookup(alias).map(|s| s.id), Some("ec.keying.keylight")); + } +} + +#[test] +fn descriptive_display_names_do_not_collide() { + let mut names = std::collections::HashSet::new(); + for spec in crate::registry() { + assert!(names.insert(spec.name.to_lowercase()), "duplicate name {}", spec.name); + let label = spec.name.to_lowercase(); + assert!(!label.starts_with("cc "), "{}", spec.name); + for mark in ["lumetri", "mercury", "mocha", "keylight"] { + assert!(!label.contains(mark), "{}", spec.name); + } + } +} + +#[test] +fn legacy_instance_names_and_parameter_data_survive_refresh() { + for &(id, old, _) in RENAMED { + let spec = find(id).unwrap(); + for stored in [old.to_string(), format!("{old} 2"), "My custom instance".to_string()] { + let mut next = 1; + let mut ids = Ids(&mut next); + let mut instance = instantiate(spec, &mut ids, &stored, [128.0, 72.0]); + let before = serde_json::to_value(&instance).unwrap(); + crate::migrate::upgrade_instance(spec, &mut instance, &mut ids, [128.0, 72.0]); + assert_eq!(instance.name, stored, "{id}: never rewrite a stored instance name"); + assert_eq!(serde_json::to_value(&instance).unwrap(), before, "{id}"); + } + } +} diff --git a/crates/effects/src/distort2.rs b/crates/effects/src/distort2.rs index 7add5fc58..bbd8738a8 100644 --- a/crates/effects/src/distort2.rs +++ b/crates/effects/src/distort2.rs @@ -1,6 +1,6 @@ //! Distort effects, batch 2: Turbulent Displace, Displacement Map (self as map), Optics -//! Compensation, Magnify, Mesh Warp, Bezier Warp, CC Bend It, CC Lens, CC Page Turn, CC Tiler, -//! CC Griddler. +//! Compensation, Magnify, Mesh Warp, Bezier Warp, Two-Point Bend, Lens Distortion, Page Curl, Scaled Tiles, +//! Tile Grid. //! //! Mesh Warp and Bezier Warp share a grid rasteriser: a (nx+1)×(ny+1) grid of destination points //! and matching source points is split into triangles, bucketed per output row and filled @@ -550,7 +550,7 @@ pub fn bezier_grid(ctx: &EffectCtx, offset: [f64; 2], scale: f64) -> (usize, Vec (n, dest, srcp) } -// ---------------------------------------------------------------- CC Bend It +// ---------------------------------------------------------------- Two-Point Bend fn bend_it(ctx: &EffectCtx, mut b: Buf) -> Buf { let mut beta = ctx.params.f("bend").to_radians(); @@ -606,7 +606,7 @@ fn bend_it(ctx: &EffectCtx, mut b: Buf) -> Buf { b } -// ---------------------------------------------------------------- CC Lens +// ---------------------------------------------------------------- Lens Distortion fn cc_lens(ctx: &EffectCtx, mut b: Buf) -> Buf { let c = b.to_px(ctx.params.v2("center")); @@ -629,7 +629,7 @@ fn cc_lens(ctx: &EffectCtx, mut b: Buf) -> Buf { b } -// ---------------------------------------------------------------- CC Page Turn +// ---------------------------------------------------------------- Page Curl fn page_turn(ctx: &EffectCtx, mut b: Buf) -> Buf { let f = b.to_px(ctx.params.v2("foldPosition")); @@ -679,7 +679,7 @@ fn page_turn(ctx: &EffectCtx, mut b: Buf) -> Buf { b } -// ---------------------------------------------------------------- CC Tiler / CC Griddler +// ---------------------------------------------------------------- Scaled Tiles / Tile Grid fn cc_tiler(ctx: &EffectCtx, mut b: Buf) -> Buf { let s = (ctx.params.f("scale") / 100.0).max(0.001); @@ -829,7 +829,7 @@ pub fn specs() -> Vec { ), spec( "ec.distort.ccbendit", - "CC Bend It", + "Two-Point Bend", vec![ p("bend", "Bend", num(0.0), ParamUi::Angle), p("start", "Start", pt(0.5, 0.2), ParamUi::Point), @@ -840,7 +840,7 @@ pub fn specs() -> Vec { ), spec( "ec.distort.cclens", - "CC Lens", + "Lens Distortion", vec![ p("center", "Center", pt(0.5, 0.5), ParamUi::Point), p("size", "Size", num(50.0), slider(0.0, 400.0, 0.0, 200.0, 1)), @@ -850,7 +850,7 @@ pub fn specs() -> Vec { ), spec( "ec.distort.ccpageturn", - "CC Page Turn", + "Page Curl", vec![ p("foldPosition", "Fold Position", pt(0.7, 0.7), ParamUi::Point), p("foldDirection", "Fold Direction", num(315.0), ParamUi::Angle), @@ -864,7 +864,7 @@ pub fn specs() -> Vec { ), spec( "ec.distort.cctiler", - "CC Tiler", + "Scaled Tiles", vec![ p("scale", "Scale", num(50.0), slider(1.0, 10000.0, 1.0, 200.0, 1)), p("center", "Center", pt(0.5, 0.5), ParamUi::Point), @@ -874,7 +874,7 @@ pub fn specs() -> Vec { ), spec( "ec.distort.ccgriddler", - "CC Griddler", + "Tile Grid", vec![ p("horizontalScale", "Horizontal Scale", num(100.0), slider(1.0, 1000.0, 1.0, 400.0, 1)), p("verticalScale", "Vertical Scale", num(100.0), slider(1.0, 1000.0, 1.0, 400.0, 1)), diff --git a/crates/effects/src/distort3.rs b/crates/effects/src/distort3.rs index a02ab5743..34fc282ed 100644 --- a/crates/effects/src/distort3.rs +++ b/crates/effects/src/distort3.rs @@ -1,5 +1,5 @@ -//! Distort effects, batch 3: CC Bender, CC Blobbylize, CC Power Pin, CC Ripple Pulse, CC Slant, -//! CC Smear, CC Split, CC Split 2, Smear and Reshape (mask driven), Twirl (Legacy), Warp (the +//! Distort effects, batch 3: Flexible Bend, Organic Displacement, Perspective Pin, Pulsed Ripple, Slant, +//! Directional Smear, Split Warp, Dual Split Warp, Smear and Reshape (mask driven), Twirl (Legacy), Warp (the //! fifteen warp styles) and Detail-preserving Upscale. //! //! All warps are inverse mapped: every output pixel finds the source point that lands on it. @@ -59,7 +59,7 @@ fn elastic_exp(i: u32) -> f64 { [1.0, 1.25, 1.5, 2.0, 2.5, 3.0, 3.5, 4.5][(i as usize).min(7)] } -// ---------------------------------------------------------------- CC Bender +// ---------------------------------------------------------------- Flexible Bend fn cc_bender(ctx: &EffectCtx, mut b: Buf) -> Buf { let amount = ctx.params.f("amount") / 100.0; @@ -107,7 +107,7 @@ fn norm3(v: [f64; 3]) -> [f64; 3] { [v[0] / l, v[1] / l, v[2] / l] } -/// f32 normal/dot helpers for the CC Blobbylize lighting pass. The GPU kernel runs in f32; +/// f32 normal/dot helpers for the Organic Displacement lighting pass. The GPU kernel runs in f32; /// matching its precision here keeps specular highlights within the CPU/GPU parity tolerance. fn norm3f(v: [f32; 3]) -> [f32; 3] { let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(1e-12); @@ -118,7 +118,7 @@ fn dot3f(a: [f32; 3], b: [f32; 3]) -> f32 { a[0] * b[0] + a[1] * b[1] + a[2] * b[2] } -// ---------------------------------------------------------------- CC Blobbylize +// ---------------------------------------------------------------- Organic Displacement fn cc_blobbylize(ctx: &EffectCtx, mut b: Buf) -> Buf { let soft = ctx.params.f("softness").max(0.0) * b.scale; @@ -221,7 +221,7 @@ fn cc_blobbylize(ctx: &EffectCtx, mut b: Buf) -> Buf { b } -// ---------------------------------------------------------------- CC Power Pin +// ---------------------------------------------------------------- Perspective Pin fn cc_power_pin(ctx: &EffectCtx, mut b: Buf) -> Buf { let (lw, lh) = (ctx.layer_size[0].max(1.0), ctx.layer_size[1].max(1.0)); @@ -304,7 +304,7 @@ fn cc_power_pin(ctx: &EffectCtx, mut b: Buf) -> Buf { b } -// ---------------------------------------------------------------- CC Ripple Pulse +// ---------------------------------------------------------------- Pulsed Ripple fn cc_ripple_pulse(ctx: &EffectCtx, mut b: Buf) -> Buf { let level = ctx.params.f("pulseLevel") / 100.0; @@ -350,7 +350,7 @@ fn cc_ripple_pulse(ctx: &EffectCtx, mut b: Buf) -> Buf { b } -// ---------------------------------------------------------------- CC Slant +// ---------------------------------------------------------------- Slant fn cc_slant(ctx: &EffectCtx, mut b: Buf) -> Buf { let k = ctx.params.f("slant") / 100.0; @@ -375,7 +375,7 @@ fn cc_slant(ctx: &EffectCtx, mut b: Buf) -> Buf { b } -// ---------------------------------------------------------------- CC Smear +// ---------------------------------------------------------------- Directional Smear fn cc_smear(ctx: &EffectCtx, mut b: Buf) -> Buf { let reach = ctx.params.f("reach") / 100.0; @@ -403,7 +403,7 @@ fn cc_smear(ctx: &EffectCtx, mut b: Buf) -> Buf { } /// The transparent pixels to add on every side of a buffer `size` pixels big (with `offset` and -/// `scale`) for CC Smear: it moves pixels by up to Reach × From→To, and they may move out to the +/// `scale`) for Directional Smear: it moves pixels by up to Reach × From→To, and they may move out to the /// layer bounds. A text or shape layer's buffer ends at its content while its bounds are /// comp-sized, so the smear was cut off at the content's box (#659). pub fn cc_smear_pad(ctx: &EffectCtx, size: [u32; 2], offset: [f64; 2], scale: f64) -> u32 { @@ -417,7 +417,7 @@ pub fn cc_smear_pad(ctx: &EffectCtx, size: [u32; 2], offset: [f64; 2], scale: f6 (moved.min(4096.0) as u32).min(crate::util::layer_cover_pad(ctx, size, offset, scale)) } -// ---------------------------------------------------------------- CC Split / CC Split 2 +// ---------------------------------------------------------------- Split Warp / Dual Split Warp fn split_impl(ctx: &EffectCtx, mut b: Buf, s1: f64, s2: f64) -> Buf { if s1 == 0.0 && s2 == 0.0 { @@ -913,7 +913,7 @@ pub fn specs() -> Vec { vec![ spec( "ec.distort.ccbender", - "CC Bender", + "Flexible Bend", vec![ p("amount", "Amount", num(0.0), slider(-200.0, 200.0, -100.0, 100.0, 1)), p("style", "Style", Value::Enum(0), popup(&["Bend", "Marilyn", "Sharp", "Boxer"])), @@ -925,7 +925,7 @@ pub fn specs() -> Vec { ), spec( "ec.distort.ccblobbylize", - "CC Blobbylize", + "Organic Displacement", vec![ p("blobLayer", "Blob Layer", Value::Layer(None), ParamUi::Layer), p("property", "Property", Value::Enum(0), popup(&crate::util::SRC_NAMES[..8])), @@ -947,7 +947,7 @@ pub fn specs() -> Vec { ), spec( "ec.distort.ccpowerpin", - "CC Power Pin", + "Perspective Pin", vec![ p("topLeft", "Top Left", pt(0.0, 0.0), ParamUi::Point), p("topRight", "Top Right", pt(1.0, 0.0), ParamUi::Point), @@ -964,7 +964,7 @@ pub fn specs() -> Vec { ), spec( "ec.distort.ccripplepulse", - "CC Ripple Pulse", + "Pulsed Ripple", vec![ p("center", "Center", pt(0.5, 0.5), ParamUi::Point), p("pulseLevel", "Pulse Level (animate)", num(0.0), slider(-1000.0, 1000.0, -100.0, 100.0, 1)), @@ -976,7 +976,7 @@ pub fn specs() -> Vec { ), spec( "ec.distort.ccslant", - "CC Slant", + "Slant", vec![ p("slant", "Slant", num(0.0), slider(-1000.0, 1000.0, -100.0, 100.0, 1)), p("stretching", "Stretching", Value::Bool(false), ParamUi::Checkbox), @@ -987,7 +987,7 @@ pub fn specs() -> Vec { ), spec( "ec.distort.ccsmear", - "CC Smear", + "Directional Smear", vec![ p("from", "From", pt(0.35, 0.5), ParamUi::Point), p("to", "To", pt(0.65, 0.5), ParamUi::Point), @@ -998,7 +998,7 @@ pub fn specs() -> Vec { ), spec( "ec.distort.ccsplit", - "CC Split", + "Split Warp", vec![ p("pointA", "Point A", pt(0.25, 0.5), ParamUi::Point), p("pointB", "Point B", pt(0.75, 0.5), ParamUi::Point), @@ -1008,7 +1008,7 @@ pub fn specs() -> Vec { ), spec( "ec.distort.ccsplit2", - "CC Split 2", + "Dual Split Warp", vec![ p("pointA", "Point A", pt(0.25, 0.5), ParamUi::Point), p("pointB", "Point B", pt(0.75, 0.5), ParamUi::Point), diff --git a/crates/effects/src/distort4.rs b/crates/effects/src/distort4.rs index b724a5830..9913c6ea5 100644 --- a/crates/effects/src/distort4.rs +++ b/crates/effects/src/distort4.rs @@ -1,6 +1,6 @@ -//! Distort: CC Flo Motion, Liquify and Rolling Shutter Repair. +//! Distort: Dual Lens Warp, Liquify and Rolling Shutter Repair. //! -//! * **CC Flo Motion** — two knots that pull the image toward themselves (positive amount) or +//! * **Dual Lens Warp** — two knots that pull the image toward themselves (positive amount) or //! push it away (negative), with a falloff exponent, optional tiling and supersampled //! antialiasing. //! * **Liquify** — brush strokes build a displacement mesh (a grid of layer-space offsets). The @@ -32,7 +32,7 @@ fn spec(id: &'static str, name: &'static str, params: Vec, ren EffectSpec { id, name, category: "Distort", params, render, gpu: false, float: true } } -// ---------------------------------------------------------------- CC Flo Motion +// ---------------------------------------------------------------- Dual Lens Warp fn flo_motion(ctx: &EffectCtx, mut b: Buf) -> Buf { let knots = [(ctx.params.v2("knot1"), ctx.params.f("amount1") / 100.0), (ctx.params.v2("knot2"), ctx.params.f("amount2") / 100.0)]; @@ -560,7 +560,7 @@ pub fn specs() -> Vec { vec![ spec( "ec.distort.ccflomotion", - "CC Flo Motion", + "Dual Lens Warp", vec![ p("knot1", "Knot 1", Value::Vec2([0.25, 0.5]), ParamUi::Point), p("amount1", "Amount 1", num(50.0), slider(-400.0, 100.0, -100.0, 100.0, 1)), diff --git a/crates/effects/src/generate2.rs b/crates/effects/src/generate2.rs index b4781b793..ad80cb9cd 100644 --- a/crates/effects/src/generate2.rs +++ b/crates/effects/src/generate2.rs @@ -1,5 +1,5 @@ //! Generate effects, batch 2: Cell Pattern (Worley), Ellipse, Lens Flare, Beam, Radio Waves, -//! Advanced Lightning, CC Light Rays, CC Light Burst 2.5, CC Light Sweep. +//! Advanced Lightning, Light Rays, Radial Light Burst, Light Sweep. //! //! All designs are original procedural renderings written from the public descriptions of what //! each effect produces. @@ -897,7 +897,7 @@ fn advanced_lightning(ctx: &EffectCtx, mut b: Buf) -> Buf { b } -// ---------------------------------------------------------------- CC Light Rays / Burst +// ---------------------------------------------------------------- Light Rays / Burst const RAY_STEPS: usize = 32; @@ -1007,7 +1007,7 @@ fn light_burst(ctx: &EffectCtx, mut b: Buf) -> Buf { b } -// ---------------------------------------------------------------- CC Light Sweep +// ---------------------------------------------------------------- Light Sweep fn light_sweep(ctx: &EffectCtx, mut b: Buf) -> Buf { let c = b.to_px(ctx.params.v2("center")); @@ -1192,7 +1192,7 @@ pub fn specs() -> Vec { ), spec( "ec.generate.cclightrays", - "CC Light Rays", + "Light Rays", vec![ p("intensity", "Intensity", num(100.0), slider(0.0, 600.0, 0.0, 200.0, 1)), p("center", "Center", pt(0.5, 0.5), ParamUi::Point), @@ -1207,7 +1207,7 @@ pub fn specs() -> Vec { ), spec( "ec.generate.cclightburst", - "CC Light Burst 2.5", + "Radial Light Burst", vec![ p("center", "Center", pt(0.5, 0.5), ParamUi::Point), p("intensity", "Intensity", num(100.0), slider(0.0, 600.0, 0.0, 200.0, 1)), @@ -1220,7 +1220,7 @@ pub fn specs() -> Vec { ), spec( "ec.generate.cclightsweep", - "CC Light Sweep", + "Light Sweep", vec![ p("center", "Center", pt(0.5, 0.5), ParamUi::Point), p("direction", "Direction", num(-30.0), ang()), diff --git a/crates/effects/src/generate3/registry.rs b/crates/effects/src/generate3/registry.rs index f3e08c2d5..baa493110 100644 --- a/crates/effects/src/generate3/registry.rs +++ b/crates/effects/src/generate3/registry.rs @@ -180,7 +180,7 @@ pub fn specs() -> Vec { ), spec( "ec.generate.ccgluegun", - "CC Glue Gun", + "Paint Beads", vec![ p("brushPosition", "Brush Position", pt(0.5, 0.5), ParamUi::Point), p("strokeWidth", "Stroke Width", num(25.0), slider(0.0, 500.0, 0.0, 100.0, 1)), @@ -194,7 +194,7 @@ pub fn specs() -> Vec { ), spec( "ec.generate.ccthreads", - "CC Threads", + "Woven Threads", vec![ p("width", "Width", num(50.0), slider(1.0, 1000.0, 1.0, 200.0, 1)), p("height", "Height", num(50.0), slider(1.0, 1000.0, 1.0, 200.0, 1)), diff --git a/crates/effects/src/keying2.rs b/crates/effects/src/keying2.rs index a03fe804d..5512d1820 100644 --- a/crates/effects/src/keying2.rs +++ b/crates/effects/src/keying2.rs @@ -317,7 +317,7 @@ fn unmult(ctx: &EffectCtx, mut b: Buf) -> Buf { b } -// ---------------------------------------------------------------- CC Simple Wire Removal +// ---------------------------------------------------------------- Line Removal fn wire_removal(ctx: &EffectCtx, mut b: Buf) -> Buf { let pa = ctx.params.v2("pointA"); @@ -464,7 +464,7 @@ pub fn specs() -> Vec { ), spec( "ec.key.ccsimplewireremoval", - "CC Simple Wire Removal", + "Line Removal", vec![ p("pointA", "Point A", Value::Vec2([0.25, 0.25]), ParamUi::Point), p("pointB", "Point B", Value::Vec2([0.75, 0.75]), ParamUi::Point), diff --git a/crates/effects/src/keylight.rs b/crates/effects/src/keylight.rs index 84085f4e3..fb36075cb 100644 --- a/crates/effects/src/keylight.rs +++ b/crates/effects/src/keylight.rs @@ -1,6 +1,6 @@ -//! Key Light — a full screen-colour keyer with the controls of After Effects' bundled +//! Screen Color Key — a full screen-colour keyer with the controls of After Effects' bundled //! "Keylight (1.2)" (that name is a trademark of its vendor, so the effect is registered as -//! **Key Light**; `lookup("Keylight (1.2)")` finds it through [`KEYLIGHT_ALIASES`]). +//! **Screen Color Key**; `lookup("Keylight (1.2)")` finds it through [`KEYLIGHT_ALIASES`]). //! //! The method is the textbook *screen difference* key, written from the public description of //! the controls: @@ -31,7 +31,7 @@ use crate::util::{Plane, gauss_plane, morph_frac, point_in_poly, premul, unpremu use crate::{Buf, EffectCtx, EffectSpec, col, num, p, popup, slider}; /// Other names the effect answers to in [`crate::lookup`]. -pub const KEYLIGHT_ALIASES: &[&str] = &["Keylight (1.2)", "Keylight"]; +pub const KEYLIGHT_ALIASES: &[&str] = &["Keylight (1.2)", "Keylight", "Key Light"]; pub const VIEWS: [&str; 11] = [ "Source", @@ -463,7 +463,7 @@ pub fn specs() -> Vec { let cc = |id: &'static str, name: &'static str| p(id, name, num(0.0), slider(-100.0, 100.0, -100.0, 100.0, 1)); vec![EffectSpec { id: "ec.keying.keylight", - name: "Key Light", + name: "Screen Color Key", category: "Keying", params: vec![ p("view", "View", Value::Enum(10), popup(&VIEWS)), @@ -622,7 +622,7 @@ mod tests { let spec = crate::find("ec.keying.keylight").unwrap(); let mut next = 1; let mut ids = Ids(&mut next); - let mut g = crate::instantiate(spec, &mut ids, "Key Light", [10.0, 10.0]); + let mut g = crate::instantiate(spec, &mut ids, "Screen Color Key", [10.0, 10.0]); // Flatten it the way older versions saved it. let mut flat = vec![]; g.walk_mut(&mut |p| flat.push(p.clone())); diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index bd239aa6f..0d78c3619 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -23,6 +23,8 @@ mod color3; mod color_fx; pub mod controls; pub mod crypto_select; +#[cfg(test)] +mod display_names; mod distort; mod distort2; mod distort3; @@ -36,6 +38,7 @@ mod keying; mod keying2; pub mod keylight; mod matte; + pub mod migrate; mod misc; pub mod mocha_shape; @@ -560,7 +563,7 @@ pub trait EffectHost: Sync { } /// Parameter `id` (a [`flatten_params`] id such as `birthRate`) of effect `effect_index` in /// the layer's stack (the running effect's [`EffectEnv::effect_index`]) at another **layer - /// time**: CC Particle World samples its Birth Rate at every step it simulates (#1373). + /// time**: Particle Emitter 3D samples its Birth Rate at every step it simulates (#1373). /// `None` when unavailable. fn param_at(&self, _effect_index: usize, _id: &str, _layer_time: f64) -> Option { None @@ -776,8 +779,82 @@ pub fn categories() -> Vec<&'static str> { v } -/// After Effects' third-party display names of effects we register under a generic name. -const ALIASES: &[(&str, &[&str])] = &[("ec.keying.keylight", keylight::KEYLIGHT_ALIASES)]; +/// Legacy display names accepted for saved scripts and searches. Never display these as +/// native feature names; stable effect IDs and external format match names stay unchanged. +const ALIASES: &[(&str, &[&str])] = &[ + ("ec.sim.ccballaction", &["CC Ball Action"]), + ("ec.distort.ccbendit", &["CC Bend It"]), + ("ec.distort.ccbender", &["CC Bender"]), + ("ec.distort.ccblobbylize", &["CC Blobbylize"]), + ("ec.stylize.ccblockload", &["CC Block Load"]), + ("ec.sim.ccbubbles", &["CC Bubbles"]), + ("ec.stylize.ccburnfilm", &["CC Burn Film"]), + ("ec.color.cccolorneutralizer", &["CC Color Neutralizer"]), + ("ec.color.cccoloroffset", &["CC Color Offset"]), + ("ec.channel.cccomposite", &["CC Composite"]), + ("ec.blur.cccross", &["CC Cross Blur"]), + ("ec.perspective.cccylinder", &["CC Cylinder"]), + ("ec.sim.ccdrizzle", &["CC Drizzle"]), + ("ec.perspective.ccenvironment", &["CC Environment"]), + ("ec.distort.ccflomotion", &["CC Flo Motion"]), + ("ec.time.ccforcemotionblur", &["CC Force Motion Blur"]), + ("ec.stylize.ccglass", &["CC Glass"]), + ("ec.transition.ccglasswipe", &["CC Glass Wipe"]), + ("ec.generate.ccgluegun", &["CC Glue Gun"]), + ("ec.transition.ccgridwipe", &["CC Grid Wipe"]), + ("ec.distort.ccgriddler", &["CC Griddler"]), + ("ec.sim.cchair", &["CC Hair"]), + ("ec.stylize.cchextile", &["CC HexTile"]), + ("ec.transition.ccimagewipe", &["CC Image Wipe"]), + ("ec.transition.ccjaws", &["CC Jaws"]), + ("ec.stylize.cckaleida", &["CC Kaleida"]), + ("ec.color.cckernel", &["CC Kernel"]), + ("ec.distort.cclens", &["CC Lens"]), + ("ec.generate.cclightburst", &["CC Light Burst 2.5"]), + ("ec.generate.cclightrays", &["CC Light Rays"]), + ("ec.generate.cclightsweep", &["CC Light Sweep"]), + ("ec.transition.cclightwipe", &["CC Light Wipe"]), + ("ec.transition.cclinesweep", &["CC Line Sweep"]), + ("ec.sim.ccmrmercury", &["CC Mr. Mercury"]), + ("ec.stylize.ccmrsmoothie", &["CC Mr. Smoothie"]), + ("ec.utility.ccoverbrights", &["CC Overbrights"]), + ("ec.distort.ccpageturn", &["CC Page Turn"]), + ("ec.sim.ccparticlesystems2", &["CC Particle Systems II"]), + ("ec.sim.ccparticleworld", &["CC Particle World"]), + ("ec.sim.ccpixelpolly", &["CC Pixel Polly"]), + ("ec.stylize.ccplastic", &["CC Plastic"]), + ("ec.distort.ccpowerpin", &["CC Power Pin"]), + ("ec.blur.ccradial", &["CC Radial Blur"]), + ("ec.blur.ccradialfast", &["CC Radial Fast Blur"]), + ("ec.transition.ccradialscalewipe", &["CC Radial ScaleWipe"]), + ("ec.sim.ccrainfall", &["CC Rainfall"]), + ("ec.stylize.ccrepetile", &["CC RepeTile"]), + ("ec.distort.ccripplepulse", &["CC Ripple Pulse"]), + ("ec.transition.ccscalewipe", &["CC Scale Wipe"]), + ("ec.sim.ccscatterize", &["CC Scatterize"]), + ("ec.key.ccsimplewireremoval", &["CC Simple Wire Removal"]), + ("ec.distort.ccslant", &["CC Slant"]), + ("ec.distort.ccsmear", &["CC Smear"]), + ("ec.sim.ccsnowfall", &["CC Snowfall"]), + ("ec.perspective.ccsphere", &["CC Sphere"]), + ("ec.distort.ccsplit", &["CC Split"]), + ("ec.distort.ccsplit2", &["CC Split 2"]), + ("ec.perspective.ccspotlight", &["CC Spotlight"]), + ("ec.sim.ccstarburst", &["CC Star Burst"]), + ("ec.generate.ccthreads", &["CC Threads"]), + ("ec.stylize.ccthreshold", &["CC Threshold"]), + ("ec.stylize.ccthresholdrgb", &["CC Threshold RGB"]), + ("ec.distort.cctiler", &["CC Tiler"]), + ("ec.color.cctoner", &["CC Toner"]), + ("ec.transition.cctwister", &["CC Twister"]), + ("ec.blur.ccvector", &["CC Vector Blur"]), + ("ec.stylize.ccvignette", &["CC Vignette"]), + ("ec.transition.ccwarpomatic", &["CC WarpoMatic"]), + ("ec.time.ccwidetime", &["CC Wide Time"]), + ("ec.color.lumetri", &["Lumetri Color"]), + ("ec.obsolete.mochashape", &["Mocha shape", "mocha shape"]), + ("ec.keying.keylight", keylight::KEYLIGHT_ALIASES), +]; /// Other names effect `id` answers to (in [`lookup`] and searches). pub fn aliases(id: &str) -> &'static [&'static str] { @@ -861,7 +938,7 @@ pub fn instantiate(spec: &EffectSpec, ids: &mut Ids, instance_name: &str, layer_ /// effects without their own table. pub const PARAM_GROUPS: &[(&str, &str)] = &[ // Color Correction: Exposure, Levels (Individual Controls), Change to Color, Colorama, - // Shadow/Highlight, Lumetri Color. + // Shadow/Highlight, Color Grading. ("master", "Master"), ("rgb", "RGB"), ("red", "Red"), @@ -990,7 +1067,7 @@ pub const PARAM_GROUPS: &[(&str, &str)] = &[ ("particleExploder", "Particle Exploder"), ("affects", "Affects"), ("options", "Options"), - // Keying, Matte, Obsolete (Key Light's groups are in `keylight::GROUPS`). + // Keying, Matte, Obsolete (Screen Color Key's groups are in `keylight::GROUPS`). ("ultraSettings", "Ultra Settings"), ("additionalForeground", "Additional Foreground"), ("additionalBackground", "Additional Background"), @@ -1075,7 +1152,7 @@ fn nested_group<'a>(g: &'a mut PropGroup, ids: &mut Ids, path: &str) -> Option<& } /// Effects the GPU compositor (`effectcraft-gpu`) implements with the CPU effect's semantics -/// (Mercury GPU Acceleration; the Effects & Presets GPU badge). Expression controls are +/// (GPU Acceleration; the Effects & Presets GPU badge). Expression controls are /// pass-throughs and count as GPU effects too. pub const GPU_EFFECTS: &[&str] = &[ "ec.blur.gaussian", @@ -1495,7 +1572,7 @@ mod tests { assert_eq!(grow_sides(lo, hi, 20, 10), Some([0, 0, 30, 20])); } - /// #2261: a 20 × 20 layer with CC Radial Fast Blur's Center far away grows to at most 16 Mpx; + /// #2261: a 20 × 20 layer with Fast Radial Blur's Center far away grows to at most 16 Mpx; /// big layers may still grow to four times their area, and small pads are untouched. #[test] fn padded_buffers_stay_within_the_budget() { diff --git a/crates/effects/src/mocha_shape.rs b/crates/effects/src/mocha_shape.rs index cf9e2bd88..c669580c6 100644 --- a/crates/effects/src/mocha_shape.rs +++ b/crates/effects/src/mocha_shape.rs @@ -1,4 +1,4 @@ -//! Matte ▸ Mocha shape: renders planar-tracked roto shapes as a matte. +//! Matte ▸ Tracked Shape Matte: renders planar-tracked roto shapes as a matte. //! //! Mocha's own clipboard / export formats are not publicly specified, so the effect reads shape //! data in a small documented JSON format of ours (in the hidden `shapeData` parameter, either the @@ -184,7 +184,7 @@ fn mocha_shape(ctx: &EffectCtx, mut b: Buf) -> Buf { pub fn specs() -> Vec { vec![EffectSpec { id: "ec.obsolete.mochashape", - name: "Mocha shape", + name: "Tracked Shape Matte", category: "Matte", params: vec![ p("blendMode", "Blend mode", Value::Enum(0), popup(&["Add", "Subtract", "Intersect", "Difference"])), diff --git a/crates/effects/src/perspective.rs b/crates/effects/src/perspective.rs index 24c561ecd..69c63caf9 100644 --- a/crates/effects/src/perspective.rs +++ b/crates/effects/src/perspective.rs @@ -1,5 +1,5 @@ -//! Perspective effects (batch 2): Bevel Alpha, Bevel Edges, Radial Shadow, CC Cylinder, -//! CC Sphere and CC Spotlight. +//! Perspective effects (batch 2): Bevel Alpha, Bevel Edges, Radial Shadow, Cylinder Wrap, +//! Sphere Wrap and Surface Spotlight. //! //! Angles follow the convention used by Drop Shadow: measured clockwise from "up", so a light //! angle `a` sits in the direction `(sin a, -cos a)` (y grows downwards). 3D vectors use x right, @@ -158,7 +158,7 @@ fn radial_shadow(ctx: &EffectCtx, mut b: Buf) -> Buf { b } -/// Phong-style shading shared by CC Cylinder and CC Sphere. +/// Phong-style shading shared by Cylinder Wrap and Sphere Wrap. struct Shading { l: [f64; 3], color: [f32; 4], @@ -451,11 +451,11 @@ pub fn specs() -> Vec { ], radial_shadow, ), - spec("ec.perspective.cccylinder", "CC Cylinder", cyl, cylinder), - spec("ec.perspective.ccsphere", "CC Sphere", sph, sphere), + spec("ec.perspective.cccylinder", "Cylinder Wrap", cyl, cylinder), + spec("ec.perspective.ccsphere", "Sphere Wrap", sph, sphere), spec( "ec.perspective.ccspotlight", - "CC Spotlight", + "Surface Spotlight", vec![ p("from", "From", pt(0.3, 0.2), ParamUi::Point), p("to", "To", pt(0.5, 0.5), ParamUi::Point), diff --git a/crates/effects/src/perspective2.rs b/crates/effects/src/perspective2.rs index 13846607f..2bc7e3e7e 100644 --- a/crates/effects/src/perspective2.rs +++ b/crates/effects/src/perspective2.rs @@ -1,4 +1,4 @@ -//! Perspective effects, batch 2: CC Environment (reflection mapping of an environment layer +//! Perspective effects, batch 2: Environment Map (reflection mapping of an environment layer //! onto the layer's luminance/alpha relief) and 3D Glasses (stereo pairs and anaglyphs from a //! left and right view layer). @@ -17,7 +17,7 @@ fn spec(id: &'static str, name: &'static str, params: Vec, ren EffectSpec { id, name, category: "Perspective", params, render, gpu: false, float: true } } -// ---------------------------------------------------------------- CC Environment +// ---------------------------------------------------------------- Environment Map /// Environment direction (x right, y down, z towards the viewer) → normalised map coordinates. fn env_uv(mapping: u32, r: [f64; 3]) -> (f64, f64) { @@ -194,7 +194,7 @@ pub fn specs() -> Vec { vec![ spec( "ec.perspective.ccenvironment", - "CC Environment", + "Environment Map", vec![ p("environment", "Environment", Value::Layer(None), ParamUi::Layer), p("mapping", "Mapping", Value::Enum(0), popup(&["Spherical", "Probe", "Vertical Cross"])), diff --git a/crates/effects/src/sim/registry.rs b/crates/effects/src/sim/registry.rs index f55b99e27..4df07c50a 100644 --- a/crates/effects/src/sim/registry.rs +++ b/crates/effects/src/sim/registry.rs @@ -32,7 +32,7 @@ pub fn specs() -> Vec { vec![ spec( "ec.sim.ccrainfall", - "CC Rainfall", + "Rainfall", vec![ p("drops", "Drops", num(5000.0), slider(0.0, 100_000.0, 0.0, 20_000.0, 0)), p("size", "Size", num(1.0), slider(0.0, 50.0, 0.0, 10.0, 2)), @@ -53,7 +53,7 @@ pub fn specs() -> Vec { ), spec( "ec.sim.ccsnowfall", - "CC Snowfall", + "Snowfall", vec![ p("flakes", "Flakes", num(10_000.0), slider(0.0, 200_000.0, 0.0, 50_000.0, 0)), p("size", "Size", num(3.0), slider(0.0, 50.0, 0.0, 20.0, 2)), @@ -79,7 +79,7 @@ pub fn specs() -> Vec { ), spec( "ec.sim.ccbubbles", - "CC Bubbles", + "Bubbles", vec![ p("bubbleAmount", "Bubble Amount", num(100.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)), p("bubbleSpeed", "Bubble Speed", num(0.5), slider(-10.0, 10.0, -2.0, 2.0, 2)), @@ -93,7 +93,7 @@ pub fn specs() -> Vec { ), spec( "ec.sim.ccdrizzle", - "CC Drizzle", + "Water Ripples", vec![ p("dripRate", "Drip Rate", num(10.0), slider(0.0, 200.0, 0.0, 50.0, 2)), p("longevity", "Longevity (sec)", num(1.0), slider(0.0, 30.0, 0.0, 5.0, 2)), @@ -114,7 +114,7 @@ pub fn specs() -> Vec { ), spec( "ec.sim.ccstarburst", - "CC Star Burst", + "Starfield", vec![ p("scatter", "Scatter", num(100.0), slider(0.0, 1000.0, 0.0, 300.0, 1)), p("speed", "Speed", num(1.0), slider(-10.0, 10.0, -4.0, 4.0, 2)), @@ -127,7 +127,7 @@ pub fn specs() -> Vec { ), spec( "ec.sim.ccparticleworld", - "CC Particle World", + "Particle Emitter 3D", vec![ p("birthRate", "Birth Rate", num(0.5), slider(0.0, 100.0, 0.0, 10.0, 2)), p("longevity", "Longevity (sec)", num(1.0), slider(0.0, 100.0, 0.0, 10.0, 2)), @@ -170,7 +170,7 @@ pub fn specs() -> Vec { ), spec( "ec.sim.ccparticlesystems2", - "CC Particle Systems II", + "Particle Emitter 2D", vec![ p("birthRate", "Birth Rate", num(2.0), slider(0.0, 100.0, 0.0, 20.0, 2)), p("longevity", "Longevity (sec)", num(1.0), slider(0.0, 100.0, 0.0, 10.0, 2)), @@ -198,7 +198,7 @@ pub fn specs() -> Vec { ), spec( "ec.sim.ccmrmercury", - "CC Mr. Mercury", + "Liquid Particles", vec![ p("radiusX", "Radius X", num(50.0), slider(0.0, 2000.0, 0.0, 300.0, 1)), p("radiusY", "Radius Y", num(50.0), slider(0.0, 2000.0, 0.0, 300.0, 1)), diff --git a/crates/effects/src/sim2/registry.rs b/crates/effects/src/sim2/registry.rs index 333916a16..dc64b7097 100644 --- a/crates/effects/src/sim2/registry.rs +++ b/crates/effects/src/sim2/registry.rs @@ -79,7 +79,7 @@ pub fn specs() -> Vec { vec![ spec( "ec.sim.ccballaction", - "CC Ball Action", + "Ball Grid", vec![ p("scatter", "Scatter", num(0.0), slider(0.0, 2000.0, 0.0, 300.0, 1)), p("rotationAxis", "Rotation Axis", Value::Enum(0), popup(&["X Axis", "Y Axis", "Z Axis", "XY Axis", "XZ Axis", "YZ Axis", "XYZ Axis"])), @@ -94,7 +94,7 @@ pub fn specs() -> Vec { ), spec( "ec.sim.ccpixelpolly", - "CC Pixel Polly", + "Pixel Shatter", vec![ p("force", "Force", num(100.0), slider(-1000.0, 1000.0, -300.0, 300.0, 1)), p("gravity", "Gravity", num(1.0), slider(-20.0, 20.0, -5.0, 5.0, 2)), @@ -111,7 +111,7 @@ pub fn specs() -> Vec { ), spec( "ec.sim.ccscatterize", - "CC Scatterize", + "Pixel Scatter", vec![ p("scatter", "Scatter", num(0.0), slider(0.0, 1000.0, 0.0, 200.0, 1)), p("rightTwist", "Right Twist", num(0.0), ParamUi::Angle), diff --git a/crates/effects/src/sim3.rs b/crates/effects/src/sim3.rs index 88a16d981..c2a286390 100644 --- a/crates/effects/src/sim3.rs +++ b/crates/effects/src/sim3.rs @@ -1,4 +1,4 @@ -//! Simulation: Particle Playground and CC Hair. +//! Simulation: Particle Playground and Surface Fibers. //! //! **Particle Playground** is a deterministic 2D particle system stepped from layer time 0 at //! [`SPS`] steps per second through [`SimCache`] (seeking anywhere gives the same frame as @@ -17,7 +17,7 @@ //! being rendered (their pixels are part of the cache key). Particles replace the layer's own //! pixels, as in After Effects. //! -//! **CC Hair** grows strands from the layer's opaque pixels (Density per 1 000 px²), each a +//! **Surface Fibers** grows strands from the layer's opaque pixels (Density per 1 000 px²), each a //! quadratic curve of Length that droops with Weight, optionally steered and scaled by a hairfall //! map layer (luminance → length, horizontal/vertical gradient → lean), and shades them with a //! Kajiya–Kay-style strand model (diffuse ∝ sin(tangent, light), specular from the half vector). @@ -934,7 +934,7 @@ impl PgPlan { } } -// ---------------------------------------------------------------- CC Hair +// ---------------------------------------------------------------- Surface Fibers fn cc_hair(ctx: &EffectCtx, b: Buf) -> Buf { let Some(mut plan) = hair_plan(ctx, &b) else { return b }; @@ -942,7 +942,7 @@ fn cc_hair(ctx: &EffectCtx, b: Buf) -> Buf { plan.finish(b) } -/// CC Hair's strands as line sprites (each coloured from its root, see [`Tint::Root`]); +/// Surface Fibers's strands as line sprites (each coloured from its root, see [`Tint::Root`]); /// `None` = no hair (the layer passes through). pub(crate) fn hair_plan(ctx: &EffectCtx, b: &Buf) -> Option { let pr = ctx.params; @@ -1144,7 +1144,7 @@ pub fn specs() -> Vec { spec("ec.sim.particleplayground", "Particle Playground", pg, particle_playground), spec( "ec.sim.cchair", - "CC Hair", + "Surface Fibers", vec![ sp("length", "Length", px(30.0, 200.0)), p("thickness", "Thickness", num(1.0), slider(0.05, 20.0, 0.1, 5.0, 2)), diff --git a/crates/effects/src/stylize2.rs b/crates/effects/src/stylize2.rs index 94eda6370..247854352 100644 --- a/crates/effects/src/stylize2.rs +++ b/crates/effects/src/stylize2.rs @@ -343,7 +343,7 @@ fn brush_strokes(ctx: &EffectCtx, mut b: Buf) -> Buf { b } -// ---- CC Kaleida ---- +// ---- Kaleidoscope ---- fn kaleida(ctx: &EffectCtx, mut b: Buf) -> Buf { let c = b.to_px(ctx.params.v2("center")); @@ -368,7 +368,7 @@ fn kaleida(ctx: &EffectCtx, mut b: Buf) -> Buf { b } -// ---- CC RepeTile ---- +// ---- Edge Tile Repeat ---- #[inline] fn tile_coord(v: f64, size: f64) -> (i64, f64) { @@ -491,7 +491,7 @@ fn color_emboss(ctx: &EffectCtx, mut b: Buf) -> Buf { b } -// ---- CC Vignette ---- +// ---- Vignette ---- fn vignette(ctx: &EffectCtx, mut b: Buf) -> Buf { let amt = ctx.params.f("amount") as f32 / 100.0; @@ -523,7 +523,7 @@ fn vignette(ctx: &EffectCtx, mut b: Buf) -> Buf { b } -// ---- CC Threshold / CC Threshold RGB ---- +// ---- Channel Threshold / RGB Threshold ---- fn cc_threshold(ctx: &EffectCtx, mut b: Buf) -> Buf { let t = ctx.params.f("threshold") as f32 / 255.0; @@ -669,7 +669,7 @@ pub fn specs() -> Vec { ), spec( "ec.stylize.cckaleida", - "CC Kaleida", + "Kaleidoscope", vec![ p("center", "Center", pt(0.5, 0.5), ParamUi::Point), p("size", "Size", num(100.0), slider(1.0, 1000.0, 1.0, 400.0, 1)), @@ -680,7 +680,7 @@ pub fn specs() -> Vec { ), spec( "ec.stylize.ccrepetile", - "CC RepeTile", + "Edge Tile Repeat", vec![ p("expandRight", "Expand Right", num(0.0), slider(0.0, 4000.0, 0.0, 500.0, 0)), p("expandLeft", "Expand Left", num(0.0), slider(0.0, 4000.0, 0.0, 500.0, 0)), @@ -718,7 +718,7 @@ pub fn specs() -> Vec { ), spec( "ec.stylize.ccvignette", - "CC Vignette", + "Vignette", vec![ p("amount", "Amount", num(50.0), slider(-100.0, 100.0, -100.0, 100.0, 1)), p("angleOfView", "Angle of View", num(60.0), slider(0.0, 179.0, 0.0, 179.0, 1)), @@ -729,7 +729,7 @@ pub fn specs() -> Vec { ), spec( "ec.stylize.ccthreshold", - "CC Threshold", + "Channel Threshold", vec![ p("threshold", "Threshold", num(127.0), thr()), p("channel", "Channel", Value::Enum(0), popup(&["Luminance", "RGB", "Saturation", "Alpha"])), @@ -740,7 +740,7 @@ pub fn specs() -> Vec { ), spec( "ec.stylize.ccthresholdrgb", - "CC Threshold RGB", + "RGB Threshold", vec![ p("redThreshold", "Red Threshold", num(127.0), thr()), p("greenThreshold", "Green Threshold", num(127.0), thr()), diff --git a/crates/effects/src/stylize3.rs b/crates/effects/src/stylize3.rs index e8a141cf4..1121f737b 100644 --- a/crates/effects/src/stylize3.rs +++ b/crates/effects/src/stylize3.rs @@ -1,5 +1,5 @@ -//! Stylize effects, batch 3: CC Block Load, CC Burn Film, CC Glass, CC HexTile, CC Mr. Smoothie -//! and CC Plastic. +//! Stylize effects, batch 3: Block Reveal, Film Burn, Glass Relief, Hexagonal Tiles, Palette Map +//! and Plastic Shading. //! //! Written from the public descriptions of what these effects do (progressive block loading, //! burning film, bump-mapped glass / plastic surfaces lit by an effect light, hexagonal tiling, @@ -27,7 +27,7 @@ fn pt(x: f64, y: f64) -> Value { Value::Vec2([x, y]) } -// ------------------------------------------------------------------ CC Block Load +// ------------------------------------------------------------------ Block Reveal /// Mean colour of every `s`×`s` block (grid of `ceil(w/s)`×`ceil(h/s)`). fn block_means(img: &Image, s: usize) -> (usize, usize, Vec) { @@ -135,7 +135,7 @@ fn block_load(ctx: &EffectCtx, mut b: Buf) -> Buf { b } -// ------------------------------------------------------------------ CC Burn Film +// ------------------------------------------------------------------ Film Burn fn burn_film(ctx: &EffectCtx, mut b: Buf) -> Buf { let burn = (ctx.params.f("burn") / 100.0).clamp(0.0, 1.0) as f32; @@ -180,7 +180,7 @@ fn burn_film(ctx: &EffectCtx, mut b: Buf) -> Buf { const BUMP_PROPS: [&str; 6] = ["Red", "Green", "Blue", "Alpha", "Luminance", "Lightness"]; -/// The bump-map layer parameter: CC Glass's Bump Map, CC Plastic's Bump Layer. +/// The bump-map layer parameter: Glass Relief's Bump Map, Plastic Shading's Bump Layer. pub fn bump_layer_id(ctx: &EffectCtx) -> &'static str { if ctx.params.get("bumpLayer").is_some() { "bumpLayer" } else { "bumpMap" } } @@ -208,7 +208,7 @@ fn height_field(ctx: &EffectCtx, b: &Buf) -> Plane { pl } -/// The effect light and surface of CC Glass / CC Plastic (shared with the GPU compositor). +/// The effect light and surface of Glass Relief / Plastic Shading (shared with the GPU compositor). pub struct BumpLight { pub intensity: f32, pub color: [f32; 3], @@ -342,7 +342,7 @@ fn plastic(ctx: &EffectCtx, mut b: Buf) -> Buf { b } -// ------------------------------------------------------------------ CC HexTile +// ------------------------------------------------------------------ Hexagonal Tiles /// Axial hex cell (pointy-top) containing (x, y) for circumradius `r`; returns the cell centre /// and integer cell coordinates. @@ -418,7 +418,7 @@ fn hextile(ctx: &EffectCtx, mut b: Buf) -> Buf { b } -// ------------------------------------------------------------------ CC Mr. Smoothie +// ------------------------------------------------------------------ Palette Map const SMOOTHIE_PROPS: [&str; 8] = ["Red", "Green", "Blue", "Alpha", "Luminance", "Lightness", "Hue", "Saturation"]; @@ -518,7 +518,7 @@ pub fn specs() -> Vec { vec![ spec( "ec.stylize.ccblockload", - "CC Block Load", + "Block Reveal", vec![ p("completion", "Completion", num(0.0), pct()), p("scanlines", "Scanlines", Value::Bool(true), ParamUi::Checkbox), @@ -530,7 +530,7 @@ pub fn specs() -> Vec { ), spec( "ec.stylize.ccburnfilm", - "CC Burn Film", + "Film Burn", vec![ p("burn", "Burn", num(0.0), pct()), p("center", "Center", pt(0.5, 0.5), ParamUi::Point), @@ -538,10 +538,10 @@ pub fn specs() -> Vec { ], burn_film, ), - spec("ec.stylize.ccglass", "CC Glass", glass_p, glass), + spec("ec.stylize.ccglass", "Glass Relief", glass_p, glass), spec( "ec.stylize.cchextile", - "CC HexTile", + "Hexagonal Tiles", vec![ p("render", "Render", Value::Enum(0), popup(&["Normal", "Mirrored", "Rotated"])), p("radius", "Radius", num(50.0), slider(1.0, 4000.0, 2.0, 500.0, 1)), @@ -553,7 +553,7 @@ pub fn specs() -> Vec { ), spec( "ec.stylize.ccmrsmoothie", - "CC Mr. Smoothie", + "Palette Map", vec![ p("flowLayer", "Flow Layer", Value::Layer(None), ParamUi::Layer), p("property", "Property", Value::Enum(4), popup(&SMOOTHIE_PROPS)), @@ -565,7 +565,7 @@ pub fn specs() -> Vec { ], smoothie, ), - spec("ec.stylize.ccplastic", "CC Plastic", plastic_p, plastic), + spec("ec.stylize.ccplastic", "Plastic Shading", plastic_p, plastic), ] } diff --git a/crates/effects/src/time_fx.rs b/crates/effects/src/time_fx.rs index 79b0ce479..b9d20e2a7 100644 --- a/crates/effects/src/time_fx.rs +++ b/crates/effects/src/time_fx.rs @@ -1,9 +1,9 @@ //! Time effects (Effect > Time): Echo, Posterize Time, Time Difference, Time Displacement, -//! Timewarp, CC Force Motion Blur, CC Wide Time and Pixel Motion Blur. +//! Timewarp, Temporal Motion Blur, Temporal Blend and Pixel Motion Blur. //! //! These read the layer at *other* layer times through [`EffectHost::self_at`]: its source with //! its masks and the effects applied before them, as After Effects renders them (SceneYard: -//! Echo after CC Particle Systems II echoes the fireworks, not the black solid under them; +//! Echo after Particle Emitter 2D echoes the fireworks, not the black solid under them; //! Posterize Time after Turbulent Displace steps the displacement). Timewarp is the exception: //! it retimes the layer's source with its masks and ignores the effects above it (After //! Effects 2026: a red solid with a Gradient Ramp and then Timewarp shows the red solid). @@ -123,7 +123,7 @@ pub fn specs() -> Vec { ), spec( "ec.time.ccforcemotionblur", - "CC Force Motion Blur", + "Temporal Motion Blur", vec![ p("motionBlurLevels", "Motion Blur Levels", num(8.0), slider(2.0, 64.0, 2.0, 32.0, 0)), p("overrideShutterAngle", "Override Shutter Angle", Value::Bool(true), ParamUi::Checkbox), @@ -134,7 +134,7 @@ pub fn specs() -> Vec { ), spec( "ec.time.ccwidetime", - "CC Wide Time", + "Temporal Blend", vec![ p("forwardSteps", "Forward Steps", num(3.0), slider(0.0, 45.0, 0.0, 15.0, 0)), p("backwardSteps", "Backward Steps", num(3.0), slider(0.0, 45.0, 0.0, 15.0, 0)), @@ -994,7 +994,7 @@ fn matte_image(m: &[f32], like: &Image) -> Image { out } -// ---------------------------------------------------------------- CC Force Motion Blur / CC Wide Time / Pixel Motion Blur +// ---------------------------------------------------------------- Temporal Motion Blur / Temporal Blend / Pixel Motion Blur fn force_motion_blur(ctx: &EffectCtx, b: Buf) -> Buf { let n = ctx.params.f("motionBlurLevels").round().clamp(2.0, 64.0) as usize; diff --git a/crates/effects/src/transition.rs b/crates/effects/src/transition.rs index e465ffd1b..da3e85694 100644 --- a/crates/effects/src/transition.rs +++ b/crates/effects/src/transition.rs @@ -1,5 +1,5 @@ //! Transition effects (batch 2): Block Dissolve, Gradient Wipe (gradient layer or self), Iris Wipe, Card -//! Wipe (3D cards with camera, lighting and jitter), CC Grid Wipe, CC Radial ScaleWipe, CC Scale Wipe and CC Light Wipe. +//! Wipe (3D cards with camera, lighting and jitter), Grid Wipe, Radial Scale Wipe, Scale Wipe and Light Wipe. //! //! Every transition is the identity at 0 % completion. Angles are clockwise from "up". @@ -595,7 +595,7 @@ pub fn specs() -> Vec { ), spec( "ec.transition.ccgridwipe", - "CC Grid Wipe", + "Grid Wipe", vec![ cc_done(), p("center", "Center", pt(0.5, 0.5), ParamUi::Point), @@ -609,13 +609,13 @@ pub fn specs() -> Vec { ), spec( "ec.transition.ccradialscalewipe", - "CC Radial ScaleWipe", + "Radial Scale Wipe", vec![cc_done(), p("center", "Center", pt(0.5, 0.5), ParamUi::Point), p("reverse", "Reverse Transition", Value::Bool(false), ParamUi::Checkbox)], radial_scale_wipe, ), spec( "ec.transition.ccscalewipe", - "CC Scale Wipe", + "Scale Wipe", vec![ p("stretch", "Stretch", num(0.0), slider(0.0, 1000.0, 0.0, 50.0, 2)), p("center", "Center", pt(0.5, 0.5), ParamUi::Point), @@ -625,7 +625,7 @@ pub fn specs() -> Vec { ), spec( "ec.transition.cclightwipe", - "CC Light Wipe", + "Light Wipe", vec![ cc_done(), p("center", "Center", pt(0.5, 0.5), ParamUi::Point), diff --git a/crates/effects/src/transition2.rs b/crates/effects/src/transition2.rs index 2e1738b28..6f19a660b 100644 --- a/crates/effects/src/transition2.rs +++ b/crates/effects/src/transition2.rs @@ -1,5 +1,5 @@ -//! Transition effects, batch 2: CC Glass Wipe, CC Image Wipe, CC Jaws, CC Line Sweep, -//! CC Twister and CC WarpoMatic. +//! Transition effects, batch 2: Glass Wipe, Luminance Image Wipe, Serrated Wipe, Line Sweep, +//! Twist Wipe and Warp Dissolve. //! //! Written from the public descriptions of these transitions (gradient-driven wipes with glass //! refraction or warping, jagged "jaws" opening, staggered line sweeps, a ribbon twist that @@ -68,7 +68,7 @@ fn grad(pl: &Plane, x: usize, y: usize) -> (f32, f32) { ((pl.get_clamped(xi + 1, yi) - pl.get_clamped(xi - 1, yi)) * 0.5, (pl.get_clamped(xi, yi + 1) - pl.get_clamped(xi, yi - 1)) * 0.5) } -// ------------------------------------------------------------------ CC Glass Wipe +// ------------------------------------------------------------------ Glass Wipe fn glass_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf { let c = completion(ctx); @@ -105,7 +105,7 @@ fn glass_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf { b } -// ------------------------------------------------------------------ CC Image Wipe +// ------------------------------------------------------------------ Luminance Image Wipe fn image_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf { let c = completion(ctx); @@ -132,7 +132,7 @@ fn image_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf { b } -// ------------------------------------------------------------------ CC Jaws +// ------------------------------------------------------------------ Serrated Wipe fn jaws(ctx: &EffectCtx, mut b: Buf) -> Buf { let c = completion(ctx) as f64; @@ -193,7 +193,7 @@ fn jaws(ctx: &EffectCtx, mut b: Buf) -> Buf { b } -// ------------------------------------------------------------------ CC Line Sweep +// ------------------------------------------------------------------ Line Sweep fn line_sweep(ctx: &EffectCtx, mut b: Buf) -> Buf { let c = completion(ctx) as f64; @@ -235,7 +235,7 @@ fn line_sweep(ctx: &EffectCtx, mut b: Buf) -> Buf { b } -// ------------------------------------------------------------------ CC Twister +// ------------------------------------------------------------------ Twist Wipe fn twister(ctx: &EffectCtx, mut b: Buf) -> Buf { let c = completion(ctx) as f64; @@ -283,7 +283,7 @@ fn twister(ctx: &EffectCtx, mut b: Buf) -> Buf { b } -// ------------------------------------------------------------------ CC WarpoMatic +// ------------------------------------------------------------------ Warp Dissolve fn warpomatic(ctx: &EffectCtx, mut b: Buf) -> Buf { let c = completion(ctx); @@ -350,7 +350,7 @@ pub fn specs() -> Vec { vec![ spec( "ec.transition.ccglasswipe", - "CC Glass Wipe", + "Glass Wipe", vec![ done(), p("layerToReveal", "Layer to Reveal", Value::Layer(None), ParamUi::Layer), @@ -362,7 +362,7 @@ pub fn specs() -> Vec { ), spec( "ec.transition.ccimagewipe", - "CC Image Wipe", + "Luminance Image Wipe", vec![ done(), p("borderSoftness", "Border Softness", num(0.0), pct()), @@ -376,7 +376,7 @@ pub fn specs() -> Vec { ), spec( "ec.transition.ccjaws", - "CC Jaws", + "Serrated Wipe", vec![ done(), p("center", "Center", pt(0.5, 0.5), ParamUi::Point), @@ -389,7 +389,7 @@ pub fn specs() -> Vec { ), spec( "ec.transition.cclinesweep", - "CC Line Sweep", + "Line Sweep", vec![ done(), p("direction", "Direction", num(0.0), ParamUi::Angle), @@ -401,7 +401,7 @@ pub fn specs() -> Vec { ), spec( "ec.transition.cctwister", - "CC Twister", + "Twist Wipe", vec![ done(), p("backside", "Backside", Value::Layer(None), ParamUi::Layer), @@ -413,7 +413,7 @@ pub fn specs() -> Vec { ), spec( "ec.transition.ccwarpomatic", - "CC WarpoMatic", + "Warp Dissolve", vec![ done(), p("layerToReveal", "Layer to Reveal", Value::Layer(None), ParamUi::Layer), diff --git a/crates/effects/src/utility.rs b/crates/effects/src/utility.rs index e87c6d3ff..fca9d607e 100644 --- a/crates/effects/src/utility.rs +++ b/crates/effects/src/utility.rs @@ -365,7 +365,7 @@ pub fn specs() -> Vec { spec("ec.utility.growbounds", "Grow Bounds", vec![p("pixels", "Pixels", num(0.0), slider(0.0, 4000.0, 0.0, 500.0, 0))], grow_bounds), spec( "ec.utility.ccoverbrights", - "CC Overbrights", + "Out-of-Range Display", vec![ p("channel", "Channel", Value::Enum(0), popup(&["Max RGB", "Red", "Green", "Blue", "Alpha", "Luminance"])), p("highlightColor", "Highlight Color", col(1.0, 0.0, 0.0), ParamUi::Color), diff --git a/crates/engine/src/background/offload.rs b/crates/engine/src/background/offload.rs index f9038accb..b28527f02 100644 --- a/crates/engine/src/background/offload.rs +++ b/crates/engine/src/background/offload.rs @@ -375,7 +375,7 @@ pub(crate) fn job_accel() -> Option> { } /// Render options of an analysis's input frames: 100 %; with an accelerator, the project's -/// renderer (Mercury GPU Acceleration: its GPU effects on the device; Software Only: the CPU). +/// renderer (GPU Acceleration: its GPU effects on the device; Software Only: the CPU). pub(crate) fn analysis_opts(accel: Option<&dyn Accelerator>) -> RenderOpts { RenderOpts { scale: 1.0, backend: if accel.is_some() { Backend::Auto } else { Backend::Cpu }, ..Default::default() } } diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index f5bae86d0..8129759c5 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -719,8 +719,8 @@ fn project_settings(s: &mut Session, p: &Value) -> Result { Ok(serde_json::to_value(&s.project.settings).unwrap_or_default()) } -/// Video Rendering and Effects: report or set the renderer (Mercury GPU Acceleration when a GPU -/// adapter exists, else Mercury Software Only). +/// Video Rendering and Effects: report or set the renderer (GPU Acceleration when a GPU +/// adapter exists, else Software Rendering). fn render_backend(s: &mut Session, p: &Value) -> Result { if let Some(b) = p.get("backend").or_else(|| p.get("renderer")) { let gpu = match b { @@ -748,14 +748,14 @@ pub fn backend_status(s: &Session) -> Value { let gpu = s.project.settings.gpu_acceleration; // Why it renders on the CPU (#262). let why = if !gpu { - Some("the project's renderer is Mercury Software Only".to_string()) + Some("the project's renderer is Software Rendering".to_string()) } else if adapter.is_none() { Some(s.accel_note.clone().unwrap_or_else(|| "no GPU compositor is attached".into())) } else { None }; json!({ - "renderer": if gpu { "Mercury GPU Acceleration" } else { "Mercury Software Only" }, + "renderer": if gpu { "GPU Acceleration" } else { "Software Rendering" }, "gpuAcceleration": gpu, "adapter": adapter, "active": if gpu && adapter.is_some() { "gpu" } else { "cpu" }, diff --git a/crates/engine/src/cmd/panels_cmds.rs b/crates/engine/src/cmd/panels_cmds.rs index 40241e6f4..95bc57c3c 100644 --- a/crates/engine/src/cmd/panels_cmds.rs +++ b/crates/engine/src/cmd/panels_cmds.rs @@ -1,4 +1,4 @@ -//! Commands behind the Progress, Media Browser, Metadata and Lumetri Scopes panels. +//! Commands behind the Progress, Media Browser, Metadata and Video Scopes panels. use effectcraft_project::ItemId; use effectcraft_raster::Image; @@ -159,7 +159,7 @@ fn project_comment(s: &mut Session, p: &Value) -> Result { Ok(Value::Null) } -// ---------------------------------------------------------------- Lumetri Scopes +// ---------------------------------------------------------------- Video Scopes /// The comp frame the scopes read (the viewer's image: over the comp background), at most /// `max_side` pixels on the long side. @@ -257,7 +257,7 @@ pub fn specs() -> Vec { cmd!("project.setProjectComment", "Project Comment", [], None, "{comment}", always, project_comment), query!( "scopes.analyze", - "Lumetri Scopes", + "Video Scopes", "{comp?, time?|frame?, scope?: waveformRgb|waveformLuma|waveformYc|vectorscopeYuv|vectorscopeHls|histogram|paradeRgb|paradeYuv, standard?: rec601|rec709|rec2020, float?, clamp?, size?}", scopes_analyze ), diff --git a/crates/engine/src/home/demo.rs b/crates/engine/src/home/demo.rs index 7503178fa..ec1c353bc 100644 --- a/crates/engine/src/home/demo.rs +++ b/crates/engine/src/home/demo.rs @@ -238,7 +238,7 @@ fn showcase_3d(p: &mut Project, solids: ItemId) -> Comp { anim(&mut cam, "cameraOptions/focusDistance", keys(&[(0.0, Value::Scalar(1700.0)), (5.0, Value::Scalar(1900.0)), (10.0, Value::Scalar(1650.0))])); // Key light: warm spot casting soft shadows. Fill: cool ambient. - let mut key = build::layer(p, &c, "Key Light", LayerSource::Light { kind: effectcraft_project::LightKind::Spot }, (1920, 1080), None); + let mut key = build::layer(p, &c, "Screen Color Key", LayerSource::Light { kind: effectcraft_project::LightKind::Spot }, (1920, 1080), None); set(&mut key, "transform/position", Value::Vec3([500.0, -500.0, -700.0])); set(&mut key, "transform/poi", Value::Vec3([960.0, 600.0, 300.0])); set(&mut key, "lightOptions/intensity", Value::Scalar(150.0)); diff --git a/crates/engine/src/home/learn.rs b/crates/engine/src/home/learn.rs index cea729368..9c89b2dbf 100644 --- a/crates/engine/src/home/learn.rs +++ b/crates/engine/src/home/learn.rs @@ -259,7 +259,7 @@ pub fn tutorials() -> Vec { target: Some("menu.Layer"), accepts: &["layer.newLight"], when: Value::Null, - show: vec![("layer.newLight", json!({"kind": "Spot", "name": "Key Light"}))], + show: vec![("layer.newLight", json!({"kind": "Spot", "name": "Screen Color Key"}))], }), step(S { title: "Add a camera", diff --git a/crates/engine/src/menus.rs b/crates/engine/src/menus.rs index ed52d6e03..70acbd1fb 100644 --- a/crates/engine/src/menus.rs +++ b/crates/engine/src/menus.rs @@ -1263,7 +1263,7 @@ Window Essential Graphics | window.panel {"panel":"essentialGraphics"} Info | window.panel {"panel":"info"} | Cmd+2 Learn | help.inAppTutorials - Lumetri Scopes | window.panel {"panel":"lumetriScopes"} + Video Scopes | window.panel {"panel":"lumetriScopes"} Mask Interpolation | window.panel {"panel":"maskInterpolation"} Media Browser | window.panel {"panel":"mediaBrowser"} Metadata | window.panel {"panel":"metadata"} diff --git a/crates/engine/src/menus/tests.rs b/crates/engine/src/menus/tests.rs index 913a0e92e..8de3cc6dc 100644 --- a/crates/engine/src/menus/tests.rs +++ b/crates/engine/src/menus/tests.rs @@ -23,6 +23,25 @@ fn platform_entries(mac: bool) -> Vec<(Vec, MenuEntry)> { out } +/// Guard the generated display tree, not command IDs, format tokens or dynamic user names. +#[test] +fn builtin_menu_labels_do_not_reintroduce_vendor_feature_names() { + for mac in [true, false] { + let entries = platform_entries(mac); + for (path, entry) in &entries { + for label in path.iter().chain(std::iter::once(&entry.label)) { + let folded = label.to_lowercase(); + assert!(!folded.starts_with("cc "), "{label}"); + for mark in ["lumetri", "mercury", "mocha", "keylight"] { + assert!(!folded.contains(mark), "{label}"); + } + } + } + let scopes = entries.iter().find(|(_, e)| e.command == "window.panel" && e.params["panel"] == "lumetriScopes").unwrap(); + assert_eq!(scopes.1.label, "Video Scopes"); + } +} + #[test] fn top_level_order_matches_after_effects() { let mac: Vec = diff --git a/crates/engine/src/tests/rig3d.rs b/crates/engine/src/tests/rig3d.rs index 634d84e02..2c1557c91 100644 --- a/crates/engine/src/tests/rig3d.rs +++ b/crates/engine/src/tests/rig3d.rs @@ -439,7 +439,7 @@ const RIGGED: &str = r#"{ ], "cameras": [{"type": "perspective", "name": "Shot", "perspective": {"yfov": 0.6, "znear": 0.1}}], "extensions": {"KHR_lights_punctual": {"lights": [ - {"type": "spot", "name": "Key Light", "color": [1, 0.8, 0.6], "intensity": 1.5, "spot": {"innerConeAngle": 0.25, "outerConeAngle": 0.5}}, + {"type": "spot", "name": "Screen Color Key", "color": [1, 0.8, 0.6], "intensity": 1.5, "spot": {"innerConeAngle": 0.25, "outerConeAngle": 0.5}}, {"type": "point", "name": "Fill Light", "intensity": 0.5, "range": 4} ]}} }"#; @@ -478,7 +478,7 @@ fn cameras_and_lights_from_model() { let r = s.execute("light.fromModel", json!({})).unwrap(); let ls: Vec = r["layers"].as_array().unwrap().iter().map(|v| layer_in(&s, cid(&s), v.as_u64().unwrap())).collect(); assert_eq!(ls.len(), 2); - let key = ls.iter().find(|l| l.name == "Key Light").unwrap(); + let key = ls.iter().find(|l| l.name == "Screen Color Key").unwrap(); assert_eq!(key.source, LayerSource::Light { kind: LightKind::Spot }); assert!((key.props.prop("lightOptions/coneAngle").unwrap().value.as_f64() - 1.0f64.to_degrees()).abs() < 1e-6); assert!((key.props.prop("lightOptions/coneFeather").unwrap().value.as_f64() - 50.0).abs() < 1e-6); diff --git a/crates/engine/src/tests/settings.rs b/crates/engine/src/tests/settings.rs index 7d65061ea..b37c5ca3e 100644 --- a/crates/engine/src/tests/settings.rs +++ b/crates/engine/src/tests/settings.rs @@ -1221,7 +1221,35 @@ fn render_backend_says_why_it_renders_on_the_cpu() { assert_eq!(why(&mut s), json!("no GPU compositor is attached")); s.accel_note = Some("headless: renders on the CPU unless started with --gpu".into()); assert_eq!(why(&mut s), json!("headless: renders on the CPU unless started with --gpu")); - assert_eq!(s.execute("render.backend", json!({"backend": "cpu"})).unwrap()["why"], json!("the project's renderer is Mercury Software Only")); + assert_eq!(s.execute("render.backend", json!({"backend": "cpu"})).unwrap()["why"], json!("the project's renderer is Software Rendering")); +} + +/// Display labels are descriptive; machine-readable settings remain the same. +#[test] +fn renderer_labels_keep_their_stable_machine_values() { + let mut s = Session::default(); + for (backend, label, gpu) in [("cpu", "Software Rendering", false), ("gpu", "GPU Acceleration", true)] { + let status = s.execute("render.backend", json!({"backend": backend})).unwrap(); + assert_eq!(status["renderer"], json!(label)); + assert_eq!(status["gpuAcceleration"], json!(gpu)); + assert_eq!(status["active"], json!("cpu"), "the headless test has no GPU adapter"); + } +} + +/// The label `render.backend` reports is one it accepts back, so a caller can round-trip the +/// status instead of guessing the command's own values. +#[test] +fn renderer_status_labels_round_trip_through_the_command() { + let mut s = Session::default(); + for (backend, label, gpu) in [("cpu", "Software Rendering", false), ("gpu", "GPU Acceleration", true)] { + let reported = s.execute("render.backend", json!({"backend": backend})).unwrap()["renderer"].clone(); + assert_eq!(reported, json!(label)); + let parsed = effectcraft_render::Backend::parse(reported.as_str().unwrap()).expect("the reported renderer parses back"); + assert_eq!(parsed == effectcraft_render::Backend::Cpu, !gpu, "{reported}"); + let again = s.execute("render.backend", json!({"backend": reported})).unwrap(); + assert_eq!(again["renderer"], reported); + assert_eq!(again["gpuAcceleration"], json!(gpu)); + } } /// Settings ▸ GPU Information names what the GPU compositor runs on, or why it is off, and the diff --git a/crates/render/src/accel.rs b/crates/render/src/accel.rs index 0f271c59d..1eb8b546f 100644 --- a/crates/render/src/accel.rs +++ b/crates/render/src/accel.rs @@ -21,10 +21,13 @@ pub enum Backend { } impl Backend { + /// The backend `s` names, whatever case it is written in: the project's own values (`cpu`, + /// `gpu`, `auto`), the labels Project Settings ▸ Video Rendering and Effects draws + /// (`GPU Acceleration`, `Software Rendering`) and the older After Effects wording. pub fn parse(s: &str) -> Option { match s.to_ascii_lowercase().as_str() { - "cpu" | "software" | "mercury software only" => Some(Backend::Cpu), - "gpu" | "mercury gpu acceleration" => Some(Backend::Gpu), + "cpu" | "software" | "mercury software only" | "software rendering" => Some(Backend::Cpu), + "gpu" | "mercury gpu acceleration" | "gpu acceleration" => Some(Backend::Gpu), "auto" => Some(Backend::Auto), _ => None, } diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index 50ed65c73..59fa0fadc 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -329,7 +329,7 @@ fn card_dance_comp_camera_matches_the_default_view() { assert!(img.get(10, 50)[3] < 0.01); } -/// A black 2D solid with CC Particle World, under a camera layer panned right by `pan` px. +/// A black 2D solid with Particle Emitter 3D, under a camera layer panned right by `pan` px. fn particle_world_scene(pan: f64) -> (Project, ItemId) { let (mut p, cid, comp) = setup(); let mut l = solid(&mut p, &comp, [0.0, 0.0, 0.0], 200, 100); @@ -359,7 +359,7 @@ fn particles_x(img: &crate::Image) -> f32 { sx / m } -/// #397: CC Particle World on a 2D layer is seen through the comp's camera layer, and the layer +/// #397: Particle Emitter 3D on a 2D layer is seen through the comp's camera layer, and the layer /// cache notices the camera moving. #[test] fn particle_world_follows_the_comp_camera() { @@ -374,7 +374,7 @@ fn particle_world_follows_the_comp_camera() { assert!(b < a - 20.0, "panning the camera right moves the particles left: {a} -> {b}"); } -/// A black solid with CC Particle World whose Birth Rate is 3 until layer time `cut`, then 0 a +/// A black solid with Particle Emitter 3D whose Birth Rate is 3 until layer time `cut`, then 0 a /// frame later (`None`: 3 throughout); the layer starts at comp time `start`, stretched `stretch` %. fn burst_scene(cut: Option, start: f64, stretch: f64) -> (Project, ItemId) { burst_scene_keys(cut, start, stretch, false) @@ -624,7 +624,7 @@ fn a_text_expression_selector_is_not_served_from_the_disk_cache() { /// A Birth Rate expression reading another layer: changing that layer changes the history but /// not the current rate (0) nor this layer's properties, which is all the layer cache key sees. -/// CC Particle World layers with expressions therefore aren't layer cached. +/// Particle Emitter 3D layers with expressions therefore aren't layer cached. #[test] fn particle_world_with_a_birth_rate_expression_is_not_served_from_the_layer_cache() { let scene = |opacity: f64| { @@ -657,8 +657,8 @@ fn particle_world_with_a_birth_rate_expression_is_not_served_from_the_layer_cach assert!(light(&b) > 1.0 && light(&b) < light(&a), "fewer births: {} vs {}", light(&b), light(&a)); } -/// A grey solid (CC Mr. Mercury shades its blobs from it) with effect `id` (CC Particle Systems -/// II, CC Mr. Mercury): Birth Rate `rate`, +/// A grey solid (Liquid Particles shades its blobs from it) with effect `id` (CC Particle Systems +/// II, Liquid Particles): Birth Rate `rate`, /// or keyed 3 until 0.5 s and 0 a frame later (`None`); Longevity 1 s. fn system_scene(id: &str, rate: Option) -> (Project, ItemId) { let (mut p, cid, comp) = setup(); @@ -677,7 +677,7 @@ fn system_scene(id: &str, rate: Option) -> (Project, ItemId) { (p, cid) } -/// #1373 for CC Particle Systems II and CC Mr. Mercury through the renderer: a keyed burst +/// #1373 for Particle Emitter 2D and Liquid Particles through the renderer: a keyed burst /// draws as the constant rate during it, still draws after the rate drops to 0 (unlike a rate /// of 0 throughout), and draws as nothing born once Longevity has run out. #[test] @@ -1057,6 +1057,27 @@ fn comp_frame_ends_the_frame_when_the_render_panics() { assert_eq!(a.ends.load(Ordering::SeqCst), 1); } +/// The renderer labels Project Settings ▸ Video Rendering and Effects draws parse back as the +/// backend they name (in any case), and the older spellings still work. +#[test] +fn renderer_labels_parse_back_and_keep_the_older_spellings() { + use crate::Backend; + for (label, backend) in [ + ("GPU Acceleration", Backend::Gpu), + ("gpu acceleration", Backend::Gpu), + ("Mercury GPU Acceleration", Backend::Gpu), + ("gpu", Backend::Gpu), + ("Software Rendering", Backend::Cpu), + ("software rendering", Backend::Cpu), + ("Mercury Software Only", Backend::Cpu), + ("cpu", Backend::Cpu), + ("Auto", Backend::Auto), + ] { + assert_eq!(Backend::parse(label), Some(backend), "{label}"); + } + assert_eq!(Backend::parse("Vulkan"), None); +} + #[test] fn auto_backend_sends_2d_and_3d_comps_to_the_gpu() { let (mut p, cid, comp) = setup(); @@ -1223,11 +1244,11 @@ fn effects_on_text_reach_past_the_text_bounds() { } assert!( painted > 50 && hi.saturating_sub(lo) > 100, - "CC Tiler tiles the glyphs across the layer region, not just its text bounds: {painted} painted, x-span {lo}..{hi}" + "Scaled Tiles tiles the glyphs across the layer region, not just its text bounds: {painted} painted, x-span {lo}..{hi}" ); } -/// #658: CC Smear drags text glyphs beyond the text layer's own bounds. Pixels smeared past +/// #658: Directional Smear drags text glyphs beyond the text layer's own bounds. Pixels smeared past /// the painted rectangle used to fall off its edge, so the liquid trail never appeared. #[test] fn cc_smear_trails_text_past_its_bounds() { @@ -1246,7 +1267,7 @@ fn cc_smear_trails_text_past_its_bounds() { ); p.comp_mut(cid).unwrap().layers = vec![l].into(); let smeared = right(&render_frame(&p, cid, Tick::ZERO, 1.0)); - assert!(plain > 40 && smeared > plain + 25, "CC Smear trails the glyphs past the text bounds: right edge {plain} → {smeared}"); + assert!(plain > 40 && smeared > plain + 25, "Directional Smear trails the glyphs past the text bounds: right edge {plain} → {smeared}"); } /// A layer parameter pointing at a shape layer sees all of it (in its effect space), and the @@ -1502,7 +1523,7 @@ fn solo_excludes_other_layers_outside_the_soloed_layers_range() { assert_eq!(second.get(100, 50), [0.0, 0.0, 1.0, 1.0], "only B inside its range"); } -/// #1252: a layer blended with a CC Particle World layer, under a camera panned by `pan` px. +/// #1252: a layer blended with a Particle Emitter 3D layer, under a camera panned by `pan` px. fn scene_reader_dependency(pan: f64) -> (Project, ItemId) { let (mut p, cid, comp) = setup(); let mut source = solid(&mut p, &comp, [0.0, 0.0, 0.0], 200, 100); diff --git a/crates/ui-egui/src/dock.rs b/crates/ui-egui/src/dock.rs index c8ada9bf8..f126db257 100644 --- a/crates/ui-egui/src/dock.rs +++ b/crates/ui-egui/src/dock.rs @@ -124,7 +124,7 @@ impl PanelKind { PanelKind::EssentialGraphics => "Essential Graphics", PanelKind::ScriptPanel(_) => "ScriptUI Panel", PanelKind::Viewer(_) => "Composition", - PanelKind::LumetriScopes => "Lumetri Scopes", + PanelKind::LumetriScopes => "Video Scopes", PanelKind::Footage => "Footage", PanelKind::MediaBrowser => "Media Browser", PanelKind::Metadata => "Metadata", @@ -709,6 +709,16 @@ mod tests { w.apply(action, permissions).unwrap(); } + /// Only the title changes: existing saved workspace layouts still deserialize. + #[test] + fn scopes_label_is_descriptive_and_its_saved_identity_stays_compatible() { + let panel: PanelKind = serde_json::from_str("\"LumetriScopes\"").unwrap(); + assert_eq!(panel, LumetriScopes); + assert_eq!(panel.title(), "Video Scopes"); + assert_eq!(panel.id(), "LumetriScopes"); + assert_eq!(serde_json::to_string(&panel).unwrap(), "\"LumetriScopes\""); + } + #[test] fn after_effects_workspaces_exist_with_their_core_panels() { for name in WORKSPACES { diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 68e58f842..c998538d4 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -1054,7 +1054,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str, &str)] = &[ ("window.panel", "Essential Graphics", "基本图形"), ("window.panel", "Info", "信息"), ("help.inAppTutorials", "Learn", "学习"), - ("window.panel", "Lumetri Scopes", "Lumetri 范围"), + ("window.panel", "Video Scopes", "视频示波器"), ("window.panel", "Mask Interpolation", "蒙版插值"), ("window.panel", "Media Browser", "媒体浏览器"), ("window.panel", "Metadata", "元数据"), @@ -1690,7 +1690,7 @@ const SWEDISH: &[(&str, &str, &str)] = &[ ("window.panel", "Essential Graphics", "Grundläggande grafik"), ("window.panel", "Info", "Info"), ("help.inAppTutorials", "Learn", "Lär dig"), - ("window.panel", "Lumetri Scopes", "Lumetri-scope"), + ("window.panel", "Video Scopes", "Videoskop"), ("window.panel", "Mask Interpolation", "Maskinterpolering"), ("window.panel", "Media Browser", "Mediebläddrare"), ("window.panel", "Metadata", "Metadata"), @@ -2336,7 +2336,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str, &str)] = &[ ("window.panel", "Essential Graphics", "基本圖形"), ("window.panel", "Info", "資訊"), ("help.inAppTutorials", "Learn", "學習"), - ("window.panel", "Lumetri Scopes", "Lumetri 示波器"), + ("window.panel", "Video Scopes", "視訊示波器"), ("window.panel", "Mask Interpolation", "遮色片插補"), ("window.panel", "Media Browser", "媒體瀏覽器"), ("window.panel", "Metadata", "中繼資料"), @@ -2975,7 +2975,7 @@ const JAPANESE: &[(&str, &str, &str)] = &[ ("window.panel", "Essential Graphics", "エッセンシャルグラフィックス"), ("window.panel", "Info", "情報"), ("help.inAppTutorials", "Learn", "学習"), - ("window.panel", "Lumetri Scopes", "Lumetriスコープ"), + ("window.panel", "Video Scopes", "ビデオスコープ"), ("window.panel", "Mask Interpolation", "マスク補間"), ("window.panel", "Media Browser", "メディアブラウザー"), ("window.panel", "Metadata", "メタデータ"), @@ -3617,7 +3617,7 @@ const UKRAINIAN: &[(&str, &str, &str)] = &[ ("window.panel", "Essential Graphics", "Основна графіка"), ("window.panel", "Info", "Інформація"), ("help.inAppTutorials", "Learn", "Навчання"), - ("window.panel", "Lumetri Scopes", "Вимірювачі Lumetri"), + ("window.panel", "Video Scopes", "Відеоскопи"), ("window.panel", "Mask Interpolation", "Інтерполяція маски"), ("window.panel", "Media Browser", "Браузер медіа"), ("window.panel", "Metadata", "Метадані"), @@ -4260,7 +4260,7 @@ const SPANISH: &[(&str, &str, &str)] = &[ ("window.panel", "Essential Graphics", "Gráficos esenciales"), ("window.panel", "Info", "Información"), ("help.inAppTutorials", "Learn", "Aprender"), - ("window.panel", "Lumetri Scopes", "Ámbitos de Lumetri"), + ("window.panel", "Video Scopes", "Monitores de vídeo"), ("window.panel", "Mask Interpolation", "Interpolación de máscara"), ("window.panel", "Media Browser", "Navegador de medios"), ("window.panel", "Metadata", "Metadatos"), @@ -4901,7 +4901,7 @@ const PORTUGUESE: &[(&str, &str, &str)] = &[ ("window.panel", "Essential Graphics", "Gráficos essenciais"), ("window.panel", "Info", "Informações"), ("help.inAppTutorials", "Learn", "Aprender"), - ("window.panel", "Lumetri Scopes", "Escopos Lumetri"), + ("window.panel", "Video Scopes", "Escopos de vídeo"), ("window.panel", "Mask Interpolation", "Interpolação de máscara"), ("window.panel", "Media Browser", "Navegador de mídia"), ("window.panel", "Metadata", "Metadados"), @@ -5544,7 +5544,7 @@ const RUSSIAN: &[(&str, &str, &str)] = &[ ("window.panel", "Essential Graphics", "Основная графика"), ("window.panel", "Info", "Информация"), ("help.inAppTutorials", "Learn", "Обучение"), - ("window.panel", "Lumetri Scopes", "Измерители Lumetri"), + ("window.panel", "Video Scopes", "Видеоскопы"), ("window.panel", "Mask Interpolation", "Интерполяция маски"), ("window.panel", "Media Browser", "Обозреватель медиа"), ("window.panel", "Metadata", "Метаданные"), @@ -6185,7 +6185,7 @@ const FRENCH: &[(&str, &str, &str)] = &[ ("window.panel", "Essential Graphics", "Graphiques essentiels"), ("window.panel", "Info", "Informations"), ("help.inAppTutorials", "Learn", "Apprentissage"), - ("window.panel", "Lumetri Scopes", "Oscilloscopes Lumetri"), + ("window.panel", "Video Scopes", "Oscilloscopes vidéo"), ("window.panel", "Mask Interpolation", "Interpolation de masque"), ("window.panel", "Media Browser", "Navigateur multimédia"), ("window.panel", "Metadata", "Métadonnées"), @@ -6826,7 +6826,7 @@ const GERMAN: &[(&str, &str, &str)] = &[ ("window.panel", "Essential Graphics", "Essential-Grafik"), ("window.panel", "Info", "Info"), ("help.inAppTutorials", "Learn", "Lernen"), - ("window.panel", "Lumetri Scopes", "Lumetri-Scopes"), + ("window.panel", "Video Scopes", "Videoskope"), ("window.panel", "Mask Interpolation", "Maskeninterpolation"), ("window.panel", "Media Browser", "Medienbrowser"), ("window.panel", "Metadata", "Metadaten"), @@ -7464,7 +7464,7 @@ const POLISH: &[(&str, &str, &str)] = &[ ("window.panel", "Essential Graphics", "Essential Graphics"), ("window.panel", "Info", "Informacje"), ("help.inAppTutorials", "Learn", "Nauka"), - ("window.panel", "Lumetri Scopes", "Zakresy Lumetri"), + ("window.panel", "Video Scopes", "Wskaźniki wideo"), ("window.panel", "Mask Interpolation", "Interpolacja maski"), ("window.panel", "Media Browser", "Przeglądarka multimediów"), ("window.panel", "Metadata", "Metadane"), @@ -8106,7 +8106,7 @@ const CZECH: &[(&str, &str, &str)] = &[ ("window.panel", "Essential Graphics", "Základní grafika"), ("window.panel", "Info", "Informace"), ("help.inAppTutorials", "Learn", "Výuka"), - ("window.panel", "Lumetri Scopes", "Měřicí přístroje Lumetri"), + ("window.panel", "Video Scopes", "Videoskopy"), ("window.panel", "Mask Interpolation", "Interpolace masek"), ("window.panel", "Media Browser", "Prohlížeč médií"), ("window.panel", "Metadata", "Metadata"), @@ -8748,7 +8748,7 @@ const GEORGIAN: &[(&str, &str, &str)] = &[ ("window.panel", "Essential Graphics", "ძირითადი გრაფიკა"), ("window.panel", "Info", "ინფორმაცია"), ("help.inAppTutorials", "Learn", "სწავლა"), - ("window.panel", "Lumetri Scopes", "Lumetri Scopes"), + ("window.panel", "Video Scopes", "ვიდეოსკოპები"), ("window.panel", "Mask Interpolation", "ნიღბის ინტერპოლაცია"), ("window.panel", "Media Browser", "მედიის ბრაუზერი"), ("window.panel", "Metadata", "მეტამონაცემები"), @@ -8799,6 +8799,21 @@ mod tests { use serde_json::json; use std::collections::BTreeSet; + #[test] + fn renamed_menu_translations_do_not_reintroduce_vendor_names() { + for code in CATALOG_CODES { + set_current(code); + for (id, source) in [("window.panel", "Video Scopes")] { + let rendered = command_label(id, source); + let label = rendered.to_lowercase(); + for mark in ["lumetri", "mercury"] { + assert!(!label.contains(mark), "{code}: {id} -> {rendered}"); + } + } + } + set_current("en"); + } + /// The four options at the foot of the header's workspace menu say what After Effects says, /// read from its workspace button on 2026-10-31: reset, save over this one, save a copy, then /// edit the list. diff --git a/crates/ui-egui/src/i18n/effects.rs b/crates/ui-egui/src/i18n/effects.rs index ec952ccb0..55893115a 100644 --- a/crates/ui-egui/src/i18n/effects.rs +++ b/crates/ui-egui/src/i18n/effects.rs @@ -18,14 +18,19 @@ //! another effect. An effect with no row draws the registry's English name; the //! `missing_translations_report` test (run by `cargo xtask ci`) lists those per language. //! -//! A language decides for itself which names stay English: After Effects' Japanese interface -//! leaves the `CC` series in English and gives the `VR` and `OCIO` sets a Japanese tail -//! (`VR ブラー`, `OCIO CDL 変換`), while its Simplified Chinese one translates them (the `CC` -//! names come from Adobe's Chinese *documentation*, which names 68 of the 69). Repeating the -//! English name is therefore a language-level choice, not a missing row. +//! Own feature names are descriptive. The renamed effects use original translations in every +//! existing effect catalog; legacy vendor names remain search aliases in the effects crate. +//! External format tokens and stored instance names are not localized or rewritten. //! -//! The Simplified Chinese names come from the installed After Effects 2026 (26.5.0.89) -//! Simplified Chinese interface, its 21 category folder names included; `Cryptomatte`, +//! The rows of the effects this cleanup did not rename keep the provenance they had. A language +//! decides for itself which names stay English: After Effects' Japanese interface leaves the `CC` +//! series in English and gives the `VR` and `OCIO` sets a Japanese tail (`VR ブラー`, +//! `OCIO CDL 変換`), while its Simplified Chinese one translates them. Repeating the English name +//! is therefore a language-level choice, not a missing row. +//! +//! The Simplified Chinese names of unchanged effects come from the installed After Effects 2026 +//! (26.5.0.89) Simplified Chinese interface, its 21 category folder names included (the `CC` names +//! came from Adobe's Chinese *documentation*, which names 68 of the 69); `Cryptomatte`, //! `EXtractoR`, `IDentifier`, `Key Light` and `Unmult` stay in English as brand names, and //! `Face Measurements` / `Face Track Points` are EffectCraft's own effects, so their wording is //! ours. @@ -33,16 +38,15 @@ //! The Traditional Chinese names for Taiwan are written from the meaning of the English ones, with //! the terms Taiwan's creative software uses (`圖層`, `遮罩`, `筆刷`, `雜訊`, `發光`, `漸層`); they //! are not converted from the Simplified table. Brand and coined names (`Cryptomatte`, -//! `EXtractoR`, `IDentifier`, `Key Light`, `Unmult`, `Lumetri Color`, `Colorama`, `Minimax`, -//! `Vegas`, `Mocha`) stay in English; the `CC` series keeps its prefix and translates the -//! descriptive tail, and the `VR` and `OCIO` sets keep their prefix in Chinese word order. +//! `EXtractoR`, `IDentifier`, `Key Light`, `Unmult`, `Colorama`, `Minimax`, `Vegas`) stay in +//! English, and the `VR` and `OCIO` sets keep their prefix in Chinese word order. //! //! The French names are written from the meaning of the English ones, with the vocabulary of the //! French menus; they come from no other application's interface. Brand and coined names -//! (`Cryptomatte`, `EXtractoR`, `IDentifier`, `Key Light`, `Unmult`, `Lumetri Color`, `Colorama`, -//! `Minimax`, `Tritone`, `Vibrance`, `Vegas`) and the whole `CC` series stay in English; a series -//! tag that prefixes a descriptive name (`VR`, `OCIO`, `PS`, `HDR`) stays and follows the -//! French word order (`Flou VR`, `Transformation CDL OCIO`). +//! (`Cryptomatte`, `EXtractoR`, `IDentifier`, `Key Light`, `Unmult`, `Colorama`, `Minimax`, +//! `Tritone`, `Vibrance`, `Vegas`) stay in English; a series tag that prefixes a descriptive name +//! (`VR`, `OCIO`, `PS`, `HDR`) stays and follows the French word order (`Flou VR`, +//! `Transformation CDL OCIO`). use std::collections::HashMap; use std::sync::OnceLock; @@ -122,19 +126,21 @@ pub(super) fn translated_rows(language: &str) -> usize { /// 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`). /// -/// A name that is neither the registry's nor the registry's plus an index was typed by the user -/// (Effect Controls ▸ Rename), so it stays verbatim — as does the whole string in a language -/// with no row. +/// Current and legacy default names (including numeric instance suffixes) draw the current +/// name. Other names were supplied by the user and stay verbatim. Stored data is never changed. pub(crate) fn group(id: &str, stored: &str) -> String { - let Some((english, translated)) = catalog(current()).and_then(|c| c.row(id)) else { return stored.to_string() }; - if stored == english { - return translated.to_string(); - } - if let Some(index) = stored.strip_prefix(english).and_then(|rest| rest.strip_prefix(' ')) - && !index.is_empty() - && index.bytes().all(|b| b.is_ascii_digit()) - { - return format!("{translated} {index}"); + let Some(spec) = effectcraft_engine::effects::find(id) else { return stored.to_string() }; + let translated = name(id, spec.name); + for source in std::iter::once(spec.name).chain(effectcraft_engine::effects::aliases(id).iter().copied()) { + if stored == source { + return translated; + } + if let Some(index) = stored.strip_prefix(source).and_then(|rest| rest.strip_prefix(' ')) + && !index.is_empty() + && index.bytes().all(|b| b.is_ascii_digit()) + { + return format!("{translated} {index}"); + } } stored.to_string() } @@ -149,6 +155,7 @@ pub(crate) fn category(source: &str) -> String { /// `query` (lowercase)? Both names match, so a translated interface can be searched either way. pub(crate) fn matches(id: &str, english: &str, query: &str) -> bool { english.to_lowercase().contains(query) + || effectcraft_engine::effects::find(id).is_some_and(|spec| effectcraft_engine::effects::name_matches(spec, query)) || catalog(current()).and_then(|c| c.row(id)).is_some_and(|(_, translated)| translated.to_lowercase().contains(query)) } @@ -192,10 +199,10 @@ const SIMPLIFIED_CHINESE: &[Row] = &[ ("ec.blur.bilateral", "Bilateral Blur", "双向模糊"), ("ec.blur.cameralens", "Camera Lens Blur", "摄像机镜头模糊"), ("ec.blur.camerashakedeblur", "Camera-Shake Deblur", "摄像机抖动去模糊"), - ("ec.blur.cccross", "CC Cross Blur", "CC 交叉模糊"), - ("ec.blur.ccradial", "CC Radial Blur", "CC 径向模糊"), - ("ec.blur.ccradialfast", "CC Radial Fast Blur", "CC 径向快速模糊"), - ("ec.blur.ccvector", "CC Vector Blur", "CC 矢量模糊"), + ("ec.blur.cccross", "Cross Blur", "十字模糊"), + ("ec.blur.ccradial", "Variable Radial Blur", "可变径向模糊"), + ("ec.blur.ccradialfast", "Fast Radial Blur", "快速径向模糊"), + ("ec.blur.ccvector", "Vector Blur", "矢量模糊"), ("ec.blur.channel", "Channel Blur", "通道模糊"), ("ec.blur.compound", "Compound Blur", "复合模糊"), ("ec.blur.directional", "Directional Blur", "定向模糊"), @@ -209,7 +216,7 @@ const SIMPLIFIED_CHINESE: &[Row] = &[ ("ec.channel.arithmetic", "Arithmetic", "算术"), ("ec.channel.blend", "Blend", "混合"), ("ec.channel.calculations", "Calculations", "计算"), - ("ec.channel.cccomposite", "CC Composite", "CC 合成"), + ("ec.channel.cccomposite", "Source Composite", "源图合成"), ("ec.channel.combiner", "Channel Combiner", "通道合成器"), ("ec.channel.compoundarithmetic", "Compound Arithmetic", "复合运算"), ("ec.channel.invert", "Invert", "反转"), @@ -225,10 +232,10 @@ const SIMPLIFIED_CHINESE: &[Row] = &[ ("ec.color.blackwhite", "Black & White", "黑色和白色"), ("ec.color.brightnesscontrast", "Brightness & Contrast", "亮度和对比度"), ("ec.color.broadcast", "Broadcast Colors", "广播颜色"), - ("ec.color.cccolorneutralizer", "CC Color Neutralizer", "CC 颜色中和器"), - ("ec.color.cccoloroffset", "CC Color Offset", "CC 颜色偏移"), - ("ec.color.cckernel", "CC Kernel", "CC 内核"), - ("ec.color.cctoner", "CC Toner", "CC 调色器"), + ("ec.color.cccolorneutralizer", "Color Neutralizer", "色偏校正"), + ("ec.color.cccoloroffset", "Channel Offset", "通道偏移"), + ("ec.color.cckernel", "Convolution Kernel", "卷积核"), + ("ec.color.cctoner", "Multi-Tone Tint", "多色调着色"), ("ec.color.changecolor", "Change Color", "更改颜色"), ("ec.color.changetocolor", "Change to Color", "更改为颜色"), ("ec.color.channelmixer", "Channel Mixer", "通道混合器"), @@ -245,7 +252,7 @@ const SIMPLIFIED_CHINESE: &[Row] = &[ ("ec.color.leavecolor", "Leave Color", "保留颜色"), ("ec.color.levels", "Levels", "色阶"), ("ec.color.levelsic", "Levels (Individual Controls)", "色阶 (单独控件)"), - ("ec.color.lumetri", "Lumetri Color", "Lumetri 颜色"), + ("ec.color.lumetri", "Color Grading", "色彩分级"), ("ec.color.ociocdl", "OCIO CDL Transform", "OCIO CDL 变换"), ("ec.color.ociocolorspace", "OCIO Color Space Transform", "OCIO 颜色空间变换"), ("ec.color.ociodisplay", "OCIO Display Transform", "OCIO 显示变换"), @@ -270,20 +277,20 @@ const SIMPLIFIED_CHINESE: &[Row] = &[ ("ec.control.stereo3d", "Stereo 3D Controls", "立体 3D 控制"), ("ec.distort.bezierwarp", "Bezier Warp", "贝塞尔曲线变形"), ("ec.distort.bulge", "Bulge", "凸出"), - ("ec.distort.ccbender", "CC Bender", "CC 弯曲器"), - ("ec.distort.ccbendit", "CC Bend It", "CC 弯曲"), - ("ec.distort.ccblobbylize", "CC Blobbylize", "CC 融化"), - ("ec.distort.ccflomotion", "CC Flo Motion", "CC 流动运动"), - ("ec.distort.ccgriddler", "CC Griddler", "CC 网格生成器"), - ("ec.distort.cclens", "CC Lens", "CC 镜头"), - ("ec.distort.ccpageturn", "CC Page Turn", "CC 翻页"), - ("ec.distort.ccpowerpin", "CC Power Pin", "CC 强力边角定位"), - ("ec.distort.ccripplepulse", "CC Ripple Pulse", "CC 波纹脉冲"), - ("ec.distort.ccslant", "CC Slant", "CC 倾斜"), - ("ec.distort.ccsmear", "CC Smear", "CC 漩涡条纹"), - ("ec.distort.ccsplit", "CC Split", "CC 拆分"), - ("ec.distort.ccsplit2", "CC Split 2", "CC 拆分 2"), - ("ec.distort.cctiler", "CC Tiler", "CC 拼贴器"), + ("ec.distort.ccbender", "Flexible Bend", "柔性弯曲"), + ("ec.distort.ccbendit", "Two-Point Bend", "两点弯曲"), + ("ec.distort.ccblobbylize", "Organic Displacement", "有机置换"), + ("ec.distort.ccflomotion", "Dual Lens Warp", "双镜头变形"), + ("ec.distort.ccgriddler", "Tile Grid", "平铺网格"), + ("ec.distort.cclens", "Lens Distortion", "镜头畸变"), + ("ec.distort.ccpageturn", "Page Curl", "页面卷曲"), + ("ec.distort.ccpowerpin", "Perspective Pin", "透视定位"), + ("ec.distort.ccripplepulse", "Pulsed Ripple", "脉冲涟漪"), + ("ec.distort.ccslant", "Slant", "倾斜"), + ("ec.distort.ccsmear", "Directional Smear", "定向涂抹"), + ("ec.distort.ccsplit", "Split Warp", "分割变形"), + ("ec.distort.ccsplit2", "Dual Split Warp", "双重分割变形"), + ("ec.distort.cctiler", "Scaled Tiles", "缩放平铺"), ("ec.distort.cornerpin", "Corner Pin", "边角定位"), ("ec.distort.displacementmap", "Displacement Map", "置换图"), ("ec.distort.liquify", "Liquify", "液化"), @@ -311,11 +318,11 @@ const SIMPLIFIED_CHINESE: &[Row] = &[ ("ec.generate.audiospectrum", "Audio Spectrum", "音频频谱"), ("ec.generate.audiowaveform", "Audio Waveform", "音频波形"), ("ec.generate.beam", "Beam", "光束"), - ("ec.generate.ccgluegun", "CC Glue Gun", "CC 胶水枪"), - ("ec.generate.cclightburst", "CC Light Burst 2.5", "CC 光束放射 2.5"), - ("ec.generate.cclightrays", "CC Light Rays", "CC 光线"), - ("ec.generate.cclightsweep", "CC Light Sweep", "CC 光线扫描"), - ("ec.generate.ccthreads", "CC Threads", "CC 编织条"), + ("ec.generate.ccgluegun", "Paint Beads", "颜料珠"), + ("ec.generate.cclightburst", "Radial Light Burst", "径向光芒"), + ("ec.generate.cclightrays", "Light Rays", "光线"), + ("ec.generate.cclightsweep", "Light Sweep", "扫光"), + ("ec.generate.ccthreads", "Woven Threads", "编织线"), ("ec.generate.cellpattern", "Cell Pattern", "单元格图案"), ("ec.generate.checkerboard", "Checkerboard", "棋盘"), ("ec.generate.circle", "Circle", "圆形"), @@ -334,7 +341,7 @@ const SIMPLIFIED_CHINESE: &[Row] = &[ ("ec.generate.vegas", "Vegas", "勾画"), ("ec.generate.writeon", "Write-on", "写入"), ("ec.key.advancedspill", "Advanced Spill Suppressor", "高级溢出抑制器"), - ("ec.key.ccsimplewireremoval", "CC Simple Wire Removal", "CC 简单丝线移除"), + ("ec.key.ccsimplewireremoval", "Line Removal", "线条移除"), ("ec.key.colordifference", "Color Difference Key", "颜色差值键"), ("ec.key.colorkey", "Color Key", "颜色键"), ("ec.key.colorrange", "Color Range", "颜色范围"), @@ -347,7 +354,7 @@ const SIMPLIFIED_CHINESE: &[Row] = &[ ("ec.key.screen", "Screen Key", "屏幕键"), ("ec.key.spill", "Spill Suppressor", "溢出抑制"), ("ec.key.unmult", "Unmult", "Unmult"), - ("ec.keying.keylight", "Key Light", "Key Light"), + ("ec.keying.keylight", "Screen Color Key", "屏幕色键"), ("ec.matte.mattechoker", "Matte Choker", "遮罩阻塞工具"), ("ec.matte.objectmatte", "Object Matte", "对象遮罩"), ("ec.matte.refinehard", "Refine Hard Matte", "调整实边遮罩"), @@ -371,50 +378,50 @@ const SIMPLIFIED_CHINESE: &[Row] = &[ ("ec.obsolete.basictext", "Basic Text", "基本文字"), ("ec.obsolete.gaussianlegacy", "Gaussian Blur (Legacy)", "高斯模糊 (旧版)"), ("ec.obsolete.lightning", "Lightning", "闪电"), - ("ec.obsolete.mochashape", "Mocha shape", "Mocha 形状"), + ("ec.obsolete.mochashape", "Tracked Shape Matte", "跟踪形状遮罩"), ("ec.obsolete.pathtext", "Path Text", "路径文本"), ("ec.paint.paint", "Paint", "绘画"), ("ec.perspective.3dglasses", "3D Glasses", "3D 眼镜"), ("ec.perspective.bevelalpha", "Bevel Alpha", "斜面 Alpha"), ("ec.perspective.beveledges", "Bevel Edges", "边缘斜面"), ("ec.perspective.cameratracker", "3D Camera Tracker", "3D 摄像机跟踪器"), - ("ec.perspective.cccylinder", "CC Cylinder", "CC 圆柱体"), - ("ec.perspective.ccenvironment", "CC Environment", "CC 环境"), - ("ec.perspective.ccsphere", "CC Sphere", "CC 球体"), - ("ec.perspective.ccspotlight", "CC Spotlight", "CC 聚光"), + ("ec.perspective.cccylinder", "Cylinder Wrap", "圆柱包裹"), + ("ec.perspective.ccenvironment", "Environment Map", "环境映射"), + ("ec.perspective.ccsphere", "Sphere Wrap", "球面包裹"), + ("ec.perspective.ccspotlight", "Surface Spotlight", "表面聚光灯"), ("ec.perspective.dropshadow", "Drop Shadow", "投影"), ("ec.perspective.radialshadow", "Radial Shadow", "径向阴影"), ("ec.sim.carddance", "Card Dance", "卡片动画"), ("ec.sim.caustics", "Caustics", "焦散"), - ("ec.sim.ccballaction", "CC Ball Action", "CC 球动作"), - ("ec.sim.ccbubbles", "CC Bubbles", "CC 气泡"), - ("ec.sim.ccdrizzle", "CC Drizzle", "CC 细雨波纹"), - ("ec.sim.cchair", "CC Hair", "CC 毛发"), - ("ec.sim.ccmrmercury", "CC Mr. Mercury", "CC 水银流体"), - ("ec.sim.ccparticlesystems2", "CC Particle Systems II", "CC 粒子系统 II"), - ("ec.sim.ccparticleworld", "CC Particle World", "CC 粒子世界"), - ("ec.sim.ccpixelpolly", "CC Pixel Polly", "CC 像素多边形"), - ("ec.sim.ccrainfall", "CC Rainfall", "CC 降雨"), - ("ec.sim.ccscatterize", "CC Scatterize", "CC 散射"), - ("ec.sim.ccsnowfall", "CC Snowfall", "CC 降雪"), - ("ec.sim.ccstarburst", "CC Star Burst", "CC 星爆"), + ("ec.sim.ccballaction", "Ball Grid", "球体网格"), + ("ec.sim.ccbubbles", "Bubbles", "气泡"), + ("ec.sim.ccdrizzle", "Water Ripples", "水面涟漪"), + ("ec.sim.cchair", "Surface Fibers", "表面纤维"), + ("ec.sim.ccmrmercury", "Liquid Particles", "液体粒子"), + ("ec.sim.ccparticlesystems2", "Particle Emitter 2D", "二维粒子发射器"), + ("ec.sim.ccparticleworld", "Particle Emitter 3D", "三维粒子发射器"), + ("ec.sim.ccpixelpolly", "Pixel Shatter", "像素破碎"), + ("ec.sim.ccrainfall", "Rainfall", "降雨"), + ("ec.sim.ccscatterize", "Pixel Scatter", "像素散射"), + ("ec.sim.ccsnowfall", "Snowfall", "降雪"), + ("ec.sim.ccstarburst", "Starfield", "星空"), ("ec.sim.foam", "Foam", "泡沫"), ("ec.sim.particleplayground", "Particle Playground", "粒子运动场"), ("ec.sim.shatter", "Shatter", "碎片"), ("ec.sim.waveworld", "Wave World", "波形环境"), ("ec.stylize.brushstrokes", "Brush Strokes", "画笔描边"), ("ec.stylize.cartoon", "Cartoon", "卡通"), - ("ec.stylize.ccblockload", "CC Block Load", "CC 块加载"), - ("ec.stylize.ccburnfilm", "CC Burn Film", "CC 烧毁胶片"), - ("ec.stylize.ccglass", "CC Glass", "CC 玻璃"), - ("ec.stylize.cchextile", "CC HexTile", "CC 六边形拼贴"), - ("ec.stylize.cckaleida", "CC Kaleida", "CC 万花筒"), - ("ec.stylize.ccmrsmoothie", "CC Mr. Smoothie", "CC 像素溶解"), - ("ec.stylize.ccplastic", "CC Plastic", "CC 塑料"), - ("ec.stylize.ccrepetile", "CC RepeTile", "CC 重复拼贴"), - ("ec.stylize.ccthreshold", "CC Threshold", "CC 阈值"), - ("ec.stylize.ccthresholdrgb", "CC Threshold RGB", "CC 阈值 RGB"), - ("ec.stylize.ccvignette", "CC Vignette", "CC 暗角"), + ("ec.stylize.ccblockload", "Block Reveal", "块状显现"), + ("ec.stylize.ccburnfilm", "Film Burn", "胶片灼烧"), + ("ec.stylize.ccglass", "Glass Relief", "玻璃浮雕"), + ("ec.stylize.cchextile", "Hexagonal Tiles", "六边形平铺"), + ("ec.stylize.cckaleida", "Kaleidoscope", "万花筒"), + ("ec.stylize.ccmrsmoothie", "Palette Map", "调色板映射"), + ("ec.stylize.ccplastic", "Plastic Shading", "塑料着色"), + ("ec.stylize.ccrepetile", "Edge Tile Repeat", "边缘平铺重复"), + ("ec.stylize.ccthreshold", "Channel Threshold", "通道阈值"), + ("ec.stylize.ccthresholdrgb", "RGB Threshold", "RGB 阈值"), + ("ec.stylize.ccvignette", "Vignette", "晕影"), ("ec.stylize.coloremboss", "Color Emboss", "彩色浮雕"), ("ec.stylize.emboss", "Emboss", "浮雕"), ("ec.stylize.findedges", "Find Edges", "查找边缘"), @@ -429,8 +436,8 @@ const SIMPLIFIED_CHINESE: &[Row] = &[ ("ec.stylize.threshold", "Threshold", "阈值"), ("ec.text.numbers", "Numbers", "编号"), ("ec.text.timecode", "Timecode", "时间码"), - ("ec.time.ccforcemotionblur", "CC Force Motion Blur", "CC 强制运动模糊"), - ("ec.time.ccwidetime", "CC Wide Time", "CC 宽泛时间"), + ("ec.time.ccforcemotionblur", "Temporal Motion Blur", "时间运动模糊"), + ("ec.time.ccwidetime", "Temporal Blend", "时间混合"), ("ec.time.echo", "Echo", "残影"), ("ec.time.pixelmotionblur", "Pixel Motion Blur", "像素运动模糊"), ("ec.time.posterizetime", "Posterize Time", "抽帧"), @@ -439,23 +446,23 @@ const SIMPLIFIED_CHINESE: &[Row] = &[ ("ec.time.timewarp", "Timewarp", "时间扭曲"), ("ec.transition.blockdissolve", "Block Dissolve", "块溶解"), ("ec.transition.cardwipe", "Card Wipe", "卡片擦除"), - ("ec.transition.ccglasswipe", "CC Glass Wipe", "CC 玻璃擦除"), - ("ec.transition.ccgridwipe", "CC Grid Wipe", "CC 网格擦除"), - ("ec.transition.ccimagewipe", "CC Image Wipe", "CC 图像擦除"), - ("ec.transition.ccjaws", "CC Jaws", "CC 锯齿"), - ("ec.transition.cclightwipe", "CC Light Wipe", "CC 光线擦除"), - ("ec.transition.cclinesweep", "CC Line Sweep", "CC 线条扫掠"), - ("ec.transition.ccradialscalewipe", "CC Radial ScaleWipe", "CC 径向缩放擦除"), - ("ec.transition.ccscalewipe", "CC Scale Wipe", "CC 缩放擦除"), - ("ec.transition.cctwister", "CC Twister", "CC 扭曲器"), - ("ec.transition.ccwarpomatic", "CC WarpoMatic", "CC 自动扭曲"), + ("ec.transition.ccglasswipe", "Glass Wipe", "玻璃擦除"), + ("ec.transition.ccgridwipe", "Grid Wipe", "网格擦除"), + ("ec.transition.ccimagewipe", "Luminance Image Wipe", "亮度图像擦除"), + ("ec.transition.ccjaws", "Serrated Wipe", "锯齿擦除"), + ("ec.transition.cclightwipe", "Light Wipe", "光线擦除"), + ("ec.transition.cclinesweep", "Line Sweep", "线条扫过"), + ("ec.transition.ccradialscalewipe", "Radial Scale Wipe", "径向缩放擦除"), + ("ec.transition.ccscalewipe", "Scale Wipe", "缩放擦除"), + ("ec.transition.cctwister", "Twist Wipe", "扭转擦除"), + ("ec.transition.ccwarpomatic", "Warp Dissolve", "变形溶解"), ("ec.transition.gradientwipe", "Gradient Wipe", "渐变擦除"), ("ec.transition.iriswipe", "Iris Wipe", "光圈擦除"), ("ec.transition.linearwipe", "Linear Wipe", "线性擦除"), ("ec.transition.radialwipe", "Radial Wipe", "径向擦除"), ("ec.transition.venetian", "Venetian Blinds", "百叶窗"), ("ec.utility.applylut", "Apply Color LUT", "应用颜色 LUT"), - ("ec.utility.ccoverbrights", "CC Overbrights", "CC 高光溢出"), + ("ec.utility.ccoverbrights", "Out-of-Range Display", "超出范围显示"), ("ec.utility.cineon", "Cineon Converter", "Cineon 转换器"), ("ec.utility.colorprofileconverter", "Color Profile Converter", "颜色配置文件转换器"), ("ec.utility.facemeasurements", "Face Measurements", "面部测量"), @@ -480,9 +487,9 @@ const SIMPLIFIED_CHINESE: &[Row] = &[ /// [`SIMPLIFIED_CHINESE`]. Written from the meaning of the English names with the vocabulary /// Taiwan's creative software uses (圖層, 遮罩, 筆刷, 雜訊, 發光, 漸層), not converted from the /// Simplified table. Brand and coined names (`Cryptomatte`, `EXtractoR`, `IDentifier`, `Key Light`, -/// `Unmult`, `Lumetri Color`, `Colorama`, `Minimax`, `Vegas`, `Mocha`) stay in English; the `CC` -/// series keeps its prefix and translates the descriptive tail, and the `VR` and `OCIO` sets keep -/// their prefix in Chinese word order. +/// `Unmult`, `Colorama`, `Minimax`, `Vegas`) stay in English; the `VR` and `OCIO` sets keep their +/// prefix in Chinese word order. The effects this cleanup renamed are translated from their new +/// descriptive names. const TRADITIONAL_CHINESE: &[Row] = &[ ("ec.3d.channelextract", "3D Channel Extract", "3D 通道擷取"), ("ec.3d.cryptomatte", "Cryptomatte", "Cryptomatte"), @@ -509,10 +516,10 @@ const TRADITIONAL_CHINESE: &[Row] = &[ ("ec.blur.bilateral", "Bilateral Blur", "雙邊模糊"), ("ec.blur.cameralens", "Camera Lens Blur", "相機鏡頭模糊"), ("ec.blur.camerashakedeblur", "Camera-Shake Deblur", "相機晃動去模糊"), - ("ec.blur.cccross", "CC Cross Blur", "CC 十字模糊"), - ("ec.blur.ccradial", "CC Radial Blur", "CC 徑向模糊"), - ("ec.blur.ccradialfast", "CC Radial Fast Blur", "CC 徑向快速模糊"), - ("ec.blur.ccvector", "CC Vector Blur", "CC 向量模糊"), + ("ec.blur.cccross", "Cross Blur", "十字模糊"), + ("ec.blur.ccradial", "Variable Radial Blur", "可變放射狀模糊"), + ("ec.blur.ccradialfast", "Fast Radial Blur", "快速放射狀模糊"), + ("ec.blur.ccvector", "Vector Blur", "向量模糊"), ("ec.blur.channel", "Channel Blur", "通道模糊"), ("ec.blur.compound", "Compound Blur", "複合模糊"), ("ec.blur.directional", "Directional Blur", "方向模糊"), @@ -526,7 +533,7 @@ const TRADITIONAL_CHINESE: &[Row] = &[ ("ec.channel.arithmetic", "Arithmetic", "算術"), ("ec.channel.blend", "Blend", "混合"), ("ec.channel.calculations", "Calculations", "運算"), - ("ec.channel.cccomposite", "CC Composite", "CC 合成"), + ("ec.channel.cccomposite", "Source Composite", "來源影像合成"), ("ec.channel.combiner", "Channel Combiner", "通道合成器"), ("ec.channel.compoundarithmetic", "Compound Arithmetic", "複合算術"), ("ec.channel.invert", "Invert", "反轉"), @@ -542,10 +549,10 @@ const TRADITIONAL_CHINESE: &[Row] = &[ ("ec.color.blackwhite", "Black & White", "黑白"), ("ec.color.brightnesscontrast", "Brightness & Contrast", "亮度與對比"), ("ec.color.broadcast", "Broadcast Colors", "廣播色彩"), - ("ec.color.cccolorneutralizer", "CC Color Neutralizer", "CC 色彩中和器"), - ("ec.color.cccoloroffset", "CC Color Offset", "CC 色彩偏移"), - ("ec.color.cckernel", "CC Kernel", "CC 核心"), - ("ec.color.cctoner", "CC Toner", "CC 色調"), + ("ec.color.cccolorneutralizer", "Color Neutralizer", "色偏校正"), + ("ec.color.cccoloroffset", "Channel Offset", "色版偏移"), + ("ec.color.cckernel", "Convolution Kernel", "卷積核心"), + ("ec.color.cctoner", "Multi-Tone Tint", "多色調著色"), ("ec.color.changecolor", "Change Color", "變更色彩"), ("ec.color.changetocolor", "Change to Color", "變更為色彩"), ("ec.color.channelmixer", "Channel Mixer", "通道混合器"), @@ -562,7 +569,7 @@ const TRADITIONAL_CHINESE: &[Row] = &[ ("ec.color.leavecolor", "Leave Color", "保留色彩"), ("ec.color.levels", "Levels", "色階"), ("ec.color.levelsic", "Levels (Individual Controls)", "色階(個別控制項)"), - ("ec.color.lumetri", "Lumetri Color", "Lumetri Color"), + ("ec.color.lumetri", "Color Grading", "色彩分級"), ("ec.color.ociocdl", "OCIO CDL Transform", "OCIO CDL 轉換"), ("ec.color.ociocolorspace", "OCIO Color Space Transform", "OCIO 色彩空間轉換"), ("ec.color.ociodisplay", "OCIO Display Transform", "OCIO 顯示轉換"), @@ -587,20 +594,20 @@ const TRADITIONAL_CHINESE: &[Row] = &[ ("ec.control.stereo3d", "Stereo 3D Controls", "立體 3D 控制"), ("ec.distort.bezierwarp", "Bezier Warp", "貝茲彎曲"), ("ec.distort.bulge", "Bulge", "凸出"), - ("ec.distort.ccbender", "CC Bender", "CC 彎曲"), - ("ec.distort.ccbendit", "CC Bend It", "CC 彎折"), - ("ec.distort.ccblobbylize", "CC Blobbylize", "CC 團塊化"), - ("ec.distort.ccflomotion", "CC Flo Motion", "CC 流動"), - ("ec.distort.ccgriddler", "CC Griddler", "CC 格線"), - ("ec.distort.cclens", "CC Lens", "CC 鏡頭"), - ("ec.distort.ccpageturn", "CC Page Turn", "CC 翻頁"), - ("ec.distort.ccpowerpin", "CC Power Pin", "CC 強力釘選"), - ("ec.distort.ccripplepulse", "CC Ripple Pulse", "CC 漣漪脈衝"), - ("ec.distort.ccslant", "CC Slant", "CC 傾斜"), - ("ec.distort.ccsmear", "CC Smear", "CC 塗抹"), - ("ec.distort.ccsplit", "CC Split", "CC 分割"), - ("ec.distort.ccsplit2", "CC Split 2", "CC 分割 2"), - ("ec.distort.cctiler", "CC Tiler", "CC 並排"), + ("ec.distort.ccbender", "Flexible Bend", "柔性彎曲"), + ("ec.distort.ccbendit", "Two-Point Bend", "兩點彎曲"), + ("ec.distort.ccblobbylize", "Organic Displacement", "有機置換"), + ("ec.distort.ccflomotion", "Dual Lens Warp", "雙鏡頭變形"), + ("ec.distort.ccgriddler", "Tile Grid", "拼貼網格"), + ("ec.distort.cclens", "Lens Distortion", "鏡頭畸變"), + ("ec.distort.ccpageturn", "Page Curl", "頁面捲曲"), + ("ec.distort.ccpowerpin", "Perspective Pin", "透視定位"), + ("ec.distort.ccripplepulse", "Pulsed Ripple", "脈衝漣漪"), + ("ec.distort.ccslant", "Slant", "傾斜"), + ("ec.distort.ccsmear", "Directional Smear", "定向塗抹"), + ("ec.distort.ccsplit", "Split Warp", "分割變形"), + ("ec.distort.ccsplit2", "Dual Split Warp", "雙重分割變形"), + ("ec.distort.cctiler", "Scaled Tiles", "縮放平鋪"), ("ec.distort.cornerpin", "Corner Pin", "邊角釘選"), ("ec.distort.displacementmap", "Displacement Map", "位移映射"), ("ec.distort.liquify", "Liquify", "液化"), @@ -628,11 +635,11 @@ const TRADITIONAL_CHINESE: &[Row] = &[ ("ec.generate.audiospectrum", "Audio Spectrum", "音訊頻譜"), ("ec.generate.audiowaveform", "Audio Waveform", "音訊波形"), ("ec.generate.beam", "Beam", "光束"), - ("ec.generate.ccgluegun", "CC Glue Gun", "CC 膠水槍"), - ("ec.generate.cclightburst", "CC Light Burst 2.5", "CC 光爆 2.5"), - ("ec.generate.cclightrays", "CC Light Rays", "CC 光線"), - ("ec.generate.cclightsweep", "CC Light Sweep", "CC 光線掃掠"), - ("ec.generate.ccthreads", "CC Threads", "CC 絲線"), + ("ec.generate.ccgluegun", "Paint Beads", "顏料珠"), + ("ec.generate.cclightburst", "Radial Light Burst", "放射狀光芒"), + ("ec.generate.cclightrays", "Light Rays", "光線"), + ("ec.generate.cclightsweep", "Light Sweep", "掃光"), + ("ec.generate.ccthreads", "Woven Threads", "編織線"), ("ec.generate.cellpattern", "Cell Pattern", "細胞圖樣"), ("ec.generate.checkerboard", "Checkerboard", "棋盤格"), ("ec.generate.circle", "Circle", "圓形"), @@ -651,7 +658,7 @@ const TRADITIONAL_CHINESE: &[Row] = &[ ("ec.generate.vegas", "Vegas", "Vegas"), ("ec.generate.writeon", "Write-on", "書寫"), ("ec.key.advancedspill", "Advanced Spill Suppressor", "進階溢色抑制"), - ("ec.key.ccsimplewireremoval", "CC Simple Wire Removal", "CC 簡易鋼絲移除"), + ("ec.key.ccsimplewireremoval", "Line Removal", "線條移除"), ("ec.key.colordifference", "Color Difference Key", "色彩差異去背"), ("ec.key.colorkey", "Color Key", "色彩去背"), ("ec.key.colorrange", "Color Range", "色彩範圍"), @@ -664,7 +671,7 @@ const TRADITIONAL_CHINESE: &[Row] = &[ ("ec.key.screen", "Screen Key", "螢幕去背"), ("ec.key.spill", "Spill Suppressor", "溢色抑制"), ("ec.key.unmult", "Unmult", "Unmult"), - ("ec.keying.keylight", "Key Light", "Key Light"), + ("ec.keying.keylight", "Screen Color Key", "螢幕色鍵"), ("ec.matte.mattechoker", "Matte Choker", "遮罩收縮器"), ("ec.matte.objectmatte", "Object Matte", "物件遮罩"), ("ec.matte.refinehard", "Refine Hard Matte", "調整硬邊遮罩"), @@ -688,50 +695,50 @@ const TRADITIONAL_CHINESE: &[Row] = &[ ("ec.obsolete.basictext", "Basic Text", "基礎文字"), ("ec.obsolete.gaussianlegacy", "Gaussian Blur (Legacy)", "高斯模糊(舊版)"), ("ec.obsolete.lightning", "Lightning", "閃電"), - ("ec.obsolete.mochashape", "Mocha shape", "Mocha 形狀"), + ("ec.obsolete.mochashape", "Tracked Shape Matte", "追蹤形狀遮罩"), ("ec.obsolete.pathtext", "Path Text", "路徑文字"), ("ec.paint.paint", "Paint", "繪畫"), ("ec.perspective.3dglasses", "3D Glasses", "3D 眼鏡"), ("ec.perspective.bevelalpha", "Bevel Alpha", "Alpha 斜角"), ("ec.perspective.beveledges", "Bevel Edges", "邊緣斜角"), ("ec.perspective.cameratracker", "3D Camera Tracker", "3D 攝影機追蹤"), - ("ec.perspective.cccylinder", "CC Cylinder", "CC 圓柱"), - ("ec.perspective.ccenvironment", "CC Environment", "CC 環境"), - ("ec.perspective.ccsphere", "CC Sphere", "CC 球體"), - ("ec.perspective.ccspotlight", "CC Spotlight", "CC 聚光燈"), + ("ec.perspective.cccylinder", "Cylinder Wrap", "圓柱包覆"), + ("ec.perspective.ccenvironment", "Environment Map", "環境貼圖"), + ("ec.perspective.ccsphere", "Sphere Wrap", "球面包覆"), + ("ec.perspective.ccspotlight", "Surface Spotlight", "表面聚光燈"), ("ec.perspective.dropshadow", "Drop Shadow", "陰影"), ("ec.perspective.radialshadow", "Radial Shadow", "徑向陰影"), ("ec.sim.carddance", "Card Dance", "卡片舞蹈"), ("ec.sim.caustics", "Caustics", "焦散"), - ("ec.sim.ccballaction", "CC Ball Action", "CC 球狀動作"), - ("ec.sim.ccbubbles", "CC Bubbles", "CC 泡泡"), - ("ec.sim.ccdrizzle", "CC Drizzle", "CC 細雨"), - ("ec.sim.cchair", "CC Hair", "CC 毛髮"), - ("ec.sim.ccmrmercury", "CC Mr. Mercury", "CC 水銀先生"), - ("ec.sim.ccparticlesystems2", "CC Particle Systems II", "CC 粒子系統 II"), - ("ec.sim.ccparticleworld", "CC Particle World", "CC 粒子世界"), - ("ec.sim.ccpixelpolly", "CC Pixel Polly", "CC 像素破碎"), - ("ec.sim.ccrainfall", "CC Rainfall", "CC 下雨"), - ("ec.sim.ccscatterize", "CC Scatterize", "CC 散開"), - ("ec.sim.ccsnowfall", "CC Snowfall", "CC 下雪"), - ("ec.sim.ccstarburst", "CC Star Burst", "CC 星爆"), + ("ec.sim.ccballaction", "Ball Grid", "球體網格"), + ("ec.sim.ccbubbles", "Bubbles", "氣泡"), + ("ec.sim.ccdrizzle", "Water Ripples", "水面漣漪"), + ("ec.sim.cchair", "Surface Fibers", "表面纖維"), + ("ec.sim.ccmrmercury", "Liquid Particles", "液體粒子"), + ("ec.sim.ccparticlesystems2", "Particle Emitter 2D", "二維粒子發射器"), + ("ec.sim.ccparticleworld", "Particle Emitter 3D", "三維粒子發射器"), + ("ec.sim.ccpixelpolly", "Pixel Shatter", "像素破碎"), + ("ec.sim.ccrainfall", "Rainfall", "降雨"), + ("ec.sim.ccscatterize", "Pixel Scatter", "像素散射"), + ("ec.sim.ccsnowfall", "Snowfall", "降雪"), + ("ec.sim.ccstarburst", "Starfield", "星空"), ("ec.sim.foam", "Foam", "泡沫"), ("ec.sim.particleplayground", "Particle Playground", "粒子遊樂場"), ("ec.sim.shatter", "Shatter", "粉碎"), ("ec.sim.waveworld", "Wave World", "波浪世界"), ("ec.stylize.brushstrokes", "Brush Strokes", "筆觸"), ("ec.stylize.cartoon", "Cartoon", "卡通"), - ("ec.stylize.ccblockload", "CC Block Load", "CC 區塊載入"), - ("ec.stylize.ccburnfilm", "CC Burn Film", "CC 燒灼底片"), - ("ec.stylize.ccglass", "CC Glass", "CC 玻璃"), - ("ec.stylize.cchextile", "CC HexTile", "CC 六角並排"), - ("ec.stylize.cckaleida", "CC Kaleida", "CC 萬花筒"), - ("ec.stylize.ccmrsmoothie", "CC Mr. Smoothie", "CC 平滑先生"), - ("ec.stylize.ccplastic", "CC Plastic", "CC 塑膠"), - ("ec.stylize.ccrepetile", "CC RepeTile", "CC 重複並排"), - ("ec.stylize.ccthreshold", "CC Threshold", "CC 臨界值"), - ("ec.stylize.ccthresholdrgb", "CC Threshold RGB", "CC 臨界值 RGB"), - ("ec.stylize.ccvignette", "CC Vignette", "CC 暗角"), + ("ec.stylize.ccblockload", "Block Reveal", "區塊顯現"), + ("ec.stylize.ccburnfilm", "Film Burn", "膠片灼燒"), + ("ec.stylize.ccglass", "Glass Relief", "玻璃浮雕"), + ("ec.stylize.cchextile", "Hexagonal Tiles", "六角形拼貼"), + ("ec.stylize.cckaleida", "Kaleidoscope", "萬花筒"), + ("ec.stylize.ccmrsmoothie", "Palette Map", "調色盤對應"), + ("ec.stylize.ccplastic", "Plastic Shading", "塑膠著色"), + ("ec.stylize.ccrepetile", "Edge Tile Repeat", "邊緣拼貼重複"), + ("ec.stylize.ccthreshold", "Channel Threshold", "色版臨界值"), + ("ec.stylize.ccthresholdrgb", "RGB Threshold", "RGB 臨界值"), + ("ec.stylize.ccvignette", "Vignette", "暗角"), ("ec.stylize.coloremboss", "Color Emboss", "彩色浮雕"), ("ec.stylize.emboss", "Emboss", "浮雕"), ("ec.stylize.findedges", "Find Edges", "尋找邊緣"), @@ -746,8 +753,8 @@ const TRADITIONAL_CHINESE: &[Row] = &[ ("ec.stylize.threshold", "Threshold", "臨界值"), ("ec.text.numbers", "Numbers", "數字"), ("ec.text.timecode", "Timecode", "時間碼"), - ("ec.time.ccforcemotionblur", "CC Force Motion Blur", "CC 強制動態模糊"), - ("ec.time.ccwidetime", "CC Wide Time", "CC 寬廣時間"), + ("ec.time.ccforcemotionblur", "Temporal Motion Blur", "時間動態模糊"), + ("ec.time.ccwidetime", "Temporal Blend", "時間混合"), ("ec.time.echo", "Echo", "回音"), ("ec.time.pixelmotionblur", "Pixel Motion Blur", "像素動態模糊"), ("ec.time.posterizetime", "Posterize Time", "色調分離時間"), @@ -756,23 +763,23 @@ const TRADITIONAL_CHINESE: &[Row] = &[ ("ec.time.timewarp", "Timewarp", "時間扭曲"), ("ec.transition.blockdissolve", "Block Dissolve", "區塊溶解"), ("ec.transition.cardwipe", "Card Wipe", "卡片擦拭"), - ("ec.transition.ccglasswipe", "CC Glass Wipe", "CC 玻璃擦拭"), - ("ec.transition.ccgridwipe", "CC Grid Wipe", "CC 格線擦拭"), - ("ec.transition.ccimagewipe", "CC Image Wipe", "CC 影像擦拭"), - ("ec.transition.ccjaws", "CC Jaws", "CC 巨顎"), - ("ec.transition.cclightwipe", "CC Light Wipe", "CC 光線擦拭"), - ("ec.transition.cclinesweep", "CC Line Sweep", "CC 線條掃掠"), - ("ec.transition.ccradialscalewipe", "CC Radial ScaleWipe", "CC 徑向縮放擦拭"), - ("ec.transition.ccscalewipe", "CC Scale Wipe", "CC 縮放擦拭"), - ("ec.transition.cctwister", "CC Twister", "CC 扭轉"), - ("ec.transition.ccwarpomatic", "CC WarpoMatic", "CC 自動彎曲"), + ("ec.transition.ccglasswipe", "Glass Wipe", "玻璃擦拭"), + ("ec.transition.ccgridwipe", "Grid Wipe", "網格擦拭"), + ("ec.transition.ccimagewipe", "Luminance Image Wipe", "亮度影像擦拭"), + ("ec.transition.ccjaws", "Serrated Wipe", "鋸齒擦拭"), + ("ec.transition.cclightwipe", "Light Wipe", "光線擦拭"), + ("ec.transition.cclinesweep", "Line Sweep", "線條掃過"), + ("ec.transition.ccradialscalewipe", "Radial Scale Wipe", "放射狀縮放擦拭"), + ("ec.transition.ccscalewipe", "Scale Wipe", "縮放擦拭"), + ("ec.transition.cctwister", "Twist Wipe", "扭轉擦拭"), + ("ec.transition.ccwarpomatic", "Warp Dissolve", "變形溶解"), ("ec.transition.gradientwipe", "Gradient Wipe", "漸層擦拭"), ("ec.transition.iriswipe", "Iris Wipe", "光圈擦拭"), ("ec.transition.linearwipe", "Linear Wipe", "線性擦拭"), ("ec.transition.radialwipe", "Radial Wipe", "徑向擦拭"), ("ec.transition.venetian", "Venetian Blinds", "百葉窗"), ("ec.utility.applylut", "Apply Color LUT", "套用色彩 LUT"), - ("ec.utility.ccoverbrights", "CC Overbrights", "CC 過亮"), + ("ec.utility.ccoverbrights", "Out-of-Range Display", "超出範圍顯示"), ("ec.utility.cineon", "Cineon Converter", "Cineon 轉換器"), ("ec.utility.colorprofileconverter", "Color Profile Converter", "色彩設定檔轉換器"), ("ec.utility.facemeasurements", "Face Measurements", "臉部測量"), @@ -796,18 +803,18 @@ const TRADITIONAL_CHINESE: &[Row] = &[ /// Japanese (`ja`): reserved for the Japanese data point on #821, so both languages share this /// one catalog instead of inventing a second mechanism. A translator fills one row per effect, -/// ids from [`SIMPLIFIED_CHINESE`]; the names Adobe's Japanese interface leaves in English (the -/// `CC` series, `Cryptomatte`, `EXtractoR`, `IDentifier` and `Key Light`) repeat that English name -/// here rather than being left out; the `VR` and `OCIO` sets take a Japanese tail instead, and -/// `Unmult` keeps its English name in both catalogs. An effect with no row draws its English name -/// anyway, which is what `Twirl (Legacy)` and the two EffectCraft-only effects would do without -/// their rows. +/// ids from [`SIMPLIFIED_CHINESE`]; the names Adobe's Japanese interface leaves in English +/// (`Cryptomatte`, `EXtractoR`, `IDentifier` and `Key Light`) repeat that English name here rather +/// than being left out; the `VR` and `OCIO` sets take a Japanese tail instead, and `Unmult` keeps +/// its English name in both catalogs. An effect with no row draws its English name anyway, which +/// is what `Twirl (Legacy)` and the two EffectCraft-only effects would do without their rows. /// -/// The rows below come from AE 2026's Japanese interface where it was read directly (its Effect menu -/// and its Effects & Presets panel) and from Adobe's own Japanese help where the panel was not to -/// hand. A published glossary is used to cross-check, never as the only source: it normalises the -/// typography (half-width parentheses where AE draws full-width) and still carries CS6-era names for -/// the `CC` series, which AE 2026 leaves in English. +/// The rows of the effects this cleanup did not rename come from AE 2026's Japanese interface +/// where it was read directly (its Effect menu and its Effects & Presets panel) and from Adobe's +/// own Japanese help where the panel was not to hand. A published glossary is used to cross-check, +/// never as the only source: it normalises the typography (half-width parentheses where AE draws +/// full-width) and still carries CS6-era names for the `CC` series. The renamed effects are +/// translated from their new descriptive names. const JAPANESE_EFFECTS: &[Row] = &[ // Effects this application adds itself have no interface to read a name from, so they follow // the Chinese catalog's choice to translate them (顔の測定, 顔のトラッキングポイント) rather than @@ -847,10 +854,10 @@ const JAPANESE_EFFECTS: &[Row] = &[ ("ec.blur.bilateral", "Bilateral Blur", "ブラー(バイラテラル)"), ("ec.blur.cameralens", "Camera Lens Blur", "ブラー(カメラレンズ)"), ("ec.blur.camerashakedeblur", "Camera-Shake Deblur", "カメラぶれ除去"), - ("ec.blur.cccross", "CC Cross Blur", "CC Cross Blur"), - ("ec.blur.ccradial", "CC Radial Blur", "CC Radial Blur"), - ("ec.blur.ccradialfast", "CC Radial Fast Blur", "CC Radial Fast Blur"), - ("ec.blur.ccvector", "CC Vector Blur", "CC Vector Blur"), + ("ec.blur.cccross", "Cross Blur", "十字ぼかし"), + ("ec.blur.ccradial", "Variable Radial Blur", "可変放射状ぼかし"), + ("ec.blur.ccradialfast", "Fast Radial Blur", "高速放射状ぼかし"), + ("ec.blur.ccvector", "Vector Blur", "ベクトルぼかし"), ("ec.blur.channel", "Channel Blur", "ブラー(チャンネル)"), ("ec.blur.compound", "Compound Blur", "ブラー(合成)"), ("ec.blur.directional", "Directional Blur", "ブラー(方向)"), @@ -864,7 +871,7 @@ const JAPANESE_EFFECTS: &[Row] = &[ ("ec.channel.arithmetic", "Arithmetic", "アリスマチック"), ("ec.channel.blend", "Blend", "ブレンド"), ("ec.channel.calculations", "Calculations", "計算"), - ("ec.channel.cccomposite", "CC Composite", "CC Composite"), + ("ec.channel.cccomposite", "Source Composite", "元画像合成"), ("ec.channel.combiner", "Channel Combiner", "チャンネルコンバイナー"), ("ec.channel.compoundarithmetic", "Compound Arithmetic", "合成アリスマチック"), ("ec.channel.invert", "Invert", "反転"), @@ -879,10 +886,10 @@ const JAPANESE_EFFECTS: &[Row] = &[ ("ec.color.autolevels", "Auto Levels", "自動レベル補正"), ("ec.color.blackwhite", "Black & White", "白黒"), ("ec.color.brightnesscontrast", "Brightness & Contrast", "輝度&コントラスト"), - ("ec.color.cccolorneutralizer", "CC Color Neutralizer", "CC Color Neutralizer"), - ("ec.color.cccoloroffset", "CC Color Offset", "CC Color Offset"), - ("ec.color.cckernel", "CC Kernel", "CC Kernel"), - ("ec.color.cctoner", "CC Toner", "CC Toner"), + ("ec.color.cccolorneutralizer", "Color Neutralizer", "色かぶり補正"), + ("ec.color.cccoloroffset", "Channel Offset", "チャンネルオフセット"), + ("ec.color.cckernel", "Convolution Kernel", "畳み込みカーネル"), + ("ec.color.cctoner", "Multi-Tone Tint", "多色調ティント"), ("ec.color.changecolor", "Change Color", "色を変更"), ("ec.color.changetocolor", "Change to Color", "他のカラーへ変更"), ("ec.color.channelmixer", "Channel Mixer", "チャンネルミキサー"), @@ -899,7 +906,7 @@ const JAPANESE_EFFECTS: &[Row] = &[ ("ec.color.leavecolor", "Leave Color", "色抜き"), ("ec.color.levels", "Levels", "レベル補正"), ("ec.color.levelsic", "Levels (Individual Controls)", "レベル(個々の制御)"), - ("ec.color.lumetri", "Lumetri Color", "Lumetri カラー"), + ("ec.color.lumetri", "Color Grading", "カラーグレーディング"), ("ec.color.ociocdl", "OCIO CDL Transform", "OCIO CDL 変換"), ("ec.color.ociocolorspace", "OCIO Color Space Transform", "OCIO 色空間変換"), ("ec.color.ociodisplay", "OCIO Display Transform", "OCIO 表示変換"), @@ -909,7 +916,7 @@ const JAPANESE_EFFECTS: &[Row] = &[ ("ec.color.psarbitrarymap", "PS Arbitrary Map", "PS不定マップ"), ("ec.color.selectivecolor", "Selective Color", "特定色域の選択"), ("ec.color.shadowhighlight", "Shadow/Highlight", "シャドウ・ハイライト"), - ("ec.color.tint", "Tint", "色かぶり補正"), + ("ec.color.tint", "Tint", "着色"), ("ec.color.tritone", "Tritone", "トライトーン"), ("ec.color.vibrance", "Vibrance", "自然な彩度"), ("ec.color.videolimiter", "Video Limiter", "ビデオリミッター"), @@ -923,20 +930,20 @@ const JAPANESE_EFFECTS: &[Row] = &[ ("ec.control.slider", "Slider Control", "スライダー制御"), ("ec.distort.bezierwarp", "Bezier Warp", "ベジェワープ"), ("ec.distort.bulge", "Bulge", "バルジ"), - ("ec.distort.ccbender", "CC Bender", "CC Bender"), - ("ec.distort.ccbendit", "CC Bend It", "CC Bend It"), - ("ec.distort.ccblobbylize", "CC Blobbylize", "CC Blobbylize"), - ("ec.distort.ccflomotion", "CC Flo Motion", "CC Flo Motion"), - ("ec.distort.ccgriddler", "CC Griddler", "CC Griddler"), - ("ec.distort.cclens", "CC Lens", "CC Lens"), - ("ec.distort.ccpageturn", "CC Page Turn", "CC Page Turn"), - ("ec.distort.ccpowerpin", "CC Power Pin", "CC Power Pin"), - ("ec.distort.ccripplepulse", "CC Ripple Pulse", "CC Ripple Pulse"), - ("ec.distort.ccslant", "CC Slant", "CC Slant"), - ("ec.distort.ccsmear", "CC Smear", "CC Smear"), - ("ec.distort.ccsplit", "CC Split", "CC Split"), - ("ec.distort.ccsplit2", "CC Split 2", "CC Split 2"), - ("ec.distort.cctiler", "CC Tiler", "CC Tiler"), + ("ec.distort.ccbender", "Flexible Bend", "柔軟ベンド"), + ("ec.distort.ccbendit", "Two-Point Bend", "2点ベンド"), + ("ec.distort.ccblobbylize", "Organic Displacement", "有機変位"), + ("ec.distort.ccflomotion", "Dual Lens Warp", "ダブルレンズ変形"), + ("ec.distort.ccgriddler", "Tile Grid", "タイルグリッド"), + ("ec.distort.cclens", "Lens Distortion", "レンズ歪み"), + ("ec.distort.ccpageturn", "Page Curl", "ページカール"), + ("ec.distort.ccpowerpin", "Perspective Pin", "遠近ピン"), + ("ec.distort.ccripplepulse", "Pulsed Ripple", "パルス波紋"), + ("ec.distort.ccslant", "Slant", "傾斜"), + ("ec.distort.ccsmear", "Directional Smear", "方向性にじみ"), + ("ec.distort.ccsplit", "Split Warp", "分割変形"), + ("ec.distort.ccsplit2", "Dual Split Warp", "二重分割変形"), + ("ec.distort.cctiler", "Scaled Tiles", "スケールタイル"), ("ec.distort.cornerpin", "Corner Pin", "コーナーピン"), ("ec.distort.displacementmap", "Displacement Map", "ディスプレイスメントマップ"), ("ec.distort.liquify", "Liquify", "ゆがみ"), @@ -963,11 +970,11 @@ const JAPANESE_EFFECTS: &[Row] = &[ ("ec.generate.audiospectrum", "Audio Spectrum", "オーディオスペクトラム"), ("ec.generate.audiowaveform", "Audio Waveform", "オーディオウェーブフォーム"), ("ec.generate.beam", "Beam", "レーザー"), - ("ec.generate.ccgluegun", "CC Glue Gun", "CC Glue Gun"), - ("ec.generate.cclightburst", "CC Light Burst 2.5", "CC Light Burst 2.5"), - ("ec.generate.cclightrays", "CC Light Rays", "CC Light Rays"), - ("ec.generate.cclightsweep", "CC Light Sweep", "CC Light Sweep"), - ("ec.generate.ccthreads", "CC Threads", "CC Threads"), + ("ec.generate.ccgluegun", "Paint Beads", "絵の具の粒"), + ("ec.generate.cclightburst", "Radial Light Burst", "放射状の光"), + ("ec.generate.cclightrays", "Light Rays", "光線"), + ("ec.generate.cclightsweep", "Light Sweep", "光のスイープ"), + ("ec.generate.ccthreads", "Woven Threads", "織り糸"), ("ec.generate.cellpattern", "Cell Pattern", "セルパターン"), ("ec.generate.checkerboard", "Checkerboard", "チェッカーボード"), ("ec.generate.circle", "Circle", "円"), @@ -986,7 +993,7 @@ const JAPANESE_EFFECTS: &[Row] = &[ ("ec.generate.vegas", "Vegas", "ベガス"), ("ec.generate.writeon", "Write-on", "ブラシアニメーション"), ("ec.key.advancedspill", "Advanced Spill Suppressor", "Advanced Spill Suppressor"), - ("ec.key.ccsimplewireremoval", "CC Simple Wire Removal", "CC Simple Wire Removal"), + ("ec.key.ccsimplewireremoval", "Line Removal", "ライン除去"), ("ec.key.colordifference", "Color Difference Key", "カラー差キー"), ("ec.key.colorkey", "Color Key", "カラーキー"), ("ec.key.colorrange", "Color Range", "色範囲"), @@ -997,7 +1004,7 @@ const JAPANESE_EFFECTS: &[Row] = &[ ("ec.key.linearcolor", "Linear Color Key", "リニアカラーキー"), ("ec.key.luma", "Luma Key", "ルミナンスキー"), ("ec.key.spill", "Spill Suppressor", "スピルサプレッション"), - ("ec.keying.keylight", "Key Light", "Keylight (1.2)"), + ("ec.keying.keylight", "Screen Color Key", "スクリーンカラーキー"), ("ec.matte.mattechoker", "Matte Choker", "マットチョーク"), ("ec.matte.refinehard", "Refine Hard Matte", "ハードマットを調整"), ("ec.matte.refinesoft", "Refine Soft Matte", "ソフトマットを調整"), @@ -1019,49 +1026,49 @@ const JAPANESE_EFFECTS: &[Row] = &[ ("ec.obsolete.basictext", "Basic Text", "基本テキスト"), ("ec.obsolete.gaussianlegacy", "Gaussian Blur (Legacy)", "ブラー(ガウス)(レガシー)"), ("ec.obsolete.lightning", "Lightning", "稲妻"), - ("ec.obsolete.mochashape", "Mocha shape", "mocha shape"), + ("ec.obsolete.mochashape", "Tracked Shape Matte", "トラッキングシェイプマット"), ("ec.obsolete.pathtext", "Path Text", "パステキスト"), ("ec.perspective.3dglasses", "3D Glasses", "3Dメガネ"), ("ec.perspective.bevelalpha", "Bevel Alpha", "ベベルアルファ"), ("ec.perspective.beveledges", "Bevel Edges", "ベベルエッジ"), ("ec.perspective.cameratracker", "3D Camera Tracker", "3D カメラトラッカー"), - ("ec.perspective.cccylinder", "CC Cylinder", "CC Cylinder"), - ("ec.perspective.ccenvironment", "CC Environment", "CC Environment"), - ("ec.perspective.ccsphere", "CC Sphere", "CC Sphere"), - ("ec.perspective.ccspotlight", "CC Spotlight", "CC Spotlight"), + ("ec.perspective.cccylinder", "Cylinder Wrap", "円筒ラップ"), + ("ec.perspective.ccenvironment", "Environment Map", "環境マップ"), + ("ec.perspective.ccsphere", "Sphere Wrap", "球面ラップ"), + ("ec.perspective.ccspotlight", "Surface Spotlight", "表面スポットライト"), ("ec.perspective.dropshadow", "Drop Shadow", "ドロップシャドウ"), ("ec.perspective.radialshadow", "Radial Shadow", "放射状シャドウ"), ("ec.sim.carddance", "Card Dance", "カードダンス"), ("ec.sim.caustics", "Caustics", "コースティック"), - ("ec.sim.ccballaction", "CC Ball Action", "CC Ball Action"), - ("ec.sim.ccbubbles", "CC Bubbles", "CC Bubbles"), - ("ec.sim.ccdrizzle", "CC Drizzle", "CC Drizzle"), - ("ec.sim.cchair", "CC Hair", "CC Hair"), - ("ec.sim.ccmrmercury", "CC Mr. Mercury", "CC Mr. Mercury"), - ("ec.sim.ccparticlesystems2", "CC Particle Systems II", "CC Particle Systems II"), - ("ec.sim.ccparticleworld", "CC Particle World", "CC Particle World"), - ("ec.sim.ccpixelpolly", "CC Pixel Polly", "CC Pixel Polly"), - ("ec.sim.ccrainfall", "CC Rainfall", "CC Rainfall"), - ("ec.sim.ccscatterize", "CC Scatterize", "CC Scatterize"), - ("ec.sim.ccsnowfall", "CC Snowfall", "CC Snowfall"), - ("ec.sim.ccstarburst", "CC Star Burst", "CC Star Burst"), + ("ec.sim.ccballaction", "Ball Grid", "球体グリッド"), + ("ec.sim.ccbubbles", "Bubbles", "気泡"), + ("ec.sim.ccdrizzle", "Water Ripples", "水面の波紋"), + ("ec.sim.cchair", "Surface Fibers", "表面の繊維"), + ("ec.sim.ccmrmercury", "Liquid Particles", "液体パーティクル"), + ("ec.sim.ccparticlesystems2", "Particle Emitter 2D", "2Dパーティクルエミッター"), + ("ec.sim.ccparticleworld", "Particle Emitter 3D", "3Dパーティクルエミッター"), + ("ec.sim.ccpixelpolly", "Pixel Shatter", "ピクセル破砕"), + ("ec.sim.ccrainfall", "Rainfall", "雨"), + ("ec.sim.ccscatterize", "Pixel Scatter", "ピクセル散乱"), + ("ec.sim.ccsnowfall", "Snowfall", "雪"), + ("ec.sim.ccstarburst", "Starfield", "星空"), ("ec.sim.foam", "Foam", "泡"), ("ec.sim.particleplayground", "Particle Playground", "パーティクルプレイグラウンド"), ("ec.sim.shatter", "Shatter", "シャター"), ("ec.sim.waveworld", "Wave World", "ウェーブワールド"), ("ec.stylize.brushstrokes", "Brush Strokes", "ブラシストローク"), ("ec.stylize.cartoon", "Cartoon", "カートゥーン"), - ("ec.stylize.ccblockload", "CC Block Load", "CC Block Load"), - ("ec.stylize.ccburnfilm", "CC Burn Film", "CC Burn Film"), - ("ec.stylize.ccglass", "CC Glass", "CC Glass"), - ("ec.stylize.cchextile", "CC HexTile", "CC HexTile"), - ("ec.stylize.cckaleida", "CC Kaleida", "CC Kaleida"), - ("ec.stylize.ccmrsmoothie", "CC Mr. Smoothie", "CC Mr. Smoothie"), - ("ec.stylize.ccplastic", "CC Plastic", "CC Plastic"), - ("ec.stylize.ccrepetile", "CC RepeTile", "CC RepeTile"), - ("ec.stylize.ccthreshold", "CC Threshold", "CC Threshold"), - ("ec.stylize.ccthresholdrgb", "CC Threshold RGB", "CC Threshold RGB"), - ("ec.stylize.ccvignette", "CC Vignette", "CC Vignette"), + ("ec.stylize.ccblockload", "Block Reveal", "ブロック表示"), + ("ec.stylize.ccburnfilm", "Film Burn", "フィルム焼け"), + ("ec.stylize.ccglass", "Glass Relief", "ガラスレリーフ"), + ("ec.stylize.cchextile", "Hexagonal Tiles", "六角形タイル"), + ("ec.stylize.cckaleida", "Kaleidoscope", "万華鏡"), + ("ec.stylize.ccmrsmoothie", "Palette Map", "パレットマップ"), + ("ec.stylize.ccplastic", "Plastic Shading", "プラスチックシェーディング"), + ("ec.stylize.ccrepetile", "Edge Tile Repeat", "エッジタイル反復"), + ("ec.stylize.ccthreshold", "Channel Threshold", "チャンネルしきい値"), + ("ec.stylize.ccthresholdrgb", "RGB Threshold", "RGBしきい値"), + ("ec.stylize.ccvignette", "Vignette", "ビネット"), ("ec.stylize.coloremboss", "Color Emboss", "カラーエンボス"), ("ec.stylize.emboss", "Emboss", "エンボス"), ("ec.stylize.findedges", "Find Edges", "輪郭検出"), @@ -1076,8 +1083,8 @@ const JAPANESE_EFFECTS: &[Row] = &[ ("ec.stylize.threshold", "Threshold", "しきい値"), ("ec.text.numbers", "Numbers", "番号"), ("ec.text.timecode", "Timecode", "タイムコード"), - ("ec.time.ccforcemotionblur", "CC Force Motion Blur", "CC Force Motion Blur"), - ("ec.time.ccwidetime", "CC Wide Time", "CC Wide Time"), + ("ec.time.ccforcemotionblur", "Temporal Motion Blur", "時間方向モーションブラー"), + ("ec.time.ccwidetime", "Temporal Blend", "時間ブレンド"), ("ec.time.echo", "Echo", "エコー"), ("ec.time.pixelmotionblur", "Pixel Motion Blur", "ピクセルモーションブラー"), ("ec.time.posterizetime", "Posterize Time", "ポスタリゼーション時間"), @@ -1086,23 +1093,23 @@ const JAPANESE_EFFECTS: &[Row] = &[ ("ec.time.timewarp", "Timewarp", "タイムワープ"), ("ec.transition.blockdissolve", "Block Dissolve", "ブロックディゾルブ"), ("ec.transition.cardwipe", "Card Wipe", "カードワイプ"), - ("ec.transition.ccglasswipe", "CC Glass Wipe", "CC Glass Wipe"), - ("ec.transition.ccgridwipe", "CC Grid Wipe", "CC Grid Wipe"), - ("ec.transition.ccimagewipe", "CC Image Wipe", "CC Image Wipe"), - ("ec.transition.ccjaws", "CC Jaws", "CC Jaws"), - ("ec.transition.cclightwipe", "CC Light Wipe", "CC Light Wipe"), - ("ec.transition.cclinesweep", "CC Line Sweep", "CC Line Sweep"), - ("ec.transition.ccradialscalewipe", "CC Radial ScaleWipe", "CC Radial ScaleWipe"), - ("ec.transition.ccscalewipe", "CC Scale Wipe", "CC Scale Wipe"), - ("ec.transition.cctwister", "CC Twister", "CC Twister"), - ("ec.transition.ccwarpomatic", "CC WarpoMatic", "CC WarpoMatic"), + ("ec.transition.ccglasswipe", "Glass Wipe", "ガラスワイプ"), + ("ec.transition.ccgridwipe", "Grid Wipe", "グリッドワイプ"), + ("ec.transition.ccimagewipe", "Luminance Image Wipe", "輝度画像ワイプ"), + ("ec.transition.ccjaws", "Serrated Wipe", "鋸歯状ワイプ"), + ("ec.transition.cclightwipe", "Light Wipe", "ライトワイプ"), + ("ec.transition.cclinesweep", "Line Sweep", "ラインスイープ"), + ("ec.transition.ccradialscalewipe", "Radial Scale Wipe", "放射状スケールワイプ"), + ("ec.transition.ccscalewipe", "Scale Wipe", "スケールワイプ"), + ("ec.transition.cctwister", "Twist Wipe", "ツイストワイプ"), + ("ec.transition.ccwarpomatic", "Warp Dissolve", "変形ディゾルブ"), ("ec.transition.gradientwipe", "Gradient Wipe", "グラデーションワイプ"), ("ec.transition.iriswipe", "Iris Wipe", "アイリスワイプ"), ("ec.transition.linearwipe", "Linear Wipe", "リニアワイプ"), ("ec.transition.radialwipe", "Radial Wipe", "放射状ワイプ"), ("ec.transition.venetian", "Venetian Blinds", "ブラインド"), ("ec.utility.applylut", "Apply Color LUT", "カラー LUT を適用"), - ("ec.utility.ccoverbrights", "CC Overbrights", "CC Overbrights"), + ("ec.utility.ccoverbrights", "Out-of-Range Display", "範囲外表示"), ("ec.utility.cineon", "Cineon Converter", "Cineon コンバーター"), ("ec.utility.colorprofileconverter", "Color Profile Converter", "カラープロファイルコンバーター"), ("ec.utility.growbounds", "Grow Bounds", "範囲拡張"), @@ -1176,9 +1183,9 @@ const JAPANESE_CATEGORIES: &[(&str, &str)] = &[ /// Georgian (`ka`): one row per effect, ids from [`SIMPLIFIED_CHINESE`]. Written from the meaning /// of the English names, with the terms the Georgian menus and panels use (FilmCraft's Georgian -/// for the effects the two apps share). The `CC` series and the brand names (`Cryptomatte`, -/// `EXtractoR`, `IDentifier`, `Key Light`, `Unmult`, `Lumetri Color`) stay in English, as -/// Adobe's Japanese interface keeps the `CC` series; `VR` and `OCIO` keep their prefix. +/// for the effects the two apps share). The brand names (`Cryptomatte`, `EXtractoR`, `IDentifier`, +/// `Key Light`, `Unmult`) stay in English; `VR` and `OCIO` keep their prefix. The renamed effects +/// are translated from their new descriptive names. const GEORGIAN: &[Row] = &[ ("ec.3d.channelextract", "3D Channel Extract", "3D არხის ამოღება"), ("ec.3d.cryptomatte", "Cryptomatte", "Cryptomatte"), @@ -1205,10 +1212,10 @@ const GEORGIAN: &[Row] = &[ ("ec.blur.bilateral", "Bilateral Blur", "ბილატერალური გაბუნდოვნება"), ("ec.blur.cameralens", "Camera Lens Blur", "კამერის ობიექტივის გაბუნდოვნება"), ("ec.blur.camerashakedeblur", "Camera-Shake Deblur", "კამერის რყევით გაბუნდოვნების მოცილება"), - ("ec.blur.cccross", "CC Cross Blur", "CC Cross Blur"), - ("ec.blur.ccradial", "CC Radial Blur", "CC Radial Blur"), - ("ec.blur.ccradialfast", "CC Radial Fast Blur", "CC Radial Fast Blur"), - ("ec.blur.ccvector", "CC Vector Blur", "CC Vector Blur"), + ("ec.blur.cccross", "Cross Blur", "ჯვარედინი დაბინდვა"), + ("ec.blur.ccradial", "Variable Radial Blur", "ცვლადი რადიალური დაბინდვა"), + ("ec.blur.ccradialfast", "Fast Radial Blur", "სწრაფი რადიალური დაბინდვა"), + ("ec.blur.ccvector", "Vector Blur", "ვექტორული დაბინდვა"), ("ec.blur.channel", "Channel Blur", "არხის გაბუნდოვნება"), ("ec.blur.compound", "Compound Blur", "შედგენილი გაბუნდოვნება"), ("ec.blur.directional", "Directional Blur", "მიმართული გაბუნდოვნება"), @@ -1222,7 +1229,7 @@ const GEORGIAN: &[Row] = &[ ("ec.channel.arithmetic", "Arithmetic", "არითმეტიკა"), ("ec.channel.blend", "Blend", "შერევა"), ("ec.channel.calculations", "Calculations", "გამოთვლები"), - ("ec.channel.cccomposite", "CC Composite", "CC Composite"), + ("ec.channel.cccomposite", "Source Composite", "წყაროს კომპოზიცია"), ("ec.channel.combiner", "Channel Combiner", "არხების გამაერთიანებელი"), ("ec.channel.compoundarithmetic", "Compound Arithmetic", "შედგენილი არითმეტიკა"), ("ec.channel.invert", "Invert", "ინვერსია"), @@ -1238,10 +1245,10 @@ const GEORGIAN: &[Row] = &[ ("ec.color.blackwhite", "Black & White", "შავ-თეთრი"), ("ec.color.brightnesscontrast", "Brightness & Contrast", "სიკაშკაშე და კონტრასტი"), ("ec.color.broadcast", "Broadcast Colors", "სამაუწყებლო ფერები"), - ("ec.color.cccolorneutralizer", "CC Color Neutralizer", "CC Color Neutralizer"), - ("ec.color.cccoloroffset", "CC Color Offset", "CC Color Offset"), - ("ec.color.cckernel", "CC Kernel", "CC Kernel"), - ("ec.color.cctoner", "CC Toner", "CC Toner"), + ("ec.color.cccolorneutralizer", "Color Neutralizer", "ფერის ნეიტრალიზაცია"), + ("ec.color.cccoloroffset", "Channel Offset", "არხების წანაცვლება"), + ("ec.color.cckernel", "Convolution Kernel", "კონვოლუციის ბირთვი"), + ("ec.color.cctoner", "Multi-Tone Tint", "მრავალტონიანი შეფერილობა"), ("ec.color.changecolor", "Change Color", "ფერის შეცვლა"), ("ec.color.changetocolor", "Change to Color", "ფერად შეცვლა"), ("ec.color.channelmixer", "Channel Mixer", "არხების მიქშერი"), @@ -1258,7 +1265,7 @@ const GEORGIAN: &[Row] = &[ ("ec.color.leavecolor", "Leave Color", "ფერის დატოვება"), ("ec.color.levels", "Levels", "დონეები"), ("ec.color.levelsic", "Levels (Individual Controls)", "დონეები (ცალკეული მართვა)"), - ("ec.color.lumetri", "Lumetri Color", "Lumetri Color"), + ("ec.color.lumetri", "Color Grading", "ფერთა კორექცია"), ("ec.color.ociocdl", "OCIO CDL Transform", "OCIO CDL გარდაქმნა"), ("ec.color.ociocolorspace", "OCIO Color Space Transform", "OCIO ფერთა სივრცის გარდაქმნა"), ("ec.color.ociodisplay", "OCIO Display Transform", "OCIO ეკრანის გარდაქმნა"), @@ -1283,20 +1290,20 @@ const GEORGIAN: &[Row] = &[ ("ec.control.stereo3d", "Stereo 3D Controls", "სტერეო 3D მართვა"), ("ec.distort.bezierwarp", "Bezier Warp", "ბეზიეს დეფორმაცია"), ("ec.distort.bulge", "Bulge", "გამობერილობა"), - ("ec.distort.ccbender", "CC Bender", "CC Bender"), - ("ec.distort.ccbendit", "CC Bend It", "CC Bend It"), - ("ec.distort.ccblobbylize", "CC Blobbylize", "CC Blobbylize"), - ("ec.distort.ccflomotion", "CC Flo Motion", "CC Flo Motion"), - ("ec.distort.ccgriddler", "CC Griddler", "CC Griddler"), - ("ec.distort.cclens", "CC Lens", "CC Lens"), - ("ec.distort.ccpageturn", "CC Page Turn", "CC Page Turn"), - ("ec.distort.ccpowerpin", "CC Power Pin", "CC Power Pin"), - ("ec.distort.ccripplepulse", "CC Ripple Pulse", "CC Ripple Pulse"), - ("ec.distort.ccslant", "CC Slant", "CC Slant"), - ("ec.distort.ccsmear", "CC Smear", "CC Smear"), - ("ec.distort.ccsplit", "CC Split", "CC Split"), - ("ec.distort.ccsplit2", "CC Split 2", "CC Split 2"), - ("ec.distort.cctiler", "CC Tiler", "CC Tiler"), + ("ec.distort.ccbender", "Flexible Bend", "მოქნილი მოღუნვა"), + ("ec.distort.ccbendit", "Two-Point Bend", "ორწერტილიანი მოღუნვა"), + ("ec.distort.ccblobbylize", "Organic Displacement", "ორგანული გადაადგილება"), + ("ec.distort.ccflomotion", "Dual Lens Warp", "ორმაგი ლინზით დეფორმაცია"), + ("ec.distort.ccgriddler", "Tile Grid", "ფილების ბადე"), + ("ec.distort.cclens", "Lens Distortion", "ლინზის დამახინჯება"), + ("ec.distort.ccpageturn", "Page Curl", "გვერდის დახვევა"), + ("ec.distort.ccpowerpin", "Perspective Pin", "პერსპექტიული დამაგრება"), + ("ec.distort.ccripplepulse", "Pulsed Ripple", "პულსური ტალღა"), + ("ec.distort.ccslant", "Slant", "დახრა"), + ("ec.distort.ccsmear", "Directional Smear", "მიმართულებითი გაწელვა"), + ("ec.distort.ccsplit", "Split Warp", "გაყოფილი დეფორმაცია"), + ("ec.distort.ccsplit2", "Dual Split Warp", "ორმაგად გაყოფილი დეფორმაცია"), + ("ec.distort.cctiler", "Scaled Tiles", "მასშტაბირებული ფილები"), ("ec.distort.cornerpin", "Corner Pin", "კუთხეების მიმაგრება"), ("ec.distort.displacementmap", "Displacement Map", "წანაცვლების რუკა"), ("ec.distort.liquify", "Liquify", "გათხევადება"), @@ -1324,11 +1331,11 @@ const GEORGIAN: &[Row] = &[ ("ec.generate.audiospectrum", "Audio Spectrum", "აუდიოს სპექტრი"), ("ec.generate.audiowaveform", "Audio Waveform", "აუდიოს ტალღის ფორმა"), ("ec.generate.beam", "Beam", "სხივი"), - ("ec.generate.ccgluegun", "CC Glue Gun", "CC Glue Gun"), - ("ec.generate.cclightburst", "CC Light Burst 2.5", "CC Light Burst 2.5"), - ("ec.generate.cclightrays", "CC Light Rays", "CC Light Rays"), - ("ec.generate.cclightsweep", "CC Light Sweep", "CC Light Sweep"), - ("ec.generate.ccthreads", "CC Threads", "CC Threads"), + ("ec.generate.ccgluegun", "Paint Beads", "საღებავის მარცვლები"), + ("ec.generate.cclightburst", "Radial Light Burst", "რადიალური ნათება"), + ("ec.generate.cclightrays", "Light Rays", "სინათლის სხივები"), + ("ec.generate.cclightsweep", "Light Sweep", "სინათლის გავლა"), + ("ec.generate.ccthreads", "Woven Threads", "ნაქსოვი ძაფები"), ("ec.generate.cellpattern", "Cell Pattern", "უჯრედების პატერნი"), ("ec.generate.checkerboard", "Checkerboard", "ჭადრაკის დაფა"), ("ec.generate.circle", "Circle", "წრე"), @@ -1347,7 +1354,7 @@ const GEORGIAN: &[Row] = &[ ("ec.generate.vegas", "Vegas", "ვეგასი"), ("ec.generate.writeon", "Write-on", "თანდათანობითი წერა"), ("ec.key.advancedspill", "Advanced Spill Suppressor", "ფერის გადაღვრის გაფართოებული ჩამხშობი"), - ("ec.key.ccsimplewireremoval", "CC Simple Wire Removal", "CC Simple Wire Removal"), + ("ec.key.ccsimplewireremoval", "Line Removal", "ხაზის მოცილება"), ("ec.key.colordifference", "Color Difference Key", "ფერთა სხვაობის გასაღები"), ("ec.key.colorkey", "Color Key", "ფერის გასაღები"), ("ec.key.colorrange", "Color Range", "ფერების დიაპაზონი"), @@ -1360,7 +1367,7 @@ const GEORGIAN: &[Row] = &[ ("ec.key.screen", "Screen Key", "ეკრანის გასაღები"), ("ec.key.spill", "Spill Suppressor", "ფერის გადაღვრის ჩამხშობი"), ("ec.key.unmult", "Unmult", "Unmult"), - ("ec.keying.keylight", "Key Light", "Key Light"), + ("ec.keying.keylight", "Screen Color Key", "ეკრანის ფერის გამოყოფა"), ("ec.matte.mattechoker", "Matte Choker", "მატის შემკუმშველი"), ("ec.matte.objectmatte", "Object Matte", "ობიექტის მატი"), ("ec.matte.refinehard", "Refine Hard Matte", "მკვეთრი მატის დახვეწა"), @@ -1384,50 +1391,50 @@ const GEORGIAN: &[Row] = &[ ("ec.obsolete.basictext", "Basic Text", "საბაზო ტექსტი"), ("ec.obsolete.gaussianlegacy", "Gaussian Blur (Legacy)", "გაუსის გაბუნდოვნება (მოძველებული)"), ("ec.obsolete.lightning", "Lightning", "ელვა"), - ("ec.obsolete.mochashape", "Mocha shape", "Mocha-ს ფიგურა"), + ("ec.obsolete.mochashape", "Tracked Shape Matte", "თვალთვალის ფორმის ნიღაბი"), ("ec.obsolete.pathtext", "Path Text", "ტექსტი ტრაექტორიაზე"), ("ec.paint.paint", "Paint", "ხატვა"), ("ec.perspective.3dglasses", "3D Glasses", "3D სათვალე"), ("ec.perspective.bevelalpha", "Bevel Alpha", "ალფას ფასკა"), ("ec.perspective.beveledges", "Bevel Edges", "კიდეების ფასკა"), ("ec.perspective.cameratracker", "3D Camera Tracker", "3D კამერის ტრეკერი"), - ("ec.perspective.cccylinder", "CC Cylinder", "CC Cylinder"), - ("ec.perspective.ccenvironment", "CC Environment", "CC Environment"), - ("ec.perspective.ccsphere", "CC Sphere", "CC Sphere"), - ("ec.perspective.ccspotlight", "CC Spotlight", "CC Spotlight"), + ("ec.perspective.cccylinder", "Cylinder Wrap", "ცილინდრზე შემოხვევა"), + ("ec.perspective.ccenvironment", "Environment Map", "გარემოს რუკა"), + ("ec.perspective.ccsphere", "Sphere Wrap", "სფეროზე შემოხვევა"), + ("ec.perspective.ccspotlight", "Surface Spotlight", "ზედაპირის პროჟექტორი"), ("ec.perspective.dropshadow", "Drop Shadow", "ჩრდილი"), ("ec.perspective.radialshadow", "Radial Shadow", "რადიალური ჩრდილი"), ("ec.sim.carddance", "Card Dance", "მოცეკვავე ბარათები"), ("ec.sim.caustics", "Caustics", "კაუსტიკა"), - ("ec.sim.ccballaction", "CC Ball Action", "CC Ball Action"), - ("ec.sim.ccbubbles", "CC Bubbles", "CC Bubbles"), - ("ec.sim.ccdrizzle", "CC Drizzle", "CC Drizzle"), - ("ec.sim.cchair", "CC Hair", "CC Hair"), - ("ec.sim.ccmrmercury", "CC Mr. Mercury", "CC Mr. Mercury"), - ("ec.sim.ccparticlesystems2", "CC Particle Systems II", "CC Particle Systems II"), - ("ec.sim.ccparticleworld", "CC Particle World", "CC Particle World"), - ("ec.sim.ccpixelpolly", "CC Pixel Polly", "CC Pixel Polly"), - ("ec.sim.ccrainfall", "CC Rainfall", "CC Rainfall"), - ("ec.sim.ccscatterize", "CC Scatterize", "CC Scatterize"), - ("ec.sim.ccsnowfall", "CC Snowfall", "CC Snowfall"), - ("ec.sim.ccstarburst", "CC Star Burst", "CC Star Burst"), + ("ec.sim.ccballaction", "Ball Grid", "ბურთულების ბადე"), + ("ec.sim.ccbubbles", "Bubbles", "ბუშტები"), + ("ec.sim.ccdrizzle", "Water Ripples", "წყლის ტალღები"), + ("ec.sim.cchair", "Surface Fibers", "ზედაპირის ბოჭკოები"), + ("ec.sim.ccmrmercury", "Liquid Particles", "თხევადი ნაწილაკები"), + ("ec.sim.ccparticlesystems2", "Particle Emitter 2D", "ნაწილაკების 2D წყარო"), + ("ec.sim.ccparticleworld", "Particle Emitter 3D", "ნაწილაკების 3D წყარო"), + ("ec.sim.ccpixelpolly", "Pixel Shatter", "პიქსელების დაშლა"), + ("ec.sim.ccrainfall", "Rainfall", "წვიმა"), + ("ec.sim.ccscatterize", "Pixel Scatter", "პიქსელების გაფანტვა"), + ("ec.sim.ccsnowfall", "Snowfall", "თოვა"), + ("ec.sim.ccstarburst", "Starfield", "ვარსკვლავების ველი"), ("ec.sim.foam", "Foam", "ქაფი"), ("ec.sim.particleplayground", "Particle Playground", "ნაწილაკების სიმულაცია"), ("ec.sim.shatter", "Shatter", "დამსხვრევა"), ("ec.sim.waveworld", "Wave World", "ტალღების სამყარო"), ("ec.stylize.brushstrokes", "Brush Strokes", "ფუნჯის შტრიხები"), ("ec.stylize.cartoon", "Cartoon", "მულტფილმის სტილი"), - ("ec.stylize.ccblockload", "CC Block Load", "CC Block Load"), - ("ec.stylize.ccburnfilm", "CC Burn Film", "CC Burn Film"), - ("ec.stylize.ccglass", "CC Glass", "CC Glass"), - ("ec.stylize.cchextile", "CC HexTile", "CC HexTile"), - ("ec.stylize.cckaleida", "CC Kaleida", "CC Kaleida"), - ("ec.stylize.ccmrsmoothie", "CC Mr. Smoothie", "CC Mr. Smoothie"), - ("ec.stylize.ccplastic", "CC Plastic", "CC Plastic"), - ("ec.stylize.ccrepetile", "CC RepeTile", "CC RepeTile"), - ("ec.stylize.ccthreshold", "CC Threshold", "CC Threshold"), - ("ec.stylize.ccthresholdrgb", "CC Threshold RGB", "CC Threshold RGB"), - ("ec.stylize.ccvignette", "CC Vignette", "CC Vignette"), + ("ec.stylize.ccblockload", "Block Reveal", "ბლოკებით გამოჩენა"), + ("ec.stylize.ccburnfilm", "Film Burn", "ფირის დაწვა"), + ("ec.stylize.ccglass", "Glass Relief", "მინის რელიეფი"), + ("ec.stylize.cchextile", "Hexagonal Tiles", "ექვსკუთხა ფილები"), + ("ec.stylize.cckaleida", "Kaleidoscope", "კალეიდოსკოპი"), + ("ec.stylize.ccmrsmoothie", "Palette Map", "პალიტრის რუკა"), + ("ec.stylize.ccplastic", "Plastic Shading", "პლასტმასის დაჩრდილვა"), + ("ec.stylize.ccrepetile", "Edge Tile Repeat", "კიდეების განმეორება"), + ("ec.stylize.ccthreshold", "Channel Threshold", "არხის ზღვარი"), + ("ec.stylize.ccthresholdrgb", "RGB Threshold", "RGB ზღვარი"), + ("ec.stylize.ccvignette", "Vignette", "ვინიეტირება"), ("ec.stylize.coloremboss", "Color Emboss", "ფერადი რელიეფი"), ("ec.stylize.emboss", "Emboss", "რელიეფი"), ("ec.stylize.findedges", "Find Edges", "კიდეების პოვნა"), @@ -1442,8 +1449,8 @@ const GEORGIAN: &[Row] = &[ ("ec.stylize.threshold", "Threshold", "ზღვარი"), ("ec.text.numbers", "Numbers", "რიცხვები"), ("ec.text.timecode", "Timecode", "თაიმკოდი"), - ("ec.time.ccforcemotionblur", "CC Force Motion Blur", "CC Force Motion Blur"), - ("ec.time.ccwidetime", "CC Wide Time", "CC Wide Time"), + ("ec.time.ccforcemotionblur", "Temporal Motion Blur", "დროითი მოძრაობის დაბინდვა"), + ("ec.time.ccwidetime", "Temporal Blend", "დროითი შერევა"), ("ec.time.echo", "Echo", "ექო"), ("ec.time.pixelmotionblur", "Pixel Motion Blur", "პიქსელების მოძრაობის გაბუნდოვნება"), ("ec.time.posterizetime", "Posterize Time", "დროის პოსტერიზაცია"), @@ -1452,23 +1459,23 @@ const GEORGIAN: &[Row] = &[ ("ec.time.timewarp", "Timewarp", "Timewarp"), ("ec.transition.blockdissolve", "Block Dissolve", "ბლოკური გადადნობა"), ("ec.transition.cardwipe", "Card Wipe", "ბარათებით წმენდა"), - ("ec.transition.ccglasswipe", "CC Glass Wipe", "CC Glass Wipe"), - ("ec.transition.ccgridwipe", "CC Grid Wipe", "CC Grid Wipe"), - ("ec.transition.ccimagewipe", "CC Image Wipe", "CC Image Wipe"), - ("ec.transition.ccjaws", "CC Jaws", "CC Jaws"), - ("ec.transition.cclightwipe", "CC Light Wipe", "CC Light Wipe"), - ("ec.transition.cclinesweep", "CC Line Sweep", "CC Line Sweep"), - ("ec.transition.ccradialscalewipe", "CC Radial ScaleWipe", "CC Radial ScaleWipe"), - ("ec.transition.ccscalewipe", "CC Scale Wipe", "CC Scale Wipe"), - ("ec.transition.cctwister", "CC Twister", "CC Twister"), - ("ec.transition.ccwarpomatic", "CC WarpoMatic", "CC WarpoMatic"), + ("ec.transition.ccglasswipe", "Glass Wipe", "მინის გადასვლა"), + ("ec.transition.ccgridwipe", "Grid Wipe", "ბადის გადასვლა"), + ("ec.transition.ccimagewipe", "Luminance Image Wipe", "სიკაშკაშის გამოსახულებით გადასვლა"), + ("ec.transition.ccjaws", "Serrated Wipe", "დაკბილული გადასვლა"), + ("ec.transition.cclightwipe", "Light Wipe", "სინათლის გადასვლა"), + ("ec.transition.cclinesweep", "Line Sweep", "ხაზების გავლა"), + ("ec.transition.ccradialscalewipe", "Radial Scale Wipe", "რადიალური მასშტაბის გადასვლა"), + ("ec.transition.ccscalewipe", "Scale Wipe", "მასშტაბის გადასვლა"), + ("ec.transition.cctwister", "Twist Wipe", "მოგრეხილი გადასვლა"), + ("ec.transition.ccwarpomatic", "Warp Dissolve", "დეფორმაციით გადასვლა"), ("ec.transition.gradientwipe", "Gradient Wipe", "გრადიენტული წმენდა"), ("ec.transition.iriswipe", "Iris Wipe", "დიაფრაგმული წმენდა"), ("ec.transition.linearwipe", "Linear Wipe", "წრფივი წმენდა"), ("ec.transition.radialwipe", "Radial Wipe", "რადიალური წმენდა"), ("ec.transition.venetian", "Venetian Blinds", "ჟალუზი"), ("ec.utility.applylut", "Apply Color LUT", "ფერის LUT-ის გამოყენება"), - ("ec.utility.ccoverbrights", "CC Overbrights", "CC Overbrights"), + ("ec.utility.ccoverbrights", "Out-of-Range Display", "დიაპაზონის გარეთ ჩვენება"), ("ec.utility.cineon", "Cineon Converter", "Cineon-ის გარდამქმნელი"), ("ec.utility.colorprofileconverter", "Color Profile Converter", "ფერის პროფილის გარდამქმნელი"), ("ec.utility.facemeasurements", "Face Measurements", "სახის გაზომვები"), @@ -1517,8 +1524,8 @@ const GEORGIAN_CATEGORIES: &[(&str, &str)] = &[ /// French (`fr`): one row per effect, ids from [`SIMPLIFIED_CHINESE`]. Written from the meaning of the /// English names, with the terms the French menus use (calque, estampe for a matte, incrustation for -/// keying). The `CC` series and the brand names stay in English, `VR`, `OCIO`, `PS` and `HDR` keep -/// their tag after the translated name. +/// keying). The brand names stay in English, `VR`, `OCIO`, `PS` and `HDR` keep their tag after the +/// translated name. const FRENCH: &[Row] = &[ ("ec.3d.channelextract", "3D Channel Extract", "Extraction de canal 3D"), ("ec.3d.cryptomatte", "Cryptomatte", "Cryptomatte"), @@ -1544,10 +1551,10 @@ const FRENCH: &[Row] = &[ ("ec.blur.bilateral", "Bilateral Blur", "Flou bilatéral"), ("ec.blur.cameralens", "Camera Lens Blur", "Flou de l’objectif de la caméra"), ("ec.blur.camerashakedeblur", "Camera-Shake Deblur", "Correction du flou de bougé de caméra"), - ("ec.blur.cccross", "CC Cross Blur", "CC Cross Blur"), - ("ec.blur.ccradial", "CC Radial Blur", "CC Radial Blur"), - ("ec.blur.ccradialfast", "CC Radial Fast Blur", "CC Radial Fast Blur"), - ("ec.blur.ccvector", "CC Vector Blur", "CC Vector Blur"), + ("ec.blur.cccross", "Cross Blur", "Flou croisé"), + ("ec.blur.ccradial", "Variable Radial Blur", "Flou radial variable"), + ("ec.blur.ccradialfast", "Fast Radial Blur", "Flou radial rapide"), + ("ec.blur.ccvector", "Vector Blur", "Flou vectoriel"), ("ec.blur.channel", "Channel Blur", "Flou de canal"), ("ec.blur.compound", "Compound Blur", "Flou composé"), ("ec.blur.directional", "Directional Blur", "Flou directionnel"), @@ -1561,7 +1568,7 @@ const FRENCH: &[Row] = &[ ("ec.channel.arithmetic", "Arithmetic", "Arithmétique"), ("ec.channel.blend", "Blend", "Fusion"), ("ec.channel.calculations", "Calculations", "Calculs"), - ("ec.channel.cccomposite", "CC Composite", "CC Composite"), + ("ec.channel.cccomposite", "Source Composite", "Composition de la source"), ("ec.channel.combiner", "Channel Combiner", "Combinateur de canaux"), ("ec.channel.compoundarithmetic", "Compound Arithmetic", "Arithmétique composée"), ("ec.channel.invert", "Invert", "Inversion"), @@ -1577,10 +1584,10 @@ const FRENCH: &[Row] = &[ ("ec.color.blackwhite", "Black & White", "Noir et blanc"), ("ec.color.brightnesscontrast", "Brightness & Contrast", "Luminosité et contraste"), ("ec.color.broadcast", "Broadcast Colors", "Couleurs de diffusion"), - ("ec.color.cccolorneutralizer", "CC Color Neutralizer", "CC Color Neutralizer"), - ("ec.color.cccoloroffset", "CC Color Offset", "CC Color Offset"), - ("ec.color.cckernel", "CC Kernel", "CC Kernel"), - ("ec.color.cctoner", "CC Toner", "CC Toner"), + ("ec.color.cccolorneutralizer", "Color Neutralizer", "Neutralisation des couleurs"), + ("ec.color.cccoloroffset", "Channel Offset", "Décalage des canaux"), + ("ec.color.cckernel", "Convolution Kernel", "Noyau de convolution"), + ("ec.color.cctoner", "Multi-Tone Tint", "Teinte multiton"), ("ec.color.changecolor", "Change Color", "Changer la couleur"), ("ec.color.changetocolor", "Change to Color", "Changer en couleur"), ("ec.color.channelmixer", "Channel Mixer", "Mélangeur de canaux"), @@ -1597,7 +1604,7 @@ const FRENCH: &[Row] = &[ ("ec.color.leavecolor", "Leave Color", "Conserver la couleur"), ("ec.color.levels", "Levels", "Niveaux"), ("ec.color.levelsic", "Levels (Individual Controls)", "Niveaux (contrôles individuels)"), - ("ec.color.lumetri", "Lumetri Color", "Lumetri Color"), + ("ec.color.lumetri", "Color Grading", "Étalonnage des couleurs"), ("ec.color.ociocdl", "OCIO CDL Transform", "Transformation CDL OCIO"), ("ec.color.ociocolorspace", "OCIO Color Space Transform", "Transformation d’espace colorimétrique OCIO"), ("ec.color.ociodisplay", "OCIO Display Transform", "Transformation d’affichage OCIO"), @@ -1622,20 +1629,20 @@ const FRENCH: &[Row] = &[ ("ec.control.stereo3d", "Stereo 3D Controls", "Contrôles stéréo 3D"), ("ec.distort.bezierwarp", "Bezier Warp", "Déformation Bézier"), ("ec.distort.bulge", "Bulge", "Renflement"), - ("ec.distort.ccbender", "CC Bender", "CC Bender"), - ("ec.distort.ccbendit", "CC Bend It", "CC Bend It"), - ("ec.distort.ccblobbylize", "CC Blobbylize", "CC Blobbylize"), - ("ec.distort.ccflomotion", "CC Flo Motion", "CC Flo Motion"), - ("ec.distort.ccgriddler", "CC Griddler", "CC Griddler"), - ("ec.distort.cclens", "CC Lens", "CC Lens"), - ("ec.distort.ccpageturn", "CC Page Turn", "CC Page Turn"), - ("ec.distort.ccpowerpin", "CC Power Pin", "CC Power Pin"), - ("ec.distort.ccripplepulse", "CC Ripple Pulse", "CC Ripple Pulse"), - ("ec.distort.ccslant", "CC Slant", "CC Slant"), - ("ec.distort.ccsmear", "CC Smear", "CC Smear"), - ("ec.distort.ccsplit", "CC Split", "CC Split"), - ("ec.distort.ccsplit2", "CC Split 2", "CC Split 2"), - ("ec.distort.cctiler", "CC Tiler", "CC Tiler"), + ("ec.distort.ccbender", "Flexible Bend", "Courbure souple"), + ("ec.distort.ccbendit", "Two-Point Bend", "Courbure à deux points"), + ("ec.distort.ccblobbylize", "Organic Displacement", "Déplacement organique"), + ("ec.distort.ccflomotion", "Dual Lens Warp", "Déformation à deux lentilles"), + ("ec.distort.ccgriddler", "Tile Grid", "Grille de tuiles"), + ("ec.distort.cclens", "Lens Distortion", "Distorsion de lentille"), + ("ec.distort.ccpageturn", "Page Curl", "Page enroulée"), + ("ec.distort.ccpowerpin", "Perspective Pin", "Épinglage en perspective"), + ("ec.distort.ccripplepulse", "Pulsed Ripple", "Ondulation pulsée"), + ("ec.distort.ccslant", "Slant", "Inclinaison"), + ("ec.distort.ccsmear", "Directional Smear", "Étalement directionnel"), + ("ec.distort.ccsplit", "Split Warp", "Déformation divisée"), + ("ec.distort.ccsplit2", "Dual Split Warp", "Déformation à double division"), + ("ec.distort.cctiler", "Scaled Tiles", "Tuiles mises à l’échelle"), ("ec.distort.cornerpin", "Corner Pin", "Épinglage d’angles"), ("ec.distort.displacementmap", "Displacement Map", "Carte de déplacement"), ("ec.distort.liquify", "Liquify", "Liquéfier"), @@ -1663,11 +1670,11 @@ const FRENCH: &[Row] = &[ ("ec.generate.audiospectrum", "Audio Spectrum", "Spectre audio"), ("ec.generate.audiowaveform", "Audio Waveform", "Forme d’onde audio"), ("ec.generate.beam", "Beam", "Faisceau"), - ("ec.generate.ccgluegun", "CC Glue Gun", "CC Glue Gun"), - ("ec.generate.cclightburst", "CC Light Burst 2.5", "CC Light Burst 2.5"), - ("ec.generate.cclightrays", "CC Light Rays", "CC Light Rays"), - ("ec.generate.cclightsweep", "CC Light Sweep", "CC Light Sweep"), - ("ec.generate.ccthreads", "CC Threads", "CC Threads"), + ("ec.generate.ccgluegun", "Paint Beads", "Perles de peinture"), + ("ec.generate.cclightburst", "Radial Light Burst", "Éclat lumineux radial"), + ("ec.generate.cclightrays", "Light Rays", "Rayons lumineux"), + ("ec.generate.cclightsweep", "Light Sweep", "Balayage lumineux"), + ("ec.generate.ccthreads", "Woven Threads", "Fils tissés"), ("ec.generate.cellpattern", "Cell Pattern", "Motif cellulaire"), ("ec.generate.checkerboard", "Checkerboard", "Damier"), ("ec.generate.circle", "Circle", "Cercle"), @@ -1686,7 +1693,7 @@ const FRENCH: &[Row] = &[ ("ec.generate.vegas", "Vegas", "Vegas"), ("ec.generate.writeon", "Write-on", "Écriture progressive"), ("ec.key.advancedspill", "Advanced Spill Suppressor", "Suppresseur de débordement avancé"), - ("ec.key.ccsimplewireremoval", "CC Simple Wire Removal", "CC Simple Wire Removal"), + ("ec.key.ccsimplewireremoval", "Line Removal", "Suppression de ligne"), ("ec.key.colordifference", "Color Difference Key", "Incrustation par différence de couleur"), ("ec.key.colorkey", "Color Key", "Incrustation couleur"), ("ec.key.colorrange", "Color Range", "Plage de couleurs"), @@ -1699,7 +1706,7 @@ const FRENCH: &[Row] = &[ ("ec.key.screen", "Screen Key", "Incrustation d’écran"), ("ec.key.spill", "Spill Suppressor", "Suppresseur de débordement"), ("ec.key.unmult", "Unmult", "Unmult"), - ("ec.keying.keylight", "Key Light", "Key Light"), + ("ec.keying.keylight", "Screen Color Key", "Incrustation par couleur d’écran"), ("ec.matte.mattechoker", "Matte Choker", "Réducteur d’estampe"), ("ec.matte.objectmatte", "Object Matte", "Estampe d’objet"), ("ec.matte.refinehard", "Refine Hard Matte", "Affiner l’estampe nette"), @@ -1723,50 +1730,50 @@ const FRENCH: &[Row] = &[ ("ec.obsolete.basictext", "Basic Text", "Texte de base"), ("ec.obsolete.gaussianlegacy", "Gaussian Blur (Legacy)", "Flou gaussien (hérité)"), ("ec.obsolete.lightning", "Lightning", "Foudre"), - ("ec.obsolete.mochashape", "Mocha shape", "Forme Mocha"), + ("ec.obsolete.mochashape", "Tracked Shape Matte", "Cache de forme suivie"), ("ec.obsolete.pathtext", "Path Text", "Texte sur tracé"), ("ec.paint.paint", "Paint", "Peinture"), ("ec.perspective.3dglasses", "3D Glasses", "Lunettes 3D"), ("ec.perspective.bevelalpha", "Bevel Alpha", "Biseau alpha"), ("ec.perspective.beveledges", "Bevel Edges", "Biseau des bords"), ("ec.perspective.cameratracker", "3D Camera Tracker", "Suivi de caméra 3D"), - ("ec.perspective.cccylinder", "CC Cylinder", "CC Cylinder"), - ("ec.perspective.ccenvironment", "CC Environment", "CC Environment"), - ("ec.perspective.ccsphere", "CC Sphere", "CC Sphere"), - ("ec.perspective.ccspotlight", "CC Spotlight", "CC Spotlight"), + ("ec.perspective.cccylinder", "Cylinder Wrap", "Enroulement cylindrique"), + ("ec.perspective.ccenvironment", "Environment Map", "Carte d’environnement"), + ("ec.perspective.ccsphere", "Sphere Wrap", "Enroulement sphérique"), + ("ec.perspective.ccspotlight", "Surface Spotlight", "Projecteur de surface"), ("ec.perspective.dropshadow", "Drop Shadow", "Ombre portée"), ("ec.perspective.radialshadow", "Radial Shadow", "Ombre radiale"), ("ec.sim.carddance", "Card Dance", "Danse de cartes"), ("ec.sim.caustics", "Caustics", "Caustiques"), - ("ec.sim.ccballaction", "CC Ball Action", "CC Ball Action"), - ("ec.sim.ccbubbles", "CC Bubbles", "CC Bubbles"), - ("ec.sim.ccdrizzle", "CC Drizzle", "CC Drizzle"), - ("ec.sim.cchair", "CC Hair", "CC Hair"), - ("ec.sim.ccmrmercury", "CC Mr. Mercury", "CC Mr. Mercury"), - ("ec.sim.ccparticlesystems2", "CC Particle Systems II", "CC Particle Systems II"), - ("ec.sim.ccparticleworld", "CC Particle World", "CC Particle World"), - ("ec.sim.ccpixelpolly", "CC Pixel Polly", "CC Pixel Polly"), - ("ec.sim.ccrainfall", "CC Rainfall", "CC Rainfall"), - ("ec.sim.ccscatterize", "CC Scatterize", "CC Scatterize"), - ("ec.sim.ccsnowfall", "CC Snowfall", "CC Snowfall"), - ("ec.sim.ccstarburst", "CC Star Burst", "CC Star Burst"), + ("ec.sim.ccballaction", "Ball Grid", "Grille de sphères"), + ("ec.sim.ccbubbles", "Bubbles", "Bulles"), + ("ec.sim.ccdrizzle", "Water Ripples", "Ondulations de l’eau"), + ("ec.sim.cchair", "Surface Fibers", "Fibres de surface"), + ("ec.sim.ccmrmercury", "Liquid Particles", "Particules liquides"), + ("ec.sim.ccparticlesystems2", "Particle Emitter 2D", "Émetteur de particules 2D"), + ("ec.sim.ccparticleworld", "Particle Emitter 3D", "Émetteur de particules 3D"), + ("ec.sim.ccpixelpolly", "Pixel Shatter", "Éclatement des pixels"), + ("ec.sim.ccrainfall", "Rainfall", "Pluie"), + ("ec.sim.ccscatterize", "Pixel Scatter", "Dispersion des pixels"), + ("ec.sim.ccsnowfall", "Snowfall", "Neige"), + ("ec.sim.ccstarburst", "Starfield", "Champ d’étoiles"), ("ec.sim.foam", "Foam", "Mousse"), ("ec.sim.particleplayground", "Particle Playground", "Terrain de jeu de particules"), ("ec.sim.shatter", "Shatter", "Éclatement"), ("ec.sim.waveworld", "Wave World", "Monde des vagues"), ("ec.stylize.brushstrokes", "Brush Strokes", "Coups de pinceau"), ("ec.stylize.cartoon", "Cartoon", "Dessin animé"), - ("ec.stylize.ccblockload", "CC Block Load", "CC Block Load"), - ("ec.stylize.ccburnfilm", "CC Burn Film", "CC Burn Film"), - ("ec.stylize.ccglass", "CC Glass", "CC Glass"), - ("ec.stylize.cchextile", "CC HexTile", "CC HexTile"), - ("ec.stylize.cckaleida", "CC Kaleida", "CC Kaleida"), - ("ec.stylize.ccmrsmoothie", "CC Mr. Smoothie", "CC Mr. Smoothie"), - ("ec.stylize.ccplastic", "CC Plastic", "CC Plastic"), - ("ec.stylize.ccrepetile", "CC RepeTile", "CC RepeTile"), - ("ec.stylize.ccthreshold", "CC Threshold", "CC Threshold"), - ("ec.stylize.ccthresholdrgb", "CC Threshold RGB", "CC Threshold RGB"), - ("ec.stylize.ccvignette", "CC Vignette", "CC Vignette"), + ("ec.stylize.ccblockload", "Block Reveal", "Révélation par blocs"), + ("ec.stylize.ccburnfilm", "Film Burn", "Brûlure de pellicule"), + ("ec.stylize.ccglass", "Glass Relief", "Relief de verre"), + ("ec.stylize.cchextile", "Hexagonal Tiles", "Tuiles hexagonales"), + ("ec.stylize.cckaleida", "Kaleidoscope", "Kaléidoscope"), + ("ec.stylize.ccmrsmoothie", "Palette Map", "Palettmappning"), + ("ec.stylize.ccplastic", "Plastic Shading", "Ombrage plastique"), + ("ec.stylize.ccrepetile", "Edge Tile Repeat", "Répétition des tuiles aux bords"), + ("ec.stylize.ccthreshold", "Channel Threshold", "Seuil de canal"), + ("ec.stylize.ccthresholdrgb", "RGB Threshold", "Seuil RVB"), + ("ec.stylize.ccvignette", "Vignette", "Vignettage"), ("ec.stylize.coloremboss", "Color Emboss", "Estampage couleur"), ("ec.stylize.emboss", "Emboss", "Estampage"), ("ec.stylize.findedges", "Find Edges", "Détection des contours"), @@ -1781,8 +1788,8 @@ const FRENCH: &[Row] = &[ ("ec.stylize.threshold", "Threshold", "Seuil"), ("ec.text.numbers", "Numbers", "Nombres"), ("ec.text.timecode", "Timecode", "Timecode"), - ("ec.time.ccforcemotionblur", "CC Force Motion Blur", "CC Force Motion Blur"), - ("ec.time.ccwidetime", "CC Wide Time", "CC Wide Time"), + ("ec.time.ccforcemotionblur", "Temporal Motion Blur", "Flou de mouvement temporel"), + ("ec.time.ccwidetime", "Temporal Blend", "Fusion temporelle"), ("ec.time.echo", "Echo", "Écho"), ("ec.time.pixelmotionblur", "Pixel Motion Blur", "Flou de mouvement des pixels"), ("ec.time.posterizetime", "Posterize Time", "Postérisation temporelle"), @@ -1791,23 +1798,23 @@ const FRENCH: &[Row] = &[ ("ec.time.timewarp", "Timewarp", "Déformation temporelle"), ("ec.transition.blockdissolve", "Block Dissolve", "Fondu par blocs"), ("ec.transition.cardwipe", "Card Wipe", "Volet de cartes"), - ("ec.transition.ccglasswipe", "CC Glass Wipe", "CC Glass Wipe"), - ("ec.transition.ccgridwipe", "CC Grid Wipe", "CC Grid Wipe"), - ("ec.transition.ccimagewipe", "CC Image Wipe", "CC Image Wipe"), - ("ec.transition.ccjaws", "CC Jaws", "CC Jaws"), - ("ec.transition.cclightwipe", "CC Light Wipe", "CC Light Wipe"), - ("ec.transition.cclinesweep", "CC Line Sweep", "CC Line Sweep"), - ("ec.transition.ccradialscalewipe", "CC Radial ScaleWipe", "CC Radial ScaleWipe"), - ("ec.transition.ccscalewipe", "CC Scale Wipe", "CC Scale Wipe"), - ("ec.transition.cctwister", "CC Twister", "CC Twister"), - ("ec.transition.ccwarpomatic", "CC WarpoMatic", "CC WarpoMatic"), + ("ec.transition.ccglasswipe", "Glass Wipe", "Volet de verre"), + ("ec.transition.ccgridwipe", "Grid Wipe", "Volet en grille"), + ("ec.transition.ccimagewipe", "Luminance Image Wipe", "Volet par luminance d’image"), + ("ec.transition.ccjaws", "Serrated Wipe", "Volet dentelé"), + ("ec.transition.cclightwipe", "Light Wipe", "Volet lumineux"), + ("ec.transition.cclinesweep", "Line Sweep", "Balayage de lignes"), + ("ec.transition.ccradialscalewipe", "Radial Scale Wipe", "Volet d’échelle radial"), + ("ec.transition.ccscalewipe", "Scale Wipe", "Volet d’échelle"), + ("ec.transition.cctwister", "Twist Wipe", "Volet torsadé"), + ("ec.transition.ccwarpomatic", "Warp Dissolve", "Fondu déformant"), ("ec.transition.gradientwipe", "Gradient Wipe", "Volet en dégradé"), ("ec.transition.iriswipe", "Iris Wipe", "Volet en iris"), ("ec.transition.linearwipe", "Linear Wipe", "Volet linéaire"), ("ec.transition.radialwipe", "Radial Wipe", "Volet radial"), ("ec.transition.venetian", "Venetian Blinds", "Stores vénitiens"), ("ec.utility.applylut", "Apply Color LUT", "Appliquer une LUT de couleur"), - ("ec.utility.ccoverbrights", "CC Overbrights", "CC Overbrights"), + ("ec.utility.ccoverbrights", "Out-of-Range Display", "Affichage hors plage"), ("ec.utility.cineon", "Cineon Converter", "Convertisseur Cineon"), ("ec.utility.colorprofileconverter", "Color Profile Converter", "Convertisseur de profil colorimétrique"), ("ec.utility.facemeasurements", "Face Measurements", "Mesures du visage"), @@ -1909,10 +1916,10 @@ const SWEDISH: &[Row] = &[ ("ec.blur.bilateral", "Bilateral Blur", "Bilateral oskärpa"), ("ec.blur.cameralens", "Camera Lens Blur", "Kameralinsoskärpa"), ("ec.blur.camerashakedeblur", "Camera-Shake Deblur", "Ta bort oskärpa från kameraskakning"), - ("ec.blur.cccross", "CC Cross Blur", "CC Korsoskärpa"), - ("ec.blur.ccradial", "CC Radial Blur", "CC Radiell oskärpa"), - ("ec.blur.ccradialfast", "CC Radial Fast Blur", "CC Snabb radiell oskärpa"), - ("ec.blur.ccvector", "CC Vector Blur", "CC Vektoroskärpa"), + ("ec.blur.cccross", "Cross Blur", "Korsoskärpa"), + ("ec.blur.ccradial", "Variable Radial Blur", "Variabel radiell oskärpa"), + ("ec.blur.ccradialfast", "Fast Radial Blur", "Snabb radiell oskärpa"), + ("ec.blur.ccvector", "Vector Blur", "Vektoroskärpa"), ("ec.blur.channel", "Channel Blur", "Kanaloskärpa"), ("ec.blur.compound", "Compound Blur", "Sammansatt oskärpa"), ("ec.blur.directional", "Directional Blur", "Riktad oskärpa"), @@ -1926,7 +1933,7 @@ const SWEDISH: &[Row] = &[ ("ec.channel.arithmetic", "Arithmetic", "Aritmetik"), ("ec.channel.blend", "Blend", "Blanda"), ("ec.channel.calculations", "Calculations", "Beräkningar"), - ("ec.channel.cccomposite", "CC Composite", "CC Komposit"), + ("ec.channel.cccomposite", "Source Composite", "Källkomposition"), ("ec.channel.combiner", "Channel Combiner", "Kanalkombinerare"), ("ec.channel.compoundarithmetic", "Compound Arithmetic", "Sammansatt aritmetik"), ("ec.channel.invert", "Invert", "Invertera"), @@ -1942,10 +1949,10 @@ const SWEDISH: &[Row] = &[ ("ec.color.blackwhite", "Black & White", "Svartvitt"), ("ec.color.brightnesscontrast", "Brightness & Contrast", "Ljusstyrka och kontrast"), ("ec.color.broadcast", "Broadcast Colors", "Broadcast-färger"), - ("ec.color.cccolorneutralizer", "CC Color Neutralizer", "CC Färgneutraliserare"), - ("ec.color.cccoloroffset", "CC Color Offset", "CC Färgförskjutning"), - ("ec.color.cckernel", "CC Kernel", "CC Kärna"), - ("ec.color.cctoner", "CC Toner", "CC Toner"), + ("ec.color.cccolorneutralizer", "Color Neutralizer", "Färgneutralisering"), + ("ec.color.cccoloroffset", "Channel Offset", "Kanalförskjutning"), + ("ec.color.cckernel", "Convolution Kernel", "Faltningskärna"), + ("ec.color.cctoner", "Multi-Tone Tint", "Flertonsfärgning"), ("ec.color.changecolor", "Change Color", "Ändra färg"), ("ec.color.changetocolor", "Change to Color", "Ändra till färg"), ("ec.color.channelmixer", "Channel Mixer", "Kanalmixer"), @@ -1962,7 +1969,7 @@ const SWEDISH: &[Row] = &[ ("ec.color.leavecolor", "Leave Color", "Behåll färg"), ("ec.color.levels", "Levels", "Nivåer"), ("ec.color.levelsic", "Levels (Individual Controls)", "Nivåer (individuella kontroller)"), - ("ec.color.lumetri", "Lumetri Color", "Lumetri-färg"), + ("ec.color.lumetri", "Color Grading", "Färggradering"), ("ec.color.ociocdl", "OCIO CDL Transform", "OCIO CDL-transformering"), ("ec.color.ociocolorspace", "OCIO Color Space Transform", "OCIO färgrymdstransformering"), ("ec.color.ociodisplay", "OCIO Display Transform", "OCIO visningstransformering"), @@ -1987,20 +1994,20 @@ const SWEDISH: &[Row] = &[ ("ec.control.stereo3d", "Stereo 3D Controls", "Stereo 3D-kontroller"), ("ec.distort.bezierwarp", "Bezier Warp", "Bezierförvrängning"), ("ec.distort.bulge", "Bulge", "Utbuktning"), - ("ec.distort.ccbender", "CC Bender", "CC Bender"), - ("ec.distort.ccbendit", "CC Bend It", "CC Bend It"), - ("ec.distort.ccblobbylize", "CC Blobbylize", "CC Blobbylize"), - ("ec.distort.ccflomotion", "CC Flo Motion", "CC Flo Motion"), - ("ec.distort.ccgriddler", "CC Griddler", "CC Griddler"), - ("ec.distort.cclens", "CC Lens", "CC Objektiv"), - ("ec.distort.ccpageturn", "CC Page Turn", "CC Sidvändning"), - ("ec.distort.ccpowerpin", "CC Power Pin", "CC Power Pin"), - ("ec.distort.ccripplepulse", "CC Ripple Pulse", "CC Krusningspuls"), - ("ec.distort.ccslant", "CC Slant", "CC Lutning"), - ("ec.distort.ccsmear", "CC Smear", "CC Utsmetning"), - ("ec.distort.ccsplit", "CC Split", "CC Dela"), - ("ec.distort.ccsplit2", "CC Split 2", "CC Dela 2"), - ("ec.distort.cctiler", "CC Tiler", "CC Tiler"), + ("ec.distort.ccbender", "Flexible Bend", "Flexibel böjning"), + ("ec.distort.ccbendit", "Two-Point Bend", "Tvåpunktsböjning"), + ("ec.distort.ccblobbylize", "Organic Displacement", "Organisk förskjutning"), + ("ec.distort.ccflomotion", "Dual Lens Warp", "Dubbel linsförvrängning"), + ("ec.distort.ccgriddler", "Tile Grid", "Plattnät"), + ("ec.distort.cclens", "Lens Distortion", "Linsförvrängning"), + ("ec.distort.ccpageturn", "Page Curl", "Sidkrullning"), + ("ec.distort.ccpowerpin", "Perspective Pin", "Perspektivnålar"), + ("ec.distort.ccripplepulse", "Pulsed Ripple", "Pulserande krusning"), + ("ec.distort.ccslant", "Slant", "Lutning"), + ("ec.distort.ccsmear", "Directional Smear", "Riktningssmetning"), + ("ec.distort.ccsplit", "Split Warp", "Delad förvrängning"), + ("ec.distort.ccsplit2", "Dual Split Warp", "Dubbeldelad förvrängning"), + ("ec.distort.cctiler", "Scaled Tiles", "Skalade plattor"), ("ec.distort.cornerpin", "Corner Pin", "Hörnförankring"), ("ec.distort.displacementmap", "Displacement Map", "Förskjutningskarta"), ("ec.distort.liquify", "Liquify", "Gör flytande"), @@ -2028,11 +2035,11 @@ const SWEDISH: &[Row] = &[ ("ec.generate.audiospectrum", "Audio Spectrum", "Ljudspektrum"), ("ec.generate.audiowaveform", "Audio Waveform", "Ljudvågform"), ("ec.generate.beam", "Beam", "Stråle"), - ("ec.generate.ccgluegun", "CC Glue Gun", "CC Glue Gun"), - ("ec.generate.cclightburst", "CC Light Burst 2.5", "CC Ljusutbrott 2.5"), - ("ec.generate.cclightrays", "CC Light Rays", "CC Ljusstrålar"), - ("ec.generate.cclightsweep", "CC Light Sweep", "CC Ljussvep"), - ("ec.generate.ccthreads", "CC Threads", "CC Trådar"), + ("ec.generate.ccgluegun", "Paint Beads", "Färgpärlor"), + ("ec.generate.cclightburst", "Radial Light Burst", "Radiellt ljusutbrott"), + ("ec.generate.cclightrays", "Light Rays", "Ljusstrålar"), + ("ec.generate.cclightsweep", "Light Sweep", "Ljussvep"), + ("ec.generate.ccthreads", "Woven Threads", "Vävda trådar"), ("ec.generate.cellpattern", "Cell Pattern", "Cellmönster"), ("ec.generate.checkerboard", "Checkerboard", "Schackbräde"), ("ec.generate.circle", "Circle", "Cirkel"), @@ -2051,7 +2058,7 @@ const SWEDISH: &[Row] = &[ ("ec.generate.vegas", "Vegas", "Vegas"), ("ec.generate.writeon", "Write-on", "Framskrivning"), ("ec.key.advancedspill", "Advanced Spill Suppressor", "Avancerad spilldämpning"), - ("ec.key.ccsimplewireremoval", "CC Simple Wire Removal", "CC Enkel trådborttagning"), + ("ec.key.ccsimplewireremoval", "Line Removal", "Linjeborttagning"), ("ec.key.colordifference", "Color Difference Key", "Färgdifferensnyckel"), ("ec.key.colorkey", "Color Key", "Färgnyckel"), ("ec.key.colorrange", "Color Range", "Färgomfång"), @@ -2064,7 +2071,7 @@ const SWEDISH: &[Row] = &[ ("ec.key.screen", "Screen Key", "Skärmnyckel"), ("ec.key.spill", "Spill Suppressor", "Spilldämpning"), ("ec.key.unmult", "Unmult", "Unmult"), - ("ec.keying.keylight", "Key Light", "Key Light"), + ("ec.keying.keylight", "Screen Color Key", "Skärmfärgsnyckling"), ("ec.matte.mattechoker", "Matte Choker", "Mattekrympning"), ("ec.matte.objectmatte", "Object Matte", "Objektmatte"), ("ec.matte.refinehard", "Refine Hard Matte", "Förfina hård matte"), @@ -2088,50 +2095,50 @@ const SWEDISH: &[Row] = &[ ("ec.obsolete.basictext", "Basic Text", "Grundläggande text"), ("ec.obsolete.gaussianlegacy", "Gaussian Blur (Legacy)", "Gaussisk oskärpa (äldre)"), ("ec.obsolete.lightning", "Lightning", "Blixt"), - ("ec.obsolete.mochashape", "Mocha shape", "Mocha-form"), + ("ec.obsolete.mochashape", "Tracked Shape Matte", "Spårad formmask"), ("ec.obsolete.pathtext", "Path Text", "Text längs bana"), ("ec.paint.paint", "Paint", "Måla"), ("ec.perspective.3dglasses", "3D Glasses", "3D-glasögon"), ("ec.perspective.bevelalpha", "Bevel Alpha", "Alfafasning"), ("ec.perspective.beveledges", "Bevel Edges", "Fasade kanter"), ("ec.perspective.cameratracker", "3D Camera Tracker", "3D-kameraspårning"), - ("ec.perspective.cccylinder", "CC Cylinder", "CC Cylinder"), - ("ec.perspective.ccenvironment", "CC Environment", "CC Miljö"), - ("ec.perspective.ccsphere", "CC Sphere", "CC Sfär"), - ("ec.perspective.ccspotlight", "CC Spotlight", "CC Strålkastare"), + ("ec.perspective.cccylinder", "Cylinder Wrap", "Cylinderomslag"), + ("ec.perspective.ccenvironment", "Environment Map", "Miljökarta"), + ("ec.perspective.ccsphere", "Sphere Wrap", "Sfäromslag"), + ("ec.perspective.ccspotlight", "Surface Spotlight", "Ytstrålkastare"), ("ec.perspective.dropshadow", "Drop Shadow", "Slagskugga"), ("ec.perspective.radialshadow", "Radial Shadow", "Radiell skugga"), ("ec.sim.carddance", "Card Dance", "Kortdans"), ("ec.sim.caustics", "Caustics", "Kaustik"), - ("ec.sim.ccballaction", "CC Ball Action", "CC Bollåtgärd"), - ("ec.sim.ccbubbles", "CC Bubbles", "CC Bubblor"), - ("ec.sim.ccdrizzle", "CC Drizzle", "CC Duggregn"), - ("ec.sim.cchair", "CC Hair", "CC Hår"), - ("ec.sim.ccmrmercury", "CC Mr. Mercury", "CC Mr. Mercury"), - ("ec.sim.ccparticlesystems2", "CC Particle Systems II", "CC Partikelsystem II"), - ("ec.sim.ccparticleworld", "CC Particle World", "CC Partikelvärld"), - ("ec.sim.ccpixelpolly", "CC Pixel Polly", "CC Pixel Polly"), - ("ec.sim.ccrainfall", "CC Rainfall", "CC Regn"), - ("ec.sim.ccscatterize", "CC Scatterize", "CC Utspridning"), - ("ec.sim.ccsnowfall", "CC Snowfall", "CC Snöfall"), - ("ec.sim.ccstarburst", "CC Star Burst", "CC Stjärnexplosion"), + ("ec.sim.ccballaction", "Ball Grid", "Kulnät"), + ("ec.sim.ccbubbles", "Bubbles", "Bubblor"), + ("ec.sim.ccdrizzle", "Water Ripples", "Vattenkrusningar"), + ("ec.sim.cchair", "Surface Fibers", "Ytfibrer"), + ("ec.sim.ccmrmercury", "Liquid Particles", "Vätskepartiklar"), + ("ec.sim.ccparticlesystems2", "Particle Emitter 2D", "Partikelsändare 2D"), + ("ec.sim.ccparticleworld", "Particle Emitter 3D", "Partikelsändare 3D"), + ("ec.sim.ccpixelpolly", "Pixel Shatter", "Pixelsplittring"), + ("ec.sim.ccrainfall", "Rainfall", "Regn"), + ("ec.sim.ccscatterize", "Pixel Scatter", "Pixelspridning"), + ("ec.sim.ccsnowfall", "Snowfall", "Snöfall"), + ("ec.sim.ccstarburst", "Starfield", "Stjärnfält"), ("ec.sim.foam", "Foam", "Skum"), ("ec.sim.particleplayground", "Particle Playground", "Partikellekplats"), ("ec.sim.shatter", "Shatter", "Krossa"), ("ec.sim.waveworld", "Wave World", "Vågvärld"), ("ec.stylize.brushstrokes", "Brush Strokes", "Penseldrag"), ("ec.stylize.cartoon", "Cartoon", "Tecknat"), - ("ec.stylize.ccblockload", "CC Block Load", "CC Blockladdning"), - ("ec.stylize.ccburnfilm", "CC Burn Film", "CC Bränn film"), - ("ec.stylize.ccglass", "CC Glass", "CC Glas"), - ("ec.stylize.cchextile", "CC HexTile", "CC HexTile"), - ("ec.stylize.cckaleida", "CC Kaleida", "CC Kaleida"), - ("ec.stylize.ccmrsmoothie", "CC Mr. Smoothie", "CC Mr. Smoothie"), - ("ec.stylize.ccplastic", "CC Plastic", "CC Plast"), - ("ec.stylize.ccrepetile", "CC RepeTile", "CC RepeTile"), - ("ec.stylize.ccthreshold", "CC Threshold", "CC Tröskel"), - ("ec.stylize.ccthresholdrgb", "CC Threshold RGB", "CC Tröskel RGB"), - ("ec.stylize.ccvignette", "CC Vignette", "CC Vinjett"), + ("ec.stylize.ccblockload", "Block Reveal", "Blockframträdande"), + ("ec.stylize.ccburnfilm", "Film Burn", "Filmbränning"), + ("ec.stylize.ccglass", "Glass Relief", "Glasrelief"), + ("ec.stylize.cchextile", "Hexagonal Tiles", "Sexkantiga plattor"), + ("ec.stylize.cckaleida", "Kaleidoscope", "Kalejdoskop"), + ("ec.stylize.ccmrsmoothie", "Palette Map", "Mappage de palette"), + ("ec.stylize.ccplastic", "Plastic Shading", "Plastskuggning"), + ("ec.stylize.ccrepetile", "Edge Tile Repeat", "Kantupprepning"), + ("ec.stylize.ccthreshold", "Channel Threshold", "Kanaltröskel"), + ("ec.stylize.ccthresholdrgb", "RGB Threshold", "RGB-tröskel"), + ("ec.stylize.ccvignette", "Vignette", "Vinjettering"), ("ec.stylize.coloremboss", "Color Emboss", "Färgrelief"), ("ec.stylize.emboss", "Emboss", "Relief"), ("ec.stylize.findedges", "Find Edges", "Hitta kanter"), @@ -2146,8 +2153,8 @@ const SWEDISH: &[Row] = &[ ("ec.stylize.threshold", "Threshold", "Tröskel"), ("ec.text.numbers", "Numbers", "Siffror"), ("ec.text.timecode", "Timecode", "Tidskod"), - ("ec.time.ccforcemotionblur", "CC Force Motion Blur", "CC Tvinga rörelseoskärpa"), - ("ec.time.ccwidetime", "CC Wide Time", "CC Bred tid"), + ("ec.time.ccforcemotionblur", "Temporal Motion Blur", "Tidsbaserad rörelseoskärpa"), + ("ec.time.ccwidetime", "Temporal Blend", "Tidsblandning"), ("ec.time.echo", "Echo", "Eko"), ("ec.time.pixelmotionblur", "Pixel Motion Blur", "Pixelrörelseoskärpa"), ("ec.time.posterizetime", "Posterize Time", "Posterisera tid"), @@ -2156,23 +2163,23 @@ const SWEDISH: &[Row] = &[ ("ec.time.timewarp", "Timewarp", "Tidsförvrängning"), ("ec.transition.blockdissolve", "Block Dissolve", "Blockövertoning"), ("ec.transition.cardwipe", "Card Wipe", "Kortridå"), - ("ec.transition.ccglasswipe", "CC Glass Wipe", "CC Glasridå"), - ("ec.transition.ccgridwipe", "CC Grid Wipe", "CC Rutnätsridå"), - ("ec.transition.ccimagewipe", "CC Image Wipe", "CC Bildridå"), - ("ec.transition.ccjaws", "CC Jaws", "CC Käftar"), - ("ec.transition.cclightwipe", "CC Light Wipe", "CC Ljusridå"), - ("ec.transition.cclinesweep", "CC Line Sweep", "CC Linjesvep"), - ("ec.transition.ccradialscalewipe", "CC Radial ScaleWipe", "CC Radiell skalridå"), - ("ec.transition.ccscalewipe", "CC Scale Wipe", "CC Skalridå"), - ("ec.transition.cctwister", "CC Twister", "CC Virvel"), - ("ec.transition.ccwarpomatic", "CC WarpoMatic", "CC WarpoMatic"), + ("ec.transition.ccglasswipe", "Glass Wipe", "Glasövergång"), + ("ec.transition.ccgridwipe", "Grid Wipe", "Rutnätsövergång"), + ("ec.transition.ccimagewipe", "Luminance Image Wipe", "Luminansbildsövergång"), + ("ec.transition.ccjaws", "Serrated Wipe", "Tandad övergång"), + ("ec.transition.cclightwipe", "Light Wipe", "Ljusövergång"), + ("ec.transition.cclinesweep", "Line Sweep", "Linjesvep"), + ("ec.transition.ccradialscalewipe", "Radial Scale Wipe", "Radiell skalövergång"), + ("ec.transition.ccscalewipe", "Scale Wipe", "Skalövergång"), + ("ec.transition.cctwister", "Twist Wipe", "Vridövergång"), + ("ec.transition.ccwarpomatic", "Warp Dissolve", "Förvrängningsupplösning"), ("ec.transition.gradientwipe", "Gradient Wipe", "Gradientridå"), ("ec.transition.iriswipe", "Iris Wipe", "Irisridå"), ("ec.transition.linearwipe", "Linear Wipe", "Linjär ridå"), ("ec.transition.radialwipe", "Radial Wipe", "Radiell ridå"), ("ec.transition.venetian", "Venetian Blinds", "Persienner"), ("ec.utility.applylut", "Apply Color LUT", "Använd färg-LUT"), - ("ec.utility.ccoverbrights", "CC Overbrights", "CC Överljust"), + ("ec.utility.ccoverbrights", "Out-of-Range Display", "Visa utanför intervallet"), ("ec.utility.cineon", "Cineon Converter", "Cineon-konverterare"), ("ec.utility.colorprofileconverter", "Color Profile Converter", "Färgprofilkonverterare"), ("ec.utility.facemeasurements", "Face Measurements", "Ansiktsmått"), @@ -2226,6 +2233,38 @@ mod tests { use crate::i18n::set_current; use effectcraft_engine::effects; + #[test] + fn legacy_effect_names_draw_current_labels_without_changing_custom_names() { + for language in ["en", "ja", "zh-hans", "zh-hant", "fr", "sv", "ka", "cs", "de", "es", "pl", "pt-br", "ru", "uk"] { + set_current(language); + for spec in effects::registry() { + let translated = name(spec.id, spec.name); + for alias in effects::aliases(spec.id) { + assert_eq!(group(spec.id, alias), translated, "{language}: {alias}"); + assert!(matches(spec.id, alias, &spec.name.to_lowercase()), "{language}: search current name of {alias}"); + assert!(matches(spec.id, spec.name, &alias.to_lowercase()), "{language}: search legacy name {alias}"); + assert_eq!(group(spec.id, &format!("{alias} 2")), format!("{translated} 2")); + let custom = format!("{alias} custom"); + assert_eq!(group(spec.id, &custom), custom); + } + } + } + set_current("en"); + } + + #[test] + fn localized_effect_names_do_not_reintroduce_legacy_branding() { + for (language, rows) in COMPLETE.iter().chain(PARTIAL) { + for (_, _, translated) in *rows { + let label = translated.to_lowercase(); + assert!(!label.starts_with("cc "), "{language}: {translated}"); + for mark in ["lumetri", "mercury", "mocha", "keylight"] { + assert!(!label.contains(mark), "{language}: {translated}"); + } + } + } + } + /// Every row names an effect in the registry and still carries its English name, so a rename /// or a removal fails here rather than attaching a translation to a different effect. #[test] @@ -2242,6 +2281,20 @@ mod tests { } } + /// Two effects that draw one name cannot be told apart in the Effect menu, Effect Controls or + /// Effects & Presets, so every catalog names each effect it covers distinctly. + #[test] + fn display_names_are_unique_in_every_catalog() { + for (language, rows) in COMPLETE.iter().chain(PARTIAL) { + let mut seen: std::collections::HashMap<&str, &str> = std::collections::HashMap::new(); + for (id, _, translated) in *rows { + if let Some(other) = seen.insert(translated, id) { + assert_ne!(other, *id, "{language}: {id} and {other} are both {translated:?}"); + } + } + } + } + /// Rows are keyed by id, so a repeated id would shadow a translation. #[test] fn ids_are_unique() { diff --git a/crates/ui-egui/src/i18n/ui.rs b/crates/ui-egui/src/i18n/ui.rs index 15bf774f9..ac6504493 100644 --- a/crates/ui-egui/src/i18n/ui.rs +++ b/crates/ui-egui/src/i18n/ui.rs @@ -708,8 +708,8 @@ const JAPANESE: &[(&str, &str)] = &[ ("Import Photoshop File", "Photoshopファイルを読み込み"), ("Interpret Unlabeled Alpha As", "ラベルのないアルファの解釈"), ("Join the ArtCraft Discord", "ArtCraftのDiscordに参加"), - ("Mercury GPU Acceleration ({})", "Mercury GPUアクセラレーション({})"), - ("Mercury Software Only", "Mercuryソフトウェアのみ"), + ("GPU Acceleration ({})", "GPUアクセラレーション({})"), + ("Software Rendering", "ソフトウェアレンダリング"), ("OCIO (built-in ACES config)", "OCIO(組み込みACES設定)"), ("Off (Final Quality)", "オフ(最終品質)"), ("OpenGL (compatibility)", "OpenGL(互換)"), @@ -1118,7 +1118,7 @@ const JAPANESE: &[(&str, &str)] = &[ ("Script Console", "スクリプトコンソール"), ("Essential Graphics", "エッセンシャルグラフィックス"), ("ScriptUI Panel", "ScriptUIパネル"), - ("Lumetri Scopes", "Lumetriスコープ"), + ("Video Scopes", "ビデオスコープ"), ("Media Browser", "メディアブラウザー"), ("Content-Aware Fill", "コンテンツに応じた塗りつぶし"), ("Create Nulls From Paths", "パスからヌルを作成"), @@ -1318,6 +1318,7 @@ const JAPANESE: &[(&str, &str)] = &[ ("per second", "毎秒"), ("Waveform", "波形"), ("steps", "ステップ"), + ("GPU Acceleration (no GPU: software)", "GPUアクセラレーション(GPUなし:ソフトウェア)"), ]; const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[ @@ -1837,8 +1838,8 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[ ("Import Photoshop File", "导入 Photoshop 文件"), ("Interpret Unlabeled Alpha As", "将未标记的 Alpha 解释为"), ("Join the ArtCraft Discord", "加入 ArtCraft Discord"), - ("Mercury GPU Acceleration ({})", "Mercury GPU 加速({})"), - ("Mercury Software Only", "仅 Mercury 软件"), + ("GPU Acceleration ({})", "GPU 加速({})"), + ("Software Rendering", "软件渲染"), ("OCIO (built-in ACES config)", "OCIO(内置 ACES 配置)"), ("Off (Final Quality)", "关闭(最终质量)"), ("OpenGL (compatibility)", "OpenGL(兼容)"), @@ -2280,7 +2281,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[ ("Script Console", "脚本控制台"), ("Essential Graphics", "基本图形"), ("ScriptUI Panel", "ScriptUI 面板"), - ("Lumetri Scopes", "Lumetri 示波器"), + ("Video Scopes", "视频示波器"), ("Media Browser", "媒体浏览器"), ("Content-Aware Fill", "内容识别填充"), ("Create Nulls From Paths", "从路径创建空对象"), @@ -2433,6 +2434,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[ ("Blend Mode", "混合模式"), ("Show mesh", "显示网格"), ("Record Options...", "记录选项..."), + ("GPU Acceleration (no GPU: software)", "GPU 加速(无 GPU:软件)"), ]; /// Swedish (`sv`), written from the English source. @@ -2950,8 +2952,8 @@ const SWEDISH: &[(&str, &str)] = &[ ("Import Photoshop File", "Importera Photoshop-fil"), ("Interpret Unlabeled Alpha As", "Tolka omärkt alfa som"), ("Join the ArtCraft Discord", "Gå med i ArtCraft på Discord"), - ("Mercury GPU Acceleration ({})", "Mercury GPU-acceleration ({})"), - ("Mercury Software Only", "Endast Mercury-programvara"), + ("GPU Acceleration ({})", "GPU-acceleration ({})"), + ("Software Rendering", "Programvarurendering"), ("OCIO (built-in ACES config)", "OCIO (inbyggd ACES-konfiguration)"), ("Off (Final Quality)", "Av (slutlig kvalitet)"), ("OpenGL (compatibility)", "OpenGL (kompatibilitet)"), @@ -3191,7 +3193,7 @@ const SWEDISH: &[(&str, &str)] = &[ ("Drop {} files to import", "Släpp {} filer för att importera"), ("Effects", "Effekter"), ("Label of {}: {}", "Etikett för {}: {}"), - ("Mercury GPU Acceleration (no GPU: software)", "Mercury GPU-acceleration (ingen GPU: programvara)"), + ("GPU Acceleration (no GPU: software)", "GPU-acceleration (ingen GPU: programvara)"), ("Only one project can be opened per drop; skipped {} more", "Bara ett projekt kan öppnas per släpp; hoppade över {} till"), ("Open in Essential Graphics", "Öppna i Grundläggande grafik"), ("Original color: click to go back to it", "Originalfärg: klicka för att återgå till den"), @@ -3440,7 +3442,7 @@ const SWEDISH: &[(&str, &str)] = &[ ("Script Console", "Skriptkonsol"), ("Essential Graphics", "Grundläggande grafik"), ("ScriptUI Panel", "ScriptUI-panel"), - ("Lumetri Scopes", "Lumetri-scope"), + ("Video Scopes", "Videoskop"), ("Media Browser", "Mediebläddrare"), ("Content-Aware Fill", "Innehållsmedveten fyllning"), ("Create Nulls From Paths", "Skapa nullobjekt från banor"), @@ -4608,7 +4610,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[ ("Script Console", "指令碼主控台"), ("Essential Graphics", "基本圖形"), ("ScriptUI Panel", "ScriptUI 面板"), - ("Lumetri Scopes", "Lumetri 示波器"), + ("Video Scopes", "視訊示波器"), ("Media Browser", "媒體瀏覽器"), ("Content-Aware Fill", "內容感知填色"), ("Create Nulls From Paths", "從路徑建立空物件"), @@ -4761,6 +4763,9 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[ ("Blend Mode", "混合模式"), ("Show mesh", "顯示網格"), ("Record Options...", "記錄選項..."), + ("Software Rendering", "軟體算繪"), + ("GPU Acceleration ({})", "GPU 加速({})"), + ("GPU Acceleration (no GPU: software)", "GPU 加速(無 GPU:軟體)"), ]; /// Ukrainian (uk): original translations of the same interface rows as the Chinese catalogs. @@ -5301,8 +5306,8 @@ const UKRAINIAN: &[(&str, &str)] = &[ ("Import Photoshop File", "Імпорт файлу Photoshop"), ("Interpret Unlabeled Alpha As", "Інтерпретувати непозначену альфу як"), ("Join the ArtCraft Discord", "Приєднатися до Discord ArtCraft"), - ("Mercury GPU Acceleration ({})", "Прискорення Mercury GPU ({})"), - ("Mercury Software Only", "Лише програмний Mercury"), + ("GPU Acceleration ({})", "Прискорення GPU ({})"), + ("Software Rendering", "Програмне відтворення"), ("OCIO (built-in ACES config)", "OCIO (вбудована конфігурація ACES)"), ("Off (Final Quality)", "Вимкнено (остаточна якість)"), ("OpenGL (compatibility)", "OpenGL (сумісність)"), @@ -5348,6 +5353,8 @@ const UKRAINIAN: &[(&str, &str)] = &[ ("Project", "Проєкт"), ("Total Offset", "Загальний зсув"), ("Per Layer", "На шар"), + ("Video Scopes", "Відеоскопи"), + ("GPU Acceleration (no GPU: software)", "Прискорення GPU (без GPU: програмне)"), ]; // Brazilian Portuguese. @@ -5979,7 +5986,7 @@ const PORTUGUESE: &[(&str, &str)] = &[ ("Guess", "Adivinhar"), ("Import Image Sequence", "Importar sequência de imagens"), ("Import PDF / Illustrator File", "Importar arquivo PDF / Illustrator"), - ("Mercury Software Only", "Apenas software Mercury"), + ("Software Rendering", "Renderização por software"), ("OCIO (built-in ACES config)", "OCIO (config do ACES integrado)"), ("Still Duration", "Duração da imagem fixa"), ("Straight", "Direto"), @@ -6086,7 +6093,7 @@ const PORTUGUESE: &[(&str, &str)] = &[ ("Light", "Claro"), ("Lines", "Linhas"), ("Memory & CPU", "Memória e CPU"), - ("Mercury GPU Acceleration ({})", "Aceleração de GPU Mercury ({})"), + ("GPU Acceleration ({})", "Aceleração de GPU ({})"), ("Off", "Desativado"), ("Options", "Opções"), ("Overlay", "Sobrepor"), @@ -6111,6 +6118,8 @@ const PORTUGUESE: &[(&str, &str)] = &[ ("Video", "Vídeo"), ("channel", "canal"), ("files", "arquivos"), + ("Video Scopes", "Escopos de vídeo"), + ("GPU Acceleration (no GPU: software)", "Aceleração de GPU (sem GPU: software)"), ]; /// Polish (pl): original translations covering every row in the Chinese panel catalogs. @@ -6629,8 +6638,8 @@ const POLISH: &[(&str, &str)] = &[ ("Import Photoshop File", "Importuj plik Photoshopa"), ("Interpret Unlabeled Alpha As", "Interpretuj nieoznaczoną alfę jako"), ("Join the ArtCraft Discord", "Dołącz do Discorda ArtCraft"), - ("Mercury GPU Acceleration ({})", "Akceleracja Mercury GPU ({})"), - ("Mercury Software Only", "Tylko programowy Mercury"), + ("GPU Acceleration ({})", "Akceleracja GPU ({})"), + ("Software Rendering", "Renderowanie programowe"), ("OCIO (built-in ACES config)", "OCIO (wbudowana konfiguracja ACES)"), ("Off (Final Quality)", "Wył. (jakość końcowa)"), ("OpenGL (compatibility)", "OpenGL (zgodność)"), @@ -6868,6 +6877,8 @@ const POLISH: &[(&str, &str)] = &[ ("Resolution:", "Rozdzielczość:"), ("Guide Layer", "Warstwa pomocnicza"), ("Layer Styles", "Style warstwy"), + ("Video Scopes", "Wskaźniki wideo"), + ("GPU Acceleration (no GPU: software)", "Akceleracja GPU (bez GPU: programowa)"), ]; /// Czech (cs): original translations covering every row in the Chinese panel catalogs. @@ -7380,8 +7391,8 @@ const CZECH: &[(&str, &str)] = &[ ("Import Photoshop File", "Importovat soubor Photoshop"), ("Interpret Unlabeled Alpha As", "Interpretovat neoznačenou alfu jako"), ("Join the ArtCraft Discord", "Připojit se na Discord ArtCraft"), - ("Mercury GPU Acceleration ({})", "GPU akcelerace Mercury ({})"), - ("Mercury Software Only", "Pouze softwarové zpracování Mercury"), + ("GPU Acceleration ({})", "Akcelerace GPU ({})"), + ("Software Rendering", "Softwarové vykreslování"), ("OCIO (built-in ACES config)", "OCIO (vestavěná konfigurace ACES)"), ("Off (Final Quality)", "Vypnuto (finální kvalita)"), ("OpenGL (compatibility)", "OpenGL (kompatibilita)"), @@ -7432,6 +7443,8 @@ const CZECH: &[(&str, &str)] = &[ ("Variable fonts only", "Jen proměnná písma"), ("Total Offset", "Celkový posun"), ("Per Layer", "Na vrstvu"), + ("Video Scopes", "Videoskopy"), + ("GPU Acceleration (no GPU: software)", "Akcelerace GPU (bez GPU: software)"), ]; /// Spanish: neutral, tú, sentence case; the same source strings as the Chinese catalogs, in the same @@ -7960,8 +7973,8 @@ const SPANISH: &[(&str, &str)] = &[ ("Import Photoshop File", "Importar archivo de Photoshop"), ("Interpret Unlabeled Alpha As", "Interpretar alfa sin etiquetar como"), ("Join the ArtCraft Discord", "Únete al Discord de ArtCraft"), - ("Mercury GPU Acceleration ({})", "Aceleración por GPU Mercury ({})"), - ("Mercury Software Only", "Solo software Mercury"), + ("GPU Acceleration ({})", "Aceleración por GPU ({})"), + ("Software Rendering", "Renderizado por software"), ("OCIO (built-in ACES config)", "OCIO (configuración ACES integrada)"), ("Off (Final Quality)", "Desactivado (calidad final)"), ("OpenGL (compatibility)", "OpenGL (compatibilidad)"), @@ -8002,6 +8015,8 @@ const SPANISH: &[(&str, &str)] = &[ ("After Effects project", "Proyecto de After Effects"), ("Lottie animation", "Animación de Lottie"), ("Project", "Proyecto"), + ("Video Scopes", "Monitores de vídeo"), + ("GPU Acceleration (no GPU: software)", "Aceleración por GPU (sin GPU: software)"), ]; /// Georgian (ka): every row of the Chinese catalogs, in the same order (the tests keep them in step). @@ -8528,8 +8543,8 @@ const GEORGIAN: &[(&str, &str)] = &[ ("Import Photoshop File", "Photoshop ფაილის იმპორტი"), ("Interpret Unlabeled Alpha As", "უცნობი ალფას ინტერპრეტაცია"), ("Join the ArtCraft Discord", "შემოგვიერთდით Discord-ზე"), - ("Mercury GPU Acceleration ({})", "GPU აჩქარება ({})"), - ("Mercury Software Only", "მხოლოდ Mercury Software"), + ("GPU Acceleration ({})", "GPU აჩქარება ({})"), + ("Software Rendering", "პროგრამული რენდერი"), ("OCIO (built-in ACES config)", "OCIO (ჩაშენებული ACES კონფიგურაცია)"), ("Off (Final Quality)", "გამორთული (საბოლოო ხარისხი)"), ("OpenGL (compatibility)", "OpenGL (თავსებადობა)"), @@ -8761,6 +8776,7 @@ const GEORGIAN: &[(&str, &str)] = &[ ("Total Offset", "ჯამური წანაცვლება"), ("Per Layer", "შრიდან შრემდე"), ("Track", "ტრეკინგი"), + ("Video Scopes", "ვიდეოსკოპები"), ]; /// Georgian rows for interface text no other catalog has yet: strings the panels already send @@ -8951,7 +8967,7 @@ const GEORGIAN_PANELS: &[(&str, &str)] = &[ ("Solid Settings", "სოლიდის პარამეტრები"), ("Save as Template", "შაბლონად შენახვა"), ("My Custom RGB", "ჩემი მორგებული RGB"), - ("Mercury GPU Acceleration (no GPU: software)", "Mercury GPU აჩქარება (GPU არ არის: პროგრამული)"), + ("GPU Acceleration (no GPU: software)", "GPU აჩქარება (GPU არ არის: პროგრამული)"), ("Sort by {}, {}", "დალაგება: {}, {}"), ("Sort by {}", "დალაგება: {}"), ("Label of {}: {}", "{}-ის იარლიყი: {}"), @@ -9510,8 +9526,8 @@ const FRENCH: &[(&str, &str)] = &[ ("Import Photoshop File", "Importer un fichier Photoshop"), ("Interpret Unlabeled Alpha As", "Interpréter l’alpha non étiqueté comme"), ("Join the ArtCraft Discord", "Rejoindre le Discord ArtCraft"), - ("Mercury GPU Acceleration ({})", "Accélération GPU Mercury ({})"), - ("Mercury Software Only", "Mercury, logiciel uniquement"), + ("GPU Acceleration ({})", "Accélération GPU ({})"), + ("Software Rendering", "Rendu logiciel"), ("OCIO (built-in ACES config)", "OCIO (configuration ACES intégrée)"), ("Off (Final Quality)", "Désactivé (qualité finale)"), ("OpenGL (compatibility)", "OpenGL (compatibilité)"), @@ -9926,7 +9942,7 @@ const FRENCH: &[(&str, &str)] = &[ ("Solid Settings", "Paramètres du solide"), ("Save as Template", "Enregistrer en tant que modèle"), ("My Custom RGB", "Mon RVB personnalisé"), - ("Mercury GPU Acceleration (no GPU: software)", "Accélération GPU Mercury (aucun GPU : logiciel)"), + ("GPU Acceleration (no GPU: software)", "Accélération GPU (aucun GPU : logiciel)"), ("Sort by {}, {}", "Trier par {}, {}"), ("Sort by {}", "Trier par {}"), ("Label of {}: {}", "Étiquette de {} : {}"), @@ -10047,7 +10063,7 @@ const FRENCH: &[(&str, &str)] = &[ ("Script Console", "Console de scripts"), ("Essential Graphics", "Graphiques essentiels"), ("ScriptUI Panel", "Panneau ScriptUI"), - ("Lumetri Scopes", "Scopes Lumetri"), + ("Video Scopes", "Oscilloscopes vidéo"), ("Media Browser", "Explorateur de médias"), ("Content-Aware Fill", "Remplissage adapté au contenu"), ("Create Nulls From Paths", "Créer des objets nuls à partir de tracés"), @@ -10266,6 +10282,26 @@ const FRENCH: &[(&str, &str)] = &[ mod tests { use super::*; + /// Check rendered feature labels only. Format names, compatibility tokens and user text + /// are not subject to this guard, and a missing translation keeps its English fallback. + #[test] + fn renamed_panel_labels_do_not_reintroduce_vendor_names() { + for language in ["en", "ja", "zh-hans", "zh-hant", "fr", "sv", "ka", "cs", "de", "es", "pl", "pt-br", "ru", "uk"] { + set_current(language); + for source in ["GPU Acceleration ({})", "GPU Acceleration (no GPU: software)", "Software Rendering", "Video Scopes"] { + let rendered = tr(source); + assert!(!rendered.is_empty(), "{language}: {source}"); + let label = rendered.to_lowercase(); + for mark in ["lumetri", "mercury", "mocha", "keylight"] { + assert!(!label.contains(mark), "{language}: {source} -> {rendered}"); + } + } + // A real interchange label remains factual, not rewritten or rejected. + assert!(!tr("Export Adobe Premiere Pro Project").is_empty()); + } + set_current("en"); + } + /// The interface text that has moved into this catalog. Add a panel here when you convert it; /// `every_row_is_used_by_a_converted_panel` then looks for its rows' English sources in it. pub(super) const CONVERTED: &[(&str, &str)] = &[ diff --git a/crates/ui-egui/src/panels/forms.rs b/crates/ui-egui/src/panels/forms.rs index 635ef9a43..e83530fa4 100644 --- a/crates/ui-egui/src/panels/forms.rs +++ b/crates/ui-egui/src/panels/forms.rs @@ -338,11 +338,11 @@ fn interpret_footage_form(s: &effectcraft_engine::Session, proxy: bool, base: &m /// The Project Settings dialog's title and fields. fn project_settings_form(s: &effectcraft_engine::Session) -> (String, Vec) { let st = &s.project.settings; - // The catalogue row is `Mercury GPU Acceleration ({})`, so the value goes through + // The catalogue row is `GPU Acceleration ({})`, so the value goes through // tr_args: wrapping the assembled string in tr() would look up the backend's name. let gpu_label = match &s.accel { - Some(a) => tr_args("Mercury GPU Acceleration ({})", &[&a.name()]), - None => tr("Mercury GPU Acceleration (no GPU: software)").to_string(), + Some(a) => tr_args("GPU Acceleration ({})", &[&a.name()]), + None => tr("GPU Acceleration (no GPU: software)").to_string(), }; let depth = match st.bit_depth.label() { l if l.starts_with("16") => 1, @@ -436,7 +436,7 @@ fn project_settings_form(s: &effectcraft_engine::Session) -> (String, Vec Field::choice( "renderer", "Video rendering and effects", - &[(gpu_label.as_str(), json!("gpu")), ("Mercury Software Only", json!("software"))], + &[(gpu_label.as_str(), json!("gpu")), ("Software Rendering", json!("software"))], usize::from(!st.gpu_acceleration), ), ], @@ -1050,6 +1050,18 @@ pub fn show_view_options(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Toke mod tests { use super::*; + #[test] + fn renderer_form_keeps_descriptive_labels_and_stable_option_values() { + crate::i18n::set_current("en"); + let (_, fields) = project_settings_form(&effectcraft_engine::Session::default()); + let renderer = fields.iter().find(|field| field.key == "renderer").unwrap(); + let FieldKind::Choice { options, .. } = &renderer.kind else { panic!("renderer must be a choice") }; + assert_eq!( + options.as_slice(), + &[("GPU Acceleration (no GPU: software)".to_string(), json!("gpu")), ("Software Rendering".to_string(), json!("software"))] + ); + } + #[test] fn form_params_build_arrays_and_choices() { let f = Form { diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index 60a30e97e..612aa0a10 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -756,7 +756,7 @@ pub struct UiState { /// Flowchart panel options (layers, effects, solids, direction, root comp). #[serde(default)] pub flowchart: crate::panels::flowchart::FlowOptions, - /// Lumetri Scopes panel options. + /// Video Scopes panel options. #[serde(default)] pub scopes: crate::panels::scopes_panel::ScopesState, } diff --git a/crates/ui-egui/tests/it/ui_angles.rs b/crates/ui-egui/tests/it/ui_angles.rs index 007e11286..40ad17bc1 100644 --- a/crates/ui-egui/tests/it/ui_angles.rs +++ b/crates/ui-egui/tests/it/ui_angles.rs @@ -38,7 +38,7 @@ fn harness(panel: Panel, angle: f64) -> (Harness<'static, EffectcraftApp>, u64, s.execute("comp.new", json!({"name": "Angles", "width": 320, "height": 180, "duration": 4})).unwrap(); let layer = s.execute("layer.newSolid", json!({"name": "Plate", "color": "#40b080"})).unwrap()["layer"].as_u64().unwrap(); let uid = if matches!(panel, Panel::EffectControls) { - let fx = s.execute("effect.apply", json!({"layer": layer, "effect": "CC Bend It"})).unwrap()["effects"][0].as_u64().unwrap(); + let fx = s.execute("effect.apply", json!({"layer": layer, "effect": "Two-Point Bend"})).unwrap()["effects"][0].as_u64().unwrap(); s.active_comp().unwrap().layer(LayerId(layer)).unwrap().props.find_group(fx).unwrap().get("bend").unwrap().uid } else { s.active_comp().unwrap().layer(LayerId(layer)).unwrap().props.prop("transform/rotation").unwrap().uid diff --git a/crates/ui-egui/tests/it/ui_color_hex.rs b/crates/ui-egui/tests/it/ui_color_hex.rs index 4f8a8ab14..8a5a39b8e 100644 --- a/crates/ui-egui/tests/it/ui_color_hex.rs +++ b/crates/ui-egui/tests/it/ui_color_hex.rs @@ -1,4 +1,4 @@ -//! The colour picker every colour input opens: its hex field sets an effect's colour (CC Toner), +//! The colour picker every colour input opens: its hex field sets an effect's colour (Multi-Tone Tint), //! the dialogs' colour button stores sRGB (egui's own `color_edit_button_rgba` read the stored //! sRGB values as linear, so mid-grey `#808080` was stored as `#373737`), the square, slider and //! HSB / RGB fields set the colour, and Cancel / Escape / the original swatch restore it. @@ -45,7 +45,7 @@ fn hex_code_sets_an_effect_colour() { let mut s = Session::default(); s.execute("comp.new", json!({"name": "Hex", "width": 320, "height": 180, "duration": 4})).unwrap(); let layer = s.execute("layer.newSolid", json!({"name": "Plate", "color": "#40b080"})).unwrap()["layer"].as_u64().unwrap(); - let fx = s.execute("effect.apply", json!({"layer": layer, "effect": "CC Toner"})).unwrap()["effects"][0].as_u64().unwrap(); + let fx = s.execute("effect.apply", json!({"layer": layer, "effect": "Multi-Tone Tint"})).unwrap()["effects"][0].as_u64().unwrap(); let uid = s.active_comp().unwrap().layer(LayerId(layer)).unwrap().props.find_group(fx).unwrap().get("midtones").unwrap().uid; s.state.selected_layers = vec![LayerId(layer)]; let mut app = EffectcraftApp::new(s); diff --git a/crates/ui-egui/tests/it/ui_effect_controls.rs b/crates/ui-egui/tests/it/ui_effect_controls.rs index 1db059220..15802d889 100644 --- a/crates/ui-egui/tests/it/ui_effect_controls.rs +++ b/crates/ui-egui/tests/it/ui_effect_controls.rs @@ -260,14 +260,14 @@ fn the_eyedropper_stays_armed_until_the_viewer_has_a_frame() { } } -/// Key Light's Screen Colour eyedropper picks from the effect's input: the shown frame is +/// Screen Color Key's Screen Colour eyedropper picks from the effect's input: the shown frame is /// already keyed (the default screen colour takes most of a green), so sampling it would miss. #[test] fn keyer_eyedropper_picks_the_screen_from_the_effect_input() { let mut s = Session::default(); s.execute("comp.new", json!({"name": "Key", "width": 320, "height": 180, "frameRate": 30, "duration": 1})).unwrap(); let screen = s.execute("layer.newSolid", json!({"name": "Screen", "color": "#30b050"})).unwrap()["layer"].as_u64().unwrap(); - let fx = s.execute("effect.apply", json!({"layers": [screen], "effect": "Key Light"})).unwrap()["effects"][0].as_u64().unwrap(); + let fx = s.execute("effect.apply", json!({"layers": [screen], "effect": "Screen Color Key"})).unwrap()["effects"][0].as_u64().unwrap(); let prop = layer(&s, screen).effects().unwrap().groups().find(|g| g.uid == fx).and_then(|g| g.get("screenColour")).map(|p| p.uid).unwrap(); s.state.selected_layers = vec![LayerId(screen)]; let mut app = EffectcraftApp::new(s); @@ -295,7 +295,7 @@ fn keyer_eyedropper_picks_the_screen_from_the_effect_input() { fn command_clicking_an_effect_twirl_closes_and_opens_its_groups() { let (app, x) = app(); let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| app); - let fx = h.state_mut().session.execute("effect.apply", json!({"layers": [x.small], "effect": "Lumetri Color"})).unwrap()["effects"][0].as_u64().unwrap(); + let fx = h.state_mut().session.execute("effect.apply", json!({"layers": [x.small], "effect": "Color Grading"})).unwrap()["effects"][0].as_u64().unwrap(); // Only this effect open, so its header is on screen. let l = layer(&h.state().session, x.small); let others: Vec = l.effects().unwrap().groups().map(|g| g.uid).filter(|u| *u != fx).collect(); diff --git a/crates/ui-egui/tests/it/ui_effect_drop_highlight.rs b/crates/ui-egui/tests/it/ui_effect_drop_highlight.rs index 6c0ede47a..ddef6c94b 100644 --- a/crates/ui-egui/tests/it/ui_effect_drop_highlight.rs +++ b/crates/ui-egui/tests/it/ui_effect_drop_highlight.rs @@ -13,7 +13,7 @@ fn rect(h: &Harness<'_, EffectcraftApp>, id: &str) -> Rect { fn drag_to(h: &mut Harness<'_, EffectcraftApp>, p: Pos2) { h.input_mut().events.push(Event::PointerMoved(p)); h.input_mut().events.push(Event::PointerButton { pos: p, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE }); - egui::DragAndDrop::set_payload(&h.ctx, DragPayload::Effect("CC Bend It".into())); + egui::DragAndDrop::set_payload(&h.ctx, DragPayload::Effect("Two-Point Bend".into())); h.run_steps(2); } diff --git a/crates/ui-egui/tests/it/ui_fx_editors.rs b/crates/ui-egui/tests/it/ui_fx_editors.rs index b9530fdf4..b4d999311 100644 --- a/crates/ui-egui/tests/it/ui_fx_editors.rs +++ b/crates/ui-egui/tests/it/ui_fx_editors.rs @@ -130,7 +130,7 @@ fn undo(h: &mut Harness<'_, EffectcraftApp>) { #[test] fn lumetri_rgb_and_hue_saturation_curve_editors() { - let (s0, l, fx) = with_effect("Lumetri Color"); + let (s0, l, fx) = with_effect("Color Grading"); let mut h = harness(s0); only_groups(&mut h, l, fx, &["curves", "rgbCurves", "hueSaturationCurves"]); let pre = format!("effectControls.effect.{fx}.lumetri.rgbCurves"); diff --git a/crates/ui-egui/tests/it/ui_live_preview.rs b/crates/ui-egui/tests/it/ui_live_preview.rs index e72856bfc..abe3cc427 100644 --- a/crates/ui-egui/tests/it/ui_live_preview.rs +++ b/crates/ui-egui/tests/it/ui_live_preview.rs @@ -77,12 +77,12 @@ fn position_anchor_and_effect_edits_present_intermediate_pixels() { (None, "transform/position", json!([22, 18, 0]), json!([18, 18, 0])), (None, "transform/anchor", json!([9, 8, 0]), json!([6, 8, 0])), (Some("Gaussian Blur"), "blurriness", json!(3), json!(6)), - (Some("CC Radial Fast Blur"), "center", json!([4, 4]), json!([18, 12])), + (Some("Fast Radial Blur"), "center", json!([4, 4]), json!([18, 12])), ] { let mut session = Session::default(); session.execute("comp.new", json!({"width":64,"height":36,"duration":1})).unwrap(); let layer = session.execute("layer.newSolid", json!({"color":"#ffffff","width":24,"height":16})).unwrap()["layer"].as_u64().unwrap(); - if effect == Some("CC Radial Fast Blur") { + if effect == Some("Fast Radial Blur") { // A uniform solid is invariant under a radial blur; give the point edit visible detail. session.execute("effect.apply", json!({"layers":[layer],"effect":"Gradient Ramp"})).unwrap(); } diff --git a/crates/ui-egui/tests/it/ui_parameter_spacing.rs b/crates/ui-egui/tests/it/ui_parameter_spacing.rs index 71b70a14e..167029f7c 100644 --- a/crates/ui-egui/tests/it/ui_parameter_spacing.rs +++ b/crates/ui-egui/tests/it/ui_parameter_spacing.rs @@ -54,7 +54,7 @@ fn nested_groups_keep_indentation_value_alignment_and_effect_gaps_at_fixed_compa let mut s = Session::default(); s.execute("comp.new", json!({"width":64,"height":36,"duration":1})).unwrap(); s.execute("layer.newSolid", json!({"name":"Plate","color":"#406080"})).unwrap(); - let fx = s.execute("effect.apply", json!({"effect":"Lumetri Color"})).unwrap()["effects"][0].as_u64().unwrap(); + let fx = s.execute("effect.apply", json!({"effect":"Color Grading"})).unwrap()["effects"][0].as_u64().unwrap(); let next = s.execute("effect.apply", json!({"effect":"Checkbox Control"})).unwrap()["effects"][0].as_u64().unwrap(); let g = s.active_comp().unwrap().layers[0].props.find_group(fx).unwrap(); let basic = g.sub("basicCorrection").unwrap(); diff --git a/crates/ui-egui/tests/it/ui_viewer.rs b/crates/ui-egui/tests/it/ui_viewer.rs index 7b32fa2ec..e01608e16 100644 --- a/crates/ui-egui/tests/it/ui_viewer.rs +++ b/crates/ui-egui/tests/it/ui_viewer.rs @@ -2370,7 +2370,7 @@ fn command_clicking_a_group_twirl_opens_its_whole_tree() { // twirls open with it, and closed again with it. let mut h = harness(); let id = layer_id(&h, "Box"); - h.state_mut().session.execute("effect.apply", json!({"effect": "Lumetri Color", "layers": [id]})).unwrap(); + h.state_mut().session.execute("effect.apply", json!({"effect": "Color Grading", "layers": [id]})).unwrap(); fn walk(g: &effectcraft_engine::project::PropGroup, out: &mut Vec) { for sg in g.groups() { out.push(sg.uid); diff --git a/docs/architecture.md b/docs/architecture.md index 83c03f14f..a04907aec 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -121,7 +121,7 @@ are not rewritten, so an expression that computes an effect point on a shape or other values may need `+ [thisComp.width / 2, thisComp.height / 2]`. Effects that use the layer bounds (edge pinning, wipes, Offset, generators) now cover the whole comp-sized bounds instead of their bottom-right quarter. Tracking data of effects that analyse footage (Warp Stabilizer, 3D -Camera Tracker, Mocha shape) is left as it is: footage has a source rectangle, whose effect space +Camera Tracker, Tracked Shape Matte) is left as it is: footage has a source rectangle, whose effect space did not change. ## 4. Rendering @@ -331,7 +331,7 @@ before conversion (a quarter of the conversions; the result differs from convert in blocks where colours clip) and resampled from there, as other frames are from full size. **GPU compositor** (`crates/gpu`; Project Settings ▸ Video Rendering and Effects ▸ Mercury GPU -Acceleration, the default, or Mercury Software Only; `render.backend`). The CPU renderer is the +Acceleration, the default, or Software Rendering; `render.backend`). The CPU renderer is the reference and keeps rendering layer content (sources, masks, CPU effects, layer styles) into the layer cache. The render crate defines an `Accelerator` trait; `effectcraft_gpu::Gpu` implements it on wgpu compute shaders. `RenderOpts::backend` picks `Cpu`, `Gpu` or `Auto` (an accelerator @@ -341,7 +341,7 @@ per comp, output scale and path (readback or viewer display) it averages CPU and (warm-up on both sides, the fastest warm-up frame starting the average, single stalls capped), picks the GPU unless the CPU is clearly faster, and re-measures the other side every 48 frames. Render Queue jobs don't use Auto: every frame of a job renders on the project's renderer (the -GPU compositor with Mercury GPU Acceleration), so runs of a render don't differ by timing. +GPU compositor with GPU Acceleration), so runs of a render don't differ by timing. Light comps (a few small layers, which the CPU composites only within their bounds) then stay on the CPU instead of paying for full-frame passes and a readback. A GPU frame walks the comp like `draw_comp`: cached layer buffers are uploaded once per buffer, then transformed with the CPU's @@ -370,16 +370,16 @@ Preserve Transparency / blend kernels (a 3D track matte renders solo through the Environment Light Background layers draw their sky in a kernel (`adv_sky`, from `Renderer::sky_draw`) before the rest of the run, in Classic and Advanced 3D comps. Wireframe outlines draw on the GPU from the CPU's pixel list (`Renderer::wireframe_pixels`). GPU effects -(`effects::GPU_EFFECTS`, 280 of them: blurs, colour correction, keying incl. Key Light, mattes, -channel, stylize, distortion and warps (Warp, Bezier Warp, Smear, Reshape, CC Bend It, CC Page -Turn, CC Bender, CC Blobbylize), Cartoon, bevels, shapes, the transitions (CC light family, CC -transitions, Block Dissolve), perspective (Radial Shadow, CC Cylinder / Sphere / Spotlight / +(`effects::GPU_EFFECTS`, 280 of them: blurs, colour correction, keying incl. Screen Color Key, mattes, +channel, stylize, distortion and warps (Warp, Bezier Warp, Smear, Reshape, Two-Point Bend, CC Page +Turn, Flexible Bend, Organic Displacement), Cartoon, bevels, shapes, the transitions (CC light family, CC +transitions, Block Dissolve), perspective (Radial Shadow, Cylinder Wrap / Sphere / Spotlight / Environment, 3D Glasses), generators, noise, grain, text (Numbers, Timecode), time, 3D Channel (aux channels as an extra texture), Immersive Video, colour management (colour programs from `effects::color_program`, LUTs in a storage buffer) and the simulations' render passes (the simulation stays on the CPU and hands its plan — sprites, pieces, blobs, grids — to a tiled -rasteriser; the particle effects too, `gpu::fx_particles`: CC Particle World / Systems II, -Particle Playground with its Layer Map frames in one atlas, CC Ball Action, CC Pixel Polly, CC +rasteriser; the particle effects too, `gpu::fx_particles`: Particle Emitter 3D / Systems II, +Particle Playground with its Layer Map frames in one atlas, Ball Grid, Pixel Shatter, CC Scatterize; plans too large for one item table draw in several passes over the previous one), and the geometry generators (Lightning, Advanced Lightning, Radio Waves, Stroke, Scribble, Vegas, Write-on, Audio Spectrum / Waveform, Basic / Path Text: bolts, strokes, waves, audio @@ -390,7 +390,7 @@ as kernels, in one module per family (`gpu::fx_*` with `shaders/fx_*.wgsl`); con effects run as one chain with one upload and one readback. Statistics that need the whole frame (Auto Levels / Contrast / Color, Equalize, Shadow/Highlight, Color Stabilizer, Remove Grain's noise level) are measured on the CPU from one readback and applied on the GPU; effects reading -other frames (Echo, Posterize Time, Time Difference, Time Displacement, CC Force Motion Blur, CC +other frames (Echo, Posterize Time, Time Difference, Time Displacement, Temporal Motion Blur, CC Wide Time, Pixel Motion Blur, Timewarp) upload the frames the host renders (`time_frames`) and combine them on the GPU; Time Displacement's per-pixel times and Timewarp's motion vectors (`time_fx::timewarp_plan`) are computed on the CPU. Numbers and Timecode rasterise their glyph @@ -433,12 +433,12 @@ sentinel is implemented here. Browser scope futures are not synchronously awaite forwards shared-device errors, while owned worker devices install their own handlers. On the web (WebGPU) the GPU composites viewer frames; steps that need a readback fall back to the CPU (the Info panel's pixel readout reads GPU frames back asynchronously). **GPU particles** -(`effects::psim`, `gpu::particles`): CC Particle World, CC Particle Systems II and Particle +(`effects::psim`, `gpu::particles`): Particle Emitter 3D, Particle Emitter 2D and Particle Playground cannons without interacting forces evolve every particle independently, so the GPU backend (reached through `EffectHost::particles` when the GPU compositor is active) simulates one particle per invocation from the CPU's birth schedule and keeps per-key state checkpoints on the GPU, like `SimCache`; the CPU simulation stays the oracle (tests compare ids and positions). -CC Particle World, CC Particle Systems II and CC Mr. Mercury sample their Birth Rate at every +Particle Emitter 3D, Particle Emitter 2D and Liquid Particles sample their Birth Rate at every step (`EffectHost::param_at`, #1373), so a keyed rate only decides the births of its own time; the rate is left out of the simulation key, and a checkpoint is resumed only with the same rate history (the CPU state's `hist`, the GPU checkpoint's births). The rate's keyframes are read at the step's exact layer time @@ -629,7 +629,7 @@ Unix milliseconds. The layer's Start Time becomes `dataStart + videoStart − fi (`videoStart` given, or the file's creation date on the desktop; reduced to a time of day when the data has no dates). -**Lumetri Scopes** (`raster::scopes`) are density plots of the viewer frame (rendered over the +**Video Scopes** (`raster::scopes`) are density plots of the viewer frame (rendered over the comp background) with BT.601 / BT.709 / BT.2020 luma and colour-difference coefficients. ## 5. Expressions diff --git a/docs/effects.md b/docs/effects.md index d9f726481..723edd0d0 100644 --- a/docs/effects.md +++ b/docs/effects.md @@ -48,21 +48,21 @@ Effects are addressed by id (`ec..`) or display name in commands | Effect | Id | Params | GPU | 32 | Status | |---|---|---:|:-:|:-:|---| | Bilateral Blur | `ec.blur.bilateral` | 3 | GPU | 32 | Implemented | -| CC Cross Blur | `ec.blur.cccross` | 3 | GPU | 32 | Implemented | -| CC Radial Blur | `ec.blur.ccradial` | 4 | GPU | 32 | Implemented | -| CC Radial Fast Blur | `ec.blur.ccradialfast` | 3 | GPU | 32 | Implemented | -| CC Vector Blur | `ec.blur.ccvector` | 4 | GPU | 32 | Implemented | | Camera Lens Blur | `ec.blur.cameralens` | 16 | GPU | 32 | Implemented | | Camera-Shake Deblur | `ec.blur.camerashakedeblur` | 8 | | 32 | Implemented | | Channel Blur | `ec.blur.channel` | 6 | GPU | 32 | Implemented | | Compound Blur | `ec.blur.compound` | 4 | GPU | 32 | Implemented | +| Cross Blur | `ec.blur.cccross` | 3 | GPU | 32 | Implemented | | Directional Blur | `ec.blur.directional` | 2 | GPU | 32 | Implemented | | Fast Box Blur | `ec.blur.fastbox` | 4 | GPU | 32 | Implemented | +| Fast Radial Blur | `ec.blur.ccradialfast` | 3 | GPU | 32 | Implemented | | Gaussian Blur | `ec.blur.gaussian` | 3 | GPU | 32 | Implemented | | Radial Blur | `ec.blur.radial` | 4 | GPU | 32 | Implemented | | Sharpen | `ec.blur.sharpen` | 1 | GPU | 32 | Implemented | | Smart Blur | `ec.blur.smart` | 3 | GPU | 32 | Implemented | | Unsharp Mask | `ec.blur.unsharp` | 3 | GPU | 32 | Implemented | +| Variable Radial Blur | `ec.blur.ccradial` | 4 | GPU | 32 | Implemented | +| Vector Blur | `ec.blur.ccvector` | 4 | GPU | 32 | Implemented | ## Channel @@ -70,7 +70,6 @@ Effects are addressed by id (`ec..`) or display name in commands |---|---|---:|:-:|:-:|---| | Arithmetic | `ec.channel.arithmetic` | 5 | GPU | 32 | Implemented | | Blend | `ec.channel.blend` | 4 | GPU | 32 | Implemented | -| CC Composite | `ec.channel.cccomposite` | 3 | GPU | 32 | Implemented | | Calculations | `ec.channel.calculations` | 9 | GPU | 32 | Implemented | | Channel Combiner | `ec.channel.combiner` | 6 | GPU | 32 | Implemented | | Compound Arithmetic | `ec.channel.compoundarithmetic` | 6 | GPU | 32 | Implemented | @@ -81,6 +80,7 @@ Effects are addressed by id (`ec..`) or display name in commands | Set Matte | `ec.channel.setmatte` | 6 | GPU | 32 | Implemented | | Shift Channels | `ec.channel.shiftchannels` | 4 | GPU | 32 | Implemented | | Solid Composite | `ec.channel.solidcomposite` | 4 | GPU | 32 | Implemented | +| Source Composite | `ec.channel.cccomposite` | 3 | GPU | 32 | Implemented | ## Color Correction @@ -92,18 +92,18 @@ Effects are addressed by id (`ec..`) or display name in commands | Black & White | `ec.color.blackwhite` | 8 | GPU | 32 | Implemented | | Brightness & Contrast | `ec.color.brightnesscontrast` | 3 | GPU | 32 | Implemented | | Broadcast Colors | `ec.color.broadcast` | 3 | GPU | 32 | Implemented | -| CC Color Neutralizer | `ec.color.cccolorneutralizer` | 11 | GPU | 32 | Implemented | -| CC Color Offset | `ec.color.cccoloroffset` | 4 | GPU | 32 | Implemented | -| CC Kernel | `ec.color.cckernel` | 12 | GPU | 32 | Implemented | -| CC Toner | `ec.color.cctoner` | 7 | GPU | 32 | Implemented | | Change Color | `ec.color.changecolor` | 9 | GPU | 32 | Implemented | | Change to Color | `ec.color.changetocolor` | 9 | GPU | 32 | Implemented | | Channel Mixer | `ec.color.channelmixer` | 13 | GPU | 32 | Implemented | +| Channel Offset | `ec.color.cccoloroffset` | 4 | GPU | 32 | Implemented | | Color Balance | `ec.color.colorbalance` | 10 | GPU | 32 | Implemented | | Color Balance (HLS) | `ec.color.colorbalancehls` | 3 | GPU | 32 | Implemented | +| Color Grading | `ec.color.lumetri` | 66 | GPU | 32 | Implemented | | Color Link | `ec.color.colorlink` | 6 | GPU | 32 | Implemented | +| Color Neutralizer | `ec.color.cccolorneutralizer` | 11 | GPU | 32 | Implemented | | Color Stabilizer | `ec.color.colorstabilizer` | 6 | GPU | 32 | Implemented | | Colorama | `ec.color.colorama` | 20 | GPU | 32 | Implemented | +| Convolution Kernel | `ec.color.cckernel` | 12 | GPU | 32 | Implemented | | Curves | `ec.color.curves` | 0 | GPU | 32 | Implemented | | Equalize | `ec.color.equalize` | 2 | GPU | 32 | Implemented | | Exposure | `ec.color.exposure` | 14 | GPU | 32 | Implemented | @@ -112,7 +112,7 @@ Effects are addressed by id (`ec..`) or display name in commands | Leave Color | `ec.color.leavecolor` | 5 | GPU | 32 | Implemented | | Levels | `ec.color.levels` | 36 | GPU | 32 | Implemented | | Levels (Individual Controls) | `ec.color.levelsic` | 27 | GPU | 32 | Implemented | -| Lumetri Color | `ec.color.lumetri` | 66 | GPU | 32 | Implemented | +| Multi-Tone Tint | `ec.color.cctoner` | 7 | GPU | 32 | Implemented | | OCIO CDL Transform | `ec.color.ociocdl` | 12 | GPU | 32 | Implemented | | OCIO Color Space Transform | `ec.color.ociocolorspace` | 7 | GPU | 32 | Implemented | | OCIO Display Transform | `ec.color.ociodisplay` | 9 | GPU | 32 | Implemented | @@ -133,40 +133,40 @@ Effects are addressed by id (`ec..`) or display name in commands |---|---|---:|:-:|:-:|---| | Bezier Warp | `ec.distort.bezierwarp` | 13 | GPU | 32 | Implemented | | Bulge | `ec.distort.bulge` | 6 | GPU | 32 | Implemented | -| CC Bend It | `ec.distort.ccbendit` | 4 | GPU | 32 | Implemented | -| CC Bender | `ec.distort.ccbender` | 5 | GPU | 32 | Implemented | -| CC Blobbylize | `ec.distort.ccblobbylize` | 15 | GPU | 32 | Implemented | -| CC Flo Motion | `ec.distort.ccflomotion` | 7 | GPU | 32 | Implemented | -| CC Griddler | `ec.distort.ccgriddler` | 5 | GPU | 32 | Implemented | -| CC Lens | `ec.distort.cclens` | 3 | GPU | 32 | Implemented | -| CC Page Turn | `ec.distort.ccpageturn` | 7 | GPU | 32 | Implemented | -| CC Power Pin | `ec.distort.ccpowerpin` | 10 | GPU | 32 | Implemented | -| CC Ripple Pulse | `ec.distort.ccripplepulse` | 5 | GPU | 32 | Implemented | -| CC Slant | `ec.distort.ccslant` | 4 | GPU | 32 | Implemented | -| CC Smear | `ec.distort.ccsmear` | 4 | GPU | 32 | Implemented | -| CC Split | `ec.distort.ccsplit` | 3 | GPU | 32 | Implemented | -| CC Split 2 | `ec.distort.ccsplit2` | 4 | GPU | 32 | Implemented | -| CC Tiler | `ec.distort.cctiler` | 3 | GPU | 32 | Implemented | | Corner Pin | `ec.distort.cornerpin` | 4 | GPU | 32 | Implemented | | Detail-preserving Upscale | `ec.distort.upscale` | 4 | | 32 | Implemented | +| Directional Smear | `ec.distort.ccsmear` | 4 | GPU | 32 | Implemented | | Displacement Map | `ec.distort.displacementmap` | 8 | GPU | 32 | Implemented | +| Dual Lens Warp | `ec.distort.ccflomotion` | 7 | GPU | 32 | Implemented | +| Dual Split Warp | `ec.distort.ccsplit2` | 4 | GPU | 32 | Implemented | +| Flexible Bend | `ec.distort.ccbender` | 5 | GPU | 32 | Implemented | +| Lens Distortion | `ec.distort.cclens` | 3 | GPU | 32 | Implemented | | Liquify | `ec.distort.liquify` | 13 | GPU | 32 | Implemented | | Magnify | `ec.distort.magnify` | 10 | GPU | 32 | Implemented | | Mesh Warp | `ec.distort.meshwarp` | 3 | GPU | 32 | Implemented | | Mirror | `ec.distort.mirror` | 2 | GPU | 32 | Implemented | | Offset | `ec.distort.offset` | 2 | GPU | 32 | Implemented | | Optics Compensation | `ec.distort.opticscompensation` | 6 | GPU | 32 | Implemented | +| Organic Displacement | `ec.distort.ccblobbylize` | 15 | GPU | 32 | Implemented | +| Page Curl | `ec.distort.ccpageturn` | 7 | GPU | 32 | Implemented | +| Perspective Pin | `ec.distort.ccpowerpin` | 10 | GPU | 32 | Implemented | | Polar Coordinates | `ec.distort.polar` | 2 | GPU | 32 | Implemented | +| Pulsed Ripple | `ec.distort.ccripplepulse` | 5 | GPU | 32 | Implemented | | Puppet | `ec.distort.puppet` | 1 | | 32 | Implemented | | Reshape | `ec.distort.reshape` | 6 | GPU | 32 | Implemented | | Ripple | `ec.distort.ripple` | 7 | GPU | 32 | Implemented | | Rolling Shutter Repair | `ec.distort.rollingshutterrepair` | 5 | | 32 | Implemented | +| Scaled Tiles | `ec.distort.cctiler` | 3 | GPU | 32 | Implemented | +| Slant | `ec.distort.ccslant` | 4 | GPU | 32 | Implemented | | Smear | `ec.distort.smear` | 8 | GPU | 32 | Implemented | | Spherize | `ec.distort.spherize` | 2 | GPU | 32 | Implemented | +| Split Warp | `ec.distort.ccsplit` | 3 | GPU | 32 | Implemented | +| Tile Grid | `ec.distort.ccgriddler` | 5 | GPU | 32 | Implemented | | Transform | `ec.distort.transform` | 12 | GPU | 32 | Implemented | | Turbulent Displace | `ec.distort.turbulentdisplace` | 11 | GPU | 32 | Implemented | | Twirl | `ec.distort.twirl` | 3 | GPU | 32 | Implemented | | Twirl (Legacy) | `ec.distort.twirllegacy` | 3 | GPU | 32 | Implemented | +| Two-Point Bend | `ec.distort.ccbendit` | 4 | GPU | 32 | Implemented | | Warp | `ec.distort.warp` | 5 | GPU | 32 | Implemented | | Warp Stabilizer | `ec.distort.warpstabilizer` | 20 | | 32 | Implemented | | Wave Warp | `ec.distort.wavewarp` | 7 | GPU | 32 | Implemented | @@ -194,11 +194,6 @@ Effects are addressed by id (`ec..`) or display name in commands | Audio Spectrum | `ec.generate.audiospectrum` | 23 | GPU | 32 | Implemented | | Audio Waveform | `ec.generate.audiowaveform` | 17 | GPU | 32 | Implemented | | Beam | `ec.generate.beam` | 11 | GPU | 32 | Implemented | -| CC Glue Gun | `ec.generate.ccgluegun` | 7 | GPU | 32 | Implemented | -| CC Light Burst 2.5 | `ec.generate.cclightburst` | 6 | GPU | 32 | Implemented | -| CC Light Rays | `ec.generate.cclightrays` | 8 | GPU | 32 | Implemented | -| CC Light Sweep | `ec.generate.cclightsweep` | 9 | GPU | 32 | Implemented | -| CC Threads | `ec.generate.ccthreads` | 8 | GPU | 32 | Implemented | | Cell Pattern | `ec.generate.cellpattern` | 14 | GPU | 32 | Implemented | | Checkerboard | `ec.generate.checkerboard` | 10 | GPU | 32 | Implemented | | Circle | `ec.generate.circle` | 11 | GPU | 32 | Implemented | @@ -209,11 +204,16 @@ Effects are addressed by id (`ec..`) or display name in commands | Gradient Ramp | `ec.generate.gradientramp` | 7 | GPU | 32 | Implemented | | Grid | `ec.generate.grid` | 12 | GPU | 32 | Implemented | | Lens Flare | `ec.generate.lensflare` | 4 | GPU | 32 | Implemented | +| Light Rays | `ec.generate.cclightrays` | 8 | GPU | 32 | Implemented | +| Light Sweep | `ec.generate.cclightsweep` | 9 | GPU | 32 | Implemented | +| Paint Beads | `ec.generate.ccgluegun` | 7 | GPU | 32 | Implemented | | Paint Bucket | `ec.generate.paintbucket` | 11 | GPU | 32 | Implemented | +| Radial Light Burst | `ec.generate.cclightburst` | 6 | GPU | 32 | Implemented | | Radio Waves | `ec.generate.radiowaves` | 31 | GPU | 32 | Implemented | | Scribble | `ec.generate.scribble` | 25 | GPU | 32 | Implemented | | Stroke | `ec.generate.stroke` | 11 | GPU | 32 | Implemented | | Vegas | `ec.generate.vegas` | 26 | GPU | 32 | Implemented | +| Woven Threads | `ec.generate.ccthreads` | 8 | GPU | 32 | Implemented | | Write-on | `ec.generate.writeon` | 10 | GPU | 32 | Implemented | ## Immersive Video @@ -238,15 +238,15 @@ Effects are addressed by id (`ec..`) or display name in commands | Effect | Id | Params | GPU | 32 | Status | |---|---|---:|:-:|:-:|---| | Advanced Spill Suppressor | `ec.key.advancedspill` | 8 | GPU | 32 | Implemented | -| CC Simple Wire Removal | `ec.key.ccsimplewireremoval` | 7 | GPU | 32 | Implemented | | Color Difference Key | `ec.key.colordifference` | 16 | GPU | 32 | Implemented | | Color Range | `ec.key.colorrange` | 8 | GPU | 32 | Implemented | | Difference Matte | `ec.key.differencematte` | 6 | GPU | 32 | Implemented | | Extract | `ec.key.extract` | 6 | GPU | 32 | Implemented | | Inner/Outer Key | `ec.key.innerouter` | 88 | GPU | 32 | Implemented | | Key Cleaner | `ec.key.keycleaner` | 4 | GPU | 32 | Implemented | -| Key Light | `ec.keying.keylight` | 56 | GPU | 32 | Implemented | +| Line Removal | `ec.key.ccsimplewireremoval` | 7 | GPU | 32 | Implemented | | Linear Color Key | `ec.key.linearcolor` | 6 | GPU | 32 | Implemented | +| Screen Color Key | `ec.keying.keylight` | 56 | GPU | 32 | Implemented | | Screen Key | `ec.key.screen` | 11 | GPU | 32 | Implemented | | Unmult | `ec.key.unmult` | 6 | GPU | 32 | Implemented | @@ -255,12 +255,12 @@ Effects are addressed by id (`ec..`) or display name in commands | Effect | Id | Params | GPU | 32 | Status | |---|---|---:|:-:|:-:|---| | Matte Choker | `ec.matte.mattechoker` | 7 | GPU | 32 | Implemented | -| Mocha shape | `ec.obsolete.mochashape` | 6 | | 32 | Implemented | | Object Matte | `ec.matte.objectmatte` | 13 | | 32 | Implemented | | Refine Hard Matte | `ec.matte.refinehard` | 16 | GPU | 32 | Implemented | | Refine Soft Matte | `ec.matte.refinesoft` | 18 | GPU | 32 | Implemented | | Roto Brush & Refine Edge | `ec.matte.rotobrush` | 25 | | 32 | Implemented | | Simple Choker | `ec.matte.simplechoker` | 2 | GPU | 32 | Implemented | +| Tracked Shape Matte | `ec.obsolete.mochashape` | 6 | | 32 | Implemented | ## Noise & Grain @@ -308,65 +308,65 @@ Effects are addressed by id (`ec..`) or display name in commands | 3D Glasses | `ec.perspective.3dglasses` | 8 | GPU | 32 | Implemented | | Bevel Alpha | `ec.perspective.bevelalpha` | 4 | GPU | 32 | Implemented | | Bevel Edges | `ec.perspective.beveledges` | 4 | GPU | 32 | Implemented | -| CC Cylinder | `ec.perspective.cccylinder` | 15 | GPU | 32 | Implemented | -| CC Environment | `ec.perspective.ccenvironment` | 4 | GPU | 32 | Implemented | -| CC Sphere | `ec.perspective.ccsphere` | 14 | GPU | 32 | Implemented | -| CC Spotlight | `ec.perspective.ccspotlight` | 8 | GPU | 32 | Implemented | +| Cylinder Wrap | `ec.perspective.cccylinder` | 15 | GPU | 32 | Implemented | | Drop Shadow | `ec.perspective.dropshadow` | 6 | GPU | 32 | Implemented | +| Environment Map | `ec.perspective.ccenvironment` | 4 | GPU | 32 | Implemented | | Radial Shadow | `ec.perspective.radialshadow` | 9 | GPU | 32 | Implemented | +| Sphere Wrap | `ec.perspective.ccsphere` | 14 | GPU | 32 | Implemented | +| Surface Spotlight | `ec.perspective.ccspotlight` | 8 | GPU | 32 | Implemented | ## Simulation | Effect | Id | Params | GPU | 32 | Status | |---|---|---:|:-:|:-:|---| -| CC Ball Action | `ec.sim.ccballaction` | 8 | GPU | 32 | Implemented | -| CC Bubbles | `ec.sim.ccbubbles` | 7 | GPU | 32 | Implemented | -| CC Drizzle | `ec.sim.ccdrizzle` | 14 | GPU | 32 | Implemented | -| CC Hair | `ec.sim.cchair` | 20 | GPU | 32 | Implemented | -| CC Mr. Mercury | `ec.sim.ccmrmercury` | 23 | GPU | 32 | Implemented | -| CC Particle Systems II | `ec.sim.ccparticlesystems2` | 21 | GPU | 32 | Implemented | -| CC Particle World | `ec.sim.ccparticleworld` | 36 | GPU | 32 | Implemented | -| CC Pixel Polly | `ec.sim.ccpixelpolly` | 10 | GPU | 32 | Implemented | -| CC Rainfall | `ec.sim.ccrainfall` | 14 | GPU | 32 | Implemented | -| CC Scatterize | `ec.sim.ccscatterize` | 4 | GPU | 32 | Implemented | -| CC Snowfall | `ec.sim.ccsnowfall` | 19 | GPU | 32 | Implemented | -| CC Star Burst | `ec.sim.ccstarburst` | 6 | GPU | 32 | Implemented | +| Ball Grid | `ec.sim.ccballaction` | 8 | GPU | 32 | Implemented | +| Bubbles | `ec.sim.ccbubbles` | 7 | GPU | 32 | Implemented | | Card Dance | `ec.sim.carddance` | 53 | GPU | 32 | Implemented | | Caustics | `ec.sim.caustics` | 28 | GPU | 32 | Implemented | | Foam | `ec.sim.foam` | 36 | GPU | 32 | Implemented | +| Liquid Particles | `ec.sim.ccmrmercury` | 23 | GPU | 32 | Implemented | +| Particle Emitter 2D | `ec.sim.ccparticlesystems2` | 21 | GPU | 32 | Implemented | +| Particle Emitter 3D | `ec.sim.ccparticleworld` | 36 | GPU | 32 | Implemented | | Particle Playground | `ec.sim.particleplayground` | 92 | GPU | 32 | Implemented | +| Pixel Scatter | `ec.sim.ccscatterize` | 4 | GPU | 32 | Implemented | +| Pixel Shatter | `ec.sim.ccpixelpolly` | 10 | GPU | 32 | Implemented | +| Rainfall | `ec.sim.ccrainfall` | 14 | GPU | 32 | Implemented | | Shatter | `ec.sim.shatter` | 60 | GPU | 32 | Implemented | +| Snowfall | `ec.sim.ccsnowfall` | 19 | GPU | 32 | Implemented | +| Starfield | `ec.sim.ccstarburst` | 6 | GPU | 32 | Implemented | +| Surface Fibers | `ec.sim.cchair` | 20 | GPU | 32 | Implemented | +| Water Ripples | `ec.sim.ccdrizzle` | 14 | GPU | 32 | Implemented | | Wave World | `ec.sim.waveworld` | 35 | GPU | 32 | Implemented | ## Stylize | Effect | Id | Params | GPU | 32 | Status | |---|---|---:|:-:|:-:|---| +| Block Reveal | `ec.stylize.ccblockload` | 5 | GPU | 32 | Implemented | | Brush Strokes | `ec.stylize.brushstrokes` | 7 | GPU | 32 | Implemented | -| CC Block Load | `ec.stylize.ccblockload` | 5 | GPU | 32 | Implemented | -| CC Burn Film | `ec.stylize.ccburnfilm` | 3 | GPU | 32 | Implemented | -| CC Glass | `ec.stylize.ccglass` | 17 | GPU | 32 | Implemented | -| CC HexTile | `ec.stylize.cchextile` | 5 | GPU | 32 | Implemented | -| CC Kaleida | `ec.stylize.cckaleida` | 4 | GPU | 32 | Implemented | -| CC Mr. Smoothie | `ec.stylize.ccmrsmoothie` | 7 | GPU | 32 | Implemented | -| CC Plastic | `ec.stylize.ccplastic` | 18 | GPU | 32 | Implemented | -| CC RepeTile | `ec.stylize.ccrepetile` | 5 | GPU | 32 | Implemented | -| CC Threshold | `ec.stylize.ccthreshold` | 4 | GPU | 32 | Implemented | -| CC Threshold RGB | `ec.stylize.ccthresholdrgb` | 7 | GPU | 32 | Implemented | -| CC Vignette | `ec.stylize.ccvignette` | 4 | GPU | 32 | Implemented | | Cartoon | `ec.stylize.cartoon` | 12 | GPU | 32 | Implemented | +| Channel Threshold | `ec.stylize.ccthreshold` | 4 | GPU | 32 | Implemented | | Color Emboss | `ec.stylize.coloremboss` | 4 | GPU | 32 | Implemented | +| Edge Tile Repeat | `ec.stylize.ccrepetile` | 5 | GPU | 32 | Implemented | | Emboss | `ec.stylize.emboss` | 4 | GPU | 32 | Implemented | +| Film Burn | `ec.stylize.ccburnfilm` | 3 | GPU | 32 | Implemented | | Find Edges | `ec.stylize.findedges` | 2 | GPU | 32 | Implemented | +| Glass Relief | `ec.stylize.ccglass` | 17 | GPU | 32 | Implemented | | Glow | `ec.stylize.glow` | 14 | GPU | 32 | Implemented | +| Hexagonal Tiles | `ec.stylize.cchextile` | 5 | GPU | 32 | Implemented | +| Kaleidoscope | `ec.stylize.cckaleida` | 4 | GPU | 32 | Implemented | | Mosaic | `ec.stylize.mosaic` | 3 | GPU | 32 | Implemented | | Motion Tile | `ec.stylize.motiontile` | 8 | GPU | 32 | Implemented | +| Palette Map | `ec.stylize.ccmrsmoothie` | 7 | GPU | 32 | Implemented | +| Plastic Shading | `ec.stylize.ccplastic` | 18 | GPU | 32 | Implemented | | Posterize | `ec.stylize.posterize` | 1 | GPU | 32 | Implemented | +| RGB Threshold | `ec.stylize.ccthresholdrgb` | 7 | GPU | 32 | Implemented | | Roughen Edges | `ec.stylize.roughenedges` | 13 | GPU | 32 | Implemented | | Scatter | `ec.stylize.scatter` | 3 | GPU | 32 | Implemented | | Strobe Light | `ec.stylize.strobe` | 7 | GPU | 32 | Implemented | | Texturize | `ec.stylize.texturize` | 4 | GPU | 32 | Implemented | | Threshold | `ec.stylize.threshold` | 1 | GPU | 32 | Implemented | +| Vignette | `ec.stylize.ccvignette` | 4 | GPU | 32 | Implemented | ## Text @@ -379,11 +379,11 @@ Effects are addressed by id (`ec..`) or display name in commands | Effect | Id | Params | GPU | 32 | Status | |---|---|---:|:-:|:-:|---| -| CC Force Motion Blur | `ec.time.ccforcemotionblur` | 4 | GPU | 32 | Implemented | -| CC Wide Time | `ec.time.ccwidetime` | 3 | GPU | 32 | Implemented | | Echo | `ec.time.echo` | 5 | GPU | 32 | Implemented | | Pixel Motion Blur | `ec.time.pixelmotionblur` | 4 | GPU | 32 | Implemented | | Posterize Time | `ec.time.posterizetime` | 1 | GPU | 32 | Implemented | +| Temporal Blend | `ec.time.ccwidetime` | 3 | GPU | 32 | Implemented | +| Temporal Motion Blur | `ec.time.ccforcemotionblur` | 4 | GPU | 32 | Implemented | | Time Difference | `ec.time.timedifference` | 5 | GPU | 32 | Implemented | | Time Displacement | `ec.time.timedisplacement` | 4 | GPU | 32 | Implemented | | Timewarp | `ec.time.timewarp` | 28 | GPU | 32 | Implemented | @@ -393,29 +393,28 @@ Effects are addressed by id (`ec..`) or display name in commands | Effect | Id | Params | GPU | 32 | Status | |---|---|---:|:-:|:-:|---| | Block Dissolve | `ec.transition.blockdissolve` | 5 | GPU | 32 | Implemented | -| CC Glass Wipe | `ec.transition.ccglasswipe` | 5 | GPU | 32 | Implemented | -| CC Grid Wipe | `ec.transition.ccgridwipe` | 7 | GPU | 32 | Implemented | -| CC Image Wipe | `ec.transition.ccimagewipe` | 7 | GPU | 32 | Implemented | -| CC Jaws | `ec.transition.ccjaws` | 6 | GPU | 32 | Implemented | -| CC Light Wipe | `ec.transition.cclightwipe` | 8 | GPU | 32 | Implemented | -| CC Line Sweep | `ec.transition.cclinesweep` | 5 | GPU | 32 | Implemented | -| CC Radial ScaleWipe | `ec.transition.ccradialscalewipe` | 3 | GPU | 32 | Implemented | -| CC Scale Wipe | `ec.transition.ccscalewipe` | 3 | GPU | 32 | Implemented | -| CC Twister | `ec.transition.cctwister` | 5 | GPU | 32 | Implemented | -| CC WarpoMatic | `ec.transition.ccwarpomatic` | 8 | GPU | 32 | Implemented | | Card Wipe | `ec.transition.cardwipe` | 44 | GPU | 32 | Implemented | +| Glass Wipe | `ec.transition.ccglasswipe` | 5 | GPU | 32 | Implemented | | Gradient Wipe | `ec.transition.gradientwipe` | 5 | GPU | 32 | Implemented | +| Grid Wipe | `ec.transition.ccgridwipe` | 7 | GPU | 32 | Implemented | | Iris Wipe | `ec.transition.iriswipe` | 7 | GPU | 32 | Implemented | +| Light Wipe | `ec.transition.cclightwipe` | 8 | GPU | 32 | Implemented | +| Line Sweep | `ec.transition.cclinesweep` | 5 | GPU | 32 | Implemented | | Linear Wipe | `ec.transition.linearwipe` | 3 | GPU | 32 | Implemented | +| Luminance Image Wipe | `ec.transition.ccimagewipe` | 7 | GPU | 32 | Implemented | +| Radial Scale Wipe | `ec.transition.ccradialscalewipe` | 3 | GPU | 32 | Implemented | | Radial Wipe | `ec.transition.radialwipe` | 5 | GPU | 32 | Implemented | +| Scale Wipe | `ec.transition.ccscalewipe` | 3 | GPU | 32 | Implemented | +| Serrated Wipe | `ec.transition.ccjaws` | 6 | GPU | 32 | Implemented | +| Twist Wipe | `ec.transition.cctwister` | 5 | GPU | 32 | Implemented | | Venetian Blinds | `ec.transition.venetian` | 4 | GPU | 32 | Implemented | +| Warp Dissolve | `ec.transition.ccwarpomatic` | 8 | GPU | 32 | Implemented | ## Utility | Effect | Id | Params | GPU | 32 | Status | |---|---|---:|:-:|:-:|---| | Apply Color LUT | `ec.utility.applylut` | 1 | GPU | 32 | Implemented | -| CC Overbrights | `ec.utility.ccoverbrights` | 2 | GPU | 32 | Implemented | | Cineon Converter | `ec.utility.cineon` | 7 | GPU | 32 | Implemented | | Color Profile Converter | `ec.utility.colorprofileconverter` | 7 | GPU | 32 | Implemented | | Face Measurements | `ec.utility.facemeasurements` | 14 | | 32 | Implemented | @@ -423,4 +422,5 @@ Effects are addressed by id (`ec..`) or display name in commands | Grow Bounds | `ec.utility.growbounds` | 1 | GPU | 32 | Implemented | | HDR Compander | `ec.utility.hdrcompander` | 3 | GPU | 32 | Implemented | | HDR Highlight Compression | `ec.utility.hdrcompression` | 1 | GPU | 32 | Implemented | +| Out-of-Range Display | `ec.utility.ccoverbrights` | 2 | GPU | 32 | Implemented | diff --git a/docs/feature-naming.md b/docs/feature-naming.md new file mode 100644 index 000000000..a69bc713f --- /dev/null +++ b/docs/feature-naming.md @@ -0,0 +1,108 @@ +# Descriptive feature names + +Related to #1365. This is a naming cleanup, not a trademark clearance or a product rename. + +## Naming policy + +Use short, descriptive labels for EffectCraft implementations. Do not reuse a vendor prefix or +coined feature name as our branding. Qualify similar effects by what distinguishes them; each +English display name remains unique across the registry. + +- Renderer: **GPU Acceleration** / **Software Rendering**. +- Panel: **Video Scopes**. +- The table below covers the 69 former CC effects plus color grading, the shape matte and keying. + +## Compatibility contract + +Only presentation names change. Effect IDs, command IDs, parameter IDs, enum values, panel IDs, +serialized types and AEP import/export match names stay unchanged. Old effect display names +remain case-insensitive lookup and search aliases. Existing project instance names, custom names, +expressions, animation and values are not rewritten. The UI displays a current label for an exact +legacy default name, optionally followed by its numeric instance suffix; all other names stay +verbatim. A user-entered name identical to a legacy default cannot be distinguished from it, but +its saved value is still retained. + +External format specifications, comparison documents, attribution records and compatibility +fixtures may still name the external product or effect they actually describe. + +## Display mapping + +| Stable ID | Previous display name | Current display name | +|---|---|---| +| `ec.sim.ccballaction` | CC Ball Action | Ball Grid | +| `ec.distort.ccbendit` | CC Bend It | Two-Point Bend | +| `ec.distort.ccbender` | CC Bender | Flexible Bend | +| `ec.distort.ccblobbylize` | CC Blobbylize | Organic Displacement | +| `ec.stylize.ccblockload` | CC Block Load | Block Reveal | +| `ec.sim.ccbubbles` | CC Bubbles | Bubbles | +| `ec.stylize.ccburnfilm` | CC Burn Film | Film Burn | +| `ec.color.cccolorneutralizer` | CC Color Neutralizer | Color Neutralizer | +| `ec.color.cccoloroffset` | CC Color Offset | Channel Offset | +| `ec.channel.cccomposite` | CC Composite | Source Composite | +| `ec.blur.cccross` | CC Cross Blur | Cross Blur | +| `ec.perspective.cccylinder` | CC Cylinder | Cylinder Wrap | +| `ec.sim.ccdrizzle` | CC Drizzle | Water Ripples | +| `ec.perspective.ccenvironment` | CC Environment | Environment Map | +| `ec.distort.ccflomotion` | CC Flo Motion | Dual Lens Warp | +| `ec.time.ccforcemotionblur` | CC Force Motion Blur | Temporal Motion Blur | +| `ec.stylize.ccglass` | CC Glass | Glass Relief | +| `ec.transition.ccglasswipe` | CC Glass Wipe | Glass Wipe | +| `ec.generate.ccgluegun` | CC Glue Gun | Paint Beads | +| `ec.transition.ccgridwipe` | CC Grid Wipe | Grid Wipe | +| `ec.distort.ccgriddler` | CC Griddler | Tile Grid | +| `ec.sim.cchair` | CC Hair | Surface Fibers | +| `ec.stylize.cchextile` | CC HexTile | Hexagonal Tiles | +| `ec.transition.ccimagewipe` | CC Image Wipe | Luminance Image Wipe | +| `ec.transition.ccjaws` | CC Jaws | Serrated Wipe | +| `ec.stylize.cckaleida` | CC Kaleida | Kaleidoscope | +| `ec.color.cckernel` | CC Kernel | Convolution Kernel | +| `ec.distort.cclens` | CC Lens | Lens Distortion | +| `ec.generate.cclightburst` | CC Light Burst 2.5 | Radial Light Burst | +| `ec.generate.cclightrays` | CC Light Rays | Light Rays | +| `ec.generate.cclightsweep` | CC Light Sweep | Light Sweep | +| `ec.transition.cclightwipe` | CC Light Wipe | Light Wipe | +| `ec.transition.cclinesweep` | CC Line Sweep | Line Sweep | +| `ec.sim.ccmrmercury` | CC Mr. Mercury | Liquid Particles | +| `ec.stylize.ccmrsmoothie` | CC Mr. Smoothie | Palette Map | +| `ec.utility.ccoverbrights` | CC Overbrights | Out-of-Range Display | +| `ec.distort.ccpageturn` | CC Page Turn | Page Curl | +| `ec.sim.ccparticlesystems2` | CC Particle Systems II | Particle Emitter 2D | +| `ec.sim.ccparticleworld` | CC Particle World | Particle Emitter 3D | +| `ec.sim.ccpixelpolly` | CC Pixel Polly | Pixel Shatter | +| `ec.stylize.ccplastic` | CC Plastic | Plastic Shading | +| `ec.distort.ccpowerpin` | CC Power Pin | Perspective Pin | +| `ec.blur.ccradial` | CC Radial Blur | Variable Radial Blur | +| `ec.blur.ccradialfast` | CC Radial Fast Blur | Fast Radial Blur | +| `ec.transition.ccradialscalewipe` | CC Radial ScaleWipe | Radial Scale Wipe | +| `ec.sim.ccrainfall` | CC Rainfall | Rainfall | +| `ec.stylize.ccrepetile` | CC RepeTile | Edge Tile Repeat | +| `ec.distort.ccripplepulse` | CC Ripple Pulse | Pulsed Ripple | +| `ec.transition.ccscalewipe` | CC Scale Wipe | Scale Wipe | +| `ec.sim.ccscatterize` | CC Scatterize | Pixel Scatter | +| `ec.key.ccsimplewireremoval` | CC Simple Wire Removal | Line Removal | +| `ec.distort.ccslant` | CC Slant | Slant | +| `ec.distort.ccsmear` | CC Smear | Directional Smear | +| `ec.sim.ccsnowfall` | CC Snowfall | Snowfall | +| `ec.perspective.ccsphere` | CC Sphere | Sphere Wrap | +| `ec.distort.ccsplit` | CC Split | Split Warp | +| `ec.distort.ccsplit2` | CC Split 2 | Dual Split Warp | +| `ec.perspective.ccspotlight` | CC Spotlight | Surface Spotlight | +| `ec.sim.ccstarburst` | CC Star Burst | Starfield | +| `ec.generate.ccthreads` | CC Threads | Woven Threads | +| `ec.stylize.ccthreshold` | CC Threshold | Channel Threshold | +| `ec.stylize.ccthresholdrgb` | CC Threshold RGB | RGB Threshold | +| `ec.distort.cctiler` | CC Tiler | Scaled Tiles | +| `ec.color.cctoner` | CC Toner | Multi-Tone Tint | +| `ec.transition.cctwister` | CC Twister | Twist Wipe | +| `ec.blur.ccvector` | CC Vector Blur | Vector Blur | +| `ec.stylize.ccvignette` | CC Vignette | Vignette | +| `ec.transition.ccwarpomatic` | CC WarpoMatic | Warp Dissolve | +| `ec.time.ccwidetime` | CC Wide Time | Temporal Blend | +| `ec.color.lumetri` | Lumetri Color | Color Grading | +| `ec.obsolete.mochashape` | Mocha shape | Tracked Shape Matte | +| `ec.keying.keylight` | Key Light | Screen Color Key | + +`Roto Brush & Refine Edge` and `Warp Stabilizer` keep their current names; renaming them is +deferred. + +No effect algorithms or formats are changed. diff --git a/docs/web.md b/docs/web.md index 666ffe37b..f94a687be 100644 --- a/docs/web.md +++ b/docs/web.md @@ -174,7 +174,7 @@ the same futures with `passes::block_on`; they never wait there). - **Render Queue**: `effectcraft_export::export_async` renders each frame in passes (frames with deferred readbacks render one by one instead of in parallel batches) and encodes as before (`Exporter::export_async`). As on the desktop, a job renders every frame on the - project's renderer (the worker's GPU with Mercury GPU Acceleration), not on whichever + project's renderer (the worker's GPU with GPU Acceleration), not on whichever compositor measured faster, so runs don't differ by timing. - **Analyses** (Warp Stabilizer, 3D Camera Tracker, Track Motion, mask and face tracking, Roto Brush freeze and propagation): their work functions are `async`; started with `wait` inside From 7c62272305312b88c25c59a2f24f08d676ee97b5 Mon Sep 17 00:00:00 2001 From: Valentin Klein Date: Mon, 9 Nov 2026 04:52:32 +0100 Subject: [PATCH 3/5] .aep import: placed .ai/PDF footage framed by its ArtBox, Null anchors at the corner (#3099) (#3311) * #3099: placed .ai pages fill the size After Effects stored; null anchors default to the corner Footage of an .aep keeps the size After Effects stored for a placed PDF / Illustrator / EPS file (the ArtBox, rounded up), and masks and anchors are measured against it. The page was drawn as a PDF viewer frames it (CropBox, /Rotate) at its own size, scaled by the width ratio at full resolution and stretched to the stored aspect below 3/4: masks too large, art off centre, squeezed at half resolution. - effectcraft-pdf: Framing::Placed / parse_page_framed (ArtBox clipped to the CropBox, /Rotate ignored); boxes with a non-finite corner or span are not used - Footage::placed_page (set by the .aep importer, serialized only when true): the page is framed so and filled into the stored size (effectcraft_render::vector_fit), decoded, continuously rasterised and in Create Shapes from Vector Layer; Reload keeps the size; the frame cache keys it by size and framing - .aep import/export: a null's absent Anchor Point is [0, 0], as After Effects reads it - fixture ai-mask-3099.aep (own synthetic files, scrubbed, profiles stubbed) and tests against After Effects 2026's read-back * #3099: placed-page framing only for .ai and .pdf, EPS as before; doc comments After Effects' sizing was measured with PDFs named .ai only; an EPS from an .aep keeps its bounding box at its own size (listed as not measured in docs/gaps.md and docs/aep-import.md). Doc comments on the touched functions. * #3099: shapes from a placed page keep its frame (track matte) Create Shapes from Vector Layer converted every path of a placed page, so bleed, art past the artboard or the page's background showed where the footage's raster hides them. The layers it makes from a placed page now take a hidden " Frame" shape layer (the footage's width x height) as their Alpha track matte: drawn at the comp's resolution, its edge is the raster's on shape and image layers alike. The command returns the frame layer's id too. --- ATTRIBUTION.md | 2 +- crates/aep/README.md | 8 +- crates/aep/src/export/plan.rs | 13 +- crates/aep/src/import/build.rs | 29 +- crates/aep/src/import/props.rs | 14 +- crates/aep/tests/fixtures/ai-mask-3099.aep | Bin 0 -> 134446 bytes .../fixtures/ai-mask-3099.aep.attribution | 7 + crates/aep/tests/fixtures/tools/README.md | 1 + .../aep/tests/fixtures/tools/ai-mask-3099.jsx | 94 +++++ .../aep/tests/fixtures/tools/ai-mask-3099.py | 66 ++++ crates/aep/tests/it/export/built.rs | 39 ++ crates/aep/tests/it/import.rs | 1 + crates/aep/tests/it/import/ai_mask.rs | 113 ++++++ crates/engine/src/cmd/create.rs | 29 +- crates/engine/src/footage/footage_reload.rs | 8 +- crates/engine/src/interchange/vector.rs | 64 +++- crates/host/tests/it/vector_import.rs | 356 +++++++++++++++++- crates/media/src/layered.rs | 101 ++++- crates/media/src/pool.rs | 20 +- crates/pdf/README.md | 8 +- crates/pdf/src/lib.rs | 30 +- crates/pdf/src/page.rs | 41 +- crates/pdf/src/tests.rs | 80 ++++ crates/project/src/footage.rs | 8 + crates/render/src/footage.rs | 31 +- crates/render/src/lib.rs | 2 +- docs/aep-import.md | 10 +- docs/gaps.md | 6 + 28 files changed, 1132 insertions(+), 49 deletions(-) create mode 100644 crates/aep/tests/fixtures/ai-mask-3099.aep create mode 100644 crates/aep/tests/fixtures/ai-mask-3099.aep.attribution create mode 100644 crates/aep/tests/fixtures/tools/ai-mask-3099.jsx create mode 100644 crates/aep/tests/fixtures/tools/ai-mask-3099.py create mode 100644 crates/aep/tests/it/import/ai_mask.rs diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 93b34b9bf..4bf3a044d 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -64,7 +64,7 @@ and licence texts are in [`NOTICE`](NOTICE). | `crates/model/tests/fixtures/cube.obj` | EffectCraft contributors | Original work: Wavefront OBJ test fixture (unit cube, two materials) | MIT OR Apache-2.0 | | `crates/model/tests/fixtures/cube.mtl` | EffectCraft contributors | Original work: MTL test fixture (materials of cube.obj) | MIT OR Apache-2.0 | -One small After Effects test project of `crates/aep` is in this repository: `crates/aep/tests/fixtures/synthetic.aep` (original work, made with After Effects 2026 from own data, working space None, the embedded colour profile removed; MIT OR Apache-2.0, with its `.attribution` sidecar). The other After Effects test projects of `crates/aep` are not in this repository. They are original work of an EffectCraft contributor, generated with After Effects 2026 as own test data (MIT OR Apache-2.0, each file with its `.attribution` sidecar); they are in [storytold/effectcraft-corpus](https://github.com/storytold/effectcraft-corpus) with their sidecars, fetched by `cargo xtask aep-corpus` (commit-pinned, SHA-256-verified), and the tests read them only from the folder named by `EFFECTCRAFT_AEP_FIXTURES` (opt-in). Their embedded colour profiles are replaced by stubs made with `crates/aep/examples/stubicc.rs` (a header and a description, no colour data), so no vendor profile data is in them. +Two small After Effects test projects of `crates/aep` are in this repository: `crates/aep/tests/fixtures/synthetic.aep` (original work, made with After Effects 2026 from own data, working space None, the embedded colour profile removed; MIT OR Apache-2.0, with its `.attribution` sidecar) and `crates/aep/tests/fixtures/ai-mask-3099.aep` (original work, made with After Effects 2026 from four hand-written one-page PDFs named `.ai`, footage paths and host name neutralised, the embedded colour profiles replaced by stubs made with `crates/aep/examples/stubicc.rs`; MIT OR Apache-2.0, with its `.attribution` sidecar). The other After Effects test projects of `crates/aep` are not in this repository. They are original work of an EffectCraft contributor, generated with After Effects 2026 as own test data (MIT OR Apache-2.0, each file with its `.attribution` sidecar); they are in [storytold/effectcraft-corpus](https://github.com/storytold/effectcraft-corpus) with their sidecars, fetched by `cargo xtask aep-corpus` (commit-pinned, SHA-256-verified), and the tests read them only from the folder named by `EFFECTCRAFT_AEP_FIXTURES` (opt-in). Their embedded colour profiles are replaced by stubs made with `crates/aep/examples/stubicc.rs` (a header and a description, no colour data), so no vendor profile data is in them. ## Screenshots diff --git a/crates/aep/README.md b/crates/aep/README.md index 965bea317..5e8035d8f 100644 --- a/crates/aep/README.md +++ b/crates/aep/README.md @@ -69,13 +69,19 @@ tests reach them only through `tests/it/common/mod.rs`, which hands a test its c of its own cut from the combined file; a test whose fixture is missing (or with the variable not set) prints `SKIPPED: fixture missing` and passes. -`tests/fixtures/synthetic.aep` (115 KB) is the one project in the repository: made in After Effects 2026 +`tests/fixtures/synthetic.aep` (115 KB) is one of the two projects in the repository: made in After Effects 2026 with the working space set to None (one composition, an image sequence of three 8x8 PNGs, a solid, a folder, a Levels and a Glow effect with expressions). Every After Effects project embeds the working space's ICC profile (HP sRGB IEC61966-2.1 for the default) in a JSON settings chunk; the one `Utf8` chunk of this file that held a profile was replaced by `{}`, and nothing else was changed. The tests in `tests/it/import/synthetic.rs` damage copies of it and need no other file. +`tests/fixtures/ai-mask-3099.aep` (131 KB, #3099) is the second: four hand-written one-page PDFs +named `.ai` with distinct page boxes, placed in a composition, one of them with two masks under a +Null parented to a Null (generated by `tests/fixtures/tools/ai-mask-3099.py` and +`ai-mask-3099.jsx`, scrubbed with `scrub.py` and `stubicc`). `tests/it/import/ai_mask.rs` checks +the import against After Effects' read-back, and the host's `vector_import` tests render it. + ## Tools - `tests/fixtures/tools/`: the generator of the test projects (regenerates the corpus with After diff --git a/crates/aep/src/export/plan.rs b/crates/aep/src/export/plan.rs index f8aa2b3b8..a6789eda9 100644 --- a/crates/aep/src/export/plan.rs +++ b/crates/aep/src/export/plan.rs @@ -817,7 +817,7 @@ fn plan_transform(layer: &Layer, src: &LayerSrc, comp: &Comp, source_size: (u32, let cam_light = matches!(src, LayerSrc::Camera(_) | LayerSrc::Light(_)); let is_shape = matches!(src, LayerSrc::Shape(_)); let mut props = Vec::new(); - let at = PropAt { comp, comp_name, layer: &layer.name, source: source_size, text: is_text || cam_light || is_shape }; + let at = PropAt { comp, comp_name, layer: &layer.name, source: source_size, text: is_text || cam_light || is_shape, null: matches!(src, LayerSrc::Null) }; if let Some(tr) = layer.transform() { for kind in PropKind::ALL { // A camera's or light's point of interest is After Effects' Anchor Point. @@ -1172,16 +1172,21 @@ struct PropAt<'a> { source: (u32, u32), /// The anchor is stored in pixels (text, camera, light and shape layers). text: bool, + /// A null layer: an Anchor Point that is not written is its top-left corner (#3099). + null: bool, } +/// What is written for transform property `p` (`kind`) of a layer: its value or keys in the +/// stored unit and its expression; `None` when it is at After Effects' default and has neither. fn plan_prop(kind: PropKind, label: Option<&str>, p: &Property, at: PropAt<'_>, cp: &CompPlan, col: &mut Collector) -> Option { - let PropAt { comp, comp_name, layer, source, text } = at; + let PropAt { comp, comp_name, layer, source, text, null } = at; // A text layer's anchor is stored in pixels, with the origin as its default (385). let (sw, sh) = if text { (1.0, 1.0) } else { (f64::from(source.0.max(1)), f64::from(source.1.max(1))) }; let what = format!("{comp_name}: '{layer}' {}", label.unwrap_or(kind.match_name())); let dims = kind.dims(); let default: Vec = match kind { - PropKind::Anchor if text => vec![0.0, 0.0, 0.0], + // After Effects 2026 reads a null's absent Anchor Point as `[0, 0]` (#3099). + PropKind::Anchor if text || null => vec![0.0, 0.0, 0.0], PropKind::Anchor => vec![0.5, 0.5, 0.0], PropKind::Position => vec![f64::from(comp.width) / 2.0, f64::from(comp.height) / 2.0, 0.0], PropKind::Scale => vec![1.0, 1.0, 1.0], @@ -1319,7 +1324,7 @@ fn plan_prop(kind: PropKind, label: Option<&str>, p: &Property, at: PropAt<'_>, /// back disabled). fn plan_remap(p: &Property, comp: &Comp, cp: &CompPlan, layer: &str, comp_name: &str, col: &mut Collector) -> PropPlan { // Rotation has the same layout (one component, stored as is, not spatial). - let at = PropAt { comp, comp_name, layer, source: (1, 1), text: false }; + let at = PropAt { comp, comp_name, layer, source: (1, 1), text: false, null: false }; let planned = plan_prop(PropKind::Rotation, Some("Time Remap"), p, at, cp, col); col.exported.time_remaps += 1; let mut plan = planned.unwrap_or_else(|| PropPlan { kind: PropKind::Rotation, value: vec![0.0], keys: Vec::new(), expr: None }); diff --git a/crates/aep/src/import/build.rs b/crates/aep/src/import/build.rs index 5a55c557a..7d5b902f1 100644 --- a/crates/aep/src/import/build.rs +++ b/crates/aep/src/import/build.rs @@ -285,6 +285,8 @@ fn import_footage(project: &mut Project, cx: &mut Ctx<'_>, item: &AepItem<'_, '_ cx.col.missing_footage.push(shown); } let kind = if sequence.is_empty() { footage_kind(&path, f.native_frame_rate, f.audio_sample_rate.is_some()) } else { FootageKind::Sequence }; + // A placed PDF / Illustrator file keeps After Effects' framing and size (#3099). + let placed_page = kind == FootageKind::Still && is_placed_page_path(&path); let mut footage = Footage { path, kind, @@ -295,6 +297,7 @@ fn import_footage(project: &mut Project, cx: &mut Ctx<'_>, item: &AepItem<'_, '_ // footage), and only then. has_audio: kind == FootageKind::Audio || (kind == FootageKind::Video && f.audio_sample_rate.is_some()), missing: !present, + placed_page, sequence, ..Footage::default() }; @@ -327,6 +330,14 @@ fn import_footage(project: &mut Project, cx: &mut Ctx<'_>, item: &AepItem<'_, '_ } } +/// Whether `path` names a file whose page is framed as After Effects places it +/// ([`Footage::placed_page`]): an Illustrator or PDF file, by its extension. EPS files keep +/// their bounding box at their own size: how After Effects sizes them was not measured (#3099). +fn is_placed_page_path(path: &str) -> bool { + let ext = path.rsplit(['/', '\\']).next().and_then(|name| name.rsplit_once('.')).map(|(_, ext)| ext.to_ascii_lowercase()); + matches!(ext.as_deref(), Some("pdf" | "ai")) +} + /// The name After Effects shows for a footage item that stores none: the file's name, or for a /// sequence the name with the frame range (`shot_[0101-0103].exr`). fn default_footage_name(item: &AepItem<'_, '_>, f: &crate::Footage) -> Option { @@ -453,11 +464,13 @@ fn camera_light_orient(auto_oriented: bool, light: Option) -> AutoOri } /// The unit a layer's anchor is stored in. -fn anchor_unit(camera_or_light: bool, text_or_shape: bool, size: (u32, u32)) -> AnchorUnit { +fn anchor_unit(camera_or_light: bool, text_or_shape: bool, null: bool, size: (u32, u32)) -> AnchorUnit { if camera_or_light { AnchorUnit::PointOfInterest } else if text_or_shape { AnchorUnit::Pixels + } else if null { + AnchorUnit::Null(size.0, size.1) } else { AnchorUnit::Source(size.0, size.1) } @@ -582,7 +595,7 @@ fn build_layers(project: &mut Project, cx: &mut Ctx<'_>, defs: &EffectDefinition import_layer_styles(&layer, &mut l, &comp.global_light, &mut Ids(&mut project.next_id), &tm, &what, &mut cx.col); // Text and shape layers store their anchor in pixels (385, 462); cameras and lights their // point of interest. - let anchor = anchor_unit(is_camera || light.is_some(), is_text || is_shape, size); + let anchor = anchor_unit(is_camera || light.is_some(), is_text || is_shape, is_null, size); if let Some(tr) = l.props.sub_mut("transform") { import_transform(&layer, &name, comp_size, anchor, &tm, tr, &mut cx.col); } @@ -906,6 +919,18 @@ fn resolve_parents( mod tests { use super::*; + /// #3099: an `.ai` or `.pdf` from an `.aep` is a placed page; an EPS (not measured in After + /// Effects) and other files are not. + #[test] + fn only_illustrator_and_pdf_files_are_placed_pages() { + for path in [r"C:\fixture\x\logo.ai", "/work/Logo.AI", "art.pdf", r"C:\a.b\page.PDF"] { + assert!(is_placed_page_path(path), "{path}"); + } + for path in [r"C:\fixture\x\still.eps", "a.epsf", "a.epsi", "still.png", r"C:\dir.ai\file", "ai", ""] { + assert!(!is_placed_page_path(path), "{path}"); + } + } + #[test] fn unlisted_extensions_follow_what_the_file_stores() { assert_eq!(footage_kind("C:/clips/a.MP4", 0, false), FootageKind::Video); diff --git a/crates/aep/src/import/props.rs b/crates/aep/src/import/props.rs index 838a2ee44..fd0a4f9fc 100644 --- a/crates/aep/src/import/props.rs +++ b/crates/aep/src/import/props.rs @@ -136,8 +136,12 @@ const IGNORED: [&str; 4] = ["ADBE Position_0", "ADBE Position_1", "ADBE Position /// How the stored Anchor Point becomes pixels. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum AnchorUnit { - /// A fraction of a source this big (pixels): AV layers. + /// A fraction of a source this big (pixels): AV layers; the source's centre when absent. Source(u32, u32), + /// A fraction of a null object's source this big (pixels); its top-left corner when absent: + /// After Effects 2026 reads a null's absent Anchor Point as `[0, 0]` (#3099: two nulls made + /// with `addNull`, one left at its default, read back `[0, 0, 0]` and `[10, 20, 0]`). + Null(u32, u32), /// Pixels as stored, the origin when absent: text (385) and shape (462) layers, which have no /// source. Pixels, @@ -169,6 +173,14 @@ pub(crate) fn import_transform( let (sw, sh) = (f64::from(w), f64::from(h)); (Kind::Anchor(sw, sh), tr.anchor_px(sw, sh)) } + AnchorUnit::Null(w, h) => { + let (sw, sh) = (f64::from(w), f64::from(h)); + let px = match tr.anchor_fraction.source { + ValueSource::Default | ValueSource::Unreadable => [0.0; 3], + ValueSource::Stored | ValueSource::Keyframed => tr.anchor_px(sw, sh), + }; + (Kind::Anchor(sw, sh), px) + } AnchorUnit::Pixels | AnchorUnit::PointOfInterest => (Kind::Anchor(1.0, 1.0), tr.text_anchor_px()), }; let poi = anchor == AnchorUnit::PointOfInterest; diff --git a/crates/aep/tests/fixtures/ai-mask-3099.aep b/crates/aep/tests/fixtures/ai-mask-3099.aep new file mode 100644 index 0000000000000000000000000000000000000000..7223fda82f8a520db8d01325cd5bde56de1ae26e GIT binary patch literal 134446 zcmeHw3y>toS!VU@>_aPQwU*anEre+19NZez^XS=K%}ng>%6m??VrN@YxkhH{tx4rfY{u{3VD$csKm&-ik%8mElLi{!&TP9zWXCqiO%|%YX9L zJIj#vt>l8|t&n^t$T4~~z+?jF;p4RN3F_Ib-22;G-Mb<`bmD@ zI1JQYBDu9=NfgI7fxg^{6t`KS*?WM&kh_{nz2r3QH148hZ);j~Zq`h#c}6evMLDOxYivn|Hi=;COJI-ak2gex^JV z8Y`3>A+=4Ia`iBDjPmewn~aUirFvlM5PblCr0z5@IvaTXioO}RtPNg)3&)}3lECu` zjNvFSFTHdSu73;7)%c^WceUC`P@J*S*~Pdnsl1P*;!>2;RnOHbdxQQTiOl7CWQI$S ztrvbJI49^H;yJa*tdaN$iu9sWv&!>UO|RD-j|@7^ft9O$BQlr5M7jHTxkq@p2P(G) zZw*u+*GVYj1E2dCU`F1kL}=91#veWX#lL-QsxtmH_5{@tt{%FNW$F0efKCNBi?O(E zIgz`#B-Z~po(|NU>P+Cij44YJxpMXOmA)_8kd8vpdi zNci~pAF^X+<{XI<1<(1RLuQy3_iYbiTcqHQ5SzHnUhB@e7O$7%QYK=z29HHS{d$gFZ`$^4Dp<43nQ{fKgW z%DmF}IPU{&R{2JA{o)i}_Gddl>+aHS0iGzqm2gC!jo-id=s^QpAX8seG*%Muw{Nsh=ps7;^X;-iGv zq+wX~)>PEJG}MtCB@nqnXeql}-D8Nb5vDxlyFOVw%nA&V(!+)IF}*}@rS%kEqatPi z>5!3>&eDi;S^K#O|BXiC4xGuojp{4Hgu;S|$I?^pG1{7}EfH=Rh|98V#~|kt91!7% zbm53CM%?rprQ<5SvbbNOp=rmqS3~P|VsgT-OpVxL63W3xO4Spu=dY~`Nb?<9yRsh!b z*-PIXo1@p(E>@@N!4zOdm-crNGrl@+DKt=|6?d^V3;t?gv|jdLwJF`~K#$Gel|5t9 zB@0%dHy4CTk4tX21}CHyN^tJ*bAiEc6xKU&+vtKe9;JqjGObnze|(&v^t7cuo`dc= zg&eqG3tW^UaIU5tfT@>BLzBSM_CJvx@bwye3Q|Vp3<6-wDS_4G-IG8J;0WQ$opEr$!CPLl~;SN1!`>2 zw?K^z`WC3MLEi$vrb!!>mR78FGceG7^rhQ0-zA%?yMnY#mLa+1kRL6%6GI|D6;z3q+-hvll3R@pT5_wg zK}&8mHXYgK;x6$Lpj@wz_8-h?<;gTI-5FwN$&HrWWcTG0`E?GsOFZDk z1J@&?CAZMIpe45;4SGaNZUTd(OUhw&z2O@UE+x)_cUwkq2EreBMJLqwB#0`H62yjH<3~5b+dYSyF91E zr{Lw!ScuGUWG%7(mlQ40YVRys`efu2UP=*aw;l2+qCtL{?ATs-{w_bvUZU#oAKZ(=?mj8 zv6QSMNlX7o?yS756ixGMkw?y@md-JsNclUX)t)R`6%F?C&j77{;y?aDZd$E2*9d8~ zL962*Jiau3{rdIs&%p21SAUVi|@R_>>qi{2EAuM*sw80mztVkZ273bPh^Hf?rW056E?*Ks1p5kn!y)69Q}3F z*r30T8XNT2QDcMtI)F{9u^Yd<004;*xFc%8FE2EAzcwjbk_ymYhf*y1>!81m+|5L< z5-VQv)#jKq>M;qSJmtH7(43W~FbSdbaAYopAQdb@B3YSIdL72=biH)%vL5(8M{=F+ zYwR(iw&^2WEoCT^Gm=<2S^pf7=O+9&vMz61nuO#e`s<`-RP@&YN>gd#(O*Z^4GSTL z{yIfzYytgs3ie9$*C|S4=&w^0G4$6diWvIq6h#dEbvi=~{dLe^2mN)R@30_Az0-Q6 zm+3;FxZHHiUSaH2>JK_wVPt#y=$lqhzP4rsfn!=>Q&k{=*XHQ8wTsoMdO&tiC)x~= zo?Zvtxmqq*QDy~6Wp=YR3;t?gSmkT=$nm`}vn{y>D81i!9&ETJ0(-`!8(D#;N3wl5 z`sm328~IcfRXH^$*he ze-Xbc{yOjdKl-^Bzo?Hd2A>{3=lmgoo{zKWuOqq)KW6xr^<>eiAm5k&4bbYRMn-be z>R$5K*@Lt??!0D=FZ|dy$3Ok%()cGPf9js4)f~K%G|HpD4(Y+7Tc&z-c9hS$m$I)G zPnFsu_C67As?Fu`F*yB2BBlHhuRxdRdzK2XKp(oVxe|p^c9c(v5kr5Sq<;?j>u5@e zg7={J_|ff6KQeV7W@TQfck-{v+-O1lyzEZ*_3nz^nb1k$_jQ04BpN|~oirjvc@p&3 z8373K`#R{aBh52nlyQhJ6ReQ0xAbI+W%aZUf17=o&|gPtBpMMdh5kBfY|vjvjZH@$ z82Ei13D7O_;Z(Vt*iWgwopvu>IF;&9EFz@jSm)d3b}`CyL@nsA(+1t<%c6anuVkl4 z?SuY00Fr9`DbxB}d`*UNftkhuTkGSX2ZJu#MSWZ#g5TE>d%dE+PWI4~D^(hw&|im+ zPYKNy`s)-OvGMylMG-@Pox+HrzfM6u3jKA8(ir;d6h#dEb&4W}{yIewLw}vl5JP_* z^w&Xu9q2nONK!BQ>&!40m-38f3WS=E5@vjL9{qI~PUx>gaF8e@hrI{a+@iluE`OaP zZ%#w6bIpiYs=Wfg0|avj+-|jp8>B%OE`1%6NmriEwrTB!E0(RVx@1bo6|%~aTrYTr z(!{g>^1uVf2bV~x{F^8T#j{v@KlrZ|-%wF}V<*tP(Ds8`rW+ouJS+FUzgLvoYR|t( z&sgz;@|^E@Dl9marY&vTf0JdO_K>#kEeHkI7n&?>_w5sH+_tS~GYH${iI1@S6ED5d z_QqBjq<$S++M;c8PD|SuF|dzD28Z6-A4Cvf5*z?mAKVyt^8duUKoAEAo;fpO^Xy&r3aoE8Y(NI$4`M z=bXQu6Ma}5^W57ik3ja$-%QYK5U1Z;nuz0x(Y0vL$dRAZF3RKiq){`oVcw4^*P_6yFE$M>^mvDSX@l42a(sxMwF4hSD>PWzsl>(lbS*z_&e6 zdQQ_&4xV44Ih3Araukhh#r=V@DO!}6)gCB43v^l?QF<1-6s>n=J&f!%a}Dr`W2KmNdj_m4q0|&S@G#*_qCjMYub9LxH~?oq4$~17+tt4T7?BSIf>~ zYu=7z=S8X-$U`MG&sJpTedJjvJEQCjl+DOUX{w_{Lnu2_m;`p|BV24+ z8ByV?N=MmQg^VhlBZgMnKz+=iD3*CATqlg&6>}J{JE^^X|5p4!k;Qe3}t7OowXB!M%o=Exd&xufJ@cuaNSu&OA-yC>`Y-w%giDU z7g2WJ@{U3(N>rtz>}*=gZ0D_eS$0l)Jm1ghr6bvS-dSELV6?pz*?B*E7D~@3Jp;w= zW$F1hFVQX~q-WK5+%8?>C}PP5SDtf9HZfwj@>~=#l%8`UhAYo7h)LrVO3xG#TzSTo zXYF3y#}ci!+p%_u>q66p`-OFd$8%bGetzcM3f3->jM8&#|J?<4+eaUpl6TvFkT+#V((}}62-0);X~$YGtGU#eSlUU?V$-s$ z-L@Z;pT(r;BY&jW9eazrZr^`GQErpemau%pZ(zj@tv-=>Eg|8kvvV(ZtqZ>zcIV@- zzWVCV58b%)vp;kF`oi%~-g)IS^w{EMGcRQ#b_=EGoDzM zXa&#>RscGv_oPwU{G9K+?yMvf|L|636dsXv;dW`lYTR4Z{E5(hd#}G)Uj;O~-Bcq}r z+zTL$l!!|-gnI!{Skip8h{MGu{#xLe_qd}oIeIhXZ3f6E#tPJ9bfq5SA>Pzm)?<)O z2-}2h(_;*y9^*0eY5;0$vX9wAyU~C0I2!j8`6v2_93W8}0hh!I0QMoz>~ zkI|-wcXCFV{!9wEhEG^fZ`d-SXC{QAv{4(@5FY5#m>!7{G6Y;Q;6Hhz?U;h=>c zL;DcHG!;sO$D7rI+g1>!E=kek_Y<=87_^PV9a$N8n;sO;V(s~zk10Gqw&+oE@VgY{ zHc8A0tJawFM{EC_oPUfMN|!kiyB0W>7wM7Xd!o0bkE~~udi*Gi_^1bdS4&}JhD(r_ z^t3!H<_96)0S*35ZT!*GU;NuV8ncpiJY5@zXLaX{Y%GauGqTE|8TcE$$B%Ar`jM$a zqB5^Ehj5$o8_Flgeu1Cu04?&4rMK?ZY~IjRdjFS(YPMuo){weNwMfy^eYc$)&X zaXiloa#Uw`YU8g)R&dd62aT#_cLJL|Q=27g&2ndIOMV!(B%Ix8DPtw@Yu1GuEvusL zgWKFvq3=581;=&URA+avSq^lQFn;YSv^%w_>k)J+uU%z!sZZH~o`-C9sWz04C@@+w zzGEW_La*h-##*+tlA3vP&6=c}m#3Y;GE{N{r<+bWUn<=fkP|=(kp%wi>8fR_SoIWI z#g>`jqQs>XZJ${k=-tuX7A?dOJT}om`%LL!6qebHsbHG6);=JY0Ni({ecH8)xGuHN z74ngc@?xNSVa*A{EMb=;lHO@T*&kxd2KEuqu8`FVc*9N_hE;D(Mcqq79rba62&JX$ zZgr0#!bX_#l<)e1=ys_iL!|U@VGT$x(G+Ptg*70h4CPQpQmUGXD4z$OoABRAq;CJI zG(Jt%>y{pbWydSeTei#QRhh=_G1{7}EfM|{h|98V#~|nFHsFYK;fO8PyVGx!ju%=l zpLT3}HMHKQh+R%&lN~RF*h1YhR$V=40a+K**p48!;&_f{g`paGVQ0MrG%^9$22Ssela;lX!ZDSb5%eUEj9T;L|uFCLwm&Gp&uXZxb0P&@*+5 zTSM$E2nF?4+h$RiU+2KIG$KFrQo8Tm1g9Vgv7&m%$yL+wv)4HVfpUb?E0Rh**cMhK z`P!}($&#+92|4a!ZE|4AYlAj{j&4BXfL_IR>f{Xc^|oWXa7NYe9H);@UA}OQpZ8DI z7phYsv!5gi8E$`f=(-m<5v(xt_lUKhzum++LX~`y-8|V-SagZ-qSVQ3%%jkg_)Uhy}m%K4h~H$-Jt|vubFFbLMU>d z?f7*#5{ixJa6{?T_8WBEI8#&f!OvfgteOKBzocA3#g0jH$28_J3)n@fy@czDK(eb1 zIpd7`?Xj9{XQ#++^oH++4sI^)ycH+n=ka~p#fGr zhQI1Xx0dwK>Khw+LQ0JEVA+cL#;Q;B*{iM#tIaEYV|`O&H*ea`Mzk7OH*ap-yy*-N zj*eFJ%96ifh5fqI_e5Xa)g#*vYP7`A3f8RPmIsT&aQCHa3yXt;gOG`*uleq3%@UWN zrZKq6o_)&+Z`rb=XngQCWFRub^X-7Z*g#&;5Lm&E{qo4lh_&a~{AFlodn@4)4 zr>@K|SaruFd1%*5{OLUX+r~fJ|GB4r_!po1xihyu@Z$IXXV>xQ{7yQkRa>H?X-RYH z>!cR~APor*Nf%?-*#N+NiXynWc`T8r}apO-Zcqffc#BPPJJ)@ zEj@E;&~Tr+^u5mcXPntx zR1#pdoQ(H9Hw3Nl((WoRu><_VXOsK_={W`X1(JO``Gr@D@C&61JiV*Yg{2BnRqQyspyU>ynMj}`|AxKHVw%SKmx!@r10OjZWA`8eKr57H;ME1U z@Zit=hu{B)M=yQwBR_cK9XG!I(0?B^&xP>R+ z=|q}juX`R~8-?Z_=4w?H&Hs9R|2L6ET1x`keN zMgeYtWWQ_Ng7&BI^bPny5L|*dLegI37CxGbTi9;hf?0_Ir?%tlf>O6|0$PKp^S%K; zQVuw);Q&IYz(Q&*oWOXRXorZ@s!+ z(oBP{=)($ZJy>1>XyL5=io1p@x7;GJsHK-Y3VjOzUSJd=*>M!g=>LM=)0a9Pg+L#5 zuLdy+z4a4cDL?#)@AR`t}M#xg=%j-G?#l=Bi!EhhXdN|_7@5z9?UZ*bnxUOWJPBO!b94^4wOoE z2Hu%+5A9I7@7SSoAKsyI-?>BOKDa~u;sI@9N9gQ6s)O48qVM0K{&sAKaM*qXu*3uc ze2#td%DfrOuh5+`kAd_aD?>{C`~MQMm*!#PNI23Nn)7HAt`kP?ia8AFC$-n_|7xOG znC}k#KDjUPOI&8Byqw-Em!q0A*U$60zUdC0uv2sW0c~3K$O7rjbN#_>nd_U&J*2(6 zK)qC1o6GGjT~uW*kltMGeX)M2Kru~Oo69|{{XkQ$;t9>=-oHcTKB&E~c=ej@Xs+)A z+SR69#S@y#EnWuRfe0W1dkg_S-%igh&d=D3mWyd_*_spr;!P)whwc=cQKCl*^zq)8Vf7=)}`^L{5KC>~lQLEQrQ=#%k&Go{u zjkA5QPxq2F2InMsps!4`M7PiOO_B@c8?$q;VMkzhI3k%342dh%z`PautASz7tU>h~u)tfNF^w_eXsSo&^m^TO4EjNi zfi=(MwbyOfFcx;ggPj4x{(9iQA5P8;H12AkcLv^mY$~wy$PX5M-#y2uV3HJ2o?#`B zCO=(1@r>ia7VqJE&kUsR$tunQ-#qIaxwA4fSQ#2#96B{teR8aN^1BDCV}pZ56{JtX4*chKEOntGYQnI4p`z+}>0)e5?hA3f)cQA+<01 zh>T*I9(4&s6F=Cb{Xg!ObRbx;zE4gi^h(CAjn*@axq4%?Wg5cv3ATEjAL;KAb z+vnw+K`7QT&kRVZjcUbr^4px|2F!;tD?f3&`Wfphme-2lSy42Xx{%O^nF97rnp_5& z$kiEHvGRTC^3Mz~#CM=2LVay(?U1h{ZLJ-oA>%H#LxYZu72R95%yR>V??uqmf|5mg zz7}NdDt9q4GNigB%bJ)@*<4Kt114+W6$WBvp@~f5?zEjJ=~vv&vg5MwAu$wrP7tm< uD8+dYt{93uCkR&_l;S)HR}4j-6ND=dN^zc6a2;-_8(^=bvwiEJO!)sG$5C|v literal 0 HcmV?d00001 diff --git a/crates/aep/tests/fixtures/ai-mask-3099.aep.attribution b/crates/aep/tests/fixtures/ai-mask-3099.aep.attribution new file mode 100644 index 000000000..86efa3b65 --- /dev/null +++ b/crates/aep/tests/fixtures/ai-mask-3099.aep.attribution @@ -0,0 +1,7 @@ +asset: ai-mask-3099.aep +title: Small After Effects test project: placed .ai files with masks under two parented Nulls (#3099) +author: EffectCraft contributors +source: original work (made in After Effects 2026 from own data: four hand-written one-page PDFs named .ai, generated by tools/ai-mask-3099.py, imported and laid out by tools/ai-mask-3099.jsx) +license: MIT OR Apache-2.0 +added: 2026-11-08 by EffectCraft contributors +notes: Scrubbed with tools/scrub.py (footage paths C:\fixture\x\.ai of the same length, host name HOST000, the XMP packet of an empty project of the contributor); its four embedded colour profiles replaced by stubs (description only, no colour data) with crates/aep/examples/stubicc.rs, then checked with the corpus repository's tools/aep/inspect-embedded.py (CLEAN). No Adobe asset is in it. diff --git a/crates/aep/tests/fixtures/tools/README.md b/crates/aep/tests/fixtures/tools/README.md index eee0174ee..f1c720b2e 100644 --- a/crates/aep/tests/fixtures/tools/README.md +++ b/crates/aep/tests/fixtures/tools/README.md @@ -29,6 +29,7 @@ single-case files some cases are merged from, and the contributor's own animatio | `scrub.py` | fixture hygiene of a saved `.aep`: footage alias paths replaced by neutral ones of the same length (`C:\fixtures\...`), host name `HOST000...`, neutral ascend counts, the XMP packet replaced by the base project's | | `layers-comps-scrub-aepx.py` | the same XMP swap for the `.aepx` of the pair | | `patch-text.py` | the byte edits of the text cases 732 and 733 (see below) | +| `ai-mask-3099.py`, `ai-mask-3099.jsx` | the committed `../ai-mask-3099.aep` (#3099): the `.py` writes four one-page PDFs named `.ai` with distinct page boxes, the `.jsx` (ExtendScript, run once through an After Effects MCP server's script call in an empty untitled project) imports them, builds composition `ai-mask` and returns After Effects' read-back that `../../it/import/ai_mask.rs` compares against | ## Run order (per file) diff --git a/crates/aep/tests/fixtures/tools/ai-mask-3099.jsx b/crates/aep/tests/fixtures/tools/ai-mask-3099.jsx new file mode 100644 index 000000000..bcac03d0b --- /dev/null +++ b/crates/aep/tests/fixtures/tools/ai-mask-3099.jsx @@ -0,0 +1,94 @@ +// Issue 3099: how After Effects sizes a placed .ai (PDF) and where masks / parented layers land. +// Generates ai-mask-3099.aep: run `python -I ai-mask-3099.py ` first, set `dir` to that +// folder, then run this script (ExtendScript) in an empty, untitled project. It returns After +// Effects' read-back (footage sizes, transforms, mask vertices, toComp points; the tests compare +// against it) and saves /i3099-ai-mask.aep, which is then scrubbed (README: steps 5 and 6). +(function () { + var dir = "/"; + var out = {}; + if (app.project.file !== null || app.project.numItems !== 0) { + return "ABORT: project not empty/untitled (" + app.project.numItems + " items)"; + } + var target = new File(dir + "i3099-ai-mask.aep"); + if (target.exists) { return "ABORT: target exists"; } + var names = ["i3099-a-boxes.ai", "i3099-b-frac.ai", "i3099-c-rot.ai", "i3099-d-trim.ai"]; + var items = {}; + out.footage = {}; + for (var i = 0; i < names.length; i++) { + var io = new ImportOptions(new File(dir + names[i])); + io.importAs = ImportAsType.FOOTAGE; + var it = app.project.importFile(io); + items[names[i]] = it; + out.footage[names[i]] = { w: it.width, h: it.height, par: it.pixelAspect }; + } + var comp = app.project.items.addComp("ai-mask", 1920, 1080, 1, 5, 25); + var art = comp.layers.add(items["i3099-a-boxes.ai"]); + art.name = "art"; + var artD = comp.layers.add(items["i3099-d-trim.ai"]); + artD.name = "artD"; + artD.collapseTransformation = true; + var artC = comp.layers.add(items["i3099-c-rot.ai"]); + artC.name = "artC"; + artC.property("ADBE Transform Group").property("ADBE Position").setValue([1500, 800]); + var n1 = comp.layers.addNull(); n1.name = "N1"; + var n2 = comp.layers.addNull(); n2.name = "N2"; + n2.setParentWithJump(n1); + art.setParentWithJump(n2); + artD.setParentWithJump(n2); + var t = function (l, p, v) { l.property("ADBE Transform Group").property(p).setValue(v); }; + t(n1, "ADBE Position", [700, 400]); + t(n1, "ADBE Rotate Z", 15); + t(n1, "ADBE Scale", [80, 120]); + t(n2, "ADBE Anchor Point", [10, 20]); + t(n2, "ADBE Position", [50, -30]); + t(n2, "ADBE Scale", [150, 150]); + t(art, "ADBE Position", [0, 0]); + t(artD, "ADBE Position", [300, 250]); + // Two rectangular masks on each art layer, in layer pixels. + var rect = function (l, nm, x0, y0, x1, y1) { + var m = l.property("ADBE Mask Parade").addProperty("ADBE Mask Atom"); + m.name = nm; + var s = new Shape(); + s.vertices = [[x0, y0], [x1, y0], [x1, y1], [x0, y1]]; + s.closed = true; + m.property("ADBE Mask Shape").setValue(s); + }; + rect(art, "M1", 40, 30, 200, 90); + rect(art, "M2", 220, 100, 350, 160); + rect(artD, "M1", 40, 30, 200, 90); + rect(artD, "M2", 220, 100, 350, 160); + // World positions of layer points through the parent chain (toComp), read via a Point Control. + var probe = n1.property("ADBE Effect Parade").addProperty("ADBE Point Control"); + var pt = probe.property(1); + var toComp = function (ln, p) { + pt.expression = "thisComp.layer('" + ln + "').toComp([" + p[0] + "," + p[1] + "])"; + var v = pt.valueAtTime(0, false); + return [v[0], v[1]]; + }; + var rd = function (l) { + var tg = l.property("ADBE Transform Group"); + var r = { + w: l.width, h: l.height, + anchor: tg.property("ADBE Anchor Point").value, + position: tg.property("ADBE Position").value, + scale: tg.property("ADBE Scale").value, + rot: tg.property("ADBE Rotate Z").value, + masks: [] + }; + var mp = l.property("ADBE Mask Parade"); + for (var k = 1; k <= mp.numProperties; k++) { + r.masks.push(mp.property(k).property("ADBE Mask Shape").value.vertices); + } + return r; + }; + out.layers = { art: rd(art), artD: rd(artD), artC: rd(artC), N1: rd(n1), N2: rd(n2) }; + out.world = {}; + var pts = { tl: [0, 0], tr: [art.width, 0], bl: [0, art.height], m1: [40, 30], m2: [350, 160] }; + for (var key in pts) { out.world["art_" + key] = toComp("art", pts[key]); } + var ptsD = { tl: [0, 0], tr: [artD.width, 0], bl: [0, artD.height], m1: [40, 30] }; + for (var key2 in ptsD) { out.world["artD_" + key2] = toComp("artD", ptsD[key2]); } + probe.remove(); + app.project.save(target); + out.saved = target.fsName; + return out; +})(); diff --git a/crates/aep/tests/fixtures/tools/ai-mask-3099.py b/crates/aep/tests/fixtures/tools/ai-mask-3099.py new file mode 100644 index 000000000..6b5b7facf --- /dev/null +++ b/crates/aep/tests/fixtures/tools/ai-mask-3099.py @@ -0,0 +1,66 @@ +"""Writes hand-made one-page PDFs named .ai with distinct page boxes (issue 3099 repro).""" +import os +import sys + +out = sys.argv[1] +os.makedirs(out, exist_ok=True) + + +def pdf(page_extra, content): + objs = [ + b"<< /Type /Catalog /Pages 2 0 R >>", + b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>", + b"<< /Type /Page /Parent 2 0 R " + page_extra.encode() + b" /Contents 4 0 R >>", + b"<< /Length " + str(len(content)).encode() + b" >>\nstream\n" + content + b"\nendstream", + ] + data = b"%PDF-1.4\n" + offs = [] + for i, o in enumerate(objs, 1): + offs.append(len(data)) + data += str(i).encode() + b" 0 obj\n" + o + b"\nendobj\n" + x = len(data) + data += b"xref\n0 " + str(len(objs) + 1).encode() + b"\n0000000000 65535 f \n" + for o in offs: + data += b"%010d 00000 n \n" % o + data += b"trailer\n<< /Size " + str(len(objs) + 1).encode() + b" /Root 1 0 R >>\nstartxref\n" + str(x).encode() + b"\n%%EOF\n" + return data + + +def boxes_content(boxes): + # One outline per box (different colours) plus a filled marker at the media box origin corner. + cols = ["0 0 0", "1 0 0", "0 0.6 0", "0 0 1", "1 0.5 0"] + s = b"0.85 0.85 0.85 rg " + x0, y0, x1, y1 = boxes[0] + s += b"%g %g %g %g re f\n" % (x0, y0, x1 - x0, y1 - y0) + for c, (a, b, cc, d) in zip(cols, boxes): + s += c.encode() + b" RG 1 w %g %g %g %g re S\n" % (a + 0.5, b + 0.5, cc - a - 1, d - b - 1) + # A solid square in the lower-left of the last (innermost) box: shows y orientation. + a, b, _, _ = boxes[-1] + s += b"0.2 0.2 0.8 rg %g %g 20 10 re f" % (a + 4, b + 4) + return s + + +def arr(b): + return "[%s]" % " ".join("%g" % v for v in b) + + +# A: every box distinct, nested, non-square. +media, crop, bleed, trim, art = [0, 0, 420, 220], [5, 5, 415, 215], [10, 10, 410, 210], [20, 20, 400, 200], [60, 50, 300.6, 170.4] +a = pdf( + f"/MediaBox {arr(media)} /CropBox {arr(crop)} /BleedBox {arr(bleed)} /TrimBox {arr(trim)} /ArtBox {arr(art)}", + boxes_content([media, crop, bleed, trim, art]), +) +# B: only a fractional media box. +mb = [0, 0, 300.6, 120.4] +b = pdf(f"/MediaBox {arr(mb)}", boxes_content([mb])) +# C: media box with an offset origin and /Rotate 90. +mc = [100, 50, 400, 150] +c = pdf(f"/MediaBox {arr(mc)} /Rotate 90", boxes_content([mc])) +# D: media + trim + art, no crop box (bleed around the artboard). +md, td, ad = [0, 0, 418, 218], [9, 9, 409, 209], [40, 30, 250, 150] +d = pdf(f"/MediaBox {arr(md)} /TrimBox {arr(td)} /BleedBox {arr(md)} /ArtBox {arr(ad)}", boxes_content([md, td, ad])) + +for name, data in [("i3099-a-boxes.ai", a), ("i3099-b-frac.ai", b), ("i3099-c-rot.ai", c), ("i3099-d-trim.ai", d)]: + with open(os.path.join(out, name), "wb") as f: + f.write(data) + print(name, len(data)) diff --git a/crates/aep/tests/it/export/built.rs b/crates/aep/tests/it/export/built.rs index 14f9d8795..0da683cc4 100644 --- a/crates/aep/tests/it/export/built.rs +++ b/crates/aep/tests/it/export/built.rs @@ -83,6 +83,45 @@ fn ntsc_and_odd_frame_rates_use_the_time_bases_after_effects_writes() { } } +/// #3099: After Effects reads a null's absent Anchor Point as its top-left corner `[0, 0]`. A +/// null anchored at its centre is written (it was left out as "the default" and came back at the +/// corner); one at the corner is not, and both import back as they were. +#[test] +fn null_anchor_points_round_trip() { + let mut p = Project::default(); + let c = add_comp(&mut p, "C", 800, 600, FrameRate::new(30, 1), 2.0, None); + add_layer(&mut p, c, "Corner", LayerSource::Null, (100, 100)); + add_layer(&mut p, c, "Centre", LayerSource::Null, (100, 100)); + for (idx, anchor) in [(0, [50.0, 50.0, 0.0]), (1, [0.0, 0.0, 0.0])] { + with_layer(&mut p, c, idx, |l| { + if let Some(a) = l.props.prop_mut("transform/anchor") { + a.value = Value::Vec3(anchor); + } + }); + } + let r = export_all(&p); + let file = parse(&r.bytes).expect("parse"); + let view = effectcraft_aep::Project::new(&file).expect("project"); + let comp = view.all_items().into_iter().find(|i| i.comp().is_some()).expect("comp"); + let stored: Vec<_> = + comp.comp().expect("c").layers().map(|l| (l.name().unwrap_or_default(), l.transform(800, 600).expect("transform").anchor_fraction.source)).collect(); + assert_eq!(stored, [("Centre".to_string(), effectcraft_aep::ValueSource::Stored), ("Corner".to_string(), effectcraft_aep::ValueSource::Default)]); + let mut back = Project::default(); + let none = |_: &str| false; + let res = effectcraft_aep::import::import( + &mut back, + &r.bytes, + &effectcraft_aep::import::ImportOptions { allow_losses: true, ..effectcraft_aep::import::ImportOptions::new(&none) }, + ) + .expect("import"); + let comp = res.comps.first().and_then(|id| back.comp(*id)).expect("comp"); + for (name, want) in [("Centre", [50.0, 50.0]), ("Corner", [0.0, 0.0])] { + let l = comp.layers.iter().find(|l| l.name == name).expect("layer"); + let a = l.props.prop("transform/anchor").expect("anchor").value.as_vec2(); + assert!(close(a[0], want[0]) && close(a[1], want[1]), "{name}: {a:?}"); + } +} + #[test] fn layer_header_fields() { let mut p = Project::default(); diff --git a/crates/aep/tests/it/import.rs b/crates/aep/tests/it/import.rs index eb6963d6f..2535edcff 100644 --- a/crates/aep/tests/it/import.rs +++ b/crates/aep/tests/it/import.rs @@ -4,6 +4,7 @@ mod helpers; mod aepx; +mod ai_mask; mod blend_modes; mod cameras; mod cases; diff --git a/crates/aep/tests/it/import/ai_mask.rs b/crates/aep/tests/it/import/ai_mask.rs new file mode 100644 index 000000000..e5f57abe0 --- /dev/null +++ b/crates/aep/tests/it/import/ai_mask.rs @@ -0,0 +1,113 @@ +//! A placed Illustrator file with masks under two parented Nulls (#3099), against After Effects +//! 2026's own read-back of the committed project `tests/fixtures/ai-mask-3099.aep` (generator: +//! `tests/fixtures/tools/ai-mask-3099.py` and `ai-mask-3099.jsx`). Most of these guard After +//! Effects' values the importer already read so (the drawing of the page is tested in +//! `effectcraft-media` and the host's `vector_import` tests); the Anchor Point of the null left +//! at its default (`[0, 0]`, not the centre) was wrong before #3099. +//! +//! The four `.ai` files are hand-written one-page PDFs with distinct page boxes. After Effects +//! sized them by their ArtBox (a, d), by the MediaBox rounded up (b: 300.6 × 120.4 → 301 × 121) +//! and ignored `/Rotate 90` (c). Composition `ai-mask` (1920 × 1080): `art` (a) and `artD` (d, +//! Continuously Rasterize) are children of Null `N2`, itself a child of Null `N1`; both carry two +//! rectangular masks; `artC` (c) is on its own. + +use effectcraft_aep::import::{ImportOptions, import}; +use effectcraft_keyframe::Value; +use effectcraft_project::{ItemKind, Layer, Project}; + +use crate::import::helpers::comp; + +const AI_MASK: &[u8] = include_bytes!("../../fixtures/ai-mask-3099.aep"); + +/// The fixture imported into an empty project (losses allowed, no footage file present). +fn load() -> (Project, effectcraft_aep::import::ImportResult) { + let mut project = Project::default(); + let none = |_: &str| false; + let r = import(&mut project, AI_MASK, &ImportOptions { allow_losses: true, ..ImportOptions::new(&none) }).unwrap(); + (project, r) +} + +/// The first two components of the static value of property `path` of `l`. +fn vec2(l: &Layer, path: &str) -> [f64; 2] { + let v = l.props.prop(path).unwrap_or_else(|| panic!("{}: {path}", l.name)).value.as_vec2(); + [v[0], v[1]] +} + +/// `a` and `b` agree within float32 noise. +fn near(a: [f64; 2], b: [f64; 2]) -> bool { + (a[0] - b[0]).abs() < 1e-3 && (a[1] - b[1]).abs() < 1e-3 +} + +/// The four `.ai` items have After Effects' sizes and are placed pages. +#[test] +fn placed_ai_sizes_are_after_effects_sizes() { + let (p, _) = load(); + let mut sizes: Vec<(String, (u32, u32))> = p + .items + .values() + .filter_map(|i| match &i.kind { + ItemKind::Footage(f) if f.path.ends_with(".ai") => { + // Framed and drawn as After Effects places the page (#3099). + assert!(f.placed_page, "{}", i.name); + Some((i.name.clone(), (f.width, f.height))) + } + _ => None, + }) + .collect(); + sizes.sort(); + assert_eq!( + sizes, + [ + ("i3099-a-boxes.ai".to_string(), (241, 121)), + ("i3099-b-frac.ai".to_string(), (301, 121)), + ("i3099-c-rot.ai".to_string(), (300, 100)), + ("i3099-d-trim.ai".to_string(), (210, 120)), + ] + ); +} + +/// Parents, transforms (the default null anchor at `[0, 0]`) and both masks of each art layer +/// as After Effects read them back. +#[test] +fn masks_anchors_and_parents_are_after_effects_values() { + let (p, r) = load(); + let (_, c) = comp(&p, &r, "ai-mask"); + let layer = |name: &str| c.layers.iter().find(|l| l.name == name).unwrap_or_else(|| panic!("no layer {name}")); + let (art, art_d, art_c, n1, n2) = (layer("art"), layer("artD"), layer("artC"), layer("N1"), layer("N2")); + // Parents: art, artD -> N2 -> N1. + assert_eq!(art.parent, Some(n2.id)); + assert_eq!(art_d.parent, Some(n2.id)); + assert_eq!(n2.parent, Some(n1.id)); + assert_eq!(n1.parent, None); + assert_eq!(art_c.parent, None); + // Transforms as After Effects read them back (anchor = half the source size for footage). + for (l, anchor, position, scale) in [ + (art, [120.5, 60.5], [0.0, 0.0], [100.0, 100.0]), + (art_d, [105.0, 60.0], [300.0, 250.0], [100.0, 100.0]), + (art_c, [150.0, 50.0], [1500.0, 800.0], [100.0, 100.0]), + (n1, [0.0, 0.0], [700.0, 400.0], [80.0, 120.0]), + (n2, [10.0, 20.0], [50.0, -30.0], [150.0, 150.0]), + ] { + assert!(near(vec2(l, "transform/anchor"), anchor), "{} anchor {:?}", l.name, vec2(l, "transform/anchor")); + assert!(near(vec2(l, "transform/position"), position), "{} position {:?}", l.name, vec2(l, "transform/position")); + assert!(near(vec2(l, "transform/scale"), scale), "{} scale {:?}", l.name, vec2(l, "transform/scale")); + } + match n1.props.prop("transform/rotation").map(|p| &p.value) { + Some(Value::Scalar(v)) => assert!((v - 15.0).abs() < 1e-9, "N1 rotation {v}"), + other => panic!("N1 rotation {other:?}"), + } + // Both art layers: M1 (40, 30)-(200, 90) and M2 (220, 100)-(350, 160) in layer pixels, the + // second reaching past the layer, as After Effects read the vertices back. + let rects = [[[40.0, 30.0], [200.0, 30.0], [200.0, 90.0], [40.0, 90.0]], [[220.0, 100.0], [350.0, 100.0], [350.0, 160.0], [220.0, 160.0]]]; + for l in [art, art_d] { + let masks: Vec<_> = l.masks().expect("masks").groups().collect(); + assert_eq!(masks.len(), 2, "{}", l.name); + for (m, want) in masks.iter().zip(&rects) { + let Some(Value::Path(path)) = m.get("path").map(|p| &p.value) else { panic!("{}: {} has no path", l.name, m.name) }; + assert_eq!(path.vertices.len(), 4, "{}: {}", l.name, m.name); + for (v, w) in path.vertices.iter().zip(want) { + assert!(near(*v, *w), "{}: {} vertex {v:?} != {w:?}", l.name, m.name); + } + } + } +} diff --git a/crates/engine/src/cmd/create.rs b/crates/engine/src/cmd/create.rs index 6c4b02c87..250ca836d 100644 --- a/crates/engine/src/cmd/create.rs +++ b/crates/engine/src/cmd/create.rs @@ -5,7 +5,7 @@ use effectcraft_color::Label; use effectcraft_keyframe::Value as KV; use effectcraft_project::build::{self, Ids}; -use effectcraft_project::{ItemKind, LayerSource, Solid}; +use effectcraft_project::{ItemKind, LayerSource, MatteKind, Solid, TrackMatte}; use serde_json::{Value, json}; use super::{CommandSpec, bad, has_layers, layers_p, str_p}; @@ -13,6 +13,8 @@ use crate::{EngineError, Result, Session, cmd}; const OPS: &str = "editableText|shapesFromText|masksFromText|shapesFromVector|instance3d"; +/// Layer ▸ Create (`layer.create`): the `op` ([`OPS`]) on the given or selected layers; returns +/// the created layer ids and what was skipped. fn create(s: &mut Session, p: &Value) -> Result { let op = str_p(p, "op").ok_or_else(|| bad("layer.create", format!("missing `op` ({OPS})")))?; let (cid, lids) = layers_p(s, p)?; @@ -99,8 +101,11 @@ fn create(s: &mut Session, p: &Value) -> Result { continue; }; let bytes = s.services.read_file(&f.path).map_err(|e| EngineError::Other(format!("cannot read {}: {e}", f.path)))?; - let doc = crate::vector::vector_doc(&f.path, &bytes, f.layer.as_ref(), f.page) + // In the source layer's pixels, as the footage is drawn (a placed page of an After + // Effects project fills the stored size, #3099). + let doc = crate::vector::footage_doc(&f, &bytes) .unwrap_or_else(|| Err(format!("{}: not a vector file", f.path))) + .map(|d| crate::vector::fitted_to_footage(d, &f)) .map_err(EngineError::Other)?; // Images become footage layers (PNG files written next to the source and // imported), parented to the shape layer below it. @@ -123,7 +128,7 @@ fn create(s: &mut Session, p: &Value) -> Result { // Without images: one shape layer. With images: the shapes between them // in layers of their own, so the stack keeps the document's paint order. let segments = if image_items.is_empty() { vec![doc.clone()] } else { crate::vector::split_at_images(&doc) }; - let top = crate::vector::shapes_from_vector(proj, &comp, &src, segments.last().unwrap_or(&doc)); + let mut top = crate::vector::shapes_from_vector(proj, &comp, &src, segments.last().unwrap_or(&doc)); // Top to bottom: the last segment, image n, segment n − 1, … image 1, segment 0. let mut stack = vec![]; for k in (0..segments.len().saturating_sub(1)).rev() { @@ -140,9 +145,25 @@ fn create(s: &mut Session, p: &Value) -> Result { sl.name = format!("{} {}", top.name, k + 1); stack.push(sl); } + // A placed page shows only its frame (the footage's raster ends there): every + // layer made from it takes a hidden frame layer as its track matte (#3099). + let matte = crate::vector::frame_matte(proj, &comp, &src, &f); + if let Some(m) = &matte { + let tm = Some(TrackMatte { layer: m.id, kind: MatteKind::Alpha }); + top.track_matte = tm; + for l in &mut stack { + l.track_matte = tm; + } + } let mut ids = vec![top.id]; ids.extend(stack.iter().filter(|l| matches!(l.source, LayerSource::Shape)).map(|l| l.id)); - // Each directly above the source: the top layer first. + // The frame matte is made too (last: the selection stays on the top layer). + ids.extend(matte.as_ref().map(|m| m.id)); + // Each directly above the source: the frame matte (when there is one) above + // all, then the top layer first. + if let Some(m) = matte { + insert_above(proj, cid, lid, m, true)?; + } insert_above(proj, cid, lid, top, true)?; for l in stack { insert_above(proj, cid, lid, l, true)?; diff --git a/crates/engine/src/footage/footage_reload.rs b/crates/engine/src/footage/footage_reload.rs index e56e16a96..5a0576f10 100644 --- a/crates/engine/src/footage/footage_reload.rs +++ b/crates/engine/src/footage/footage_reload.rs @@ -176,9 +176,11 @@ fn psd_layer(s: &Session, path: &str, l: &SourceLayer) -> Option<(u32, Option<(u Some((u32::try_from(found.index).ok()?, size)) } +/// The pixel size of vector footage `f` as its file is now (its page, framing and layer), or +/// `None` when the file cannot be read or shows nothing. fn vector_size(s: &Session, f: &Footage) -> Option<(u32, u32)> { let bytes = s.services.read_file(&f.path).ok()?; - let doc = crate::vector::vector_doc(&f.path, &bytes, f.layer.as_ref(), f.page)?.ok()?; + let doc = crate::vector::footage_doc(f, &bytes)?.ok()?; (!doc.root.children.is_empty()).then(|| doc.pixel_size()) } @@ -229,6 +231,10 @@ fn refreshed(s: &Session, f: &Footage, probed: Footage) -> Footage { // The layer is gone from the file: keep the item as it was. None => (w, h) = (f.width, f.height), }, + // A placed page of an After Effects project keeps the size After Effects stored: the + // layers' anchors and masks are measured against it, and the page is drawn to fill it + // (the probe would measure the page as a viewer frames it, #3099). + None if f.placed_page => (w, h) = (f.width, f.height), // A later page of a PDF: the probe measures the first page. None if f.page != 0 => (w, h) = (f.width, f.height), None => {} diff --git a/crates/engine/src/interchange/vector.rs b/crates/engine/src/interchange/vector.rs index 324d3a82a..b52517cb3 100644 --- a/crates/engine/src/interchange/vector.rs +++ b/crates/engine/src/interchange/vector.rs @@ -261,15 +261,73 @@ pub fn is_vector(f: &effectcraft_project::Footage) -> bool { effectcraft_render::is_vector_footage(f) || f.path.to_ascii_lowercase().ends_with(".svg") } +/// `doc` (the document of footage `f`) in the footage's layer pixels, as the renderer draws it +/// ([`effectcraft_render::vector_fit`]): a placed page of an After Effects project is scaled to +/// fill the size stored for it; other footage is unchanged. +pub fn fitted_to_footage(mut doc: Doc, f: &effectcraft_project::Footage) -> Doc { + let (_, [sx, sy]) = effectcraft_render::vector_fit(f, [doc.width, doc.height], doc.pixel_size(), 1.0); + if f.placed_page { + doc.root.transform = Affine::scale_non_uniform(sx, sy) * doc.root.transform; + (doc.width, doc.height) = (doc.width * sx, doc.height * sy); + } + doc +} + +/// Create Shapes from Vector Layer on a placed page of an After Effects project: a hidden shape +/// layer filling the frame the footage is drawn in (its `width` × `height` raster, in the layer +/// pixels of [`fitted_to_footage`]'s document), to be the Alpha track matte of every layer made +/// from the page, so bleed, art past the artboard and the page's background stay hidden as on +/// the footage (#3099). `None` for other footage, whose shapes are converted whole as before. +/// +/// A track matte is drawn at the composition's resolution, so its edge is the footage's raster +/// edge on shape and image layers alike; a mask on an image layer would be drawn at the image's +/// own resolution and blur its edge when the image is scaled up. +pub fn frame_matte(proj: &mut Project, comp: &Comp, src: &Layer, f: &effectcraft_project::Footage) -> Option { + if !f.placed_page || f.width == 0 || f.height == 0 { + return None; + } + let svg = format!( + r##""##, + f.width, f.height + ); + let doc = effectcraft_svg::parse(svg.as_bytes()).ok()?; + let mut l = build::layer(proj, comp, &format!("{} Frame", src.name), LayerSource::Shape, (0, 0), None); + add_doc_shapes(proj, &mut l, &doc); + copy_transform(proj, src, &mut l); + like_source(&mut l, src); + // The layers it mattes carry the source's opacity already. + if let Some(o) = l.props.prop_mut("transform/opacity") { + o.value = Value::Scalar(100.0); + o.keys.clear(); + o.expr = None; + } + l.name = comp.unique_layer_name(&format!("{} Frame", src.name)); + l.switches.video = false; + Some(l) +} + +/// The vector document of footage `f` (its file read as `bytes`; `None` for other formats): its +/// page, framed as [`effectcraft_project::Footage::placed_page`] says, restricted to its layer. +pub fn footage_doc(f: &effectcraft_project::Footage, bytes: &[u8]) -> Option> { + let framing = if f.placed_page { effectcraft_pdf::Framing::Placed } else { effectcraft_pdf::Framing::Viewer }; + vector_doc(&f.path, bytes, f.layer.as_ref(), f.page, framing) +} + /// The vector document of an SVG / PDF / AI / EPS file (`None` for other formats): page -/// `page` of a PDF, restricted to one layer when `layer` names it. -pub fn vector_doc(path: &str, bytes: &[u8], layer: Option<&effectcraft_project::SourceLayer>, page: u32) -> Option> { +/// `page` of a PDF framed as `framing` says, restricted to one layer when `layer` names it. +pub fn vector_doc( + path: &str, + bytes: &[u8], + layer: Option<&effectcraft_project::SourceLayer>, + page: u32, + framing: effectcraft_pdf::Framing, +) -> Option> { if path.to_ascii_lowercase().ends_with(".svg") || effectcraft_svg::looks_like_svg(bytes) { return Some(effectcraft_svg::parse(bytes).map_err(|e| format!("{path}: {e}"))); } effectcraft_pdf::sniff(bytes)?; Some( - effectcraft_pdf::parse_page(bytes, page as usize) + effectcraft_pdf::parse_page_framed(bytes, page as usize, framing) .map(|d| match layer { Some(l) => effectcraft_pdf::layer_doc_with_sizes(&d, l.index as usize, l.layer_size).0, None => d, diff --git a/crates/host/tests/it/vector_import.rs b/crates/host/tests/it/vector_import.rs index badd60b66..eff5b4b4b 100644 --- a/crates/host/tests/it/vector_import.rs +++ b/crates/host/tests/it/vector_import.rs @@ -466,6 +466,11 @@ fn psd_smart_objects_with_perspective_and_warp_bake_as_placed() { /// A PDF with two placed images (one turned a quarter) between a background and a shape. fn image_pdf() -> Vec { + image_pdf_with("") +} + +/// [`image_pdf`] with `boxes` added to the page dictionary. +fn image_pdf_with(boxes: &str) -> Vec { let content = "0.9 0.9 0.9 rg 0 0 120 80 re f\nq 60 0 0 30 10 40 cm /Im Do Q\nq 0 40 -20 0 110 10 cm /Im Do Q\n0 0 1 rg 20 5 30 20 re f\n"; // 8×8 pixels in quadrants: red, green / white, black. let quad = |x: usize, y: usize| -> [u8; 3] { [[[255, 0, 0], [0, 255, 0]], [[255, 255, 255], [0, 0, 0]]][y / 4][x / 4] }; @@ -474,7 +479,7 @@ fn image_pdf() -> Vec { &[ (1, "<< /Type /Catalog /Pages 2 0 R >>".into(), None), (2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), None), - (3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 120 80] /Resources << /XObject << /Im 5 0 R >> >> /Contents 4 0 R >>".into(), None), + (3, format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 120 80] {boxes} /Resources << /XObject << /Im 5 0 R >> >> /Contents 4 0 R >>"), None), (4, "<< >>".into(), Some(content.as_bytes().to_vec())), (5, "<< /Type /XObject /Subtype /Image /Width 8 /Height 8 /ColorSpace /DeviceRGB /BitsPerComponent 8 >>".into(), Some(px)), ], @@ -546,6 +551,355 @@ fn retain_pdf(width: u32) -> Vec { ) } +/// A 200 × 100 pt page: red left half, blue right half. +fn halves_pdf() -> Vec { + effectcraft_pdf::write::pdf( + &[ + (1, "<< /Type /Catalog /Pages 2 0 R >>".into(), None), + (2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), None), + (3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 100] /Contents 4 0 R >>".into(), None), + (4, "<< >>".into(), Some(b"1 0 0 rg 0 0 100 100 re f 0 0 1 rg 100 0 100 100 re f".to_vec())), + ], + 1, + ) +} + +/// #3099: footage from an After Effects project ([`effectcraft_project::Footage::placed_page`]) +/// keeps the size After Effects stored for a placed .ai, which can differ from the page as read +/// here. The page fills that size at full and at reduced resolution alike: it was drawn at its own +/// aspect at full resolution and stretched to the stored size below 3/4 (the art jumped when the +/// viewer changed resolution, and the masks, stored against that size, came out too large at +/// full resolution). Two placed items of one file at different stored sizes each get their own +/// pixels, and neither reuses the file's frame at its own size, decoded first. +#[test] +fn placed_vector_footage_fills_its_stored_size_at_every_resolution() { + use effectcraft_project::ItemKind; + use effectcraft_time::Tick; + + let path = tmp("stored-size.ai"); + std::fs::write(&path, halves_pdf()).unwrap(); + let mut s = effectcraft_host::session(); + let item = ItemId(s.execute_checked("file.import", json!({"paths": [path]})).unwrap()["items"][0].as_u64().unwrap()); + let red = |p: [f32; 4]| p[3] > 0.99 && p[0] > 0.9 && p[2] < 0.1; + let blue = |p: [f32; 4]| p[3] > 0.99 && p[2] > 0.9 && p[0] < 0.1; + // At the file's own size first (its frame goes to the pool's cache). + s.execute("comp.new", json!({"name": "Own", "width": 200, "height": 100, "frameRate": 25, "duration": 1})).unwrap(); + s.execute_checked("layer.addItem", json!({"item": item.0})).unwrap(); + let own_comp = s.active_comp_id().unwrap(); + let own = s.render(own_comp, Tick::ZERO, RenderOpts::default()); + assert!(red(own.get(50, 90)) && blue(own.get(150, 90))); + // Two placed items of the file with the sizes an After Effects project stores (other aspects). + let proj = std::sync::Arc::make_mut(&mut s.project); + let ItemKind::Footage(f) = &mut proj.item_mut(item).unwrap().kind else { panic!("footage") }; + (f.width, f.height, f.placed_page) = (100, 80, true); + let tall = effectcraft_project::Footage { width: 60, height: 120, ..f.clone() }; + let tall = proj.add_item("tall", Default::default(), None, ItemKind::Footage(tall)); + for (item, (w, h)) in [(item, (100u32, 80u32)), (tall, (60, 120))] { + s.execute("comp.new", json!({"name": format!("Stored {w}"), "width": w, "height": h, "frameRate": 25, "duration": 1})).unwrap(); + let lid = s.execute_checked("layer.addItem", json!({"item": item.0})).unwrap()["layer"].as_u64().unwrap(); + let cid = s.active_comp_id().unwrap(); + let (l, r, top, bottom) = (i64::from(w / 4), i64::from(w * 3 / 4), 3, i64::from(h) - 4); + for collapse in [false, true] { + s.execute("layer.setSwitch", json!({"layers": [lid], "switch": "collapse", "value": collapse})).unwrap(); + let full = s.render(cid, Tick::ZERO, RenderOpts::default()); + assert_eq!((full.width, full.height), (w, h)); + for y in [top, bottom] { + let (a, b) = (full.get(l, y), full.get(r, y)); + assert!(red(a) && blue(b), "{w}×{h}, collapse {collapse}, full resolution, row {y}: {a:?} {b:?}"); + } + let half = s.render(cid, Tick::ZERO, RenderOpts { scale: 0.5, ..RenderOpts::default() }); + assert_eq!((half.width, half.height), (w / 2, h / 2)); + for y in [top / 2, bottom / 2] { + let (a, b) = (half.get(l / 2, y), half.get(r / 2, y)); + assert!(red(a) && blue(b), "{w}×{h}, collapse {collapse}, half resolution, row {y}: {a:?} {b:?}"); + } + } + // Create Shapes from Vector Layer draws the shapes where the footage is drawn. + s.execute_checked("layer.create", json!({"op": "shapesFromVector", "layers": [lid]})).unwrap(); + s.execute("layer.setSwitch", json!({"layers": [lid], "switch": "video", "value": false})).unwrap(); + let shapes = s.render(cid, Tick::ZERO, RenderOpts::default()); + for y in [top, bottom] { + let (a, b) = (shapes.get(l, y), shapes.get(r, y)); + assert!(red(a) && blue(b), "{w}×{h}, shapes, row {y}: {a:?} {b:?}"); + } + } +} + +/// #3099 review: Create Shapes from Vector Layer on a placed page keeps the page's frame. The +/// footage's raster shows only the ArtBox (241 × 121) of `art_box_ai()`, whose grey media box +/// reaches past it on every side; in a composition larger than the footage, the outlines show +/// the same colours inside the frame and nothing (the background) outside it. Without the clip +/// they showed the whole page: the grey background around the art. +#[test] +fn shapes_from_a_placed_page_stay_inside_its_frame() { + use effectcraft_project::ItemKind; + use effectcraft_time::Tick; + + let path = tmp("placed-frame.ai"); + std::fs::write(&path, art_box_ai()).unwrap(); + let mut s = effectcraft_host::session(); + let item = ItemId(s.execute_checked("file.import", json!({"paths": [path]})).unwrap()["items"][0].as_u64().unwrap()); + // As an After Effects project stores it: the ArtBox's size, placed. + let ItemKind::Footage(f) = &mut std::sync::Arc::make_mut(&mut s.project).item_mut(item).unwrap().kind else { panic!("footage") }; + (f.width, f.height, f.placed_page) = (241, 121, true); + s.execute("comp.new", json!({"name": "Big", "width": 400, "height": 300, "frameRate": 25, "duration": 1})).unwrap(); + let lid = s.execute_checked("layer.addItem", json!({"item": item.0})).unwrap()["layer"].as_u64().unwrap(); + let cid = s.active_comp_id().unwrap(); + // The layer is centred: layer pixel (x, y) is comp (x + 79.5, y + 89.5). + let at = |img: &Image, x: f64, y: f64| img.get((x + 79.5).floor() as i64, (y + 89.5).floor() as i64); + let red = |p: [f32; 4]| p[3] > 0.99 && p[0] > 0.9 && p[1] < 0.1 && p[2] < 0.1; + let blue = |p: [f32; 4]| p[3] > 0.99 && p[2] > 0.9 && p[0] < 0.1 && p[1] < 0.1; + let empty = |p: [f32; 4]| p[0] < 0.02 && p[1] < 0.02 && p[2] < 0.02; + let inside = [([20.0, 20.0], true), ([110.0, 100.0], true), ([130.0, 20.0], false), ([230.0, 110.0], false)]; + // Outside the frame, where the page's grey media box would show: left, right, above, below. + let outside = [[-20.0, 60.0], [-55.0, 100.0], [260.0, 60.0], [300.0, 10.0], [120.0, -20.0], [120.0, 140.0]]; + let check = |img: &Image, what: &str| { + for (p, want_red) in inside { + let px = at(img, p[0], p[1]); + assert!(if want_red { red(px) } else { blue(px) }, "{what}: inside the frame at {p:?}: {px:?}"); + } + for p in outside { + let px = at(img, p[0], p[1]); + assert!(empty(px), "{what}: outside the frame at {p:?}: {px:?}"); + } + }; + check(&s.render(cid, Tick::ZERO, RenderOpts::default()), "footage"); + s.execute_checked("layer.create", json!({"op": "shapesFromVector", "layers": [lid]})).unwrap(); + let comp = s.project.comp(cid).unwrap(); + assert!(!comp.layers.iter().find(|l| l.id.0 == lid).unwrap().switches.video, "the source is switched off"); + check(&s.render(cid, Tick::ZERO, RenderOpts::default()), "shapes"); +} + +/// #3099 review: the images of a placed page become footage layers clipped to the frame too. +/// `image_pdf_with` an ArtBox [20 15 100 70]: the upright image reaches past the frame's left +/// edge, the turned one past its right and bottom edges, the grey page around it. After the +/// conversion the layers inside the frame match the footage, and outside it nothing shows. +#[test] +fn images_from_a_placed_page_stay_inside_its_frame() { + use effectcraft_project::{ItemKind, MatteKind, TrackMatte}; + use effectcraft_time::Tick; + + let path = tmp("placed-images.pdf"); + std::fs::write(&path, image_pdf_with("/ArtBox [20 15 100 70]")).unwrap(); + let mut s = effectcraft_host::session(); + let item = ItemId(s.execute_checked("file.import", json!({"paths": [path]})).unwrap()["items"][0].as_u64().unwrap()); + let ItemKind::Footage(f) = &mut std::sync::Arc::make_mut(&mut s.project).item_mut(item).unwrap().kind else { panic!("footage") }; + (f.width, f.height, f.placed_page) = (80, 55, true); + s.execute("comp.new", json!({"name": "Big", "width": 200, "height": 150, "frameRate": 25, "duration": 1})).unwrap(); + let lid = s.execute_checked("layer.addItem", json!({"item": item.0})).unwrap()["layer"].as_u64().unwrap(); + let cid = s.active_comp_id().unwrap(); + // Centred: layer pixel (x, y) is comp (x + 60, y + 47.5). + let at = |img: &Image, x: f64, y: f64| img.get((x + 60.0).floor() as i64, (y + 47.5).floor() as i64); + let empty = |p: [f32; 4]| p[0] < 0.02 && p[1] < 0.02 && p[2] < 0.02; + // Inside: both images and the grey page; outside: the images past the frame, the grey page. + let inside = [[10.0, 8.0], [40.0, 8.0], [10.0, 22.0], [40.0, 22.0], [75.0, 25.0], [75.0, 50.0], [5.0, 40.0], [60.0, 10.0]]; + let outside = [[-5.0, 15.0], [85.0, 40.0], [75.0, 58.0], [-15.0, 50.0], [40.0, -5.0]]; + let footage = s.render(cid, Tick::ZERO, RenderOpts::default()); + for p in outside { + assert!(empty(at(&footage, p[0], p[1])), "footage outside the frame at {p:?}: {:?}", at(&footage, p[0], p[1])); + } + let r = s.execute_checked("layer.create", json!({"op": "shapesFromVector", "layers": [lid]})).unwrap(); + assert_eq!(r["skipped"], json!([]), "{r}"); + // One hidden frame layer is the Alpha track matte of every layer made from the page. + let comp = s.project.comp(cid).unwrap(); + let frame = comp.layers.iter().find(|l| l.name.ends_with(" Frame")).expect("the frame matte layer"); + assert!(!frame.switches.video); + let made: Vec<_> = comp.layers.iter().filter(|l| l.id != frame.id && l.id.0 != lid).collect(); + assert_eq!(made.iter().filter(|l| l.name == "Image").count(), 2); + for l in &made { + assert_eq!(l.track_matte, Some(TrackMatte { layer: frame.id, kind: MatteKind::Alpha }), "{}", l.name); + } + let shapes = s.render(cid, Tick::ZERO, RenderOpts::default()); + for p in inside { + let (a, b) = (at(&shapes, p[0], p[1]), at(&footage, p[0], p[1])); + assert!((0..4).all(|k| (a[k] - b[k]).abs() < 0.05), "inside the frame at {p:?}: {a:?} vs footage {b:?}"); + } + for p in outside { + assert!(empty(at(&shapes, p[0], p[1])), "shapes outside the frame at {p:?}: {:?}", at(&shapes, p[0], p[1])); + } +} + +/// A PDF with an ArtBox, imported here (not from an After Effects project), and a project saved +/// with it before #3099: shown as a PDF viewer frames it (the crop box, 410 × 210) at its own +/// size, exactly as before; the project file has no new field. +#[test] +fn a_pdf_with_an_art_box_imported_here_keeps_the_viewer_framing() { + use effectcraft_project::{ItemKind, Project}; + use effectcraft_time::Tick; + + let bytes = art_box_ai(); + let path = tmp("viewer-art-box.pdf"); + std::fs::write(&path, &bytes).unwrap(); + let mut s = effectcraft_host::session(); + let item = ItemId(s.execute_checked("file.import", json!({"paths": [path]})).unwrap()["items"][0].as_u64().unwrap()); + let ItemKind::Footage(f) = &s.project.item(item).unwrap().kind else { panic!("footage") }; + assert_eq!((f.width, f.height, f.placed_page), (410, 210, false)); + s.execute("comp.new", json!({"name": "V", "width": 410, "height": 210, "frameRate": 25, "duration": 1})).unwrap(); + s.execute_checked("layer.addItem", json!({"item": item.0})).unwrap(); + let cid = s.active_comp_id().unwrap(); + let direct = effectcraft_svg::rasterize(&effectcraft_pdf::parse(&bytes).unwrap(), 410, 210, 1.0); + assert!(mean_diff(&s.render(cid, Tick::ZERO, RenderOpts::default()), &direct) < 1.0 / 255.0); + let saved = s.project.to_json(); + assert!(!saved.contains("placed_page"), "the saved project is in the earlier format"); + s.project = std::sync::Arc::new(Project::from_json(&saved).unwrap()); + assert!(mean_diff(&s.render(cid, Tick::ZERO, RenderOpts::default()), &direct) < 1.0 / 255.0, "reopened"); +} + +/// `i3099-a-boxes.ai` of `effectcraft-aep`'s `ai-mask-3099.aep` (its boxes as in +/// `tests/fixtures/tools/ai-mask-3099.py`): MediaBox 420 × 220, CropBox, BleedBox and TrimBox +/// inside it, ArtBox [60 50 300.6 170.4]. Here grey over the media box, the art box red on its +/// left half and blue on its right half. +fn art_box_ai() -> Vec { + let page = "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 420 220] /CropBox [5 5 415 215] /BleedBox [10 10 410 210] /TrimBox [20 20 400 200] /ArtBox [60 50 300.6 170.4] /Contents 4 0 R >>"; + let content = b"0.5 g 0 0 420 220 re f 1 0 0 rg 60 50 120.3 120.4 re f 0 0 1 rg 180.3 50 120.3 120.4 re f"; + effectcraft_pdf::write::pdf( + &[ + (1, "<< /Type /Catalog /Pages 2 0 R >>".into(), None), + (2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), None), + (3, page.into(), None), + (4, "<< >>".into(), Some(content.to_vec())), + ], + 1, + ) +} + +/// `i3099-d-trim.ai` of `ai-mask-3099.aep`: MediaBox 418 × 218, TrimBox, ArtBox [40 30 250 150] +/// (no CropBox). Grey over the media box, the art box red on its left half, blue on its right. +fn trim_ai() -> Vec { + let page = "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 418 218] /TrimBox [9 9 409 209] /BleedBox [0 0 418 218] /ArtBox [40 30 250 150] /Contents 4 0 R >>"; + let content = b"0.5 g 0 0 418 218 re f 1 0 0 rg 40 30 105 120 re f 0 0 1 rg 145 30 105 120 re f"; + effectcraft_pdf::write::pdf( + &[ + (1, "<< /Type /Catalog /Pages 2 0 R >>".into(), None), + (2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), None), + (3, page.into(), None), + (4, "<< >>".into(), Some(content.to_vec())), + ], + 1, + ) +} + +/// `i3099-c-rot.ai` of `ai-mask-3099.aep`: MediaBox [100 50 400 150] with `/Rotate 90`, grey, +/// with a blue 20 × 10 marker 4 pt in from the media box's lower-left corner. +fn rotated_ai() -> Vec { + effectcraft_pdf::write::pdf( + &[ + (1, "<< /Type /Catalog /Pages 2 0 R >>".into(), None), + (2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), None), + (3, "<< /Type /Page /Parent 2 0 R /MediaBox [100 50 400 150] /Rotate 90 /Contents 4 0 R >>".into(), None), + (4, "<< >>".into(), Some(b"0.5 g 100 50 300 100 re f 0 0 1 rg 104 54 20 10 re f".to_vec())), + ], + 1, + ) +} + +/// #3099 end to end, against After Effects 2026's own read-back of the committed project +/// `ai-mask-3099.aep`: a placed .ai (ArtBox inside a larger crop box) parented to a Null parented +/// to a Null, with two rectangular masks. After Effects sizes the footage 241 × 121 (the ArtBox) +/// and puts the layer's corners at the comp points below (`toComp`). The art fills the layer +/// between the masks at full and at half resolution; it was drawn from the crop box (410 × 210) +/// scaled to the layer's width, so the masks cut the wrong part of it and half resolution +/// squeezed it, and the Null left at its default anchor moved it. `artC` (`/Rotate 90`) is drawn +/// unturned as After Effects draws it, `artD` (Continuously Rasterize) as `art`. +#[test] +fn aep_ai_layer_with_masks_under_two_nulls_renders_like_after_effects() { + use effectcraft_engine::render::EvalCtx; + use effectcraft_project::ItemKind; + use effectcraft_time::Tick; + + let dir = std::path::PathBuf::from(tmp("ai-mask-3099")); + std::fs::create_dir_all(&dir).unwrap(); + let aep = dir.join("ai-mask-3099.aep"); + // The fixture belongs to `effectcraft-aep` (its sidecar and generator are there); this test + // renders it through the wired session, which that crate cannot. + std::fs::write(&aep, include_bytes!("../../../aep/tests/fixtures/ai-mask-3099.aep")).unwrap(); + let mut s = effectcraft_host::session(); + s.execute_checked("file.importAep", json!({"path": aep.to_string_lossy(), "allowLosses": true})).unwrap(); + // The project's footage paths are neutral (`C:\fixture\x\...`): the files are linked here. + for (name, bytes, size) in + [("i3099-a-boxes.ai", art_box_ai(), (241, 121)), ("i3099-c-rot.ai", rotated_ai(), (300, 100)), ("i3099-d-trim.ai", trim_ai(), (210, 120))] + { + let file = dir.join(name); + std::fs::write(&file, bytes).unwrap(); + let item = s + .project + .items + .values() + .find(|i| matches!(&i.kind, ItemKind::Footage(f) if f.path.ends_with(name))) + .map(|i| i.id) + .unwrap_or_else(|| panic!("footage {name}")); + let ItemKind::Footage(f) = &mut std::sync::Arc::make_mut(&mut s.project).item_mut(item).unwrap().kind else { panic!("footage") }; + assert_eq!((f.width, f.height, f.placed_page), (size.0, size.1, true), "{name}: After Effects' size of the placed .ai"); + (f.path, f.missing) = (file.to_string_lossy().into_owned(), false); + } + let cid = s.project.items.values().find(|i| i.name == "ai-mask").map(|i| i.id).expect("comp ai-mask"); + + // After Effects' toComp of the art layer's points (layer pixels → comp pixels). + let ae = [ + ([0.0, 0.0], [634.193413913741, 195.085717365354]), + ([241.0, 0.0], [913.539162876539, 269.936185209003]), + ([0.0, 121.0], [577.822625890412, 405.464362331113]), + ([40.0, 30.0], [666.58162514008, 259.669026149884]), + ([350.0, 160.0], [965.342375965623, 581.976354279664]), + ]; + let comp = s.project.comp(cid).unwrap(); + let art = comp.layer_by_name("art").unwrap(); + let (m, _) = EvalCtx::new(&s.project, cid, comp, Tick::ZERO).layer_to_comp(art); + let to_comp = |[u, v]: [f64; 2]| [m.0[0][0] * u + m.0[0][1] * v + m.0[0][2], m.0[1][0] * u + m.0[1][1] * v + m.0[1][2]]; + for (p, want) in ae { + let got = to_comp(p); + assert!((got[0] - want[0]).abs() < 1e-3 && (got[1] - want[1]).abs() < 1e-3, "{p:?}: {got:?} vs After Effects {want:?}"); + } + + // Inside the masks (M1: x 40..200, y 30..90; M2: x 220..350, y 100..160, cut by the layer). + let red = |p: [f32; 4]| p[0] > 0.9 && p[1] < 0.1 && p[2] < 0.1; + let blue = |p: [f32; 4]| p[2] > 0.9 && p[0] < 0.1 && p[1] < 0.1; + let full = s.render(cid, Tick::ZERO, RenderOpts::default()); + let half = s.render(cid, Tick::ZERO, RenderOpts { scale: 0.5, ..RenderOpts::default() }); + for (p, want_red) in [([60.0, 45.0], true), ([100.0, 75.0], true), ([150.0, 40.0], false), ([190.0, 80.0], false), ([232.0, 112.0], false)] { + let [x, y] = to_comp(p); + let at = |img: &Image, k: f64| img.get((x * k).round() as i64, (y * k).round() as i64); + for (img, k, what) in [(&full, 1.0, "full"), (&half, 0.5, "half")] { + let px = at(img, k); + let ok = if want_red { red(px) } else { blue(px) }; + assert!(ok, "{what} resolution, layer point {p:?}: {px:?} (want {})", if want_red { "red" } else { "blue" }); + } + } + // Outside the masks the art is cut away: the black background. + for p in [[100.0, 110.0], [20.0, 60.0]] { + let [x, y] = to_comp(p); + let px = full.get(x.round() as i64, y.round() as i64); + assert!(px[0] < 0.05 && px[1] < 0.05 && px[2] < 0.05, "layer point {p:?}: {px:?}"); + } + // artC (/Rotate 90, position (1500, 800), anchor (150, 50)): After Effects draws the page + // unturned, 300 × 100, the marker at its lower left (comp 1354..1374, 836..846). + for (img, k, what) in [(&full, 1.0, "full"), (&half, 0.5, "half")] { + let at = |x: f64, y: f64| img.get((x * k).round() as i64, (y * k).round() as i64); + let marker = at(1364.0, 841.0); + assert!(blue(marker), "{what} resolution, artC marker: {marker:?}"); + for (x, y) in [(1450.0, 780.0), (1630.0, 790.0), (1364.0, 770.0)] { + let grey = at(x, y); + assert!(grey[3] > 0.99 && (grey[0] - grey[2]).abs() < 0.02 && grey[0] > 0.1 && grey[0] < 0.9, "{what} resolution, artC at ({x}, {y}): {grey:?}"); + } + } + for (x, y) in [(1340.0, 800.0), (1500.0, 870.0)] { + let px = full.get(x as i64, y as i64); + assert!(px[0] < 0.05 && px[2] < 0.05, "outside artC ({x}, {y}): {px:?}"); + } + // artD (ArtBox 210 × 120, Continuously Rasterize, the same masks, also under N2): placed by + // After Effects' toComp of its corners. + let (tl, tr, bl) = ([883.191424310055, 728.610562914909], [1126.6047325349, 793.832962280744], [827.286510567911, 937.250541393348]); + let d_to_comp = |[u, v]: [f64; 2]| [0, 1].map(|i| tl[i] + u / 210.0 * (tr[i] - tl[i]) + v / 120.0 * (bl[i] - tl[i])); + for (p, want_red) in [([60.0, 45.0], true), ([90.0, 75.0], true), ([150.0, 40.0], false), ([190.0, 80.0], false)] { + let [x, y] = d_to_comp(p); + for (img, k, what) in [(&full, 1.0, "full"), (&half, 0.5, "half")] { + let px = img.get((x * k).round() as i64, (y * k).round() as i64); + let ok = if want_red { red(px) } else { blue(px) }; + assert!(ok, "artD, {what} resolution, layer point {p:?}: {px:?} (want {})", if want_red { "red" } else { "blue" }); + } + } +} + #[test] fn ai_composition_retains_layer_sizes_and_positions() { use effectcraft_project::{ItemKind, LayerSource, Project}; diff --git a/crates/media/src/layered.rs b/crates/media/src/layered.rs index b259c8b7f..64ade9394 100644 --- a/crates/media/src/layered.rs +++ b/crates/media/src/layered.rs @@ -14,19 +14,20 @@ fn is_svg(path: &str, bytes: &[u8]) -> bool { } /// The vector document of an SVG / PDF / AI / EPS file (`None` for other formats): page -/// `page` of a PDF, restricted to the footage's layer when it names one. +/// `page` of a PDF framed as `framing` says, restricted to the footage's layer when it names one. pub fn vector_doc( path: &str, bytes: &[u8], layer: Option<&effectcraft_project::SourceLayer>, page: u32, + framing: effectcraft_pdf::Framing, ) -> Option> { if is_svg(path, bytes) { return Some(effectcraft_svg::parse(bytes).map_err(|e| format!("{path}: {e}"))); } effectcraft_pdf::sniff(bytes)?; Some( - effectcraft_pdf::parse_page(bytes, page as usize) + effectcraft_pdf::parse_page_framed(bytes, page as usize, framing) .map(|d| match layer { Some(l) => effectcraft_pdf::layer_doc_with_sizes(&d, l.index as usize, l.layer_size).0, None => d, @@ -111,14 +112,14 @@ pub(crate) fn decode(path: &str, bytes: &[u8], footage: &Footage, op: AlphaOp) - return Ok(Some(straight_to_image(px.width, px.height, &px.data, op))); } if is_svg(path, bytes) || is_pdf_like(path, bytes) { - let doc = vector_doc(path, bytes, footage.layer.as_ref(), footage.page).unwrap_or(Err(String::new())).map_err(MediaError::Decode)?; - let (w, h) = doc.pixel_size(); + let doc = footage_doc(path, bytes, footage).unwrap_or(Err(String::new())).map_err(MediaError::Decode)?; + let ((w, h), to_image) = fit(&doc, footage, 1.0); // A declared size up to 30000² would be a 14 GB allocation that aborts the app (#2136): // past what Continuously Rasterize can draw, the footage reports an error instead. if u64::from(w) * u64::from(h) > u64::from(MAX_VECTOR_SIDE) * u64::from(MAX_VECTOR_SIDE) { return Err(MediaError::Decode(format!("{path}: {w}×{h} pixels is larger than vector footage can be drawn ({MAX_VECTOR_SIDE}×{MAX_VECTOR_SIDE})"))); } - let img = effectcraft_svg::rasterize(&doc, w, h, 1.0); + let img = effectcraft_svg::rasterize_with(&doc, w, h, to_image); // The rasteriser produces premultiplied pixels already. let _ = op; return Ok(Some(img)); @@ -129,12 +130,29 @@ pub(crate) fn decode(path: &str, bytes: &[u8], footage: &Footage, op: AlphaOp) - /// The largest side vector footage is drawn at, decoded or continuously rasterised. const MAX_VECTOR_SIDE: u32 = 16384; -/// A vector file rasterised at `scale` × its pixel size (Continuously Rasterize). -pub(crate) fn rasterize_vector(path: &str, bytes: &[u8], layer: Option<&effectcraft_project::SourceLayer>, page: u32, scale: f64) -> Option { - let doc = vector_doc(path, bytes, layer, page)?.ok()?; - let (w, h) = doc.pixel_size(); - let (sw, sh) = (((w as f64 * scale).ceil() as u32).clamp(1, MAX_VECTOR_SIDE), ((h as f64 * scale).ceil() as u32).clamp(1, MAX_VECTOR_SIDE)); - Some(effectcraft_svg::rasterize(&doc, sw, sh, scale)) +/// The vector footage `footage` (of the file `path` with these `bytes`) rasterised at `scale` × +/// its size as [`effectcraft_render::vector_fit`] draws it (Continuously Rasterize). +pub(crate) fn rasterize_vector(path: &str, bytes: &[u8], footage: &Footage, scale: f64) -> Option { + let doc = footage_doc(path, bytes, footage)?.ok()?; + let ((sw, sh), to_image) = fit(&doc, footage, scale); + Some(effectcraft_svg::rasterize_with(&doc, sw.clamp(1, MAX_VECTOR_SIDE), sh.clamp(1, MAX_VECTOR_SIDE), to_image)) +} + +/// The page framing of vector footage: as After Effects places it for footage from an After +/// Effects project ([`Footage::placed_page`]), as a PDF viewer shows it otherwise. +fn framing(footage: &Footage) -> effectcraft_pdf::Framing { + if footage.placed_page { effectcraft_pdf::Framing::Placed } else { effectcraft_pdf::Framing::Viewer } +} + +/// [`vector_doc`] of `footage`'s page, layer and framing. +fn footage_doc(path: &str, bytes: &[u8], footage: &Footage) -> Option> { + vector_doc(path, bytes, footage.layer.as_ref(), footage.page, framing(footage)) +} + +/// The image size and document → image mapping of `doc` drawn for `footage` at `scale`. +fn fit(doc: &effectcraft_svg::Doc, footage: &Footage, scale: f64) -> ((u32, u32), effectcraft_svg::Affine) { + let (size, [sx, sy]) = effectcraft_render::vector_fit(footage, [doc.width, doc.height], doc.pixel_size(), scale); + (size, effectcraft_svg::Affine::scale_non_uniform(sx, sy)) } #[cfg(test)] @@ -154,4 +172,65 @@ mod tests { let img = decode("small.svg", small, &footage, AlphaOp::new(footage.alpha, footage.premul_color)).unwrap().unwrap(); assert_eq!((img.width, img.height), (40, 20)); } + + /// A 200 × 100 pt page: red left half, blue right half. + fn halves_pdf() -> Vec { + effectcraft_pdf::write::pdf( + &[ + (1, "<< /Type /Catalog /Pages 2 0 R >>".into(), None), + (2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), None), + (3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 100] /Contents 4 0 R >>".into(), None), + (4, "<< >>".into(), Some(b"1 0 0 rg 0 0 100 100 re f 0 0 1 rg 100 0 100 100 re f".to_vec())), + ], + 1, + ) + } + + /// An opaque red pixel. + fn is_red(p: [f32; 4]) -> bool { + p[3] > 0.99 && p[0] > 0.9 && p[2] < 0.1 + } + + /// An opaque blue pixel. + fn is_blue(p: [f32; 4]) -> bool { + p[3] > 0.99 && p[2] > 0.9 && p[0] < 0.1 + } + + /// #3099: an After Effects project keeps the size After Effects measured for a placed .ai or + /// PDF, which can differ from this reader's page size. A placed page fills that size (the + /// layer's anchor and masks are measured against it), decoded and continuously rasterised; + /// other footage is drawn at the page's own size as before, whatever size it says. + #[test] + fn placed_vector_footage_fills_the_footage_size() { + let pdf = halves_pdf(); + let mut footage = probe("art.ai", &pdf).unwrap().unwrap(); + assert_eq!((footage.width, footage.height), (200, 100)); + assert!(!footage.placed_page); + let op = AlphaOp::new(footage.alpha, footage.premul_color); + let own = decode("art.ai", &pdf, &footage, op).unwrap().unwrap(); + assert_eq!((own.width, own.height), (200, 100)); + assert!(is_red(own.get(50, 50)) && is_blue(own.get(150, 50))); + + (footage.width, footage.height) = (100, 80); + let unplaced = decode("art.ai", &pdf, &footage, op).unwrap().unwrap(); + assert!(unplaced == own, "not placed: drawn as before"); + assert_eq!(rasterize_vector("art.ai", &pdf, &footage, 0.5).map(|i| (i.width, i.height)), Some((100, 50))); + + footage.placed_page = true; + let img = decode("art.ai", &pdf, &footage, op).unwrap().unwrap(); + assert_eq!((img.width, img.height), (100, 80)); + for (x, y) in [(25, 3), (25, 76)] { + assert!(is_red(img.get(x, y)), "red at ({x}, {y}): {:?}", img.get(x, y)); + } + for (x, y) in [(75, 3), (75, 76)] { + assert!(is_blue(img.get(x, y)), "blue at ({x}, {y}): {:?}", img.get(x, y)); + } + + let sharp = rasterize_vector("art.ai", &pdf, &footage, 2.0).unwrap(); + assert_eq!((sharp.width, sharp.height), (200, 160)); + assert!(is_red(sharp.get(50, 155)) && is_blue(sharp.get(150, 155))); + // Placed at the page's own size: 1:1. + (footage.width, footage.height) = (200, 100); + assert!(decode("art.ai", &pdf, &footage, op).unwrap().unwrap() == own, "placed at its own size: 1:1"); + } } diff --git a/crates/media/src/pool.rs b/crates/media/src/pool.rs index a60048b66..cd6676220 100644 --- a/crates/media/src/pool.rs +++ b/crates/media/src/pool.rs @@ -42,14 +42,16 @@ fn colour_bars(w: u32, h: u32) -> Image { struct Key { path: Arc, frame: i64, - /// The alpha interpretation (bits 0–1) and how float values are encoded (bits 2–3: sRGB, - /// as stored, a 2.4 power curve): they change the decoded pixels. + /// The alpha interpretation (bits 0–1), how float values are encoded (bits 2–3: sRGB, as + /// stored, a 2.4 power curve) and a placed PDF / Illustrator page's framing (bit 4, + /// [`Footage::placed_page`]): they change the decoded pixels. interp: u8, matte: [u32; 3], /// A movie frame made at this size for a reduced-resolution render ((0, 0): as decoded). size: (u32, u32), /// A still at a reduced `size`: the footage's nominal size, which decides whether it is - /// resampled (see `Inner::still_at_size`); (0, 0) otherwise. + /// resampled (see `Inner::still_at_size`); also at its own size for a placed PDF / + /// Illustrator page, which is drawn to fill it ([`Footage::placed_page`]); (0, 0) otherwise. nominal: (u32, u32), } @@ -756,6 +758,8 @@ impl Inner { ((d.0 as i128 * rate.num as i128 + den / 2) / den) as i64 } + /// Where the frame of `footage` at `t` comes from (file, frame or media time) and its cache + /// key, at the footage's own size. fn locate(footage: &Footage, t: Tick, float: FloatCurve) -> Loc { let alpha = match footage.alpha { AlphaMode::Straight => 0, @@ -768,7 +772,8 @@ impl Inner { FloatCurve::Srgb => 0, FloatCurve::Linear => 1, FloatCurve::Gamma24 => 2, - } << 2); + } << 2) + | (u8::from(footage.placed_page) << 4); let matte = if footage.alpha == AlphaMode::Premultiplied { footage.premul_color.map(f32::to_bits) } else { [0; 3] }; let key = |path: &str, frame| Key { path: path.into(), frame, interp, matte, size: (0, 0), nominal: (0, 0) }; match footage.kind { @@ -818,6 +823,11 @@ impl Inner { loc.key.nominal = (footage.width, footage.height); } } + // A placed PDF / Illustrator page is drawn to fill the footage's size (`layered::decode`, + // #3099): two items of one file stored at different sizes have different pixels. + if footage.placed_page && !movie { + loc.key.nominal = (footage.width, footage.height); + } let mut c = lock(&self.cache); // sequential playback of a movie: read the next frame ahead if playback && movie { @@ -1041,7 +1051,7 @@ impl FootageSource for MediaPool { return None; } let bytes = self.inner.read(&footage.path).ok()?; - crate::layered::rasterize_vector(&footage.path, &bytes, footage.layer.as_ref(), footage.page, scale).map(Arc::new) + crate::layered::rasterize_vector(&footage.path, &bytes, footage, scale).map(Arc::new) } fn set_aux_footage(&self, paths: &[String]) { diff --git a/crates/pdf/README.md b/crates/pdf/README.md index 20f655611..eb960cc6e 100644 --- a/crates/pdf/README.md +++ b/crates/pdf/README.md @@ -29,7 +29,13 @@ ffmpeg elsewhere) to check the code tables against an independent implementation the app's existing object model. References retain object and generation numbers. A bounded compatibility repair recovers referenced objects from object streams when damaged xrefs omit them; every recovery or unresolved reference is reported in `Doc::skipped` (up to 64 messages). - The existing page tree preserves inherited resources, MediaBox / CropBox and `/Rotate`. + The existing page tree preserves inherited resources, MediaBox / CropBox and `/Rotate`. A page + is its CropBox (else MediaBox), turned by `/Rotate` (`Framing::Viewer`, the default). Footage + from an After Effects project is framed as After Effects places a PDF or `.ai` + (`Framing::Placed`, `parse_page_framed`): the page's own ArtBox clipped to the CropBox (else + MediaBox) when it has one, else the CropBox, else the MediaBox, with `/Rotate` ignored + (measured in After Effects 2026 with own files, #3099). Not measured: a CropBox without an + ArtBox, files saved by Illustrator itself, `.pdf` names. Content/image filters remain app-owned: Flate (with PNG predictors), LZW, ASCIIHex, ASCII85, RunLength and CCITTFax. Encrypted files remain refused; `page_count` returns zero when the reader refuses a document. diff --git a/crates/pdf/src/lib.rs b/crates/pdf/src/lib.rs index 567dca9c0..ff914e759 100644 --- a/crates/pdf/src/lib.rs +++ b/crates/pdf/src/lib.rs @@ -92,8 +92,25 @@ pub fn parse(bytes: &[u8]) -> Result { /// Parse page `index` of a PDF (EPS files have one page). pub fn parse_page(bytes: &[u8], index: usize) -> Result { + parse_page_framed(bytes, index, Framing::Viewer) +} + +/// Which region of a PDF page becomes the document, and whether `/Rotate` turns it. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum Framing { + /// As a PDF viewer shows the page: its CropBox (else MediaBox), turned by `/Rotate`. + #[default] + Viewer, + /// As After Effects places a PDF or `.ai` as footage: the page's ArtBox clipped to the + /// CropBox (else MediaBox) when it has one, else the CropBox, else the MediaBox, with + /// `/Rotate` ignored (measured in After Effects 2026 with own files, #3099). + Placed, +} + +/// [`parse_page`] with the page framed as `framing` says (EPS files are their bounding box). +pub fn parse_page_framed(bytes: &[u8], index: usize, framing: Framing) -> Result { match sniff(bytes) { - Some(Format::Pdf) => parse_pdf(bytes, index), + Some(Format::Pdf) => parse_pdf(bytes, index, framing), Some(Format::Eps) => parse_eps(bytes), None => Err(Error::NotVector), } @@ -142,17 +159,22 @@ fn finish(nodes: Vec, flags: Vec, skipped: Vec, width: f64, Doc { width, height, root, skipped } } -fn parse_pdf(bytes: &[u8], index: usize) -> Result { +/// Page `index` of a PDF framed as `framing` says, as a document in points (y down). +fn parse_pdf(bytes: &[u8], index: usize, framing: Framing) -> Result { let file = object::File::parse(bytes)?; let pages = page::pages(&file); if pages.is_empty() { return Err(Error::NoPages); } let pg = pages.get(index).ok_or(Error::PageOutOfRange(index))?; - let [x0, y0, x1, y1] = pg.bbox; + let (frame, rotate) = match framing { + Framing::Viewer => (pg.bbox, pg.rotate), + Framing::Placed => (pg.art.unwrap_or(pg.bbox), 0), + }; + let [x0, y0, x1, y1] = frame; let (w, h) = (x1 - x0, y1 - y0); // Default user space (y up) → document pixels (y down), with the page's /Rotate. - let (to_doc, dw, dh) = match pg.rotate { + let (to_doc, dw, dh) = match rotate { 90 => (Affine::new([0.0, 1.0, 1.0, 0.0, -y0, -x0]), h, w), 180 => (Affine::new([-1.0, 0.0, 0.0, 1.0, x1, -y0]), w, h), 270 => (Affine::new([0.0, -1.0, -1.0, 0.0, y1, x1]), h, w), diff --git a/crates/pdf/src/page.rs b/crates/pdf/src/page.rs index b7eb70fe0..0ebc0421d 100644 --- a/crates/pdf/src/page.rs +++ b/crates/pdf/src/page.rs @@ -23,6 +23,9 @@ pub struct Page { pub resources: Dict, /// CropBox (else MediaBox) `[x0, y0, x1, y1]`. pub bbox: [f64; 4], + /// The page's own ArtBox clipped to [`Page::bbox`] (ISO 32000-1 §14.11.2), when it has one + /// that keeps an area there; not inherited (table 30). + pub art: Option<[f64; 4]>, pub rotate: i32, } @@ -31,13 +34,15 @@ pub fn pages(file: &File) -> Vec { let mut out = vec![]; let Some(cat) = file.catalog() else { return out }; let Some(root) = file.get_dict(cat, "Pages") else { return out }; + /// Collects the pages under `node`, passing on the attributes pages inherit (resources, + /// MediaBox, CropBox, /Rotate); bounded in depth and page count. fn walk(file: &File, node: &Dict, inherited: (Option, Option>, Option>, i32), out: &mut Vec, depth: usize) { if depth > 32 || out.len() > 10_000 { return; } let res = file.get_dict(node, "Resources").cloned().or(inherited.0); - let media = file.get(node, "MediaBox").map(|m| file.nums(m)).filter(|v| v.len() == 4).or(inherited.1); - let crop = file.get(node, "CropBox").map(|m| file.nums(m)).filter(|v| v.len() == 4).or(inherited.2); + let media = file.get(node, "MediaBox").map(|m| file.nums(m)).filter(|v| is_area(v)).or(inherited.1); + let crop = file.get(node, "CropBox").map(|m| file.nums(m)).filter(|v| is_area(v)).or(inherited.2); let rot = file.get_num(node, "Rotate").map(|r| r as i32).unwrap_or(inherited.3); match file.get(node, "Kids").and_then(Obj::array) { Some(kids) if file.get(node, "Type").and_then(Obj::name) != Some("Page") => { @@ -49,8 +54,16 @@ pub fn pages(file: &File) -> Vec { } _ => { let b = crop.or(media).unwrap_or_else(|| vec![0.0, 0.0, 612.0, 792.0]); - let bbox = [b[0].min(b[2]), b[1].min(b[3]), b[0].max(b[2]), b[1].max(b[3])]; - out.push(Page { dict: node.clone(), resources: res.unwrap_or_default(), bbox, rotate: rot.rem_euclid(360) }); + let bbox = normalized(&b).unwrap_or([0.0, 0.0, 612.0, 792.0]); + // The ArtBox is the page's own (not inherited), clipped to the crop box. + let art = file + .get(node, "ArtBox") + .map(|m| file.nums(m)) + .filter(|v| is_area(v)) + .and_then(|v| normalized(&v)) + .map(|a| [a[0].max(bbox[0]), a[1].max(bbox[1]), a[2].min(bbox[2]), a[3].min(bbox[3])]) + .filter(|a| a[2] > a[0] && a[3] > a[1]); + out.push(Page { dict: node.clone(), resources: res.unwrap_or_default(), bbox, art, rotate: rot.rem_euclid(360) }); } } } @@ -58,6 +71,26 @@ pub fn pages(file: &File) -> Vec { out } +/// A box `[x0, y0, x1, y1]` (corners in any order) with finite corners and a finite span that +/// encloses an area. +fn is_area(v: &[f64]) -> bool { + match v { + [x0, y0, x1, y1] => { + let (w, h) = (x1 - x0, y1 - y0); + v.iter().all(|c| c.is_finite()) && w.is_finite() && h.is_finite() && w.abs() > 0.0 && h.abs() > 0.0 + } + _ => false, + } +} + +/// A box with its corners ordered (`x0 < x1`, `y0 < y1`), `None` unless [`is_area`]. +fn normalized(v: &[f64]) -> Option<[f64; 4]> { + match v { + [x0, y0, x1, y1] if is_area(v) => Some([x0.min(*x1), y0.min(*y1), x0.max(*x1), y0.max(*y1)]), + _ => None, + } +} + /// The page's content streams, decoded and joined. pub fn page_content(file: &File, page: &Page) -> Vec { let mut out = vec![]; diff --git a/crates/pdf/src/tests.rs b/crates/pdf/src/tests.rs index 2d09a7406..cd3eecdb2 100644 --- a/crates/pdf/src/tests.rs +++ b/crates/pdf/src/tests.rs @@ -112,6 +112,86 @@ fn pdf_forms_rotation_cmyk_and_alpha() { assert_eq!(img.get(30, 30)[3], 0.0, "form clipped to its bounding box"); } +/// A one-page PDF with a MediaBox [0 0 420 220], `pages` added to the /Pages node and `page` to +/// the page: grey over the media box, a blue 20 × 10 marker at PDF (64, 54). +fn boxes_pdf(pages: &str, page: &str) -> Vec { + let content = "0.5 g 0 0 420 220 re f 0 0 1 rg 64 54 20 10 re f"; + pdf( + &[ + (1, "<< /Type /Catalog /Pages 2 0 R >>".to_string(), None), + (2, format!("<< /Type /Pages /Kids [3 0 R] /Count 1 {pages} >>"), None), + (3, format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 420 220] {page} /Contents 4 0 R >>"), None), + (4, "<< >>".to_string(), Some(content.as_bytes().to_vec())), + ], + 1, + ) +} + +/// The first page of `bytes` framed as After Effects places it. +fn placed(bytes: &[u8]) -> Doc { + parse_page_framed(bytes, 0, Framing::Placed).unwrap() +} + +/// #3099: After Effects 2026 places a .ai or PDF as footage by the page's ArtBox and shows only +/// that region (a page with MediaBox 420 × 220, CropBox 410 × 210, TrimBox 380 × 180 and ArtBox +/// [60 50 300.6 170.4] imports as 241 × 121). `Framing::Placed` frames it so; the viewer framing +/// (the default, used by File ▸ Import) stays the CropBox. +#[test] +fn placed_framing_is_the_art_box() { + let bytes = boxes_pdf("/CropBox [5 5 415 215]", "/TrimBox [20 20 400 200] /ArtBox [60 50 300.6 170.4]"); + let doc = placed(&bytes); + assert!((doc.width - 240.6).abs() < 1e-9 && (doc.height - 120.4).abs() < 1e-9, "{} × {}", doc.width, doc.height); + assert_eq!(doc.pixel_size(), (241, 121)); + // The art box's top-left corner is the document's origin: the marker (PDF x 64..84, y 54..64) + // is at x 4..24, y 106.4..116.4 from the top. + let (w, h) = doc.pixel_size(); + let img = effectcraft_svg::rasterize(&doc, w, h, 1.0); + let marker = img.get(10, 110); + assert!(marker[2] > 0.99 && marker[0] < 0.01, "{marker:?}"); + assert!(img.get(30, 110)[2] < 0.6, "{:?}", img.get(30, 110)); + let viewer = parse(&bytes).unwrap(); + assert_eq!((viewer.width, viewer.height), (410.0, 210.0), "the viewer framing is unchanged"); +} + +/// The ArtBox is clipped to the crop box (ISO 32000-1 §14.11.2); one that keeps no area there, +/// a degenerate or unreadable one, or one on the /Pages node (not inherited, table 30) leaves the +/// crop box (else the media box). +#[test] +fn placed_framing_clips_the_art_box_and_falls_back() { + let size = |pages: &str, page: &str| { + let d = placed(&boxes_pdf(pages, page)); + (d.width, d.height) + }; + let crop = "/CropBox [5 5 415 215]"; + assert_eq!(size(crop, "/ArtBox [-100 -100 1000 1000]"), (410.0, 210.0), "larger than the page"); + assert_eq!(size(crop, "/ArtBox [300 100 900 900]"), (115.0, 115.0), "partly outside"); + let (w, h) = size(crop, "/ArtBox [300.6 170.4 60 50]"); + assert!((w - 240.6).abs() < 1e-9 && (h - 120.4).abs() < 1e-9, "corners in any order: {w} × {h}"); + // Finite corners whose span is not. + let big = format!("1{}", "0".repeat(308)); + let unreadable = [format!("/ArtBox [-{big} 50 {big} 170]")]; + for page in ["/ArtBox [500 300 600 400]", "/ArtBox [60 50 60 170]", "/ArtBox [60 50 300]"].map(String::from).iter().chain(&unreadable) { + assert_eq!(size(crop, page), (410.0, 210.0), "{page}"); + } + assert_eq!(size("/ArtBox [60 50 300 170]", ""), (420.0, 220.0), "an ArtBox on /Pages is not inherited"); + // A crop box that is not a box leaves the media box. + assert_eq!(size("", &format!("/CropBox [-{big} 5 {big} 215]")), (420.0, 220.0)); +} + +/// After Effects ignores /Rotate on placed footage (a page [100 50 400 150] with /Rotate 90 +/// imports as 300 × 100, drawn unturned); the viewer framing turns it. +#[test] +fn placed_framing_ignores_rotate() { + let bytes = boxes_pdf("", "/Rotate 90"); + let doc = placed(&bytes); + assert_eq!((doc.width, doc.height), (420.0, 220.0)); + // Unturned: the marker at PDF (64..84, 54..64) is at x 64..84, y 156..166 from the top. + let img = effectcraft_svg::rasterize(&doc, 420, 220, 1.0); + assert!(img.get(70, 160)[2] > 0.99, "{:?}", img.get(70, 160)); + let viewer = parse(&bytes).unwrap(); + assert_eq!((viewer.width, viewer.height), (220.0, 420.0)); +} + const EPS: &str = "%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\n\ /m {moveto} bind def /l {lineto} bind def\n\ /cm { 6 array astore concat } bind def\n\ diff --git a/crates/project/src/footage.rs b/crates/project/src/footage.rs index 2cf887b1f..eeace5e81 100644 --- a/crates/project/src/footage.rs +++ b/crates/project/src/footage.rs @@ -269,6 +269,13 @@ pub struct Footage { /// made from the footage starts with them, and Update Markers From Source copies new ones. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub markers: Vec, + /// PDF / Illustrator footage (`.pdf`, `.ai`; not EPS) from an After Effects project: the page + /// is framed as After Effects places it (the ArtBox, `/Rotate` ignored) and drawn to fill + /// `width` × `height`, the size After Effects stored, which the layers' anchors and masks are + /// measured against (#3099). Other footage (and every project saved before this field) shows + /// the page as a PDF viewer does, at its own size. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub placed_page: bool, } fn is_default(v: &T) -> bool { @@ -307,6 +314,7 @@ impl Default for Footage { start_timecode: None, file_timecode: None, markers: vec![], + placed_page: false, } } } diff --git a/crates/render/src/footage.rs b/crates/render/src/footage.rs index 0e17f152d..6b7fdcbf8 100644 --- a/crates/render/src/footage.rs +++ b/crates/render/src/footage.rs @@ -76,8 +76,9 @@ pub trait FootageSource: Send + Sync { fn cache_budget(&self) -> Option { None } - /// Vector footage (SVG) rasterised at `scale` × its pixel size, for Continuously Rasterize. - /// `None` when the footage is not vector or cannot be read. + /// Vector footage (SVG, PDF, Illustrator, EPS: [`is_vector_footage`]) rasterised at `scale` × + /// its size as [`vector_fit`] draws it, for Continuously Rasterize. `None` when the footage + /// is not vector or cannot be read. fn vector_frame(&self, _item: ItemId, _footage: &Footage, _scale: f64) -> Option> { None } @@ -216,9 +217,31 @@ impl FootageSource for CancellableFootage<'_> { } } -/// Footage that can be rasterised at any scale (SVG). +/// Footage that can be rasterised at any scale: SVG, PDF, Illustrator and EPS files, and a +/// placed page of an After Effects project (its codec is not stored there). pub fn is_vector_footage(f: &Footage) -> bool { - matches!(f.codec.as_str(), "SVG" | "PDF" | "AI" | "EPS") + matches!(f.codec.as_str(), "SVG" | "PDF" | "AI" | "EPS") || f.placed_page +} + +/// How vector footage `f` is drawn at `scale` × its size: the image size, and the x and y scale +/// from document pixels (`doc` = the document's width and height, `own` = its pixel size, the +/// size rounded up) to image pixels. +/// +/// Footage from an After Effects project ([`Footage::placed_page`]) fills its stored size, which +/// can round or measure the page differently: the layer's anchor and masks are measured against +/// that size, and the renderer resamples a reduced-resolution frame to it, so drawn at its own +/// size the art was scaled by the width ratio alone at full resolution (off against the masks) +/// and stretched to the stored aspect below 3/4 resolution (#3099). Other footage is drawn 1:1 at +/// the document's own size, as it always was. +pub fn vector_fit(f: &Footage, doc: [f64; 2], own: (u32, u32), scale: f64) -> ((u32, u32), [f64; 2]) { + let scale = if scale.is_finite() && scale > 0.0 { scale } else { 1.0 }; + let side = |px: u32| (f64::from(px) * scale).ceil().clamp(1.0, f64::from(u32::MAX)) as u32; + let size = (f.width, f.height); + let fills = f.placed_page && size != own && size.0 > 0 && size.1 > 0 && doc.iter().all(|d| d.is_finite() && *d > 0.0); + if !fills { + return ((side(own.0), side(own.1)), [scale, scale]); + } + ((side(size.0), side(size.1)), [scale * f64::from(size.0) / doc[0], scale * f64::from(size.1) / doc[1]]) } /// No footage available (renders footage layers as transparent). diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index e09794de2..65f3ee772 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -47,7 +47,7 @@ mod opts; mod pipeline; pub use accel::{Accelerator, Backend, FxStep, FxTarget}; -pub use footage::{CancellableFootage, FloatCurve, FootageSource, NoFootage, is_vector_footage, missing_item_problem}; +pub use footage::{CancellableFootage, FloatCurve, FootageSource, NoFootage, is_vector_footage, missing_item_problem, vector_fit}; pub use frames::{ adaptive_samples, blend_frames, cineon_frame, content_bounds, extended_region, field_frame, frame_position, frame_size, interpret_pixels, kurbo_path, render_frame, roi_inside, diff --git a/docs/aep-import.md b/docs/aep-import.md index 8a83de81f..f41ba3081 100644 --- a/docs/aep-import.md +++ b/docs/aep-import.md @@ -135,7 +135,15 @@ Units, the "absent = default" rule and the keyframe templates are the reader's text does not have; the runs are cut to the text. A range style splits `Hello` into runs of 1, 2 and 2 characters (248, 340). - **Null objects.** The hidden placeholder solid of a null object is not an item (null layers - have no source item here). + have no source item here). A null's Anchor Point that is not stored is its top-left corner + `[0, 0]`, not the centre, as After Effects 2026 reads it back (#3099). +- **Placed Illustrator and PDF files** (`.ai`, `.pdf`; `Footage::placed_page`): the page is + framed as After Effects places it (the ArtBox clipped to the crop box, `/Rotate` ignored) and + drawn to fill the stored footage size, which masks and anchors are measured against (#3099). + Create Shapes from Vector Layer on such a layer adds a hidden ` Frame` shape layer, the + Alpha track matte of every layer it makes, so they show only the frame, as the footage does. + EPS files keep their bounding box at their own size: how After Effects sizes them is not + measured. - **Track mattes.** The mode and the matte layer's id are in the target layer's header (aep-format.md 3.1). The matte layer can be any layer of the composition (407: below its target); it keeps its stored video switch (After Effects switches it off when the matte is set, diff --git a/docs/gaps.md b/docs/gaps.md index d481f9810..d9de84675 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -266,6 +266,12 @@ Issues #41–#47 (all from the Linux AppImage 0.1.1, 5 October): panning the vie The second means reading another implementation of the format, which AGENTS.md rules out unless the clean-room scope allows it. - Also in this area: relative footage paths and relinking when a project moves (gap 18). +- Placed PDF / Illustrator footage of an `.aep` is framed as After Effects places it (the ArtBox + clipped to the crop box, `/Rotate` ignored) and drawn to fill the size After Effects stored + (#3099), measured with hand-written PDFs named `.ai`. Not yet measured: files saved by + Illustrator itself (artboards, bleed, art past the artboard, private data), a CropBox without + an ArtBox, `.pdf` names, and EPS files (they keep their bounding box at their own size, not + the placed framing). - Done when: the decision is recorded and, if approved, a corpus of real-world-shaped projects opens with its comps, layers, keyframes, effects and expressions intact. From 91f35c73e1d17a45e854b7e8306e735ff151db6e Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 8 Nov 2026 20:01:20 -0800 Subject: [PATCH 4/5] Updates: restart from the update result dialog --- crates/ui-egui/src/updates_ui.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ui-egui/src/updates_ui.rs b/crates/ui-egui/src/updates_ui.rs index bb8187f56..316e34ac4 100644 --- a/crates/ui-egui/src/updates_ui.rs +++ b/crates/ui-egui/src/updates_ui.rs @@ -91,6 +91,7 @@ pub(crate) fn show(app: &mut EffectcraftApp, ctx: &egui::Context) { rt.updater.dismiss_status(); } CheckResultResponse::CheckAgain => rt.updater.check_now(ctx), + CheckResultResponse::RestartToUpdate => rt.updater.restart_to_update(), } // A check that found a release leaves the status idle and the startup prompt takes over. if matches!(rt.updater.status(), Status::Idle) && !rt.check_requested { From 544eebf90486c654c0a415c1d8547df1bad884a7 Mon Sep 17 00:00:00 2001 From: just_anshh Date: Mon, 9 Nov 2026 15:19:53 +0530 Subject: [PATCH 5/5] WebAssembly ABI 2: neighbouring frames for effect plug-ins (#2773) (#3329) * WebAssembly ABI 2: neighbouring frames for effect plug-ins (#2773) A wasm plug-in can export ec_api_version 2 and ec_render2 to receive the layer's previous and next frames, their layer times and validity flags. Neighbours come from PluginHost::input_at, are resampled onto the current grid, and are zeroed when absent. ABI 1 modules (ec_render) load and render unchanged; the manifest api stays independent. A layer with an ABI 2 plug-in keys the render cache on layer time; the Plug-in Library accepts ABI 1-2 releases. Docs, help text and tests updated. * Fix ABI 2 neighbour times on NTSC frame boundaries ime * fps is often a few ulps off an integer at rates such as 24000/1001. The old boundary test then took the bracket branch and passed the current frame back as prev (or ext). Treat a near-integer as a frame boundary within 1e-6 frames, and test 2400 frames at 24000/1001: every boundary must get the frames either side. Also document the loader helpers for the docstring coverage check. --------- Co-authored-by: justanshh <294019246+justanshh@users.noreply.github.com> --- ATTRIBUTION.md | 2 +- crates/effects/src/lib.rs | 2 +- crates/effects/src/plugin.rs | 47 +++- crates/engine/src/cmd/effect.rs | 2 +- crates/engine/src/system/library.rs | 11 +- crates/host/tests/it/plugins.rs | 131 ++++++++- crates/plugin/Cargo.toml | 2 +- crates/plugin/src/lib.rs | 22 +- crates/plugin/src/tests.rs | 179 +++++++++++- crates/plugin/src/wasm.rs | 287 +++++++++++++++++--- crates/render/src/cache.rs | 4 +- crates/render/src/tests/time.rs | 89 ++++++ docs/architecture.md | 2 +- docs/plugins.md | 49 +++- examples/plugins/posterize-bands/Cargo.toml | 2 +- examples/plugins/posterize-bands/src/lib.rs | 2 +- 16 files changed, 741 insertions(+), 92 deletions(-) diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 4bf3a044d..5bff5cdb0 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -99,7 +99,7 @@ Screenshots of EffectCraft itself, rendered headlessly, showing only procedurall (sidecars `.attribution`). - Bundled extension (ScriptUI panel) with its twelve built-in ease presets: `extensions/scriptui-panels/Ease Presets.jsx` (sidecar `.attribution`). -- Example effect plug-in: `examples/plugins/posterize-bands` (plug-in API v1, WebAssembly). +- Example effect plug-in: `examples/plugins/posterize-bands` (WebAssembly ABI 1). ## Trained models (optional downloads, not bundled) diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index 0d78c3619..771d51068 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -1514,7 +1514,7 @@ pub const TIME_DEPENDENT: &[&str] = &[ pub fn is_time_dependent(id: &str) -> bool { // Simulations replay from layer time 0, so every frame differs even when the defaults // happen to look static at the test's two sample times. - id.starts_with("ec.sim.") || TIME_DEPENDENT.contains(&id) + id.starts_with("ec.sim.") || TIME_DEPENDENT.contains(&id) || (!id.starts_with("ec.") && plugin::reads_other_times(id)) } /// Does effect `id` read its own parameters at earlier times (the stepped particle systems sample diff --git a/crates/effects/src/plugin.rs b/crates/effects/src/plugin.rs index 5c973ac57..8bbe17856 100644 --- a/crates/effects/src/plugin.rs +++ b/crates/effects/src/plugin.rs @@ -24,10 +24,12 @@ //! * rendering must be deterministic (same inputs, same output) and may run on several threads //! at once. //! -//! API 2 (native plug-ins; the WebAssembly ABI stays at 1) adds [`EffectPlugin::padding`] and -//! [`EffectPlugin::render_v2`], which gets a [`PluginHost`] to read the effect's input or other -//! layers at other times, and the extra parameter kinds. External effect helpers -//! (`effectcraft-helper-host`) are built on it. +//! API 2 (native plug-ins) adds [`EffectPlugin::padding`] and [`EffectPlugin::render_v2`], which +//! gets a [`PluginHost`] to read the effect's input or other layers at other times, and the extra +//! parameter kinds. External effect helpers (`effectcraft-helper-host`) are built on it. +//! +//! The WebAssembly ABI is versioned separately ([`WASM_API_VERSION`]): ABI 1 is `ec_render`; +//! ABI 2 adds `ec_render2`, which receives the layer's previous and next frames. use std::sync::{Arc, OnceLock, RwLock}; @@ -39,9 +41,12 @@ use crate::{Buf, EffectCtx, EffectSpec, ParamSpec, Params, RenderFn}; /// The plug-in API version this build implements (manifests with API 1 or 2 are accepted). pub const PLUGIN_API_VERSION: u32 = 2; -/// The plug-in API a WebAssembly module implements (its `ec_api_version`). API 2 is for native -/// and helper plug-ins; WebAssembly modules stay at 1. -pub const WASM_API_VERSION: u32 = 1; +/// The newest WebAssembly ABI this build implements (a module's `ec_api_version`). ABI 1 is +/// `ec_render`; ABI 2 adds `ec_render2` and the layer's neighbouring frames. This is separate +/// from the plug-in API version: a module's manifest `api` stays 1 or 2. +pub const WASM_API_VERSION: u32 = 2; +/// The oldest WebAssembly ABI this build still loads. +pub const MIN_WASM_API_VERSION: u32 = 1; /// The most plug-in effects one process can register (commercial effect suites register a few hundred each). pub const MAX_PLUGINS: usize = 1024; @@ -269,6 +274,22 @@ impl PluginImage { fn from_buf(b: Buf) -> PluginImage { PluginImage { width: b.img.width, height: b.img.height, pixels: b.img.data, origin: b.offset, scale: b.scale } } + + /// This image resampled onto a `width × height` buffer grid at `origin`/`scale` (the current + /// frame's); the pixels unchanged when the grids already match. + pub fn on_grid(&self, width: u32, height: u32, origin: [f64; 2], scale: f64) -> Vec<[f32; 4]> { + if self.width == width && self.height == height && self.origin == origin && self.scale == scale { + return self.pixels.clone(); + } + let k = self.scale / scale.max(1e-9); + let src = crate::Image { width: self.width, height: self.height, data: self.pixels.clone() }; + crate::util::gen_image(width, height, |x, y| { + let lx = (x as f64 + 0.5 - origin[0]) * k + self.origin[0]; + let ly = (y as f64 + 0.5 - origin[1]) * k + self.origin[1]; + src.sample_bilinear(lx, ly) + }) + .data + } } /// Services available during an API 2 render ([`EffectPlugin::render_v2`]). @@ -388,6 +409,12 @@ pub trait EffectPlugin: Send + Sync { fn render_v2(&self, frame: &mut PluginFrame, params: &PluginParams, time: f64, _host: &dyn PluginHost) -> Result<(), String> { self.render(frame, params, time) } + /// Whether rendering reads the layer at other times (its neighbouring frames). Default + /// false; the render cache keys a layer with such an effect on its layer time. WebAssembly + /// ABI 2 plug-ins return true. + fn reads_other_times(&self) -> bool { + false + } } static SLOTS: [OnceLock>; MAX_PLUGINS] = [const { OnceLock::new() }; MAX_PLUGINS]; @@ -432,6 +459,12 @@ pub fn plugin(id: &str) -> Option> { SLOTS.iter().filter_map(|s| s.get()).find(|p| p.manifest().id == id).cloned() } +/// Whether the registered plug-in `id` reads the layer at other times (its neighbouring frames), +/// so the render cache must key its layer on the layer time. +pub fn reads_other_times(id: &str) -> bool { + SLOTS.iter().filter_map(|s| s.get()).any(|p| p.manifest().id == id && p.reads_other_times()) +} + fn leak(s: &str) -> &'static str { Box::leak(s.to_string().into_boxed_str()) } diff --git a/crates/engine/src/cmd/effect.rs b/crates/engine/src/cmd/effect.rs index 1d352d192..1ee132955 100644 --- a/crates/engine/src/cmd/effect.rs +++ b/crates/engine/src/cmd/effect.rs @@ -1352,7 +1352,7 @@ pub fn specs() -> Vec { "Load Effect Plug-in...", [], None, - "{path (.wasm / .wat plug-in, API v1) | folder (loads every .wasm in it)}", + "{path (.wasm / .wat plug-in, WebAssembly ABI 1 or 2) | folder (loads every .wasm in it)}", super::always, plugins_load ), diff --git a/crates/engine/src/system/library.rs b/crates/engine/src/system/library.rs index 750ed600a..5b7f63092 100644 --- a/crates/engine/src/system/library.rs +++ b/crates/engine/src/system/library.rs @@ -304,7 +304,8 @@ pub fn parse_catalog(bytes: &[u8]) -> std::result::Result<(Catalog, Vec) fn runs_here(r: &Release) -> bool { // Without a pre-release suffix (`0.18.0-dev` is 0.18.0). let app = env!("CARGO_PKG_VERSION").split(['-', '+']).next().unwrap_or_default(); - r.api == crate::effects::plugin::WASM_API_VERSION && r.min_app.as_deref().is_none_or(|m| !newer(m, app)) + (crate::effects::plugin::MIN_WASM_API_VERSION..=crate::effects::plugin::WASM_API_VERSION).contains(&r.api) + && r.min_app.as_deref().is_none_or(|m| !newer(m, app)) } /// The newest release this build can run (plug-in API and minimum app version). @@ -789,12 +790,13 @@ fn pick(s: &Session, c: &str, p: &Value) -> Result<(String, Package, Release)> { return Err(bad( c, format!( - "{} {} needs plug-in API {} and EffectCraft {} or later; this is EffectCraft {} with WebAssembly plug-in API {}", + "{} {} needs WebAssembly ABI {} and EffectCraft {} or later; this is EffectCraft {} with WebAssembly ABI {} to {}", pkg.name, rel.version, rel.api, rel.min_app.as_deref().unwrap_or("(any)"), env!("CARGO_PKG_VERSION"), + crate::effects::plugin::MIN_WASM_API_VERSION, crate::effects::plugin::WASM_API_VERSION ), )); @@ -1125,8 +1127,9 @@ mod tests { ..Default::default() }; assert_eq!(latest_compatible(&p).map(|r| r.version.as_str()), Some("1.2.0")); - assert!(runs_here(&rel("1.0.0", 1, None)), "API 1 modules still run on a build with a newer plug-in API"); - assert!(!runs_here(&rel("1.0.0", 2, None)), "WebAssembly modules are API 1: an API 2 release can't load"); + assert!(runs_here(&rel("1.0.0", 1, None)), "ABI 1 modules still run on a build with a newer ABI"); + assert!(runs_here(&rel("1.0.0", 2, None)), "ABI 2 modules run on this build"); + assert!(!runs_here(&rel("1.0.0", 3, None)), "a WebAssembly ABI this build doesn't implement can't load"); let j = package_json(&p, None, &Status::default()); assert_eq!(j["needsNewerApp"], true); assert!(latest_compatible(&Package::default()).is_none()); diff --git a/crates/host/tests/it/plugins.rs b/crates/host/tests/it/plugins.rs index 7e4839e6d..33748d366 100644 --- a/crates/host/tests/it/plugins.rs +++ b/crates/host/tests/it/plugins.rs @@ -281,7 +281,7 @@ fn plugin_loading_reports_bad_modules_and_survives_failing_renders() { let files = [ ("a-good.wasm", wat_plugin("org.test.host.good", 1, "i32.const 0").into_bytes()), ("b-garbage.wasm", b"\0asm\x01\0\0\0garbage".to_vec()), - ("c-v2.wasm", wat_plugin("org.test.host.v2", 2, "i32.const 0").into_bytes()), + ("c-v3.wasm", wat_plugin("org.test.host.v3", 3, "i32.const 0").into_bytes()), ("d-empty.wasm", vec![]), ("e-trap.wasm", wat_plugin("org.test.host.trap", 1, "unreachable").into_bytes()), ("notes.txt", b"not a plug-in".to_vec()), @@ -296,7 +296,7 @@ fn plugin_loading_reports_bad_modules_and_survives_failing_renders() { let errs: Vec = r["errors"].as_array().unwrap().iter().map(|e| e["path"].as_str().unwrap().to_string()).collect(); assert_eq!(errs.len(), 3, "{r}"); assert!(errs.iter().all(|p| !p.ends_with(".txt"))); - assert!(r["errors"].to_string().contains("API 2"), "{r}"); + assert!(r["errors"].to_string().contains("ABI 3"), "{r}"); // A single file: errors come back as command errors; loading twice is refused. let good = dir.join("a-good.wasm").to_string_lossy().to_string(); assert!(s.execute("effect.plugins.load", json!({"path": good})).unwrap_err().to_string().contains("already registered")); @@ -311,3 +311,130 @@ fn plugin_loading_reports_bad_modules_and_survives_failing_renders() { assert_eq!(px(&s), before); let _ = std::fs::remove_dir_all(&dir); } + +/// An ABI 2 plug-in (WebAssembly text) that shows the neighbouring frames it got: per pixel it +/// writes |next.red − prev.red| into red, `flags / 8` into green, `prev_time` into blue and 1 +/// into alpha, so a render test can read the frames and their times back from the image. +fn wat_neighbour_plugin(id: &str) -> String { + let manifest = format!(r#"{{"api":2,"id":"{id}","name":"Host {id}","category":"Test Plug-ins","version":"1","params":[]}}"#); + let len = manifest.len(); + let escaped = manifest.replace('"', "\\\""); + format!( + r#"(module + (memory (export "memory") 2) + (data (i32.const 16) "{escaped}") + (func (export "ec_api_version") (result i32) i32.const 2) + (func (export "ec_manifest_ptr") (result i32) i32.const 16) + (func (export "ec_manifest_len") (result i32) i32.const {len}) + (func (export "ec_alloc") (param i32) (result i32) i32.const 1024) + (func (export "ec_render2") + (param $px i32) (param $w i32) (param $h i32) (param $params i32) (param $np i32) + (param $t f64) (param $scale f64) + (param $prev i32) (param $prev_t f64) (param $next i32) (param $next_t f64) (param $flags i32) + (result i32) + (local $i i32) + (local $n i32) + local.get $w + local.get $h + i32.mul + local.set $n + block $done + loop $per_pixel + local.get $i + local.get $n + i32.ge_u + br_if $done + ;; red = |next.red - prev.red| + local.get $px + local.get $i + i32.const 16 + i32.mul + i32.add + local.get $next + local.get $i + i32.const 16 + i32.mul + i32.add + f32.load + local.get $prev + local.get $i + i32.const 16 + i32.mul + i32.add + f32.load + f32.sub + f32.abs + f32.store + ;; green = flags / 8 + local.get $px + local.get $i + i32.const 16 + i32.mul + i32.add + local.get $flags + f32.convert_i32_s + f32.const 8 + f32.div + f32.store offset=4 + ;; blue = prev_time + local.get $px + local.get $i + i32.const 16 + i32.mul + i32.add + local.get $prev_t + f32.demote_f64 + f32.store offset=8 + ;; alpha = 1 + local.get $px + local.get $i + i32.const 16 + i32.mul + i32.add + f32.const 1 + f32.store offset=12 + local.get $i + i32.const 1 + i32.add + local.set $i + br $per_pixel + end + end + i32.const 0))"# + ) +} + +/// The neighbouring frames are the layer's own pixels at other times: here a precomp whose +/// nested solid fades from 0 to 100 % over one second, at 4 fps. At 0.5 s the neighbours are +/// 0.25 s and 0.75 s, so red (the current frame) was 0.5 and |next − prev| is 0.5. +#[test] +fn abi_v2_plugins_render_with_real_neighbour_frames() { + let mut s = effectcraft_host::session(); + let inner = s.execute("comp.new", json!({"name": "Inner", "width": 40, "height": 20, "frameRate": 4, "duration": 1})).unwrap()["comp"].as_u64().unwrap(); + let solid = s.execute("layer.newSolid", json!({"color": [1.0, 0.0, 0.0], "width": 40, "height": 20})).unwrap()["layer"].as_u64().unwrap(); + s.execute("prop.addKey", json!({"layer": solid, "path": "transform/opacity", "time": 0.0, "value": 0})).unwrap(); + s.execute("prop.addKey", json!({"layer": solid, "path": "transform/opacity", "time": 1.0, "value": 100})).unwrap(); + let outer = s.execute("comp.new", json!({"name": "Outer", "width": 40, "height": 20, "frameRate": 4, "duration": 1})).unwrap()["comp"].as_u64().unwrap(); + let layer = s.execute("layer.addItem", json!({"comp": outer, "item": inner, "time": 0})).unwrap()["layer"].as_u64().unwrap(); + let outer = effectcraft_engine::project::ItemId(outer); + + let dir = tempfile::TempDir::new().unwrap(); + let wasm = dir.path().join("neighbours.wasm"); + std::fs::write(&wasm, wat_neighbour_plugin("org.test.host.neighbours")).unwrap(); + let r = s.execute("effect.plugins.load", json!({"path": wasm.to_string_lossy()})).unwrap(); + assert_eq!(r["loaded"][0]["id"], "org.test.host.neighbours", "{r}"); + s.execute("effect.apply", json!({"layer": layer, "effect": "org.test.host.neighbours"})).unwrap(); + + let p = s.render(outer, Tick::from_seconds_f64(0.5), Default::default()).get(20, 10); + assert!((p[0] - 0.5).abs() < 0.02, "precomp neighbours: {p:?}"); + // Both neighbours exist, the previous one is the 0.25 s frame, and the frame is opaque. + assert!((p[1] - 3.0 / 8.0).abs() < 0.01, "flags: {p:?}"); + assert!((p[2] - 0.25).abs() < 0.01, "prev time: {p:?}"); + assert_eq!(p[3], 1.0, "{p:?}"); + + // At the very start of the layer only the next frame exists (the previous one is zeroed); + // solids are filled with a quantised colour, hence the 8-bit step. + let p = s.render(outer, Tick::ZERO, Default::default()).get(20, 10); + assert!((p[0] - 0.25).abs() < 0.01, "the next frame's red, with no previous frame: {p:?}"); + assert!((p[1] - 2.0 / 8.0).abs() < 0.01, "only the next frame is valid: {p:?}"); +} diff --git a/crates/plugin/Cargo.toml b/crates/plugin/Cargo.toml index 4081b61b6..d9b35a91a 100644 --- a/crates/plugin/Cargo.toml +++ b/crates/plugin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "effectcraft-plugin" -description = "EffectCraft effect plug-ins: loads sandboxed WebAssembly effect modules (plug-in API v1) into the effect registry" +description = "EffectCraft effect plug-ins: loads sandboxed WebAssembly effect modules (WebAssembly ABI v1 and v2) into the effect registry" version.workspace = true edition.workspace = true license.workspace = true diff --git a/crates/plugin/src/lib.rs b/crates/plugin/src/lib.rs index 7c85e144b..da21e22a0 100644 --- a/crates/plugin/src/lib.rs +++ b/crates/plugin/src/lib.rs @@ -1,21 +1,25 @@ //! # effectcraft-plugin (L3) //! -//! Loads **WebAssembly effect plug-ins** (plug-in ABI v1, see `docs/plugins.md`; the native -//! plug-in API 2 additions such as layer parameters and host services are not part of it) into the effect -//! registry ([`effectcraft_effects::plugin`]). A plug-in is a `.wasm` (or `.wat`) module with -//! no imports that exports: +//! Loads **WebAssembly effect plug-ins** (WebAssembly ABI v1 and v2, see `docs/plugins.md`; the +//! native plug-in API 2 additions such as layer parameters and host services are not part of it) +//! into the effect registry ([`effectcraft_effects::plugin`]). A plug-in is a `.wasm` (or +//! `.wat`) module with no imports that exports: //! //! | export | signature | | //! |---|---|---| //! | `memory` | memory | linear memory shared with the host | -//! | `ec_api_version` | `() -> i32` | must return 1 | +//! | `ec_api_version` | `() -> i32` | 1 for ABI 1, 2 for ABI 2 (neighbouring frames) | //! | `ec_manifest_ptr`, `ec_manifest_len` | `() -> i32` | a UTF-8 JSON [`PluginManifest`] in memory | //! | `ec_alloc` | `(bytes: i32) -> i32` | a buffer of at least `bytes` bytes, 8-byte aligned; it may reuse the previous one | -//! | `ec_render` | `(pixels: i32, width: i32, height: i32, params: i32, nparams: i32, time: f64, scale: f64) -> i32` | process the pixels in place; 0 = ok | +//! | `ec_render` (ABI 1) | `(pixels: i32, width: i32, height: i32, params: i32, nparams: i32, time: f64, scale: f64) -> i32` | process the pixels in place; 0 = ok | +//! | `ec_render2` (ABI 2) | `ec_render`'s arguments, then `prev: i32, prev_time: f64, next: i32, next_time: f64, flags: i32` | the same, with the neighbouring frames; 0 = ok | //! //! For each render the host asks `ec_alloc` for one buffer holding the parameters (`nparams` //! little-endian `f64`s) followed by the pixels (`width × height × 4` little-endian `f32`s, -//! premultiplied RGBA, row-major), calls `ec_render`, and reads the pixels back. +//! premultiplied RGBA, row-major), calls the render export, and reads the pixels back. An ABI 2 +//! module gets two more pixel buffers after the current frame: the layer's previous and next +//! frames, resampled onto the current grid, and their layer times. The buffers are always +//! allocated (zeroed when a frame does not exist); `flags` bit 0/1 says which are real. //! //! Modules run in the [wasmi](https://github.com/wasmi-labs/wasmi) interpreter (pure Rust, //! MIT/Apache-2.0) with deterministic floats, no imports (no files, network or clock) and a @@ -26,7 +30,9 @@ #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] use effectcraft_effects::EffectSpec; -pub use effectcraft_effects::plugin::{EffectPlugin, PLUGIN_API_VERSION, PluginFrame, PluginManifest, PluginParams, WASM_API_VERSION, register_plugin}; +pub use effectcraft_effects::plugin::{ + EffectPlugin, MIN_WASM_API_VERSION, PLUGIN_API_VERSION, PluginFrame, PluginImage, PluginManifest, PluginParams, WASM_API_VERSION, register_plugin, +}; #[cfg(feature = "wasm")] mod wasm; diff --git a/crates/plugin/src/tests.rs b/crates/plugin/src/tests.rs index f68fde00a..a4fadb26b 100644 --- a/crates/plugin/src/tests.rs +++ b/crates/plugin/src/tests.rs @@ -163,11 +163,15 @@ fn wasm_plugins_are_sandboxed() { // No imports (files, clock, network…). let imports = r#"(module (import "env" "now" (func $now (result f64))) (memory (export "memory") 1))"#; assert!(load_err(load_wasm(imports.as_bytes(), "imports.wat")).contains("import")); - // Garbage, wrong API version, bad manifests. + // Garbage, an ABI this build doesn't implement, bad manifests. A module that claims ABI 2 + // must export `ec_render2` (this one still has only `ec_render`). assert!(load_wasm(b"\0asm nope", "bad.wasm").is_err()); let v2 = gain_module("org.test.v2", "") .replace("(func (export \"ec_api_version\") (result i32) i32.const 1)", "(func (export \"ec_api_version\") (result i32) i32.const 2)"); - assert!(load_err(load_wasm(v2.as_bytes(), "v2.wat")).contains("API 2")); + assert!(load_err(load_wasm(v2.as_bytes(), "v2.wat")).contains("ec_render2")); + let v3 = gain_module("org.test.v3", "") + .replace("(func (export \"ec_api_version\") (result i32) i32.const 1)", "(func (export \"ec_api_version\") (result i32) i32.const 3)"); + assert!(load_err(load_wasm(v3.as_bytes(), "v3.wat")).contains("ABI 3")); let reserved = gain_module("ec.blur.mine", ""); assert!(load_err(load_wasm(reserved.as_bytes(), "reserved.wat")).contains("reserved")); } @@ -194,14 +198,14 @@ fn abi_violations_fail_to_load_with_reasons() { assert!(load_err(load_wasm(m.as_bytes(), "x.wat")).contains("ec_api_version")); let m = edit(&base, "(param $scale f64) (result i32)", "(param $scale f32) (result i32)"); assert!(load_err(load_wasm(m.as_bytes(), "x.wat")).contains("ec_render")); - // API versions other than 1. - for v in ["0", "-1", "2147483647"] { + // WebAssembly ABIs other than 1 and 2. + for v in ["0", "-1", "3", "2147483647"] { let m = edit( &base, "(func (export \"ec_api_version\") (result i32) i32.const 1)", &format!("(func (export \"ec_api_version\") (result i32) i32.const {v})"), ); - assert!(load_err(load_wasm(m.as_bytes(), "x.wat")).contains(&format!("API {v}")), "{v}"); + assert!(load_err(load_wasm(m.as_bytes(), "x.wat")).contains(&format!("ABI {v}")), "{v}"); } // Manifest out of bounds, negative or absurd lengths, not JSON. let ptr = "(func (export \"ec_manifest_ptr\") (result i32) i32.const 16)"; @@ -317,3 +321,168 @@ fn wasm_rendering_is_deterministic() { let twin = load_wasm(gain_module("org.test.det2", body).as_bytes(), "det2.wat").unwrap(); assert_eq!(bits(&render(twin, 0.37, true)), first); } + +/// An ABI 2 module whose `ec_render2` writes what it got from the host into each pixel: red is +/// |next.red − prev.red|, green `flags / 8`, blue `prev_time`, alpha 1. +fn neighbour_module(id: &str) -> String { + let manifest = format!(r#"{{"api":2,"id":"{id}","name":"Neighbours {id}","category":"Test Plug-ins","version":"1","params":[]}}"#); + let len = manifest.len(); + let escaped = manifest.replace('\\', "\\\\").replace('"', "\\\""); + format!( + r#"(module + (memory (export "memory") 2) + (data (i32.const 16) "{escaped}") + (func (export "ec_api_version") (result i32) i32.const 2) + (func (export "ec_manifest_ptr") (result i32) i32.const 16) + (func (export "ec_manifest_len") (result i32) i32.const {len}) + (func (export "ec_alloc") (param i32) (result i32) i32.const 1024) + (func (export "ec_render2") + (param $px i32) (param $w i32) (param $h i32) (param $params i32) (param $np i32) + (param $t f64) (param $scale f64) + (param $prev i32) (param $prev_t f64) (param $next i32) (param $next_t f64) (param $flags i32) + (result i32) + (local $i i32) (local $n i32) + local.get $w + local.get $h + i32.mul + local.set $n + block $done + loop $per_pixel + local.get $i + local.get $n + i32.ge_u + br_if $done + local.get $px + local.get $i + i32.const 16 + i32.mul + i32.add + local.get $next + local.get $i + i32.const 16 + i32.mul + i32.add + f32.load + local.get $prev + local.get $i + i32.const 16 + i32.mul + i32.add + f32.load + f32.sub + f32.abs + f32.store + local.get $px + local.get $i + i32.const 16 + i32.mul + i32.add + local.get $flags + f32.convert_i32_s + f32.const 8 + f32.div + f32.store offset=4 + local.get $px + local.get $i + i32.const 16 + i32.mul + i32.add + local.get $prev_t + f32.demote_f64 + f32.store offset=8 + local.get $px + local.get $i + i32.const 16 + i32.mul + i32.add + f32.const 1 + f32.store offset=12 + local.get $i + i32.const 1 + i32.add + local.set $i + br $per_pixel + end + end + i32.const 0))"# + ) +} + +fn neighbour_image(red: f32) -> effectcraft_effects::plugin::PluginImage { + effectcraft_effects::plugin::PluginImage { width: 2, height: 1, pixels: vec![[red, 0.0, 0.0, 1.0]; 2], origin: [0.0; 2], scale: 1.0 } +} + +/// A host with frames at two layer times. +struct Host { + time: f64, + fps: f64, + prev: Option<(f64, effectcraft_effects::plugin::PluginImage)>, + next: Option<(f64, effectcraft_effects::plugin::PluginImage)>, +} + +impl effectcraft_effects::plugin::PluginHost for Host { + fn input_at(&self, layer_time: f64) -> Option { + if let Some((t, img)) = &self.prev + && (t - layer_time).abs() < 1e-9 + { + return Some(img.clone()); + } + if let Some((t, img)) = &self.next + && (t - layer_time).abs() < 1e-9 + { + return Some(img.clone()); + } + None + } + + fn layer_input(&self, _param_id: &str, _layer_time: f64) -> Option { + None + } + + fn params_at(&self, _layer_time: f64) -> Option<(Vec, Vec)> { + None + } + + fn frame_rate(&self) -> f64 { + self.fps + } + + fn time(&self) -> f64 { + self.time + } +} + +#[test] +fn abi_v2_receives_neighbour_frames_and_times() { + load_wasm(neighbour_module("org.test.neighbours").as_bytes(), "neighbours.wat").unwrap(); + let p = effectcraft_effects::plugin::plugin("org.test.neighbours").unwrap(); + assert!(effectcraft_effects::plugin::reads_other_times("org.test.neighbours")); + assert!(effectcraft_effects::is_time_dependent("org.test.neighbours")); + // The module has no parameters, so the flattened vector is empty. + let params = effectcraft_effects::plugin::PluginParams { manifest: p.manifest(), flat: &[], texts: &[] }; + let mut px = vec![[0.0f32; 4]; 2]; + let host = Host { time: 0.5, fps: 4.0, prev: Some((0.25, neighbour_image(0.1))), next: Some((0.75, neighbour_image(0.6))) }; + let mut frame = effectcraft_effects::plugin::PluginFrame { width: 2, height: 1, pixels: &mut px, scale: 1.0, origin: [0.0; 2], layer_size: [2.0, 1.0] }; + p.render_v2(&mut frame, ¶ms, 0.5, &host).unwrap(); + assert!((px[0][0] - 0.5).abs() < 1e-6, "|next − prev|: {px:?}"); + assert!((px[0][1] - 3.0 / 8.0).abs() < 1e-6, "both flags: {px:?}"); + assert!((px[0][2] - 0.25).abs() < 1e-6, "previous frame's time: {px:?}"); + assert_eq!(px[0][3], 1.0); +} + +#[test] +fn abi_v2_without_a_host_gets_zeroed_neighbours() { + let spec = load_wasm(neighbour_module("org.test.neighbours.empty").as_bytes(), "empty.wat").unwrap(); + let mut next_id = 1; + let g = effectcraft_effects::instantiate(spec, &mut Ids(&mut next_id), spec.name, [2.0, 1.0]); + let params = effectcraft_effects::flatten_params(&g, &mut |p| p.value.clone()); + let ctx = EffectCtx { params: ¶ms, time: 0.5, layer_size: [2.0, 1.0], seed: 1, adjustment: false, env: EffectEnv::default() }; + let buf = Buf { img: Image::filled(2, 1, [0.5, 0.5, 0.5, 1.0]), offset: [0.0; 2], scale: 1.0 }; + // No host: both neighbour buffers are zeroed and no flag is set. + assert_eq!((spec.render)(&ctx, buf).img.get(0, 0), [0.0, 0.0, 0.0, 1.0]); + // ABI 1 plug-ins keep the time-independent behaviour. + let gain = load_wasm(gain_module("org.test.neighbours.v1", "").as_bytes(), "v1.wat").unwrap(); + assert!(!effectcraft_effects::plugin::reads_other_times("org.test.neighbours.v1")); + assert!(!effectcraft_effects::is_time_dependent("org.test.neighbours.v1")); + assert!(gain.id.starts_with("org.test.")); +} diff --git a/crates/plugin/src/wasm.rs b/crates/plugin/src/wasm.rs index 18a34548f..499d57397 100644 --- a/crates/plugin/src/wasm.rs +++ b/crates/plugin/src/wasm.rs @@ -1,10 +1,11 @@ -//! The wasmi-backed [`EffectPlugin`] (plug-in API v1 ABI, see the crate docs). +//! The wasmi-backed [`EffectPlugin`] (WebAssembly ABI v1 and v2, see the crate docs). use std::sync::Mutex; use wasmi::{Config, Engine, Instance, Linker, Memory, Module, Store, StoreLimits, StoreLimitsBuilder, TypedFunc, WasmParams, WasmResults}; -use crate::{EffectPlugin, PluginFrame, PluginManifest, PluginParams, WASM_API_VERSION}; +use crate::{EffectPlugin, MIN_WASM_API_VERSION, PluginFrame, PluginManifest, PluginParams, WASM_API_VERSION}; +use effectcraft_effects::plugin::{PluginHost, PluginImage}; /// Instruction budget per frame: a fixed allowance plus some per pixel. Generous for per-pixel /// work, but a runaway loop stops instead of hanging the render. @@ -15,32 +16,74 @@ const MAX_MEMORY: usize = 1 << 30; /// The longest manifest read. const MAX_MANIFEST: i32 = 1 << 20; +/// ABI 1: one buffer. type RenderFn = TypedFunc<(i32, i32, i32, i32, i32, f64, f64), i32>; +/// ABI 2: the same call plus the previous and next frame buffers and their layer times. +type Render2Fn = TypedFunc<(i32, i32, i32, i32, i32, f64, f64, i32, f64, i32, f64, i32), i32>; + +enum Renderer { + V1(RenderFn), + V2(Render2Fn), +} /// One instantiated module (renders running on several threads each take their own). struct Inst { + store: Store, + memory: Memory, + alloc: TypedFunc, + render: Renderer, +} + +/// An instance whose ABI version (and so its render export) is not selected yet. +struct Pending { store: Store, instance: Instance, memory: Memory, alloc: TypedFunc, - render: RenderFn, } -impl Inst { +impl Pending { + /// A typed export by name, reporting a missing or mistyped export like [`missing`]. fn func(&self, name: &str) -> Result, String> { self.instance.get_typed_func::(&self.store, name).map_err(|_| missing(name)) } + + /// Select the render export for `abi`. + fn finish(self, abi: u32) -> Result { + let render = match abi { + 1 => Renderer::V1(self.instance.get_typed_func::<_, i32>(&self.store, "ec_render").map_err(|_| missing("ec_render"))?), + 2 => Renderer::V2(self.instance.get_typed_func::<_, i32>(&self.store, "ec_render2").map_err(|_| missing("ec_render2"))?), + v => return Err(unsupported(v as i32)), + }; + Ok(Inst { store: self.store, memory: self.memory, alloc: self.alloc, render }) + } } +/// The error for a missing or mistyped export. fn missing(name: &str) -> String { format!("the module exports no `{name}` (or with the wrong signature)") } +/// The error for an ABI this build does not implement. +fn unsupported(v: i32) -> String { + format!("the module implements WebAssembly ABI {v}, this build implements ABI {MIN_WASM_API_VERSION} to {WASM_API_VERSION}") +} + +/// The module's `ec_api_version`, when this build implements it. +fn abi_of(v: i32) -> Result { + match u32::try_from(v) { + Ok(v) if (MIN_WASM_API_VERSION..=WASM_API_VERSION).contains(&v) => Ok(v), + _ => Err(unsupported(v)), + } +} + +/// The error for a wasmi or trap failure. fn err(e: impl std::fmt::Display) -> String { e.to_string() } -fn instantiate(engine: &Engine, module: &Module) -> Result { +/// Instantiate `module` in a fresh, limited store and read its memory and allocator. +fn begin(engine: &Engine, module: &Module) -> Result { let mut store = Store::new(engine, StoreLimitsBuilder::new().memory_size(MAX_MEMORY).instances(1).build()); store.limiter(|l| l); store.set_fuel(FUEL_BASE).map_err(err)?; @@ -48,14 +91,20 @@ fn instantiate(engine: &Engine, module: &Module) -> Result { let instance = linker.instantiate_and_start(&mut store, module).map_err(|e| format!("instantiation failed: {e}"))?; let memory = instance.get_memory(&store, "memory").ok_or("the module exports no `memory`")?; let alloc = instance.get_typed_func::(&store, "ec_alloc").map_err(|_| missing("ec_alloc"))?; - let render = instance.get_typed_func::<(i32, i32, i32, i32, i32, f64, f64), i32>(&store, "ec_render").map_err(|_| missing("ec_render"))?; - Ok(Inst { store, instance, memory, alloc, render }) + Ok(Pending { store, instance, memory, alloc }) +} + +/// Instantiate `module` and select the render export for `abi`. +fn instantiate(engine: &Engine, module: &Module, abi: u32) -> Result { + begin(engine, module)?.finish(abi) } /// A loaded WebAssembly effect plug-in. pub struct WasmPlugin { manifest: PluginManifest, source: String, + /// The WebAssembly ABI the module implements (`ec_api_version`). + abi: u32, engine: Engine, module: Module, /// Idle instances. @@ -63,6 +112,7 @@ pub struct WasmPlugin { } impl WasmPlugin { + /// Load `bytes` and its manifest; errors on imports, a bad manifest or a missing export. pub fn new(bytes: &[u8], source: &str) -> Result { let mut config = Config::default(); config.consume_fuel(true); @@ -71,51 +121,42 @@ impl WasmPlugin { if let Some(i) = module.imports().next() { return Err(format!("plug-ins can't import anything (the module imports `{}.{}`)", i.module(), i.name())); } - let mut inst = instantiate(&engine, &module)?; - let version = inst.func::<(), i32>("ec_api_version")?; - let v = version.call(&mut inst.store, ()).map_err(err)?; - if u32::try_from(v).ok() != Some(WASM_API_VERSION) { - return Err(format!("the module implements plug-in API {v}, this build implements API {WASM_API_VERSION} for WebAssembly")); - } - let ptr = inst.func::<(), i32>("ec_manifest_ptr")?.call(&mut inst.store, ()).map_err(err)?; - let len = inst.func::<(), i32>("ec_manifest_len")?.call(&mut inst.store, ()).map_err(err)?; + let mut pending = begin(&engine, &module)?; + let version = pending.func::<(), i32>("ec_api_version")?; + let abi = abi_of(version.call(&mut pending.store, ()).map_err(err)?)?; + let ptr = pending.func::<(), i32>("ec_manifest_ptr")?.call(&mut pending.store, ()).map_err(err)?; + let len = pending.func::<(), i32>("ec_manifest_len")?.call(&mut pending.store, ()).map_err(err)?; if !(0..=MAX_MANIFEST).contains(&len) { return Err(format!("bad manifest length {len}")); } let mut text = vec![0u8; len as usize]; - inst.memory.read(&inst.store, ptr as u32 as usize, &mut text).map_err(|e| format!("manifest out of bounds: {e}"))?; + pending.memory.read(&pending.store, ptr as u32 as usize, &mut text).map_err(|e| format!("manifest out of bounds: {e}"))?; let manifest: PluginManifest = serde_json::from_slice(&text).map_err(|e| format!("bad manifest JSON: {e}"))?; manifest.validate()?; if let Some(p) = manifest.params.iter().find(|p| p.kind.needs_v2()) { return Err(format!("parameter `{}`: WebAssembly plug-ins use plug-in API 1 parameter types (no point3, layer, text or hidden)", p.id)); } - Ok(WasmPlugin { manifest, source: source.to_string(), engine, module, pool: Mutex::new(vec![inst]) }) + let inst = pending.finish(abi)?; + Ok(WasmPlugin { manifest, source: source.to_string(), abi, engine, module, pool: Mutex::new(vec![inst]) }) } + /// An idle instance, or a fresh one when the pool is empty. fn take(&self) -> Result { if let Some(i) = self.pool.lock().ok().and_then(|mut p| p.pop()) { return Ok(i); } - instantiate(&self.engine, &self.module) + instantiate(&self.engine, &self.module, self.abi) } + /// Return a working instance to the pool. fn give_back(&self, inst: Inst) { if let Ok(mut p) = self.pool.lock() { p.push(inst); } } -} -impl EffectPlugin for WasmPlugin { - fn manifest(&self) -> &PluginManifest { - &self.manifest - } - - fn source(&self) -> String { - self.source.clone() - } - - fn render(&self, frame: &mut PluginFrame, params: &PluginParams, time: f64) -> Result<(), String> { + /// ABI 1: parameters, then the pixels; `ec_render`. + fn render_v1(&self, frame: &mut PluginFrame, params: &PluginParams, time: f64) -> Result<(), String> { let n = frame.pixels.len(); let params_bytes = params.flat.len() * 8; let total = params_bytes + n * 16; @@ -130,25 +171,19 @@ impl EffectPlugin for WasmPlugin { for v in params.flat { buf.extend_from_slice(&v.to_le_bytes()); } - for px in frame.pixels.iter() { - for c in px { - buf.extend_from_slice(&c.to_le_bytes()); - } - } + push_pixels(&mut buf, frame.pixels); inst.memory.write(&mut inst.store, base, &buf).map_err(|e| format!("ec_alloc returned a buffer out of bounds: {e}"))?; let px_ptr = (base + params_bytes) as i32; - let r = inst - .render - .call(&mut inst.store, (px_ptr, frame.width as i32, frame.height as i32, base as i32, params.flat.len() as i32, time, frame.scale)) - .map_err(|e| format!("ec_render trapped: {e}")); + let r = match &inst.render { + Renderer::V1(f) => f + .call(&mut inst.store, (px_ptr, frame.width as i32, frame.height as i32, base as i32, params.flat.len() as i32, time, frame.scale)) + .map_err(|e| format!("ec_render trapped: {e}")), + Renderer::V2(_) => return Err("the module implements WebAssembly ABI 1 but exports ec_render2".into()), + }; let r = match r { Ok(0) => { inst.memory.read(&inst.store, base + params_bytes, &mut buf[params_bytes..]).map_err(err)?; - for (px, b) in frame.pixels.iter_mut().zip(buf[params_bytes..].as_chunks::<16>().0.iter()) { - for (c, v) in px.iter_mut().zip(b.as_chunks::<4>().0.iter()) { - *c = f32::from_le_bytes([v[0], v[1], v[2], v[3]]); - } - } + read_pixels(&buf[params_bytes..], frame.pixels); Ok(()) } Ok(code) => Err(format!("ec_render returned error {code}")), @@ -159,4 +194,170 @@ impl EffectPlugin for WasmPlugin { } r } + + /// ABI 2: parameters, the current frame, then the previous and next frames; `ec_render2`. + /// The neighbour buffers are always allocated (zeroed when absent), so a module can read + /// both without a null check; `flags` says which hold real frames. + fn render_v2( + &self, + frame: &mut PluginFrame, + params: &PluginParams, + time: f64, + prev: Option<(f64, &PluginImage)>, + next: Option<(f64, &PluginImage)>, + ) -> Result<(), String> { + let n = frame.pixels.len(); + let params_bytes = params.flat.len() * 8; + let frame_bytes = n * 16; + let total = params_bytes + frame_bytes * 3; + if total > i32::MAX as usize { + return Err("frame too large for a 32-bit plug-in".into()); + } + let prev_px = prev.map(|(t, img)| (t, img.on_grid(frame.width, frame.height, frame.origin, frame.scale))); + let next_px = next.map(|(t, img)| (t, img.on_grid(frame.width, frame.height, frame.origin, frame.scale))); + let prev = prev_px.as_ref().filter(|(_, px)| px.len() == n); + let next = next_px.as_ref().filter(|(_, px)| px.len() == n); + let mut inst = self.take()?; + inst.store.set_fuel(FUEL_BASE + FUEL_PER_PIXEL * (n as u64 * 3)).map_err(err)?; + let base = inst.alloc.call(&mut inst.store, total as i32).map_err(err)? as u32 as usize; + let mut buf: Vec = Vec::with_capacity(total); + for v in params.flat { + buf.extend_from_slice(&v.to_le_bytes()); + } + push_pixels(&mut buf, frame.pixels); + push_neighbour(&mut buf, prev, frame_bytes); + push_neighbour(&mut buf, next, frame_bytes); + inst.memory.write(&mut inst.store, base, &buf).map_err(|e| format!("ec_alloc returned a buffer out of bounds: {e}"))?; + let px_ptr = (base + params_bytes) as i32; + let prev_ptr = (base + params_bytes + frame_bytes) as i32; + let next_ptr = (base + params_bytes + frame_bytes * 2) as i32; + let flags = i32::from(prev.is_some()) | (i32::from(next.is_some()) << 1); + let prev_time = prev.map_or(0.0, |(t, _)| *t); + let next_time = next.map_or(0.0, |(t, _)| *t); + let r = match &inst.render { + Renderer::V2(f) => f + .call( + &mut inst.store, + ( + px_ptr, + frame.width as i32, + frame.height as i32, + base as i32, + params.flat.len() as i32, + time, + frame.scale, + prev_ptr, + prev_time, + next_ptr, + next_time, + flags, + ), + ) + .map_err(|e| format!("ec_render2 trapped: {e}")), + Renderer::V1(_) => return Err("the module implements WebAssembly ABI 2 but exports ec_render".into()), + }; + let r = match r { + Ok(0) => { + let mut out = vec![0u8; frame_bytes]; + inst.memory.read(&inst.store, base + params_bytes, &mut out).map_err(err)?; + read_pixels(&out, frame.pixels); + Ok(()) + } + Ok(code) => Err(format!("ec_render2 returned error {code}")), + Err(e) => Err(e), + }; + if r.is_ok() { + self.give_back(inst); + } + r + } +} + +/// Append pixels to the little-endian buffer the module reads. +fn push_pixels(buf: &mut Vec, px: &[[f32; 4]]) { + for p in px { + for c in p { + buf.extend_from_slice(&c.to_le_bytes()); + } + } +} + +/// The neighbour's pixels, or `frame_bytes` zeroed bytes when there is no frame. +fn push_neighbour(buf: &mut Vec, neighbour: Option<&(f64, Vec<[f32; 4]>)>, frame_bytes: usize) { + match neighbour { + Some((_, px)) => push_pixels(buf, px), + None => buf.resize(buf.len() + frame_bytes, 0), + } +} + +/// Read the little-endian pixels the module wrote back. +fn read_pixels(bytes: &[u8], pixels: &mut [[f32; 4]]) { + for (px, b) in pixels.iter_mut().zip(bytes.as_chunks::<16>().0.iter()) { + for (c, v) in px.iter_mut().zip(b.as_chunks::<4>().0.iter()) { + *c = f32::from_le_bytes([v[0], v[1], v[2], v[3]]); + } + } +} + +/// The layer times of the frames around `time` on a `fps` grid: the frame before and after it +/// at a frame boundary, the two frames that bracket it between frames. +fn neighbour_times(time: f64, fps: f64) -> (f64, f64) { + if !fps.is_finite() || fps <= 0.0 { + return (time - 1.0 / 30.0, time + 1.0 / 30.0); + } + let step = 1.0 / fps; + let at = time * fps; + // Frame times are not exact in floating point: at NTSC rates `time * fps` often lands a few + // ulps above or below an integer. Treat a near-integer as a frame boundary, so the current + // frame is never handed back as its own neighbour. + if (at - at.round()).abs() < 1e-6 { (time - step, time + step) } else { (at.floor() / fps, at.ceil() / fps) } +} + +impl EffectPlugin for WasmPlugin { + fn manifest(&self) -> &PluginManifest { + &self.manifest + } + + fn source(&self) -> String { + self.source.clone() + } + + fn reads_other_times(&self) -> bool { + self.abi >= 2 + } + + fn render(&self, frame: &mut PluginFrame, params: &PluginParams, time: f64) -> Result<(), String> { + if self.abi == 1 { self.render_v1(frame, params, time) } else { self.render_v2(frame, params, time, None, None) } + } + + fn render_v2(&self, frame: &mut PluginFrame, params: &PluginParams, time: f64, host: &dyn PluginHost) -> Result<(), String> { + if self.abi == 1 { + return self.render_v1(frame, params, time); + } + let (prev_time, next_time) = neighbour_times(time, host.frame_rate()); + let prev = host.input_at(prev_time); + let next = host.input_at(next_time); + let prev = prev.as_ref().map(|img| (prev_time, img)); + let next = next.as_ref().map(|img| (next_time, img)); + self.render_v2(frame, params, time, prev, next) + } +} + +#[cfg(test)] +mod tests { + use super::neighbour_times; + + /// NTSC frame times do not multiply back exactly; every frame boundary must still get the + /// frames either side of it, never the frame itself. + #[test] + fn frame_boundaries_get_their_neighbours_at_ntsc_rates() { + let fps = 24000.0 / 1001.0; + for n in 0..2400 { + // 24000/1001 fps at 254 016 000 000 ticks/s, as `Tick` stores it. + let time = (n * 10_594_584_000_i64) as f64 / 254_016_000_000.0; + let (prev, next) = neighbour_times(time, fps); + assert!((prev - (time - 1.0 / fps)).abs() < 1e-9, "frame {n}: prev {prev}, current {time}"); + assert!((next - (time + 1.0 / fps)).abs() < 1e-9, "frame {n}: next {next}, current {time}"); + } + } } diff --git a/crates/render/src/cache.rs b/crates/render/src/cache.rs index 8a3d7b8ae..0756c47f2 100644 --- a/crates/render/src/cache.rs +++ b/crates/render/src/cache.rs @@ -689,7 +689,9 @@ fn reads_other_times(layer: &Layer, effects: Option) -> bool { fx.groups().take(effects.unwrap_or(usize::MAX)).any(|g| { g.enabled && matches!(&g.kind, effectcraft_project::GroupKind::Effect { effect } - if effect.starts_with("ec.time.") || effectcraft_effects::reads_param_history(effect)) + if effect.starts_with("ec.time.") + || effectcraft_effects::reads_param_history(effect) + || (!effect.starts_with("ec.") && effectcraft_effects::plugin::reads_other_times(effect))) }) }) } diff --git a/crates/render/src/tests/time.rs b/crates/render/src/tests/time.rs index 715c37ef0..f959401bb 100644 --- a/crates/render/src/tests/time.rs +++ b/crates/render/src/tests/time.rs @@ -1,7 +1,10 @@ //! Time effects through the renderer: neighbouring frames via `EffectHost::self_at`, and the //! layer cache staying correct while scrubbing. +use std::sync::Arc; + use effectcraft_color::Label; +use effectcraft_effects::plugin::{EffectPlugin, PluginFrame, PluginManifest, PluginParams, register_plugin}; use effectcraft_keyframe::{Keyframe, ShapePath, Value}; use effectcraft_project::build::{self, Ids}; use effectcraft_project::{Comp, ItemId, ItemKind, LayerSource, MaskMode, Project, Solid}; @@ -157,3 +160,89 @@ fn input_keys_ignore_later_effects() { set(&mut p, "effects/#1/blurriness", 8.0); assert_ne!(key(&p).0, one); } + +/// A plug-in that reads the layer's neighbouring frames (WebAssembly ABI 2) makes its layer's +/// cache key depend on the layer time, even when the source and every property are static. A +/// plug-in that does not keeps the old key (#1977: a static layer stays cached across a scrub). +#[test] +fn abi2_plugin_layers_key_on_layer_time() { + struct NeighbourReader { + m: PluginManifest, + } + + impl EffectPlugin for NeighbourReader { + fn manifest(&self) -> &PluginManifest { + &self.m + } + + fn render(&self, _frame: &mut PluginFrame, _params: &PluginParams, _time: f64) -> Result<(), String> { + Ok(()) + } + + fn reads_other_times(&self) -> bool { + true + } + } + + struct Plain { + m: PluginManifest, + } + + impl EffectPlugin for Plain { + fn manifest(&self) -> &PluginManifest { + &self.m + } + + fn render(&self, _frame: &mut PluginFrame, _params: &PluginParams, _time: f64) -> Result<(), String> { + Ok(()) + } + } + + /// A static white solid with one plug-in effect, registered by `plugin`. + fn plugin_scene(plugin: Arc) -> (Project, ItemId) { + let mut p = Project::default(); + let comp = Comp::new(200, 100, FrameRate::FPS_30, Tick::from_seconds_f64(2.0)); + let cid = p.add_item("Comp", Label::Sandstone, None, ItemKind::Comp(comp.clone().into())); + let sid = p.add_item("Solid", Label::Red, None, ItemKind::Solid(Solid { color: [1.0, 1.0, 1.0], width: 200, height: 100, pixel_aspect: 1.0 })); + let mut l = build::layer(&mut p, &comp, "Solid", LayerSource::Solid { item: sid }, (200, 100), None); + let spec = register_plugin(plugin).unwrap(); + let mut next = p.next_id; + let g = effectcraft_effects::instantiate(spec, &mut Ids(&mut next), spec.name, [200.0, 100.0]); + p.next_id = next; + l.props.sub_mut("effects").unwrap().children.push(g.into()); + p.comp_mut(cid).unwrap().layers.push(l); + (p, cid) + } + + fn key(p: &Project, cid: ItemId, t: f64) -> Option { + let comp = p.comp(cid).unwrap(); + let ctx = crate::EvalCtx { + project: p, + comp_id: cid, + comp, + time: Tick::from_seconds_f64(t), + expr: None, + footage: None, + property_overrides: &[], + media_overrides: &[], + reads: None, + }; + crate::cache::layer_key(&ctx, &comp.layers[0], 1.0, false, false) + } + + let manifest = |id: &str| PluginManifest { + api: 2, + id: id.into(), + name: "Key".into(), + category: "Test Plug-ins".into(), + version: "1".into(), + author: String::new(), + description: String::new(), + params: vec![], + }; + + let (plain, c1) = plugin_scene(Arc::new(Plain { m: manifest("org.test.render.time-key.plain") })); + assert_eq!(key(&plain, c1, 0.25), key(&plain, c1, 0.5), "a plain plug-in keeps the old key"); + let (reader, c2) = plugin_scene(Arc::new(NeighbourReader { m: manifest("org.test.render.time-key.reader") })); + assert_ne!(key(&reader, c2, 0.25), key(&reader, c2, 0.5), "an ABI 2 plug-in keys on the layer time"); +} diff --git a/docs/architecture.md b/docs/architecture.md index a04907aec..1e91da215 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -43,7 +43,7 @@ cpal or muda. Everything in L0 to L4, the egui UI and the web app also build for | L3 | `interchange` | Premiere Pro interop through timeline interchange (FCP7 XML, FCPXML, OTIO, EDL, AAF, OMF via FilmCraft's `filmcraft-interchange`): sequences ↔ compositions (§6a) | | L3 | `aep` | After Effects projects in one crate (like `lottie`): `container` reads and writes the `.aep` container (RIFX `Egg!`) as a lossless chunk tree with a byte-identical round trip, bounded for hostile input, reads the `.aepx` XML form into the same tree, and has a typed view of items, layers, properties, effects, masks, shapes, text, markers, footage, cameras, lights and layer styles, from our own observations of generated files ([aep-format.md](aep-format.md)); `import` maps the typed view into a project (items, settings, compositions, every layer kind, Transform, keyframes, expressions, masks, text, shapes, cameras, lights, mattes, markers, time remapping, layer styles, the effects of a shared table, footage) with a report of everything not imported ([aep-import.md](aep-import.md)); `export` builds every chunk in code (prototypes in the layout After Effects writes, cloned and edited only at verified fields) for the same scope, with a report of everything not exported, verified by opening the output in After Effects ([aep-export.md](aep-export.md)) | | L3 | `lottie` | Lottie JSON / dotLottie import and export (layers, precomps, eased and spatial keyframes, shapes, masks, mattes) with a warnings list for what Lottie cannot express | -| L3 | `plugin` | Effect plug-ins: loads sandboxed WebAssembly effect modules (plug-in API v1; the wasmi interpreter behind the `wasm` feature, on for native hosts) into the effect registry ([plugins.md](plugins.md)) | +| L3 | `plugin` | Effect plug-ins: loads sandboxed WebAssembly effect modules (WebAssembly ABI v1 and v2, the latter with neighbouring frames; the wasmi interpreter behind the `wasm` feature, on for native hosts) into the effect registry ([plugins.md](plugins.md)) | | L3 | `helper-host` | External effect helpers: finds helper programs, runs each as a child process and registers its effects (plug-in API 2) over a versioned pipe + shared-file protocol; a crashing helper is an error on the effect, never a crash ([external-helpers.md](external-helpers.md), [ADR 0001](adr/0001-external-effect-helpers.md)). Native only | | L4 | `engine` | `Session`: project, branching undo history, editor state, the command registry and menus, the ScriptUI window model (`scriptui`) | | L4 | `script` | Scripting: JavaScript (boa) with an After Effects-style object model (`app.project`, comps, layers, properties, render queue) and ScriptUI (dialogs, palettes, dockable panels; script-host threads so dialogs block `show()`), whose edits run engine commands; AE match names | diff --git a/docs/plugins.md b/docs/plugins.md index 3fd8cf944..290ed033c 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -11,7 +11,7 @@ Guide): | After Effects | EffectCraft | | |---|---|---| -| Effect plug-ins (C/C++ SDK, `.aex` / `.plugin`) | [Effect plug-ins](#effect-plug-ins): WebAssembly modules (plug-in API v1) or Rust; the `Plug-ins` folder next to the settings loads at start-up | Different ABI: After Effects SDK plug-ins can't run (G8) | +| Effect plug-ins (C/C++ SDK, `.aex` / `.plugin`) | [Effect plug-ins](#effect-plug-ins): WebAssembly modules (WebAssembly ABI 1 and 2) or Rust; the `Plug-ins` folder next to the settings loads at start-up | Different ABI: After Effects SDK plug-ins can't run (G8) | | Scripts folder: its scripts are listed in File ▸ Scripts | `Scripts/` in the settings folder (File ▸ Scripts ▸ Install Script File… copies a script there), listed in File ▸ Scripts with the bundled samples; a subfolder (one level) is a submenu named after it | Same | | File ▸ Scripts ▸ Run Script File… (`.jsx`, `.jsxbin`) | `.jsx` / `.js` | Gap: `.jsxbin` (an undocumented binary encoding) | | File ▸ Scripts ▸ Recent Script Files: the 10 most recently run scripts; Cmd+Option+Shift+D (Ctrl+Alt+Shift+D on Windows) reruns one | File ▸ Scripts ▸ Recent Script Files lists the 10 scripts run last (script files and File ▸ Scripts entries, newest first, kept in the settings); Cmd+Alt+Shift+D runs the most recent one again (`file.runScript {"recent": 0}`, a key that can be changed in Edit ▸ Keyboard Shortcuts) | Same | @@ -152,7 +152,7 @@ example of a keyframe tool. ## Effect plug-ins -EffectCraft loads third-party effects through a small, versioned plug-in API (**API version 2**; WebAssembly ABI 1). +EffectCraft loads third-party effects through a small, versioned plug-in API (**API version 2**; WebAssembly ABI 1 and 2). A plug-in effect behaves exactly like a built-in one: it is listed in Effects & Presets (and the Effect menu) under its category, applied by id (`effect.apply {"effect": "org.example.posterize"}`) or display name, its parameters are ordinary properties (keyframes, expressions, Effect Controls, @@ -178,7 +178,8 @@ author, source, params}]}`. * **Parameters** arrive flattened to `f64`s in the order they are declared: slider, angle, checkbox (0 / 1) and popup (0-based index) take one value, point two (already in **buffer pixels**), colour four (RGBA 0–1). -* **Time** is layer time in seconds. +* **Time** is layer time in seconds. A WebAssembly module written for ABI 2 also gets the + layer's neighbouring frames at other times (see [WebAssembly ABI](#webassembly-abi)). * Rendering must be **deterministic** and may run on several threads at once (a WebAssembly plug-in gets one instance per thread). * A failed render (an error code, a trap, running out of fuel) leaves the frame unchanged. @@ -216,22 +217,36 @@ Every plug-in describes itself with a JSON manifest: parameters; a slider needs `min ≤ default ≤ max` (and `sliderMin ≤ sliderMax`); the manifest is at most 1 MiB. -### WebAssembly ABI (v1) +### WebAssembly ABI -A module with **no imports** that exports: +A module with **no imports** exports: | export | signature | | |---|---|---| | `memory` | memory | linear memory shared with the host | -| `ec_api_version` | `() -> i32` | must return `1` | +| `ec_api_version` | `() -> i32` | `1` for ABI 1, `2` for ABI 2 (neighbouring frames) | | `ec_manifest_ptr`, `ec_manifest_len` | `() -> i32` | the UTF-8 JSON manifest in memory | | `ec_alloc` | `(bytes: i32) -> i32` | a buffer of at least `bytes` bytes, 8-byte aligned; it may reuse the previous one | -| `ec_render` | `(pixels: i32, width: i32, height: i32, params: i32, nparams: i32, time: f64, scale: f64) -> i32` | process the pixels in place; return 0 on success | +| `ec_render` (ABI 1) | `(pixels: i32, width: i32, height: i32, params: i32, nparams: i32, time: f64, scale: f64) -> i32` | process the pixels in place; return 0 on success | +| `ec_render2` (ABI 2) | `ec_render`'s arguments, then `prev: i32, prev_time: f64, next: i32, next_time: f64, flags: i32` | the same, with the neighbouring frames; return 0 on success | For each frame the host calls `ec_alloc` once for a buffer holding the parameters (`nparams` -little-endian `f64`s) followed by the pixels (`width × height × 4` little-endian `f32`s), fills it, -calls `ec_render`, and reads the pixels back. Each call gets a fuel budget proportional to the -frame size; a module that loops forever stops with an error instead of hanging the app. +little-endian `f64`s) followed by the pixels (`width × height × 4` little-endian `f32`s, +premultiplied RGBA, row-major), fills it, calls the module's render export, and reads the pixels +back. An ABI 2 module gets two more pixel buffers after the current frame: the previous and next +frames, resampled onto the current buffer's grid. At a frame boundary they are the frames before +and after it (`time ± 1/fps`); between frames they are the two frames that bracket `time`. + +The neighbour buffers are always allocated (zeroed when the layer has no pixels there, so a +module can read both without a null check). `flags` bit 0 is set when `prev` holds a real frame, +bit 1 when `next` does; `prev_time` / `next_time` are those frames' layer times in seconds (0 +when absent). The ABI version is the module's `ec_api_version` and is independent of the +manifest's `api` (the native plug-in API): a module can use ABI 2 while its manifest says +`"api": 1`. Because an ABI 2 module reads other times, its layer is keyed on the layer time in +the render cache; ABI 1 modules keep the time-independent key. + +Each call gets a fuel budget proportional to the frame size; a module that loops forever stops +with an error instead of hanging the app. #### Example @@ -324,8 +339,10 @@ register_plugin(Arc::new(Swap(manifest)))?; // before the UI builds its menus ## Plug-in API 2 (native plug-ins) API 2 is for Rust plug-ins and for hosts that bring effects from elsewhere (such as -[external effect helpers](external-helpers.md)); the WebAssembly ABI stays at 1. Manifests with `"api": 1` or `2` are accepted, -`effect.plugins.list` reports `api: 2`, and up to 1024 plug-in effects can be registered. +[external effect helpers](external-helpers.md)); the WebAssembly ABI is versioned separately +(ABI 2 adds the neighbouring frames, see [WebAssembly ABI](#webassembly-abi)). Manifests with +`"api": 1` or `2` are accepted, `effect.plugins.list` reports `api: 2`, and up to 1024 plug-in +effects can be registered. * **New parameter types**: `point3` (`default` `[x, y, z]`; x, y arrive in buffer pixels, z as is), `layer` (another layer of the composition; flattens to its id, -1 for none, read with @@ -354,9 +371,11 @@ API 2 is for Rust plug-ins and for hosts that bring effects from elsewhere (such ### Compatibility -The API version is checked when a plug-in loads: a manifest or module for another version is -refused with a message. Within a version the contract above does not change; additions (new -parameter types, new optional exports) bump the version. +The API version is checked when a plug-in loads: this build implements plug-in API 1 and 2 and +WebAssembly ABI 1 and 2; a manifest or module for another version is refused with a message. +ABI 1 modules load and render unchanged; ABI 2 modules add `ec_render2` and the neighbouring +frames. Within a version the contract above does not change; additions (new parameter types, +new optional exports) bump the version. ## Plug-in Library diff --git a/examples/plugins/posterize-bands/Cargo.toml b/examples/plugins/posterize-bands/Cargo.toml index 825d704aa..65bbbaaaf 100644 --- a/examples/plugins/posterize-bands/Cargo.toml +++ b/examples/plugins/posterize-bands/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "posterize-bands" -description = "Example EffectCraft effect plug-in (WebAssembly, plug-in API v1): posterizes luminance into tinted bands" +description = "Example EffectCraft effect plug-in (WebAssembly ABI 1): posterizes luminance into tinted bands" version = "1.0.0" edition = "2024" license = "MIT OR Apache-2.0" diff --git a/examples/plugins/posterize-bands/src/lib.rs b/examples/plugins/posterize-bands/src/lib.rs index 9af709433..77026a16f 100644 --- a/examples/plugins/posterize-bands/src/lib.rs +++ b/examples/plugins/posterize-bands/src/lib.rs @@ -1,4 +1,4 @@ -//! Posterize Bands: an example EffectCraft effect plug-in (plug-in API v1, see +//! Posterize Bands: an example EffectCraft effect plug-in (WebAssembly ABI 1, see //! `docs/plugins.md`). It quantizes each pixel's luminance into `levels` bands and mixes a tint //! colour in by `mix` percent, optionally only above a `threshold`. //!