mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-13 13:52:16 +00:00
Import: a static shape path an expression draws is kept, empty, with its expression (#3606)
After Effects stores a static shape path that an expression draws (a text-box rig writes its
outline from the text's size) with no vertices. The importer dropped the whole path ('its path
has no vertices'), expression and all, so such shapes drew nothing. A path without vertices that
has an expression and no keys now arrives as an empty path with its expression; one without an
expression is still reported.
This commit is contained in:
1 parent
cf1898df4f
commit
0a6d264554
4 files changed
+119
-4
No files matched your search
@@ -50,13 +50,16 @@ fn source_rectangle(size: (u32, u32)) -> ShapePath {
|
||||
}
|
||||
}
|
||||
|
||||
/// [`to_path`]'s error for a stored path without vertices.
|
||||
pub(crate) const NO_VERTICES: &str = "its path has no vertices";
|
||||
|
||||
pub(crate) fn to_path(s: &AepPath, size: (u32, u32)) -> Result<ShapePath, &'static str> {
|
||||
if !s.points.len().is_multiple_of(3) {
|
||||
return Err("its point count is not a multiple of three");
|
||||
}
|
||||
let n = s.vertex_count();
|
||||
if n == 0 {
|
||||
return Err("its path has no vertices");
|
||||
return Err(NO_VERTICES);
|
||||
}
|
||||
if n > MAX_VERTICES {
|
||||
return Err("its path has too many vertices");
|
||||
|
||||
@@ -18,11 +18,11 @@
|
||||
//! options of extruded shapes.
|
||||
|
||||
use crate::{Layer as AepLayer, PropGroup as AepGroup, PropNode, Property as AepProp, ShapePath as AepPath};
|
||||
use effectcraft_keyframe::Gradient;
|
||||
use effectcraft_keyframe::{Gradient, ShapePath};
|
||||
use effectcraft_project::build::{self, Ids};
|
||||
use effectcraft_project::{Node, PropGroup};
|
||||
|
||||
use crate::import::masks::{import_path_keys, to_path};
|
||||
use crate::import::masks::{NO_VERTICES, import_path_keys, to_path};
|
||||
use crate::import::props::{Kind, import_expression, import_keys};
|
||||
use crate::import::report::{Collector, LossCategory};
|
||||
use crate::import::time::TimeMap;
|
||||
@@ -400,15 +400,20 @@ fn path(g: &AepGroup<'_, '_>, cx: &mut Cx<'_, '_>) -> Option<PropGroup> {
|
||||
Some(None) => Err("its path is unreadable"),
|
||||
None => Err("it has no path"),
|
||||
};
|
||||
let node = oms.child_list(b"tdbs").and_then(AepProp::from_chunk);
|
||||
// A static path an expression draws (a text-box rig writes its outline from the text's size):
|
||||
// After Effects stores it empty. The expression is the path, so it is kept, on an empty value.
|
||||
let drawn_by_expression = node.as_ref().is_some_and(|n| n.expression().is_some() && !n.is_animated());
|
||||
let first = match first {
|
||||
Ok(p) => p,
|
||||
Err(why) if why == NO_VERTICES && drawn_by_expression => ShapePath::default(),
|
||||
Err(why) => {
|
||||
cx.col.lose(LossCategory::ShapeLayers, format!("{}: a shape path not imported, {why}", cx.what));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let mut p = build::shape_path(cx.ids, first);
|
||||
if let (Some(node), Some(prop)) = (oms.child_list(b"tdbs").and_then(AepProp::from_chunk), p.get_mut("path")) {
|
||||
if let (Some(node), Some(prop)) = (node, p.get_mut("path")) {
|
||||
if node.is_animated() {
|
||||
import_path_keys(&shapes, &node, cx.what, (1, 1), cx.tm, prop, cx.col);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ mod placeholders;
|
||||
mod puppet;
|
||||
mod resaved_defaults;
|
||||
mod robustness;
|
||||
mod shape_path_expression;
|
||||
mod shapes;
|
||||
mod shapes_round2;
|
||||
mod styles;
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
//! A static shape path that an expression draws: After Effects stores the path empty and the
|
||||
//! expression builds it (text-box rigs write their outline from the text's size). The importer
|
||||
//! dropped the whole path ("its path has no vertices"), expression and all, so such shapes drew
|
||||
//! nothing. It keeps the path, empty, with its expression; an empty path without one is still
|
||||
//! reported.
|
||||
|
||||
use effectcraft_aep::import::{ImportOptions, LossCategory, import};
|
||||
use effectcraft_aep::{AepFile, Body, Chunk, parse, write};
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::Project;
|
||||
|
||||
const CASE: &str = "240-shape-custom-path-tangents.aep";
|
||||
const EXPRESSION: &str = "createPath([[0,0],[100,0],[100,50],[0,50]], [], [], true)";
|
||||
|
||||
fn visit(chunks: &mut [Chunk<'_>], f: &mut dyn FnMut(&mut Chunk<'_>)) {
|
||||
for c in chunks {
|
||||
f(c);
|
||||
visit(c.children_mut(), f);
|
||||
}
|
||||
}
|
||||
|
||||
/// The case with its shape path emptied, and with `expression` on it when given (as After Effects
|
||||
/// writes one: `tdsb` 1, the expression switches at `tdb4` 119 / 120 and a `Utf8` in `tdbs`).
|
||||
fn empty_path(expression: Option<&str>) -> Option<Vec<u8>> {
|
||||
let bytes = crate::common::try_fixture(CASE)?;
|
||||
let mut file: AepFile<'_> = parse(&bytes).unwrap();
|
||||
let mut emptied = 0;
|
||||
visit(&mut file.chunks, &mut |c| {
|
||||
if c.is_list(b"shap") {
|
||||
for k in c.children_mut() {
|
||||
if k.is_list(b"list") {
|
||||
for d in k.children_mut() {
|
||||
if let Body::Data(data) = &mut d.body {
|
||||
if d.id == *b"lhd3" {
|
||||
data.to_mut()[8..12].copy_from_slice(&0u32.to_be_bytes());
|
||||
} else if d.id == *b"ldat" {
|
||||
*data = std::borrow::Cow::Owned(Vec::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
emptied += 1;
|
||||
}
|
||||
let Some(text) = expression else { return };
|
||||
if !c.is_list(b"om-s") {
|
||||
return;
|
||||
}
|
||||
for k in c.children_mut() {
|
||||
if !k.is_list(b"tdbs") {
|
||||
continue;
|
||||
}
|
||||
for r in k.children_mut() {
|
||||
if let Body::Data(d) = &mut r.body {
|
||||
let d = d.to_mut();
|
||||
match &r.id {
|
||||
b"tdsb" => d.copy_from_slice(&1u32.to_be_bytes()),
|
||||
b"tdb4" => {
|
||||
d[119] = 0;
|
||||
d[120] = 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Body::List { children, .. } = &mut k.body {
|
||||
children.push(Chunk::data(*b"Utf8", text.as_bytes().to_vec()));
|
||||
}
|
||||
}
|
||||
});
|
||||
assert!(emptied > 0, "{CASE} has a shape path");
|
||||
Some(write(&file).unwrap())
|
||||
}
|
||||
|
||||
fn import_allowing(bytes: &[u8]) -> (Project, effectcraft_aep::import::ImportResult) {
|
||||
let mut project = Project::default();
|
||||
let none = |_: &str| false;
|
||||
let r = import(&mut project, bytes, &ImportOptions { allow_losses: true, ..ImportOptions::new(&none) }).unwrap();
|
||||
(project, r)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_path_with_an_expression_is_kept_with_it() {
|
||||
let Some(bytes) = empty_path(Some(EXPRESSION)) else { return };
|
||||
let (p, r) = import_allowing(&bytes);
|
||||
let mut paths = Vec::new();
|
||||
for l in p.comp(r.comps[0]).unwrap().layers.iter() {
|
||||
l.props.walk("", &mut |_, pr| {
|
||||
if pr.match_id == "path" {
|
||||
paths.push((pr.value.clone(), pr.expr.as_ref().map(|e| e.text.clone())));
|
||||
}
|
||||
});
|
||||
}
|
||||
assert_eq!(paths.len(), 1, "{:?}", r.report.losses);
|
||||
let (value, expr) = &paths[0];
|
||||
assert!(matches!(value, Value::Path(sp) if sp.vertices.is_empty()), "{value:?}");
|
||||
assert_eq!(expr.as_deref(), Some(EXPRESSION));
|
||||
assert_eq!(r.report.count(LossCategory::ShapeLayers), 0, "{:?}", r.report.losses);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_path_without_an_expression_is_still_reported() {
|
||||
let Some(bytes) = empty_path(None) else { return };
|
||||
let (_, r) = import_allowing(&bytes);
|
||||
assert!(r.report.losses.iter().flat_map(|l| &l.details).any(|d| d.contains("its path has no vertices")), "{:?}", r.report.losses);
|
||||
}
|
||||
Reference in new issue
Block a user