mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-10 12:31:27 +00:00
Layer: Un-precompose a selected pre-comp layer back into its parent composition (#3081)
* Layer: Un-precompose a selected pre-comp layer back into its parent composition (#2918) layer.unprecompose copies the nested composition's layers into the parent at the pre-comp layer's place with timing and position carried over, and refuses what it can't keep exactly. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Un-precompose: refuse a pre-comp layer with Solo or Guide on A soloed pre-comp layer hides the comp's other layers and a guide layer is left out of renders; the expanded copies carry neither switch, so the picture would change. Listed with the other blockers, in the command description and the agents.md Cookbook entry; the refusal test covers both switches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Un-precompose: warn instead of refusing, only a locked layer stops it A locked pre-comp layer is still refused, with or without `force`. Every other former refusal reason is a warning: without `force`, `layer.unprecompose` returns an error that lists the reasons and changes nothing. `dryRun` returns `{dryRun, warnings}`. With `force` the layer is unpacked in one undo step and the result carries `warnings`. Whatever the pre-comp layer itself carried is dropped, and other layers' parent, track matte and layer-property links to it are removed. Layer > Un-precompose in the UI lists the reasons in a prompt with Unpack Anyway / Cancel (`dialog.unprecompose.force` / `dialog.unprecompose.cancel`). A layer without reasons unpacks at once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Un-precompose: warn about expressions that refer to the pre-comp layer An expression on another layer of the comp that names the pre-comp layer would no longer find it once the layer is unpacked. The expression can name it in quotes or call `layer(<index>)` with its index, bare or on thisComp; another comp's comp("…").layer(…) is not counted. Each such property is now listed once as a warning. Like the other warnings, `force` unpacks and `dryRun` lists it. The expressions are left as written. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Un-precompose: warn about lost frame sampling and Position expressions, keep markers in order Review follow-up (CodeRabbit on #3081): - A nested comp that preserves a frame rate other than the parent's samples its layers on its own frames, and the expanded copies would lose that. Motion-blurred nested layers under a different shutter would also blur with the parent's shutter. Both are now warnings. - When the pre-comp layer's Position differs from its Anchor Point, the move is added to the stored Position of the unparented nested layers. An unparented layer whose Position, or a separated axis, has an expression may ignore that move, so it is now a warning. - Copied comp markers are sorted into time order with the comp's own. - New tests: a 10 fps nested comp with Preserve Frame Rate in a 30 fps comp, a different shutter, a Position expression with and without a move, and nested matte and layer-value remapping with shifted markers merged in order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Un-precompose: warn when an expression's layer numbers shift; the prompt keeps its target Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Un-precompose: a shorter renumbering warning Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
This commit is contained in:
11 files changed
+1009
-1
No files matched your search
@@ -31,7 +31,7 @@ use replace::{has_replace_source, replace_source};
|
||||
pub(crate) use select::select_only;
|
||||
use select::{select, select_next, select_prev, select_step};
|
||||
use switches::{blend, rename, set_comment, set_switch, track_matte};
|
||||
use timing::{arrange, move_frames, precompose, slip, timing};
|
||||
use timing::{arrange, move_frames, precompose, slip, timing, unprecompose};
|
||||
use transform::{has_layer_settings, layer_settings, transform_op};
|
||||
pub(crate) use transform::{offset_position, transform_default, update_position};
|
||||
|
||||
@@ -177,6 +177,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_layers,
|
||||
precompose
|
||||
),
|
||||
cmd!(
|
||||
"layer.unprecompose",
|
||||
"Un-precompose",
|
||||
["Layer"],
|
||||
None,
|
||||
"{layer?, force?, dryRun?} → {layers, warnings}: expands the selected pre-comp layer into its composition's layers (copies, in order, with timing and position carried over). A locked pre-comp layer is refused, changing nothing. What would change the picture (the pre-comp layer has a parent, matte, effects, masks, layer styles, a non-default Scale/Rotation/Opacity, an animated transform, time remap/stretch, a blending mode, Video/Audio off, Solo/Guide on, is 3D/collapsing/adjustment, is used by another layer or named in another layer's expression; or the nested comp has cameras, lights, adjustment, 3D, collapsing, soloed or blend-mode / Preserve Transparency layers, preserves another frame rate, has another motion-blur shutter, or has Position expressions the move can't reach) is an error listing the reasons, changing nothing, unless `force`: then it unpacks anyway in one undo step, drops what the pre-comp layer carried, removes other layers' parent/matte/property links to it and lists the reasons as `warnings`. `dryRun` returns {dryRun, warnings} and changes nothing",
|
||||
has_layers,
|
||||
unprecompose
|
||||
),
|
||||
cmd!(
|
||||
"layer.addMask",
|
||||
"New Mask",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use effectcraft_project::TrackMatte;
|
||||
|
||||
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,
|
||||
@@ -307,3 +309,317 @@ pub(super) fn precompose(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
Ok(json!({"comp": new.0.0, "layer": new.1.0}))
|
||||
}
|
||||
|
||||
/// Why expanding a pre-comp layer in place would change the picture or drop something, one entry
|
||||
/// per cause. Un-precompose keeps the picture exactly for a pre-comp layer that only places the
|
||||
/// nested composition (Position/Anchor Point, timing); anything the layer itself adds to the look,
|
||||
/// and what only holds inside the nested comp, is listed. These are warnings: `force` unpacks
|
||||
/// anyway and drops it. A lock is not listed here: it always refuses.
|
||||
fn unprecompose_warnings(comp: &effectcraft_project::Comp, layer: &Layer, nested: &effectcraft_project::Comp) -> Vec<String> {
|
||||
let mut why = vec![];
|
||||
if layer.parent.is_some() {
|
||||
why.push("the layer has a parent".to_string());
|
||||
}
|
||||
if layer.track_matte.is_some() {
|
||||
why.push("the layer has a track matte".to_string());
|
||||
}
|
||||
if comp.layers.iter().any(|o| o.id != layer.id && (o.parent == Some(layer.id) || o.track_matte.is_some_and(|m| m.layer == layer.id))) {
|
||||
why.push("another layer uses it as its parent or track matte (that link is removed)".to_string());
|
||||
}
|
||||
if layer.is_3d() || layer.switches.collapse || layer.switches.adjustment || layer.preserve_transparency {
|
||||
why.push("the layer is 3D, collapses transformations, is an adjustment layer or preserves transparency".to_string());
|
||||
}
|
||||
if !layer.switches.video || !layer.switches.audio {
|
||||
why.push("the layer's Video or Audio switch is off".to_string());
|
||||
}
|
||||
if layer.switches.solo || layer.switches.guide {
|
||||
// Solo hides the comp's other layers and a guide layer is left out of renders; the
|
||||
// expanded copies would carry neither, so the picture would change.
|
||||
why.push("the layer's Solo or Guide switch is on".to_string());
|
||||
}
|
||||
if comp.layers.iter().any(|o| o.id != layer.id && refers_to(&o.props, layer.id)) {
|
||||
why.push("a property of another layer refers to it (that reference is cleared)".to_string());
|
||||
}
|
||||
why.extend(expression_warnings(comp, layer, nested.layers.len()));
|
||||
if layer.blend_mode != effectcraft_color::BlendMode::default() {
|
||||
why.push("the layer has a blending mode".to_string());
|
||||
}
|
||||
if layer.stretch != 100.0 || layer.props.prop("timeRemap").is_some() {
|
||||
why.push("the layer is time-stretched or time-remapped".to_string());
|
||||
}
|
||||
if layer.effects().is_some_and(|e| !e.children.is_empty()) {
|
||||
why.push("the layer has effects".to_string());
|
||||
}
|
||||
if layer.masks().is_some_and(|m| !m.children.is_empty()) {
|
||||
why.push("the layer has masks".to_string());
|
||||
}
|
||||
if layer.styles_change_pixels() {
|
||||
why.push("the layer has layer styles".to_string());
|
||||
}
|
||||
if let Some(tr) = layer.transform() {
|
||||
let at = |m: &str, d: [f64; 3]| tr.get(m).map(|p| p.value.as_vec3()).unwrap_or(d);
|
||||
let animated = tr.props().any(|p| !p.keys.is_empty() || p.expr.is_some());
|
||||
let separated = tr.get("positionX").is_some();
|
||||
if animated || separated {
|
||||
why.push("its transform is animated, has an expression or has separated dimensions".to_string());
|
||||
} else {
|
||||
let (sc, rot, ori) = (at("scale", [100.0; 3]), at("rotation", [0.0; 3]), at("orientation", [0.0; 3]));
|
||||
let op = tr.get("opacity").map(|p| p.value.as_f64()).unwrap_or(100.0);
|
||||
if sc[0] != 100.0 || sc[1] != 100.0 || rot[0] != 0.0 || ori != [0.0; 3] || op != 100.0 {
|
||||
why.push("its Scale, Rotation or Opacity is not at its default".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
if nested.layers.iter().any(|l| l.is_camera() || l.is_light()) {
|
||||
why.push("the composition has a camera or light".to_string());
|
||||
}
|
||||
// Inside the nested comp these composite against the nested stack only; in the parent they
|
||||
// would act on everything below.
|
||||
if nested.layers.iter().any(|l| {
|
||||
l.switches.adjustment || l.switches.collapse || l.is_3d() || l.preserve_transparency || l.blend_mode != effectcraft_color::BlendMode::default()
|
||||
}) {
|
||||
why.push("the composition has adjustment, 3D or collapsing layers or layers with a blending mode or Preserve Transparency".to_string());
|
||||
}
|
||||
// A solo hides only the nested comp's other layers; a soloed copy would hide the parent's.
|
||||
if nested.layers.iter().any(|l| l.switches.solo) {
|
||||
why.push("the composition has a soloed layer".to_string());
|
||||
}
|
||||
why.extend(sampling_warnings(comp, nested));
|
||||
why.extend(position_expression_warnings(layer, nested));
|
||||
why
|
||||
}
|
||||
|
||||
/// What the nested comp's own frame sampling does that the expanded layers lose: a preserved
|
||||
/// frame rate other than the parent's (its layers then change only on its own frames), and
|
||||
/// motion blur with a shutter other than the parent's.
|
||||
fn sampling_warnings(comp: &effectcraft_project::Comp, nested: &effectcraft_project::Comp) -> Vec<String> {
|
||||
let mut why = vec![];
|
||||
if nested.preserve_frame_rate && nested.frame_rate != comp.frame_rate {
|
||||
why.push("the composition preserves a frame rate other than this one's (its layers would be sampled at this comp's frames)".to_string());
|
||||
}
|
||||
let shutter = nested.shutter_angle != comp.shutter_angle || nested.shutter_phase != comp.shutter_phase;
|
||||
if shutter && nested.layers.iter().any(|l| l.switches.motion_blur) {
|
||||
why.push("the composition's motion blur shutter differs from this one's".to_string());
|
||||
}
|
||||
why
|
||||
}
|
||||
|
||||
/// The move from the pre-comp layer's Position against its Anchor Point, which expansion adds to
|
||||
/// the Position of the nested comp's unparented layers.
|
||||
fn placement_offset(layer: &Layer) -> (f64, f64) {
|
||||
let tr = layer.transform();
|
||||
let at = |m: &str| tr.and_then(|t| t.get(m)).map(|p| p.value.as_vec3()).unwrap_or([0.0; 3]);
|
||||
let (anchor, position) = (at("anchor"), at("position"));
|
||||
(position[0] - anchor[0], position[1] - anchor[1])
|
||||
}
|
||||
|
||||
/// Unparented nested layers whose Position (or a separated axis) has an expression: the move from
|
||||
/// [`placement_offset`] is added to the stored value and keys, which an expression can ignore.
|
||||
fn position_expression_warnings(layer: &Layer, nested: &effectcraft_project::Comp) -> Vec<String> {
|
||||
let (dx, dy) = placement_offset(layer);
|
||||
if dx == 0.0 && dy == 0.0 {
|
||||
return vec![];
|
||||
}
|
||||
nested
|
||||
.layers
|
||||
.iter()
|
||||
.filter(|l| l.parent.is_none())
|
||||
.filter(|l| l.transform().is_some_and(|tr| ["position", "positionX", "positionY"].iter().any(|m| tr.get(m).is_some_and(|p| p.expr.is_some()))))
|
||||
.map(|l| format!("layer “{}” has a Position expression, so the move to the pre-comp layer's Position may not apply to it", l.name))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// One warning per property of another layer in `comp` whose expression names `layer`: by its
|
||||
/// name in quotes, or as `layer(<index>)` with its 1-based index (not on another comp's
|
||||
/// `comp("…").layer(…)`), and one per expression naming a layer by a number below it while the
|
||||
/// `added` layers replacing it renumber those. Detection is by text; the expressions are left as
|
||||
/// they are.
|
||||
fn expression_warnings(comp: &effectcraft_project::Comp, layer: &Layer, added: usize) -> Vec<String> {
|
||||
let index = comp.layers.iter().position(|l| l.id == layer.id).map(|i| i.saturating_add(1));
|
||||
let quoted = [format!("\"{}\"", layer.name), format!("'{}'", layer.name)];
|
||||
let names_it = |text: &str| quoted.iter().any(|q| text.contains(q.as_str())) || index.is_some_and(|i| indexed_layers(text).contains(&i));
|
||||
let renumbered = |text: &str| added != 1 && index.is_some_and(|i| indexed_layers(text).iter().any(|n| *n > i));
|
||||
let mut out = vec![];
|
||||
for o in comp.layers.iter().filter(|o| o.id != layer.id) {
|
||||
o.props.walk("", &mut |_, p| {
|
||||
if p.expr.as_ref().is_some_and(|e| !names_it(&e.text) && renumbered(&e.text)) {
|
||||
out.push(format!("layer “{}” {} names a layer by number, and the layers below “{}” are renumbered", o.name, p.name, layer.name));
|
||||
}
|
||||
if p.expr.as_ref().is_some_and(|e| names_it(&e.text)) {
|
||||
out.push(format!("layer “{}” {} has an expression that refers to “{}” (it will no longer find it)", o.name, p.name, layer.name));
|
||||
}
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The indexes expression `text` calls `layer(<index>)` with (spaces allowed) on this comp: bare,
|
||||
/// `thisComp.layer(…)`, not `comp("Other").layer(…)` or `thisLayer(…)`.
|
||||
fn indexed_layers(text: &str) -> Vec<usize> {
|
||||
text.match_indices("layer(")
|
||||
.filter_map(|(at, _)| {
|
||||
let before = text.get(..at).unwrap_or("");
|
||||
let on_other = before.ends_with(").") || before.chars().next_back().is_some_and(|c| c.is_alphanumeric() || c == '_');
|
||||
if on_other && !before.ends_with("thisComp.") {
|
||||
return None;
|
||||
}
|
||||
let rest = text.get(at + "layer(".len()..).unwrap_or("").trim_start();
|
||||
let digits: String = rest.chars().take_while(char::is_ascii_digit).collect();
|
||||
let after = rest.get(digits.len()..).unwrap_or("").trim_start();
|
||||
digits.parse::<usize>().ok().filter(|_| after.starts_with(')'))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Whether any property (value or key) in `props` holds a reference to layer `id`.
|
||||
fn refers_to(props: &effectcraft_project::PropGroup, id: LayerId) -> bool {
|
||||
let is = |v: &effectcraft_keyframe::Value| matches!(v, effectcraft_keyframe::Value::Layer(Some(l)) if *l == id.0);
|
||||
props.children.iter().any(|c| match c {
|
||||
effectcraft_project::Node::Prop(p) => is(&p.value) || p.keys.iter().any(|k| is(&k.value)),
|
||||
effectcraft_project::Node::Group(g) => refers_to(g, id),
|
||||
})
|
||||
}
|
||||
|
||||
/// Clear every reference to layer `id` (`Value::Layer`, value or key) in `props`; references to
|
||||
/// other layers stay.
|
||||
fn clear_layer_refs(props: &mut effectcraft_project::PropGroup, id: LayerId) {
|
||||
let fix = |v: &mut effectcraft_keyframe::Value| {
|
||||
if matches!(v, effectcraft_keyframe::Value::Layer(Some(l)) if *l == id.0) {
|
||||
*v = effectcraft_keyframe::Value::Layer(None);
|
||||
}
|
||||
};
|
||||
for c in &mut props.children {
|
||||
match c {
|
||||
effectcraft_project::Node::Prop(p) => {
|
||||
fix(&mut p.value);
|
||||
for k in &mut p.keys {
|
||||
fix(&mut k.value);
|
||||
}
|
||||
}
|
||||
effectcraft_project::Node::Group(g) => clear_layer_refs(g, id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The layers of `comp` other than `id` let go of it: parent, track matte and property references
|
||||
/// to it are removed, so nothing points at a layer that is gone.
|
||||
fn detach_from(comp: &mut effectcraft_project::Comp, id: LayerId) {
|
||||
for l in comp.layers.iter_mut().filter(|l| l.id != id) {
|
||||
if l.parent == Some(id) {
|
||||
l.parent = None;
|
||||
}
|
||||
if l.track_matte.is_some_and(|m| m.layer == id) {
|
||||
l.track_matte = None;
|
||||
}
|
||||
clear_layer_refs(&mut l.props, id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace layer references (`Value::Layer`) found in `props` by `map` (old id → new id), the
|
||||
/// way copies of a set of layers keep pointing at each other. A reference to a layer outside the
|
||||
/// set is cleared: it named a layer of the nested comp that is no longer there.
|
||||
fn remap_layer_values(props: &mut effectcraft_project::PropGroup, map: &[(LayerId, LayerId)]) {
|
||||
let fix = |v: &mut effectcraft_keyframe::Value| {
|
||||
if let effectcraft_keyframe::Value::Layer(Some(id)) = v {
|
||||
*v = effectcraft_keyframe::Value::Layer(map.iter().find(|(o, _)| o.0 == *id).map(|(_, n)| n.0));
|
||||
}
|
||||
};
|
||||
for c in &mut props.children {
|
||||
match c {
|
||||
effectcraft_project::Node::Prop(p) => {
|
||||
fix(&mut p.value);
|
||||
for k in &mut p.keys {
|
||||
fix(&mut k.value);
|
||||
}
|
||||
}
|
||||
effectcraft_project::Node::Group(g) => remap_layer_values(g, map),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Layer ▸ Un-precompose: expand a selected pre-comp layer back into its composition. The nested
|
||||
/// comp's layers become copies in this comp at the pre-comp layer's place, in their order and
|
||||
/// with their keyframes, effects, masks, parenting, mattes, markers and audio; their timing is
|
||||
/// moved by the pre-comp layer's Start and trimmed to the span it showed, and an Anchor Point
|
||||
/// different from Position moves the unparented ones by the difference. The nested comp stays in
|
||||
/// the project. The nested comp's frame no longer clips what the layers draw outside it.
|
||||
///
|
||||
/// A locked pre-comp layer is refused, changing nothing. Anything else that would change the
|
||||
/// picture or drop something (see [`unprecompose_warnings`]) is an error listing the reasons unless
|
||||
/// `force` is set; with `force` the layer is expanded the same way, what the pre-comp layer itself
|
||||
/// carried (effects, masks, styles, transform beyond Position, time remap, blending mode, switches,
|
||||
/// parent, matte) goes with it, and the other layers' parent, matte and property links to it are
|
||||
/// removed. The result lists the reasons as `warnings`; `dryRun` only lists them.
|
||||
pub(super) fn unprecompose(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let c = "layer.unprecompose";
|
||||
let (cid, ids) = layers_p(s, p)?;
|
||||
let [lid] = ids[..] else { return Err(bad(c, "select exactly one pre-comp layer")) };
|
||||
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?.clone();
|
||||
let layer = comp.layer(lid).ok_or(EngineError::NoComp)?.clone();
|
||||
let LayerSource::Comp { item } = layer.source else { return Err(bad(c, format!("“{}” is not a pre-comp layer", layer.name))) };
|
||||
if layer.switches.locked {
|
||||
return Err(bad(c, format!("can't un-precompose “{}”: the layer is locked (unlock it first)", layer.name)));
|
||||
}
|
||||
let nested = s.project.comp(item).ok_or_else(|| bad(c, "the layer's composition is missing"))?.clone();
|
||||
let warnings = unprecompose_warnings(&comp, &layer, &nested);
|
||||
if b_p(p, "dryRun").unwrap_or(false) {
|
||||
return Ok(json!({"dryRun": true, "warnings": warnings}));
|
||||
}
|
||||
if !warnings.is_empty() && !b_p(p, "force").unwrap_or(false) {
|
||||
let why = warnings.join("; ");
|
||||
return Err(bad(
|
||||
c,
|
||||
format!(
|
||||
"can't un-precompose “{}” without changing it: {why} (nothing changed: pass `force` to unpack anyway, or `dryRun` to list the reasons)",
|
||||
layer.name
|
||||
),
|
||||
));
|
||||
}
|
||||
let (dx, dy) = placement_offset(&layer);
|
||||
let shown = (layer.in_point.max(layer.start_time), layer.out_point.min(layer.start_time + nested.duration));
|
||||
let new = s.edit("Un-precompose", None, |proj, st| {
|
||||
let mut next = proj.next_id;
|
||||
let mut copies: Vec<Layer> = nested.layers.iter().cloned().collect();
|
||||
let mut map = vec![];
|
||||
for l in &mut copies {
|
||||
let old = l.id;
|
||||
crate::cmd::edit::reid(l, &mut next);
|
||||
map.push((old, l.id));
|
||||
}
|
||||
let link = |id: LayerId| map.iter().find(|(o, _)| *o == id).map(|(_, n)| *n);
|
||||
for l in &mut copies {
|
||||
l.parent = l.parent.and_then(link);
|
||||
l.track_matte = l.track_matte.and_then(|m| link(m.layer).map(|layer| TrackMatte { layer, ..m }));
|
||||
remap_layer_values(&mut l.props, &map);
|
||||
l.start_time += layer.start_time;
|
||||
l.in_point = (l.in_point + layer.start_time).clamp(shown.0, shown.1.max(shown.0));
|
||||
l.out_point = (l.out_point + layer.start_time).clamp(l.in_point, shown.1.max(l.in_point));
|
||||
if l.parent.is_none()
|
||||
&& let Some(tr) = l.transform_mut()
|
||||
{
|
||||
super::offset_position(tr, dx, dy);
|
||||
}
|
||||
}
|
||||
proj.next_id = next;
|
||||
let target = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
|
||||
detach_from(target, lid);
|
||||
let at = target.layers.iter().position(|l| l.id == lid).unwrap_or(0);
|
||||
target.layers.retain(|l| l.id != lid);
|
||||
let mut made = vec![];
|
||||
for (k, mut l) in copies.into_iter().enumerate() {
|
||||
l.name = target.unique_layer_name(&l.name);
|
||||
made.push(l.id);
|
||||
target.layers.insert((at + k).min(target.layers.len()), l);
|
||||
}
|
||||
for m in &nested.markers {
|
||||
let mut m = m.clone();
|
||||
m.time += layer.start_time;
|
||||
target.markers.push(m);
|
||||
}
|
||||
// Comp markers are kept in time order (`thisComp.marker.key(n)`, next/previous marker).
|
||||
target.markers.sort_by_key(|m| m.time);
|
||||
st.selected_layers = made.clone();
|
||||
Ok(made)
|
||||
})?;
|
||||
Ok(json!({"layers": new.iter().map(|l| l.0).collect::<Vec<_>>(), "warnings": warnings}))
|
||||
}
|
||||
Reference in new issue
Block a user