Files
craft-libs/crates/aep/src/camera.rs
T
Dylan M. Taylorandvangel76 4a7024cd69 craft-aep: flat read-only tree index and the Pard decoder (ports of effectcraft#3302 and #3319) (#948)
* craft-aep: a flat read-only tree index, borrowed text documents, fewer allocations

The container part of storytold/effectcraft#3302:

- `TreeIndex`: the chunk tree as one flat array in depth-first order,
  borrowed from the input, for readers that only look. Same chunks counted
  against the same limits and the same errors as `parse`. An `.aepx` is
  indexed by `index_aepx` into one buffer. The typed view reads through
  either tree (`Node`, `Root`, `Project::from_index`).
- Chunk parser: each list of siblings is counted, then reserved once.
  `.aepx`: hex decoded into an output sized once, attribute values without
  an entity borrowed. `write` reserves the exact output size.
- Text documents are parsed into a tree that borrows from the body; the
  style and paragraph defaults are built once.
- Layer markers and Source Text are read where they are stored instead of
  searched for.
- Container accessors that only tests used are removed.

What the importer and exporter in EffectCraft reach across the crate
boundary is public here: `Root`, `Draft`, `AepxDrafts`, `index_aepx`,
`TreeIndex::from_drafts`, `Project::of`, `ProjectSettings::of`,
`Node::marker_list`, `Root::file_version` and `saved_by_version`,
`StyleRun::SAMPLED`. `TimeBase::NTSC` holds the rates as plain
numerator and denominator instead of an EffectCraft type.

* craft-aep: one decoder for an effect parameter definition (`Pard`)

The container part of storytold/effectcraft#3319: the fields of a `pard`
record (type code, name, default, valid range, slider range, popup item
count) are read by one type, `Pard`, which `EffectParam` now reads
through. It is public so that an exporter's tests can read back what
they encode instead of keeping a second decoder.

The rest of that PR (import and export bounds, loss reports) is in
EffectCraft's importer and exporter.

---------

Co-authored-by: vangel76 <vangel1976@googlemail.com>
2026-11-11 19:01:12 -05:00

47 lines
2.6 KiB
Rust

//! Cameras and lights.
//!
//! Both are layers without a source (`ldta` byte 40 is 0 for a camera and `0xffff_ffff` for a light)
//! whose type is 2 (camera) or 1 (light). Their `ldta` carries the auto-orient bit (byte 38, `0x40`)
//! and, for a light, the light type (see [`LayerHeader::light_type`](crate::LayerHeader)). The
//! properties sit in the same Transform group as on other layers but follow other conventions:
//!
//! * **Position** is stored as given once the user has set it. While it is at its computed default
//! the record is an "auto state" (`tdb4` bytes 76 and 77 are 1 although there is no expression)
//! that holds `[0, 0, -zoom]`. After Effects reports and renders the stored value, also for a
//! two-node camera (256 read back `[0, 0, -2666.67]` in session S16; 537 renders from there). An
//! earlier batch-3 note had a two-node camera report the composition centre; that was not
//! reproduced.
//! * **Anchor Point** of a camera is its point of interest, in pixels (not a fraction).
//! * A light's **Intensity** (`ADBE Light Options Group/ADBE Light Intensity`, percent) is stored
//! only when it is not the default 100.
use crate::typed::{Layer, PropNode};
/// The Position of a camera or light layer as stored.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CameraPosition {
/// `[x, y, z]` in pixels as stored: the absolute position once set by the user, `[0, 0, -zoom]`
/// in the auto state (camera) or `[0, 0, -1333.33]` (default light).
pub stored: [f64; 3],
/// True while the position is derived (`tdb4` bytes 76 and 77 are 1 without an expression).
pub auto_state: bool,
}
impl<'f, 'a> Layer<'f, 'a> {
/// The Position record of a camera or light layer.
pub fn camera_position(&self) -> Option<CameraPosition> {
let Some(PropNode::Group(transform)) = self.properties()?.get("ADBE Transform Group") else { return None };
let Some(PropNode::Property(position)) = transform.get("ADBE Position") else { return None };
let v = position.static_value()?;
let stored = *v.as_slice().first_chunk::<3>()?;
let h = position.header()?;
let derived = h.get(76) == Some(&1) && h.get(77) == Some(&1) && position.expression().is_none();
Some(CameraPosition { stored, auto_state: derived })
}
}
/// The camera zoom After Effects uses when `ADBE Camera Zoom` is absent: 2666.67 px for the 50 mm
/// default camera in the 1920 px wide corpus composition. Whether it follows other composition
/// widths was not sampled.
pub const DEFAULT_CAMERA_ZOOM: f64 = 2_666.666_666_666_666_5;