Adjustment layers (docs/zh/plans/adjustment-layers-and-transitions.md): a new timeline block type whose effect chain grades the composite of every video track below it, over its own range (spanning clips or a slice of one). The graph path flushes the lower tracks at the block's track boundary and sweeps the composite through the chain via a transient texture-source node; the montage path mirrors it with AdjustmentSpan tickets (wire-compatible), so worker previews and exports agree. An empty-area context menu creates one; the block trims/moves/deletes like a clip, with undo everywhere. Transitions: seam blocks come alive - cross dissolve/fade/wipe/slide evaluate both neighbors through the graph path with progress from the transition's own range (never the whole clip). Ctrl+Shift+D or the clip menu inserts a default transition; the gpui wedges render and drag to resize offsets undoably, and TransitionRemoveCommand now restores offsets and edges on undo. The transitionfx node form runs the same shaders on an adjustment layer with progress_in auto-filled from the layer's span (explicit value wins). Also: every built-in effect name and parameter name is now translatable (360 node.* keys per locale, zh-CN fully translated, two coverage tests guard future gaps); the new nodes register in nodes/mod.rs with the factory smoke table updated; textfootage and adjustment-layer i18n keys included.
oaktimeline Rust crate (declaration draft, for review)
Status: declaration draft. Signatures + doc comments are the spec; every body is
todo!(). Not wired into any build. The crate template (FFI discipline, testing layers) followscrates/oakplugin/README.mdandcrates/oaknode/README.md.
Scope
Replaces the C++ oaktimeline module (src/timeline/src): timeline
markers and work areas, the timeline undo-command family (add/remove
tracks, place/trim/split blocks, ripple edits, slide, gap insertion),
and the shared Timeline namespace / utility helpers.
Public contract: include/timeline/*.h (error.h, displaymode.h,
marker.h, workarea.h, edit.h) — frozen, implemented verbatim by
src/ffi.rs. The timeline value handles (OakTimelineMarkerList,
OakTimelineWorkArea) and every edit command are exported through this
ABI only; consumers (the facade/app, the oaknode crate) never see the
internal Rust types.
Key architectural decisions (C++ → Rust mapping)
-
Domain modules mirror the C++ header files. The C++ module is a flat set of headers, not a deep class hierarchy, so the Rust crate keeps one module per C++ header family (
common,marker,workarea,undocommon,undotrack,undogeneral,undopointer,undoripple,undosplit,util).COVERAGE.mdmaps every C++ type to its Rust home; review that first. -
No C++
UndoCommandsubclass hierarchy. Following the oaknode crate decision (#4), each undo command is a plain Rust struct that exposesprepare()/redo()/undo()(alltodo!()here) and is surfaced to the world through the oakundo C ABI vtable (bridge::undo::oakundo_command_init, Rust callbacks asuserdata). Every command struct carries ato_command()factory that wraps it into anoakundo::undocommand::UndoCommandvalue for the undo stack. -
Value types come from
oakcore-rs. Markers and work areas are built onRational/TimeRange, so the crate depends onoakcore-rs(crates/oakcore) exactly like oaknode does; no pixel/sample formats are involved here. -
All cross-module access goes through the C ABI. Per the project rule (no cross-module C++ member calls), timeline commands touch the node graph exclusively through the oaknode C ABI (
bridge::node), undo through the oakundo C ABI (bridge::undo), and XML/config through the oak_core C ABI (bridge::common). The C++ internal helpersoakundo_capi::make_command_handle/oaknode_c_api::to_nativeare not replicated in Rust — their role is subsumed by vtable commands and by value handles treated as opaque. -
Handles.
handle.rskeeps theRefBox/CHandlescaffolding for the oakengine facade boundary (the C ABI export layer is the only place left that talks handles). Everymake_ownedvalue handle boxes anArc<Mutex<T>>— the same pattern as the oaknode project handles — so the crate's commands hold the shared marker list / work area as a plain RustArc<Mutex<…>>and the facade entries convert the handle back to theArcat the boundary.
Layout
src/
lib.rs crate doc + module map
error.rs error codes (mirrors include/timeline/error.h)
handle.rs refcounted-handle scaffolding (same pattern as node)
common.rs Timeline namespace (MovementMode/ThumbnailMode/
WaveformMode, EditToInfo) — timelinecommon.h
marker.rs TimelineMarker/MarkerList + 5 marker commands
workarea.rs TimelineWorkArea + 2 workarea commands
undocommon.rs node/block remove helpers + MultiUndoCommand
undotrack.rs track ripple/prepend/insert-after/replace commands
undogeneral.rs resize/media-in/add/remove-track/transition/gap/
enable-disable/insert-gaps/default-transition commands
undopointer.rs BlockTrimCommand/TrackSlideCommand/TrackPlaceBlockCommand
undoripple.rs ripple remove-area / ripple-tool / delete-gaps commands
undosplit.rs BlockSplitCommand/BlockSplitPreservingLinksCommand/
TrackSplitAtTimeCommand
util.rs timelineutil.h inline helpers (rat_nd, same_*,
free_detached_handle, block/track queries)
bridge/ C ABI imports: node.rs, undo.rs, common.rs
ffi.rs include/timeline/*.h export layer
tests/ contract + golden tests (see README test section)
Hard rules for the implementer
- Every
extern "C"body goes throughhandle::guard*; no panic crosses FFI. - Timeline objects never outlive their owning node; borrowed handles are created under a guard that owns the node reference.
- Behavior parity with C++ is proven by the C ABI test-suite
(
src/timeline/tests, unchanged) plus the golden tests intests/(XML save/load formats captured verbatim fromsrc/timeline/src/timelinemarker.cpp/timelineworkarea.cpp). - Where C++ behavior is genuinely load-bearing but ugly (e.g. marker
list kept sorted by time, ripple's compensation gap rules), port the
behavior, not the aesthetics; leave a
// CPP-PARITY:comment with the C++ file:line.
Dependency policy
Prefer mature third-party crates (MIT/Apache-2.0/BSD, GPL-compatible) over hand-rolling; register each addition (name + reason) here. Large existing C++ libraries (OTIO, OCIO, OIIO, FFmpeg) are NEVER rewritten — they are consumed through their C ABI / bridge layers.