Merge main

This commit is contained in:
dotCooCoo
2026-10-28 15:31:46 -07:00
233 changed files with 9081 additions and 5305 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{EngineError, Result, Session, Value};
// ---------- enablement ----------
+4 -1
View File
@@ -1,5 +1,8 @@
use super::create::{apply_text_params, text_range_p};
use super::*;
use super::{
EngineError, GroupKind, Ids, ItemId, KV, LayerId, MaskMode, Node, Project, PropGroup, Result, Session, TextDoc, Value, b_p, bad, build, json, layer_mut,
layer_p, merge_p, str_p,
};
pub(super) fn add_mask(s: &mut Session, p: &Value) -> Result<Value> {
let (cid, lid) = layer_p(s, p, "layer.addMask")?;
+4 -1
View File
@@ -1,4 +1,7 @@
use super::*;
use super::{
EngineError, Ids, ItemId, ItemKind, Justify, KV, Label, Layer, LayerId, LayerSource, Project, Result, Session, Solid, TextDoc, Tick, Value, b_p, bad,
build, comp_id, f_p, json, limits, str_p,
};
pub(crate) fn color_p(p: &Value, k: &str) -> Option<[f32; 3]> {
match p.get(k)? {
+2 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Comp, EngineError, ItemId, KV, Layer, LayerId, PropGroup, Result, Session, Tick, Value, b_p, bad, layers_p, resolve_layer};
/// Re-express a layer in its new parent space without moving its anchor.
pub(crate) struct ParentFix {
@@ -336,6 +336,7 @@ pub(super) fn set_parent(s: &mut Session, p: &Value) -> Result<Value> {
#[cfg(test)]
mod parent_chain_tests {
use super::super::json;
use super::*;
use std::sync::Arc;
+4 -1
View File
@@ -1,4 +1,7 @@
use super::*;
use super::{
EngineError, FootageKind, Ids, ItemId, ItemKind, Layer, LayerSource, Project, Result, Session, Value, bad, build, has_comp, json, layer_mut, layers_p,
unlocked,
};
/// A layer whose source is an item (footage, precomposition or solid), as in After Effects'
/// Replace Source. Text, shape, null, camera, light and 3D model layers have no AVItem source to
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{EngineError, LayerId, Result, Session, Value, b_p, comp_id, json, resolve_layer};
pub(super) fn select(s: &mut Session, p: &Value) -> Result<Value> {
let cid = comp_id(s, p)?;
+4 -1
View File
@@ -1,4 +1,7 @@
use super::*;
use super::{
BlendMode, EngineError, FrameBlend, ItemId, KV, Layer, LayerId, MatteKind, Quality, Result, Session, TrackMatte, Value, b_p, bad, json, layer_mut, layer_p,
layers_p, merge_p, resolve_layer, str_p,
};
pub(super) fn set_switch(s: &mut Session, p: &Value) -> Result<Value> {
let (cid, ids) = layers_p(s, p)?;
+4 -1
View File
@@ -1,4 +1,7 @@
use super::*;
use super::{
EngineError, ItemKind, Label, Layer, LayerId, LayerSource, Result, Session, Tick, Value, b_p, bad, build, f_p, json, layer_mut, layers_p, merge_p,
resolve_layer, str_p, unlocked, with,
};
pub(super) fn timing(s: &mut Session, p: &Value) -> Result<Value> {
let (cid, ids) = layers_p(s, p)?;
+4 -1
View File
@@ -1,5 +1,8 @@
use super::create::{color_p, pixel_aspect_p};
use super::*;
use super::{
EngineError, ItemKind, KV, LayerId, LayerSource, PropGroup, Result, Session, Tick, Value, bad, has_layers, json, layer_mut, layer_p, layers_p, limits,
str_p, unlocked,
};
/// Move Position by `(dx, dy)`, including every key. Separated dimensions move X and Y as well
/// as the combined property the channels were split from.
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Comp, EngineError, ItemId, Layer, LayerId, Result, Session, Tick, Value};
/// Hand a frontend-only command to the UI ([`crate::Event::Frontend`]).
pub(crate) fn frontend(s: &mut Session, id: &str, p: &Value) -> Result<Value> {
+10 -10
View File
@@ -121,16 +121,16 @@ fn parametric_path(g: &PropGroup, lt: effectcraft_time::Tick) -> Option<ShapePat
let path = match g.match_id.as_str() {
"rect" => effectcraft_path::rect(v2("size", [100.0; 2]), v2("position", [0.0; 2]), f("roundness", 0.0)),
"ellipse" => effectcraft_path::ellipse(v2("size", [100.0; 2]), v2("position", [0.0; 2])),
"star" => effectcraft_path::polystar(
g.prop("type").map_or(0, |pr| pr.value_at(lt).as_enum()) == 0,
f("points", 5.0),
v2("position", [0.0; 2]),
f("rotation", 0.0),
f("innerRadius", 50.0),
f("outerRadius", 100.0),
f("innerRoundness", 0.0),
f("outerRoundness", 0.0),
),
"star" => effectcraft_path::polystar(effectcraft_path::Polystar {
star: g.prop("type").map_or(0, |pr| pr.value_at(lt).as_enum()) == 0,
points: f("points", 5.0),
pos: v2("position", [0.0; 2]),
rotation: f("rotation", 0.0),
inner: f("innerRadius", 50.0),
outer: f("outerRadius", 100.0),
inner_round: f("innerRoundness", 0.0),
outer_round: f("outerRoundness", 0.0),
}),
_ => return None,
};
// A closed parametric shape is one subpath; anything else would not be this shape.
+50 -32
View File
@@ -205,23 +205,8 @@ fn apply_om_template(om: &mut OutputModule, t: &OutputModule) {
/// The `format` parameter's values.
const FORMATS: &str = "format: h264|hevc|av1|prores|uncompressed|webm|png|jpeg|tiff|exr|gif|wav|aiff";
/// Apply Output Module parameters (`template` first). `roi`: the viewer's region of interest
/// (captured by `crop.useRoi`).
fn apply_output(om: &mut OutputModule, templates: &RenderTemplates, roi: Option<[f64; 4]>, p: &Value, cmd: &str) -> Result<bool> {
let mut any = false;
if let Some(t) = str_p(p, "template") {
let t = templates.output_module(t).ok_or_else(|| bad(cmd, format!("no Output Module template “{t}”")))?;
apply_om_template(om, &t);
any = true;
}
let before = om.clone();
if let Some(f) = str_p(p, "format") {
let f = OutputFormat::from_name(f).ok_or_else(|| bad(cmd, FORMATS))?;
om.set_format(f);
}
// Before Channels: the WebM codec decides whether alpha can be written, so an explicit
// `channels: rgba` with `webmCodec: av1` is refused rather than dropped (#166).
any |= apply_codec_options(om, p, cmd)?;
/// `channels` (refusing RGBA where the format has no alpha) and `color`/`alphaMode`.
fn apply_channels_and_alpha(om: &mut OutputModule, p: &Value, cmd: &str) -> Result<()> {
if let Some(c) = str_p(p, "channels") {
om.channels = match c.to_ascii_lowercase().replace([' ', '+'], "").as_str() {
"rgb" => Channels::Rgb,
@@ -244,21 +229,11 @@ fn apply_output(om: &mut OutputModule, templates: &RenderTemplates, roi: Option<
_ => return Err(bad(cmd, "color: straight|premultiplied")),
};
}
if let Some(q) = f_p(p, "quality") {
om.quality = q.clamp(1.0, 100.0) as u8;
}
if let Some(b) = f_p(p, "bitrate") {
om.bitrate_kbps = b.clamp(50.0, 500_000.0) as u32;
}
if let Some(b) = b_p(p, "webmBitrate") {
om.webm_bitrate = b;
}
if let Some(k) = p.get("keyframeInterval").and_then(Value::as_u64) {
om.keyframe_interval = k.min(100_000) as u32;
}
if let Some(pp) = str_p(p, "proresProfile") {
om.prores_profile = ProResProfile::from_name(pp).ok_or_else(|| bad(cmd, "proresProfile: proxy|lt|standard|hq|4444|4444xq"))?;
}
Ok(())
}
/// `crop` plus the flat dialog keys (`cropTop`… and `resizeWidth`/`resizeHeight`).
fn apply_crop(om: &mut OutputModule, roi: Option<[f64; 4]>, p: &Value, cmd: &str) -> Result<()> {
match p.get("crop") {
Some(Value::Bool(b)) => om.crop.enabled = *b,
Some(Value::Object(c)) => {
@@ -298,6 +273,11 @@ fn apply_output(om: &mut OutputModule, templates: &RenderTemplates, roi: Option<
om.resize.enabled = true;
}
}
Ok(())
}
/// `resize` (bool or object).
fn apply_resize(om: &mut OutputModule, p: &Value, cmd: &str) -> Result<()> {
match p.get("resize") {
Some(Value::Bool(b)) => om.resize.enabled = *b,
Some(Value::Object(r)) => {
@@ -328,6 +308,44 @@ fn apply_output(om: &mut OutputModule, templates: &RenderTemplates, roi: Option<
Some(Value::Null) | None => {}
Some(_) => return Err(bad(cmd, "resize: bool or {enabled?, preset?, width?, height?, lockAspect?, quality?: low|high}")),
}
Ok(())
}
/// Apply Output Module parameters (`template` first). `roi`: the viewer's region of interest
/// (captured by `crop.useRoi`).
fn apply_output(om: &mut OutputModule, templates: &RenderTemplates, roi: Option<[f64; 4]>, p: &Value, cmd: &str) -> Result<bool> {
let mut any = false;
if let Some(t) = str_p(p, "template") {
let t = templates.output_module(t).ok_or_else(|| bad(cmd, format!("no Output Module template “{t}”")))?;
apply_om_template(om, &t);
any = true;
}
let before = om.clone();
if let Some(f) = str_p(p, "format") {
let f = OutputFormat::from_name(f).ok_or_else(|| bad(cmd, FORMATS))?;
om.set_format(f);
}
// Before Channels: the WebM codec decides whether alpha can be written, so an explicit
// `channels: rgba` with `webmCodec: av1` is refused rather than dropped (#166).
any |= apply_codec_options(om, p, cmd)?;
apply_channels_and_alpha(om, p, cmd)?;
if let Some(q) = f_p(p, "quality") {
om.quality = q.clamp(1.0, 100.0) as u8;
}
if let Some(b) = f_p(p, "bitrate") {
om.bitrate_kbps = b.clamp(50.0, 500_000.0) as u32;
}
if let Some(b) = b_p(p, "webmBitrate") {
om.webm_bitrate = b;
}
if let Some(k) = p.get("keyframeInterval").and_then(Value::as_u64) {
om.keyframe_interval = k.min(100_000) as u32;
}
if let Some(pp) = str_p(p, "proresProfile") {
om.prores_profile = ProResProfile::from_name(pp).ok_or_else(|| bad(cmd, "proresProfile: proxy|lt|standard|hq|4444|4444xq"))?;
}
apply_crop(om, roi, p, cmd)?;
apply_resize(om, p, cmd)?;
if let Some(a) = p.get("audio") {
om.audio = match a {
Value::Bool(true) => AudioOutput::On,
+1 -1
View File
@@ -1,5 +1,5 @@
use super::params::bad;
use super::*;
use super::{CommandSpec, Result, Value};
// ---------- parameter validation (agents) ----------
+10 -1
View File
@@ -221,7 +221,16 @@ pub fn mask_path(kind: &str, centre: [f64; 2], size: [f64; 2]) -> Option<ShapePa
"rounded" | "roundedRect" => effectcraft_path::rect(size, centre, roundness(size)),
k => {
let (star, points, outer, inner) = polystar_of(k, size)?;
effectcraft_path::polystar(star, points, centre, 0.0, inner, outer, 0.0, 0.0)
effectcraft_path::polystar(effectcraft_path::Polystar {
star,
points,
pos: centre,
rotation: 0.0,
inner,
outer,
inner_round: 0.0,
outer_round: 0.0,
})
}
};
effectcraft_path::from_kurbo(&bez).into_iter().next()