mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Merge pull request #867 from pravdomil/claude/uu-modified-defaults
Timeline: UU reveals modified options and layer styles, without false positives
This commit is contained in:
commit
cafd18eef4
2 files changed
+135
-1
No files matched your search
@@ -5,7 +5,8 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use effectcraft_keyframe::{Interp, Keyframe, Value as KV};
|
||||
use effectcraft_project::{GroupKind, LayerId, Node, PropGroup, Property, Uid};
|
||||
use effectcraft_project::build::{self, Ids};
|
||||
use effectcraft_project::{GroupKind, Layer, LayerId, LayerSource, Node, Project, PropGroup, Property, Uid};
|
||||
use effectcraft_time::Tick;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -293,6 +294,9 @@ fn modified(pr: &Property, group: &str, default_anchor: [f64; 2], comp_center: [
|
||||
match (group, pr.match_id.as_str()) {
|
||||
("transform", "anchor") => !near(&v.as_vec2(), &default_anchor),
|
||||
("transform", "position") => !near(&v.as_vec2(), &comp_center) || v.as_vec3()[2].abs() > 1e-6,
|
||||
// Separate Dimensions: X and Y Position default to the comp centre.
|
||||
("transform", "positionX") => (v.as_f64() - comp_center[0]).abs() > 1e-6,
|
||||
("transform", "positionY") => (v.as_f64() - comp_center[1]).abs() > 1e-6,
|
||||
("transform", "scale") => !near(&v.as_vec3(), &[100.0; 3]),
|
||||
("transform", "opacity") => (v.as_f64() - 100.0).abs() > 1e-6,
|
||||
("transform", "orientation") => !near(&v.as_vec3(), &[0.0; 3]),
|
||||
@@ -301,6 +305,50 @@ fn modified(pr: &Property, group: &str, default_anchor: [f64; 2], comp_center: [
|
||||
}
|
||||
}
|
||||
|
||||
/// The group a fresh layer of `l`'s kind (`fresh`) has in place of `top`, for the groups whose
|
||||
/// defaults are fixed: Audio, Material / Geometry / Camera / Light Options, a text layer's Path
|
||||
/// and More Options, and a camera's or light's Transform (whose defaults aren't a 2D layer's).
|
||||
fn default_group(top: &PropGroup, l: &Layer, fresh: &Layer) -> Option<PropGroup> {
|
||||
let mut next = 0;
|
||||
let mut ids = Ids(&mut next);
|
||||
let camera_or_light = matches!(l.source, LayerSource::Camera | LayerSource::Light { .. });
|
||||
let mut g = match top.match_id.as_str() {
|
||||
"transform" if !camera_or_light => return None,
|
||||
// Added to a layer after it is made (a source with audio, an Advanced 3D comp).
|
||||
"audio" => build::audio(&mut ids),
|
||||
"geometryOptions" if matches!(l.source, LayerSource::Text | LayerSource::Shape) => build::extrusion_geometry_options(&mut ids),
|
||||
"transform" | "cameraOptions" | "lightOptions" | "materialOptions" | "geometryOptions" | "text" => fresh.props.sub(&top.match_id)?.clone(),
|
||||
_ => return None,
|
||||
};
|
||||
match top.match_id.as_str() {
|
||||
// Source Text and the animators are the layer's content, not options with a default.
|
||||
"text" => g.children.retain(|c| matches!(c.match_id(), "pathOptions" | "moreOptions")),
|
||||
// A model's scale and clip come from its file.
|
||||
"geometryOptions" if matches!(l.source, LayerSource::Model { .. }) => g.children.retain(|c| !matches!(c.match_id(), "unitScale" | "animation")),
|
||||
_ => {}
|
||||
}
|
||||
Some(g)
|
||||
}
|
||||
|
||||
/// Static properties of `g` whose value differs from the same property of `default` (matched by
|
||||
/// match id; properties `default` lacks have no known default and are left out).
|
||||
fn changed_from(g: &PropGroup, default: &PropGroup, out: &mut Vec<Uid>) {
|
||||
for c in &g.children {
|
||||
match c {
|
||||
Node::Prop(pr) => {
|
||||
if default.get(&pr.match_id).is_some_and(|d| d.value != pr.value) && !out.contains(&pr.uid) {
|
||||
out.push(pr.uid);
|
||||
}
|
||||
}
|
||||
Node::Group(sg) => {
|
||||
if let Some(d) = default.sub(&sg.match_id) {
|
||||
changed_from(sg, d, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reveal(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let kind = str_p(p, "kind").unwrap_or("keyframes").to_string();
|
||||
let cid = super::comp_id(s, p)?;
|
||||
@@ -324,10 +372,15 @@ fn reveal(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
out.push(json!({"layer": lid.0, "prop": pr.uid}));
|
||||
}
|
||||
}
|
||||
// Modified: a fresh layer of the same kind gives the defaults to compare against.
|
||||
let fresh = (kind == "modified").then(|| build::layer(&mut Project::default(), comp, "", l.source.clone(), (w, h), None));
|
||||
let camera_or_light = matches!(l.source, LayerSource::Camera | LayerSource::Light { .. });
|
||||
for top in l.props.groups() {
|
||||
let hit = |pr: &Property| match kind.as_str() {
|
||||
"keyframes" => pr.is_animated(),
|
||||
"animation" => pr.is_animated() || pr.has_expression(),
|
||||
// A camera's or light's Transform is compared with a fresh one's below.
|
||||
_ if camera_or_light && top.match_id == "transform" => pr.is_animated() || pr.expr.is_some(),
|
||||
_ => modified(pr, &top.match_id, anchor, center),
|
||||
};
|
||||
let mut found = vec![];
|
||||
@@ -347,6 +400,23 @@ fn reveal(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
found.push(g.uid);
|
||||
}
|
||||
}
|
||||
if let Some(fresh) = &fresh {
|
||||
if let Some(default) = default_group(top, l, fresh) {
|
||||
changed_from(top, &default, &mut found);
|
||||
}
|
||||
// Every layer style counts in full, like an effect; so does each overridden
|
||||
// Essential Property.
|
||||
if top.match_id == effectcraft_project::styles::GROUP {
|
||||
found.extend(top.groups().filter(|g| g.match_id != effectcraft_project::styles::BLENDING).map(|g| g.uid));
|
||||
}
|
||||
if let GroupKind::Essential { overridden } = &top.kind {
|
||||
for u in overridden {
|
||||
if !found.contains(u) && (top.find(*u).is_some() || top.find_group(*u).is_some()) {
|
||||
found.push(*u);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out.extend(found.into_iter().map(|u| json!({"layer": lid.0, "prop": u})));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -731,6 +731,70 @@ fn reveal_shortcuts_take_in_time_remap() {
|
||||
}
|
||||
}
|
||||
|
||||
/// UU on a layer nothing was changed on shows nothing, whatever its kind: cameras, lights, text
|
||||
/// and nulls have their own Transform defaults, and Separate Dimensions keeps the comp centre.
|
||||
#[test]
|
||||
fn modified_reveal_is_empty_for_untouched_layers() {
|
||||
let (mut s, solid) = setup();
|
||||
let mut layers = vec![("solid", solid)];
|
||||
for (cmd, params) in [
|
||||
("layer.newText", json!({"text": "Hi"})),
|
||||
("layer.newShape", json!({})),
|
||||
("layer.newNull", json!({})),
|
||||
("layer.newCamera", json!({})),
|
||||
("layer.newLight", json!({})),
|
||||
("layer.newLight", json!({"type": "ambient"})),
|
||||
] {
|
||||
let r = s.execute(cmd, params.clone()).unwrap_or_else(|e| panic!("{cmd} {params}: {e}"));
|
||||
layers.push((cmd, r["layer"].as_u64().unwrap()));
|
||||
}
|
||||
s.execute("prop.separateDimensions", json!({"layer": solid})).unwrap();
|
||||
for (what, l) in layers {
|
||||
s.execute("layer.select", json!({"layers": [l]})).unwrap();
|
||||
let r = s.execute("anim.reveal", json!({"kind": "modified"})).unwrap();
|
||||
let paths: Vec<String> = r["props"].as_array().unwrap().iter().filter_map(|p| p["prop"].as_u64()).map(|u| path_of(&s, l, u)).collect();
|
||||
assert!(paths.is_empty(), "{what}: {paths:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Match-id path of property or group `uid` of `layer`.
|
||||
fn path_of(s: &Session, layer: u64, uid: u64) -> String {
|
||||
let l = s.active_comp().unwrap().layer(effectcraft_project::LayerId(layer)).unwrap();
|
||||
let mut out = String::new();
|
||||
for n in l.props.node_chain(uid).unwrap_or_default() {
|
||||
out.push('/');
|
||||
out.push_str(n.match_id());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// UU takes in static changes outside Transform: Material, Camera and Light Options, a
|
||||
/// separated position, and layer styles in full.
|
||||
#[test]
|
||||
fn modified_reveal_takes_in_options_and_layer_styles() {
|
||||
let (mut s, solid) = setup();
|
||||
let modified = |s: &mut Session, l: u64| -> Vec<String> {
|
||||
s.execute("layer.select", json!({"layers": [l]})).unwrap();
|
||||
let r = s.execute("anim.reveal", json!({"kind": "modified"})).unwrap();
|
||||
r["props"].as_array().unwrap().iter().filter_map(|p| p["prop"].as_u64()).map(|u| path_of(s, l, u)).collect()
|
||||
};
|
||||
s.execute("prop.set", json!({"layer": solid, "path": "materialOptions/diffuse", "value": 80})).unwrap();
|
||||
assert_eq!(modified(&mut s, solid), ["/materialOptions/diffuse"]);
|
||||
s.execute("prop.separateDimensions", json!({"layer": solid})).unwrap();
|
||||
s.execute("prop.set", json!({"layer": solid, "path": "transform/positionX", "value": 10})).unwrap();
|
||||
assert_eq!(modified(&mut s, solid), ["/transform/positionX", "/materialOptions/diffuse"]);
|
||||
s.execute("layer.style.dropShadow", json!({"layers": [solid]})).unwrap();
|
||||
assert!(modified(&mut s, solid).contains(&"/layerStyles/dropShadow".to_string()), "{:?}", modified(&mut s, solid));
|
||||
|
||||
let cam = s.execute("layer.newCamera", json!({})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("prop.set", json!({"layer": cam, "path": "cameraOptions/blurLevel", "value": 50})).unwrap();
|
||||
s.execute("prop.set", json!({"layer": cam, "path": "transform/rotation", "value": 15})).unwrap();
|
||||
assert_eq!(modified(&mut s, cam), ["/transform/rotation", "/cameraOptions/blurLevel"]);
|
||||
let light = s.execute("layer.newLight", json!({})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("prop.set", json!({"layer": light, "path": "lightOptions/intensity", "value": 40})).unwrap();
|
||||
assert_eq!(modified(&mut s, light), ["/lightOptions/intensity"]);
|
||||
}
|
||||
|
||||
/// Enable Time Remapping on a layer extended past its source's end (its last frame frozen) puts
|
||||
/// the end key at the source's end, not at the extended out point (#290).
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user