Files
effectcraft/crates/project/src/layer.rs
T
FredApps 5832219d76 Merge pull request #3248 from vangel76/text-layer-name-follow-3233
Text layers: the name follows the text until the user renames the layer
2026-11-08 15:44:58 +01:00

666 lines
26 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::item::{ItemId, LayerId};
use super::settings::Marker;
use super::{BlendMode, Deserialize, GroupKind, Label, Node, ParamUi, PropGroup, Property, Serialize, Tick, Value};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum LightKind {
Parallel,
Spot,
#[default]
Point,
Ambient,
Environment,
}
impl LightKind {
pub const ALL: [LightKind; 5] = [LightKind::Parallel, LightKind::Spot, LightKind::Point, LightKind::Ambient, LightKind::Environment];
pub fn label(self) -> &'static str {
match self {
LightKind::Parallel => "Parallel",
LightKind::Spot => "Spot",
LightKind::Point => "Point",
LightKind::Ambient => "Ambient",
LightKind::Environment => "Environment",
}
}
}
/// A parametric 3D primitive (Layer ▸ New ▸ Cube/Sphere/Plane/Torus/Cone/Cylinder).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PrimitiveKind {
#[default]
Cube,
Sphere,
Plane,
Torus,
Cone,
Cylinder,
}
impl PrimitiveKind {
pub const ALL: [PrimitiveKind; 6] =
[PrimitiveKind::Cube, PrimitiveKind::Sphere, PrimitiveKind::Plane, PrimitiveKind::Torus, PrimitiveKind::Cone, PrimitiveKind::Cylinder];
pub fn label(self) -> &'static str {
match self {
PrimitiveKind::Cube => "Cube",
PrimitiveKind::Sphere => "Sphere",
PrimitiveKind::Plane => "Plane",
PrimitiveKind::Torus => "Torus",
PrimitiveKind::Cone => "Cone",
PrimitiveKind::Cylinder => "Cylinder",
}
}
pub fn parse(s: &str) -> Option<PrimitiveKind> {
PrimitiveKind::ALL.into_iter().find(|k| k.label().eq_ignore_ascii_case(s.trim()))
}
}
/// What a layer shows.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum LayerSource {
/// Footage item (video, still, sequence, audio).
Footage {
item: ItemId,
},
/// Nested composition (precomp).
Comp {
item: ItemId,
},
/// Solid item (solids also back adjustment layers).
Solid {
item: ItemId,
},
Text,
Shape,
Null,
Camera,
Light {
kind: LightKind,
},
/// An imported 3D model (a footage item of kind [`FootageKind::Model`]; Advanced 3D).
Model {
item: ItemId,
},
/// A parametric 3D primitive (Advanced 3D).
Primitive {
kind: PrimitiveKind,
},
}
impl LayerSource {
pub fn item(&self) -> Option<ItemId> {
match self {
LayerSource::Footage { item } | LayerSource::Comp { item } | LayerSource::Solid { item } | LayerSource::Model { item } => Some(*item),
_ => None,
}
}
pub fn type_name(&self) -> &'static str {
match self {
LayerSource::Footage { .. } => "Footage",
LayerSource::Comp { .. } => "Composition",
LayerSource::Solid { .. } => "Solid",
LayerSource::Text => "Text",
LayerSource::Shape => "Shape",
LayerSource::Null => "Null",
LayerSource::Camera => "Camera",
LayerSource::Light { .. } => "Light",
LayerSource::Model { .. } => "3D Model",
LayerSource::Primitive { .. } => "3D Primitive",
}
}
/// Layers with pixels (cameras, lights and nulls have none).
pub fn is_av(&self) -> bool {
!matches!(self, LayerSource::Camera | LayerSource::Light { .. } | LayerSource::Null)
}
/// A 3D model or primitive layer (meshes; drawn by the Advanced 3D renderer).
pub fn is_model(&self) -> bool {
matches!(self, LayerSource::Model { .. } | LayerSource::Primitive { .. })
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum Quality {
#[default]
Best,
Draft,
Wireframe,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum Sampling {
#[default]
Bilinear,
Bicubic,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum FrameBlend {
#[default]
Off,
FrameMix,
PixelMotion,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Switches {
pub video: bool,
pub audio: bool,
pub solo: bool,
pub locked: bool,
pub shy: bool,
/// Collapse transformations (precomps) / continuously rasterize (vector layers).
pub collapse: bool,
pub quality: Quality,
pub sampling: Sampling,
pub effects: bool,
pub frame_blend: FrameBlend,
pub motion_blur: bool,
pub adjustment: bool,
pub three_d: bool,
pub guide: bool,
}
impl Default for Switches {
fn default() -> Self {
Switches {
video: true,
audio: true,
solo: false,
locked: false,
shy: false,
collapse: false,
quality: Quality::Best,
sampling: Sampling::Bilinear,
effects: true,
frame_blend: FrameBlend::Off,
motion_blur: false,
adjustment: false,
three_d: false,
guide: false,
}
}
}
// What a frame's pixels depend on. Caches key on it: the Lock and Shy switches and Hide Shy
// Layers never change pixels, and the Audio switch, the Audio group and audio effects change them
// only when something draws the layer's audio (a waveform or spectrum picks it, see
// [`crate::Project::heard_layers`]), so editing those keeps cached frames (#103).
/// Is `id` an audio effect (applied by the audio mix rather than the compositor)?
pub fn is_audio_effect(id: &str) -> bool {
id.starts_with("ec.audio.")
}
/// Does effect `id` read its own layer at other times (`EffectHost::self_at`, `layer_at` on
/// itself): the time effects (Echo, Posterize Time, Timewarp…), simulations, temporal smoothing
/// and analysis (Remove Grain, Auto Levels, Color Stabilizer's reference frame, Warp Stabilizer,
/// Roto Brush…) and Clone strokes with a time offset? On a precomp layer, a change at one nested
/// time then reaches frames of the layer far from where it shows. Plug-in effects (every id
/// outside `ec.`) count too: plug-in API 2 reads the input at other times (`PluginHost`).
// ponytail: hand-kept list of the effects that call `self_at`; a new one must be added here.
pub fn reads_other_frames(id: &str) -> bool {
!id.starts_with("ec.") || id.starts_with("ec.time.") || id.starts_with("ec.sim.") || READS_OTHER_FRAMES.contains(&id)
}
/// The effects outside `ec.time.` and `ec.sim.` that read their own layer at other times.
const READS_OTHER_FRAMES: &[&str] = &[
"ec.noise.removegrain",
"ec.color.autolevels",
"ec.color.autocontrast",
"ec.color.autocolor",
"ec.color.shadowhighlight",
"ec.color.colorstabilizer",
"ec.matte.refinesoft",
"ec.matte.refinehard",
"ec.matte.rotobrush",
"ec.matte.objectmatte",
"ec.blur.camerashakedeblur",
"ec.distort.rollingshutterrepair",
"ec.distort.warpstabilizer",
"ec.key.ccsimplewireremoval",
"ec.paint.paint",
];
impl Switches {
/// These switches with Audio, Lock and Shy at their defaults.
pub fn pixels(&self) -> Switches {
Switches { audio: true, locked: false, shy: false, ..self.clone() }
}
/// The switches that change the pixels of a layer: [`Self::pixels`], keeping Audio when
/// something draws the layer's audio (`heard`).
pub fn picture(&self, heard: bool) -> Switches {
Switches { audio: if heard { self.audio } else { true }, ..self.pixels() }
}
}
/// A node only the audio mix reads: the layer's Audio group, an audio effect.
fn audio_only(n: &Node, root: bool) -> bool {
match n {
Node::Group(g) => (root && g.match_id == "audio") || matches!(&g.kind, GroupKind::Effect { effect } if is_audio_effect(effect)),
Node::Prop(_) => false,
}
}
/// `a` and `b` are equal but for their audio-only nodes ([`audio_only`]: the root's Audio group,
/// the effects in its Effects group that are audio effects).
fn same_picture_props(a: &PropGroup, b: &PropGroup) -> bool {
fn head(a: &PropGroup, b: &PropGroup) -> bool {
// Every field (no `..`: a new field must be considered here).
let PropGroup { uid, match_id, name, kind, enabled, label, children: _ } = a;
*uid == b.uid && *match_id == b.match_id && *name == b.name && *kind == b.kind && *enabled == b.enabled && *label == b.label
}
fn children(a: &PropGroup, b: &PropGroup, root: bool) -> bool {
let mut x = a.children.iter().filter(|n| !audio_only(n, root));
let mut y = b.children.iter().filter(|n| !audio_only(n, root));
loop {
match (x.next(), y.next()) {
(None, None) => return true,
(Some(Node::Group(g)), Some(Node::Group(h))) if root && g.match_id == "effects" && h.match_id == "effects" => {
if !(head(g, h) && children(g, h, false)) {
return false;
}
}
(Some(m), Some(n)) if m == n => {}
_ => return false,
}
}
}
head(a, b) && children(a, b, true)
}
/// `g` (a layer's property tree) without its audio-only nodes ([`same_picture_props`]).
fn picture_props(g: &PropGroup) -> PropGroup {
let mut g = g.clone();
g.children.retain(|n| !audio_only(n, true));
for n in &mut g.children {
if let Node::Group(e) = n
&& e.match_id == "effects"
{
e.children.retain(|n| !audio_only(n, false));
}
}
g
}
impl Layer {
/// Draws the same pixels as `o`: equal but for the Audio, Lock and Shy switches and what only
/// the audio mix reads (nothing draws its audio).
pub fn same_pixels(&self, o: &Layer) -> bool {
self.same_pixels_with(o, true, false)
}
/// [`Self::same_pixels`], with the markers counting only when `with_markers` (they are read by
/// expressions alone, so a project without any can edit them freely) and the audio (the Audio
/// switch, the Audio group, audio effects) only when `heard`: something draws it.
pub fn same_pixels_with(&self, o: &Layer, with_markers: bool, heard: bool) -> bool {
// Every field (no `..`: a new field must be considered here); the property tree last.
let Layer {
id,
name,
// Whether the name follows the text changes no pixels until the name changes.
name_follows_text: _,
source,
label,
comment,
start_time,
in_point,
out_point,
stretch,
switches,
blend_mode,
preserve_transparency,
track_matte,
parent,
markers,
markers_locked,
auto_orient,
environment,
environment_background,
props,
} = self;
*id == o.id
&& *name == o.name
&& *source == o.source
&& *label == o.label
&& *comment == o.comment
&& *start_time == o.start_time
&& *in_point == o.in_point
&& *out_point == o.out_point
&& *stretch == o.stretch
&& switches.picture(heard) == o.switches.picture(heard)
&& *blend_mode == o.blend_mode
&& *preserve_transparency == o.preserve_transparency
&& *track_matte == o.track_matte
&& *parent == o.parent
&& (!with_markers || *markers == o.markers)
&& *markers_locked == o.markers_locked
&& *auto_orient == o.auto_orient
&& *environment == o.environment
&& *environment_background == o.environment_background
&& (if heard { *props == o.props } else { same_picture_props(props, &o.props) })
}
/// The comp-time spans where `self` draws differently than `o` when the two differ only in
/// the keyframes of Transform properties (a key's value, ease or interpolation moved in the
/// Timeline or Graph Editor): from two keys before the first changed key to two after the
/// last (auto-Bezier and continuous tangents reach the neighbouring segments), to the
/// layer's in or out point past the first or last key (values hold there). `None` for any
/// other difference, a static value, an expression on the layer (it can read the keyed
/// property at other times), roving or muted keys, a time remap or a time effect (it reads other
/// times): those reach the layer's whole span.
pub fn keyframe_spans(&self, o: &Layer, with_markers: bool, heard: bool) -> Option<Vec<(Tick, Tick)>> {
let transform = |l: &Layer| l.props.group("transform").cloned();
let (t1, t0) = (transform(self)?, transform(o)?);
let reads_time = |l: &Layer| {
l.props.get("timeRemap").is_some()
|| l.props.group("effects").is_some_and(|e| {
e.children.iter().any(|n| matches!(n, Node::Group(f) if matches!(&f.kind, GroupKind::Effect { effect } if reads_other_frames(effect))))
})
};
let has_expr = |l: &Layer| {
let mut any = false;
l.props.walk("", &mut |_, p| any |= p.has_expression());
any
};
if reads_time(self) || reads_time(o) || has_expr(self) || has_expr(o) {
return None;
}
// Everything but the Transform group draws the same.
let rest = |l: &Layer| {
let mut l = l.clone();
l.props.children.retain(|n| !matches!(n, Node::Group(g) if g.match_id == "transform"));
l
};
let head = |g: &PropGroup| PropGroup { children: vec![], ..g.clone() };
if !rest(self).same_pixels_with(&rest(o), with_markers, heard) || head(&t1) != head(&t0) || t1.children.len() != t0.children.len() {
return None;
}
let mut spans = Vec::new();
for (n1, n0) in t1.children.iter().zip(&t0.children) {
if n1 == n0 {
continue;
}
let (Node::Prop(p1), Node::Prop(p0)) = (n1, n0) else { return None };
let bare = |p: &Property| Property { keys: vec![], value: Value::Scalar(0.0), ..p.clone() };
// (muted keys are skipped: the keys around an edit that play can be any distance away)
if bare(p1) != bare(p0) || p1.keys.is_empty() || p0.keys.is_empty() || p1.keys.iter().chain(&p0.keys).any(|k| k.roving || k.muted) {
return None;
}
// Keys only one side has as they are (added, removed, edited, moved).
let changed: Vec<Tick> =
p1.keys.iter().filter(|k| !p0.keys.contains(k)).chain(p0.keys.iter().filter(|k| !p1.keys.contains(k))).map(|k| k.time).collect();
let (Some(first), Some(last)) = (changed.iter().min(), changed.iter().max()) else { continue };
// Two keys before the first changed one and two after the last, on either side (the
// first or last key when there are fewer: values hold beyond it). Past the first or
// last key the change reaches the in or out point (`None`).
let mut lo = Some(Tick(i64::MAX));
let mut hi = Some(Tick(i64::MIN));
for keys in [&p1.keys, &p0.keys] {
let before = keys.iter().filter(|k| k.time < *first).count();
let back = if before == 0 { None } else { keys.get(before.saturating_sub(2)) };
lo = lo.zip(back).map(|(a, k)| a.min(k.time));
let upto = keys.iter().filter(|k| k.time <= *last).count();
let ahead = if upto >= keys.len() { None } else { keys.get(upto.saturating_add(1).min(keys.len().saturating_sub(1))) };
hi = hi.zip(ahead).map(|(a, k)| a.max(k.time));
}
let (x, y) = (lo.map_or(self.in_point, |t| self.comp_time(t)), hi.map_or(self.out_point, |t| self.comp_time(t)));
let (x, y) = (x.min(y).max(self.in_point), x.max(y).min(self.out_point));
if x <= y {
spans.push((x, y));
}
}
Some(spans)
}
/// This layer with what doesn't change its pixels at its defaults (see
/// [`Self::same_pixels_with`]), for keys that hash it; `None` when it already is.
pub fn picture_form(&self, heard: bool) -> Option<Layer> {
let switches = self.switches.picture(heard);
let audio = !heard
&& (self.props.children.iter().any(|n| audio_only(n, true))
|| self
.props
.children
.iter()
.any(|n| matches!(n, Node::Group(e) if e.match_id == "effects" && e.children.iter().any(|n| audio_only(n, false)))));
if switches == self.switches && !audio {
return None;
}
let mut l = Layer { switches, ..self.clone() };
if audio {
l.props = picture_props(&self.props);
}
Some(l)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum MatteKind {
#[default]
Alpha,
AlphaInverted,
Luma,
LumaInverted,
}
impl MatteKind {
pub const ALL: [MatteKind; 4] = [MatteKind::Alpha, MatteKind::AlphaInverted, MatteKind::Luma, MatteKind::LumaInverted];
pub fn label(self) -> &'static str {
match self {
MatteKind::Alpha => "Alpha Matte",
MatteKind::AlphaInverted => "Alpha Inverted Matte",
MatteKind::Luma => "Luma Matte",
MatteKind::LumaInverted => "Luma Inverted Matte",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TrackMatte {
pub layer: LayerId,
pub kind: MatteKind,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum AutoOrient {
#[default]
Off,
AlongPath,
TowardsCamera,
TowardsPointOfInterest,
}
/// A stretch written as `null` (a NaN or infinite stretch: JSON holds neither, and auto-saves
/// write it so) reads as NaN, so the project still opens and loading brings it to 100 % with a
/// note ([`crate::limits::stretch`], #3247) instead of the whole file being refused. An infinite
/// stretch therefore also reopens at 100 %, not at ±9900 %.
fn stretch_or_nan<'de, D: serde::Deserializer<'de>>(d: D) -> Result<f64, D::Error> {
Ok(Option::<f64>::deserialize(d)?.unwrap_or(f64::NAN))
}
/// `t × mul / div` in ticks, within [`Tick::MIN`]..=[`Tick::MAX`] (the cast saturates, NaN is 0),
/// so a layer time of an extreme stretch can't overflow later tick arithmetic.
fn scaled(t: Tick, mul: f64, div: f64) -> Tick {
Tick(((t.0 as f64 * mul / div) as i64).clamp(Tick::MIN.0, Tick::MAX.0))
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Layer {
pub id: LayerId,
pub name: String,
/// A text layer's name follows the first line of its Source Text until the user renames it
/// (After Effects). `Some(true)`: it follows, `Some(false)`: the user named it. `None` (older
/// projects, layers made before this was recorded): see [`Layer::name_follows_text()`].
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name_follows_text: Option<bool>,
pub source: LayerSource,
pub label: Label,
#[serde(default)]
pub comment: String,
/// Comp time where the layer's time 0 sits.
pub start_time: Tick,
/// Visible span in comp time.
pub in_point: Tick,
pub out_point: Tick,
/// Time stretch in percent (100 = normal, negative = reversed).
#[serde(deserialize_with = "stretch_or_nan")]
pub stretch: f64,
pub switches: Switches,
pub blend_mode: BlendMode,
#[serde(default)]
pub preserve_transparency: bool,
#[serde(default)]
pub track_matte: Option<TrackMatte>,
#[serde(default)]
pub parent: Option<LayerId>,
#[serde(default)]
pub markers: Vec<Marker>,
/// Layer ▸ Markers ▸ Lock Markers.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub markers_locked: bool,
#[serde(default)]
pub auto_orient: AutoOrient,
/// Layer ▸ Environment Layer: the layer is the environment (image-based light and
/// reflections) of Advanced 3D comps instead of being drawn.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub environment: bool,
/// Layer ▸ Light ▸ Create Environment Light Background Layer: the layer's (equirectangular)
/// image is drawn as the 3D scene's backdrop, seen through the camera, instead of as a card.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub environment_background: bool,
/// The property tree (Masks, Effects, Transform, Text, Contents, Camera/Light options…).
pub props: PropGroup,
}
/// The name a text layer takes from its text: the first 40 characters of the first line. A line
/// ends at a line feed or at a bare carriage return (After Effects' paragraph break).
pub fn text_layer_name(text: &str) -> String {
text.split(['\r', '\n']).next().unwrap_or("").chars().take(40).collect()
}
/// A text layer's default name when its text gives none: "Text", numbered "Text 2", "Text 3"…
fn is_default_text_name(name: &str) -> bool {
name == "Text" || name.strip_prefix("Text ").is_some_and(|n| !n.is_empty() && n.chars().all(|c| c.is_ascii_digit()))
}
impl Layer {
/// Whether this text layer's name follows its Source Text, whose first line gives the name
/// `current` (see [`text_layer_name`]). Without a recorded state (an older project), a name
/// that is empty, the text's own, or the default "Text"/"Text 2"… follows; any other name was
/// given by the user.
pub fn name_follows_text(&self, current: &str) -> bool {
self.name_follows_text.unwrap_or_else(|| self.name.is_empty() || self.name == current || is_default_text_name(&self.name))
}
/// The layers this layer reads: its parent, track matte, layer parameters (Displacement Map,
/// Audio Spectrum's Audio Layer…) and tracker targets.
pub fn referenced_layers(&self) -> Vec<LayerId> {
fn walk(g: &PropGroup, depth: usize, out: &mut Vec<LayerId>) {
if depth > 64 {
return;
}
if let GroupKind::Tracker { settings } = &g.kind
&& let Some(t) = settings.target
{
out.push(t);
}
for c in &g.children {
match c {
Node::Prop(p) => {
for v in std::iter::once(&p.value).chain(p.keys.iter().map(|k| &k.value)) {
if let Value::Layer(Some(id)) = v {
out.push(LayerId(*id));
}
}
}
Node::Group(sub) => walk(sub, depth + 1, out),
}
}
}
let mut out: Vec<LayerId> = self.parent.iter().copied().chain(self.track_matte.iter().map(|m| m.layer)).collect();
walk(&self.props, 0, &mut out);
out
}
/// Layers this one picks in an effect or tracker (an Audio Spectrum's Audio Layer, a
/// Displacement Map's map layer…), itself included: [`Self::referenced_layers`] without the
/// parent and the track matte.
pub fn picked_layers(&self) -> Vec<LayerId> {
let skip = usize::from(self.parent.is_some()) + usize::from(self.track_matte.is_some());
self.referenced_layers().into_iter().skip(skip).collect()
}
pub fn is_active_at(&self, t: Tick) -> bool {
t >= self.in_point && t < self.out_point
}
/// The time stretch the layer plays at: [`Layer::stretch`] within
/// [`limits::stretch`](crate::limits::stretch) (a damaged value that got past loading, 0 or
/// NaN, never divides the layer's time by zero).
pub fn effective_stretch(&self) -> f64 {
crate::limits::stretch(self.stretch)
}
/// Layer time for a comp time (stretch-aware).
pub fn layer_time(&self, comp_t: Tick) -> Tick {
let d = comp_t - self.start_time;
let s = self.effective_stretch();
if (s - 100.0).abs() < 1e-9 { d } else { scaled(d, 100.0, s) }
}
/// Comp time for a layer time.
pub fn comp_time(&self, layer_t: Tick) -> Tick {
let s = self.effective_stretch();
if (s - 100.0).abs() < 1e-9 { self.start_time + layer_t } else { self.start_time + scaled(layer_t, s, 100.0) }
}
pub fn transform(&self) -> Option<&PropGroup> {
self.props.sub("transform")
}
pub fn transform_mut(&mut self) -> Option<&mut PropGroup> {
self.props.sub_mut("transform")
}
pub fn effects(&self) -> Option<&PropGroup> {
self.props.sub("effects")
}
pub fn masks(&self) -> Option<&PropGroup> {
self.props.sub("masks")
}
/// The labels popup property `p` of this layer offers: Text Path Options ▸ Path lists None and
/// the layer's masks (value k = mask k), every other popup its own options. `None` when `p` is
/// not a popup.
pub fn popup_options<'a>(&'a self, p: &'a Property) -> Option<std::borrow::Cow<'a, [String]>> {
if self.props.prop("text/pathOptions/path").is_some_and(|q| q.uid == p.uid) {
let masks = self.masks().into_iter().flat_map(|m| m.groups().map(|g| g.name.clone()));
return Some(std::iter::once("None".to_string()).chain(masks).collect::<Vec<_>>().into());
}
match &p.ui {
ParamUi::Popup { options } => Some(options.as_slice().into()),
_ => None,
}
}
pub fn is_camera(&self) -> bool {
matches!(self.source, LayerSource::Camera)
}
pub fn is_light(&self) -> bool {
matches!(self.source, LayerSource::Light { .. })
}
/// The layer participates in 3D (3D switch, or cameras/lights which are always 3D).
pub fn is_3d(&self) -> bool {
self.switches.three_d || self.is_camera() || self.is_light()
}
pub fn has_video(&self) -> bool {
self.source.is_av() && self.switches.video
}
}