Dragging a clip kept its transitions on their old spots, so they jumped to the
dropped position on release instead of moving with the clip. The drag preview now
draws each transition where the drop will leave it, applying the same rule the
edit does: a fade travels with its clip, and a cut transition travels only when
every clip it sits on moves to one track by one offset. A cut transition whose
one side stays behind is dropped on release; the preview keeps it on its cut
while dragging, and Option-drag copies show a ghost transition beside the
copies, duplicating only the transitions whose every clip is copied.
- edit: carried_transitions keeps only all-clips-move transitions; move_items
and copy_items drop or skip the rest
- ui: move_transition_preview draws carried transitions where they land
- tests: timeline_transition_move_ui (fade, both-clips cut, XYZ partial drop,
cross-track drop, Option-drag ghost); transitions_travel_with_moved_clips
updated to the drop-on-partial rule
rate_stretch_items skips clips on locked tracks and refuses the edit when
every clip is locked, which is what the command loop on main did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Non-ripple clip.speedDuration shortened the clip but left one-sided
out-edge transitions at the old timeline range. Reposition them with
the same edge-follow used for a regular trim.
A neighbour on only one track used to clamp that partner alone, so picture
and sound stayed linked at different rates. The tightest room now limits
the whole group.
Trim, roll, slip, slide, rate stretch and Speed/Duration now fail with
"track is locked" instead of changing clips on a locked track, matching
same-track move. Linked partners on locked tracks are left alone.
Holding Option (Alt) while dragging timeline clips now leaves the
originals in place and drops copies where the pointer lets go, as in
Premiere. The copies are selected; copies of linked clips are linked to
each other, and transitions that would travel with a move are copied.
- edit: copy_items (overwrite / insert, new ids, links and transitions)
- engine: timeline.move takes `copy: true`, returns `copied`
- ui: the drag remembers Option, shows the copy under the pointer while
the originals stay, copy cursor
- tests: engine copy/link/locked-track test, headless Option-drag UI test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: dragging a clip's edge with the Selection tool (a regular trim)
moved the edge but left a fade in from nothing / fade out to nothing where
it was; only Ripple trims moved transitions along with the cut.
Fix: after a regular trim, the trimmed clip's one-sided transitions are put
back on its edges, and shortened when the clip gets shorter than them.
Transitions between two clips are untouched (a regular trim cannot move
their cut).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: moving a clip dropped the transitions at its edges (#227, point 8
of #374). `edit::move_items` deletes the moved clips before placing them
again, and that deletion removes every transition that refers to them, so
a fade on a clip's edge, or the cut between two clips moved together,
vanished.
Fix: before the move, collect the transitions whose clips all move onto one
track by one offset, and put them back on that track shifted by the offset
once the clips have landed (overwrite and insert moves). A crossfade whose
partner stays behind is still dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: timeline.slideLeft/Right(5) slid every expanded selection member by
the raw requested delta, so when a neighbour limited only one linked partner
(e.g. a short sound-only follower), picture and sound ended up at different
starts. timeline.slide already clamped to a common delta.
Fix: add edit::slide_items, which clamps the delta by the most restrictive
item and slides all items by it, and use it from both the keyboard Slide
commands and timeline.slide.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closing a gap on a locked track skipped that track but still moved the
sync-locked ones, pulling them out of sync with it. It is now refused
with the usual "locked" error.
While an edge was dragged, the Timeline drew the clip at its new length
from the untrimmed item, so draw_waveform spread the clip's whole source
range over the shorter (or longer) box: the waveform squeezed and the
sound under the pointer moved, which made it hard to trim to where the
music starts (#374, item 13). Trim and roll previews now draw from the
item the edit will leave, so every timeline time still covered shows the
same media and the waveform is cut off at the moving edge. Video
thumbnails show the new first frame the same way. Rate Stretch, which
really fits the same media into a new length, still squeezes.
The item update is factored out of trim and ripple_trim_group into
filmcraft_edit::move_edge, which the preview uses too, so preview and
edit agree for speed-changed, reversed and frame-held clips.
Rendered mid-drag against the released result, the music clip's
waveform matched in 15% of pixel columns before and 100% after.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adobe's Change transition duration page shows the Trim-In / Trim-Out icon on a transition's end
in the Timeline: the drag trims that end, and the other stays. A centred transition dragged by one
end therefore becomes Custom Start, rather than growing on both sides. The alignment rule ("how
the transition changes length depends on the alignment") stays for typed durations: Effect
Controls' Duration and Set Transition Duration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- filmcraft_edit::transitions: where an applied transition may sit (over its cut, within the
clips it joins; a fade to or from nothing keeps its clip edge), its alignment as Effect
Controls shows it (Center at Cut / Start at Cut / End at Cut / Custom Start), set_span, remove,
and drag_span for the Timeline (an end changes the duration, the middle slides it).
- EditorState::transition_selection; timeline.select {"transitions": [...]} replaces the clip,
edit point and caption selections, and a clip selection clears it.
- edit.clear (Delete) removes the selected transitions when no clips are selected;
sequence.removeTransition removes given or selected ones; Ripple Delete stays clip-only.
- sequence.setTransition also takes duration / frames (following the alignment: Center at Cut and
Custom Start move both ends, Start at Cut the end, End at Cut the beginning), align, start, and
merge / begin so a drag is one undo step. Out-of-range times are refused before any frame
arithmetic, atomically.
- sequence inspection reports each transition's alignment and the transition selection.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict in the timeline's right-click handling (crates/ui-egui/src/panels/timeline.rs):
#220 now remembers the right-clicked clip so Unlink keeps only it selected, and this branch
opens the edit point menu on a clip's edge. Both are kept: the clip is remembered on every
right-click, and an edge also stores the edit point menu's target.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keeps #359's contiguous_starts audio timing, now per stream in Mp4Audio /
MkvAudio (starts and preroll in source sample frames). MP4 audio streams and
their decoders are built from the same (track, info) pairs so they stay
index-aligned. Adapts new main code to audio_streams / TrackItem::audio_stream.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Update MediaInfo to store all discovered audio streams in audio_streams
- Retain independent stream metadata and decoder state for MP4/MOV and Matroska/WebM
- Update audio_placement_specs to allocate successive tracks via ensure_audio_tracks
- Isolate clip rendering to read assigned audio stream rather than replaying stream 0
- Bump project schema to v13 with backwards compatibility for legacy v12 projects
Resolves#312Fixes#200Fixes#315
- filmcraft_edit::frame_size::scale_motion: Sequence Settings' "Scale motion effects
proportionally when changing frame size". Every video clip's Motion Position maps through
p' = c1 + k(p - c0) and Scale / Scale Width multiply by k = min(W1/W0, H1/H0), static values
and keyframes: a plain proportional scale for the same aspect, and on an aspect change the old
frame fits inside the new one, centred. Automatic positions stay automatic, Scale to Frame
Size clips keep their Scale (the renderer refits them), Anchor Point and Rotation stay.
- sequence.settings: new dropFrame, maxRenderQuality, workingSpace, wideGamut, autoToneMap and
scaleMotion (default false, so existing callers are unchanged) params, in the same undo step;
returns {"scaledClips": n}. Drop-frame on a rate without it is refused atomically.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Right-clicking a clip's edge on the timeline now selects that edit point and
opens Premiere's edit point menu instead of the clip menu: Ripple Trim In,
Ripple Trim Out, Roll Edit, Trim In and Trim Out (each re-selects the cut with
that trim type, greyed out when the needed side has no clip), Apply Default
Transitions, and Join Through Edits. The body of a clip still opens the clip
menu.
Join Through Edits from this menu joins only the right-clicked cut (and its
linked partner): `sequence.joinThroughEdits` takes a new `cut: [left, right]`
parameter, since `clips: [id]` also matched any other through edit touching
the same piece.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A refused ripple delete / ripple trim now names the sync-locked track whose
clip is in the way and how to proceed, instead of a generic "would break sync"
(the edit is refused the same way Premiere refuses it).
- Dragging the playhead no longer snaps onto its own position (it stuck and moved
in SNAP_PX jumps), snaps on the initial press too, and "Snap playhead in
Timeline when Snap is enabled" is on by default, as in Premiere.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A linked clip that spans the cut while its partner reaches past it too is
no L cut, and still refuses, like the same clip unlinked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A ripple trim that shortens a clip's head was refused when the previous
clip's linked sound ran on past the cut on a sync-locked track (an L cut).
That clip never moves, so its sound stays put and the L cut still ends
where it did. Unlinked material across the cut (a music bed) still refuses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sync-locked tracks rippled from the grabbed member's start, so grabbing a
J cut's early sound checked and shifted from before the picture's cut and
was refused. For a head trim the cut is the latest member's start. Adds the
test only the new stretch catches: a clip starting at the cut on a
sync-locked track, which the old stretch let fall out of sync.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A ripple trim that shortens a clip's head was refused as a sync-lock
conflict whenever a sync-locked track held material just BEFORE the cut,
because the check looked at the stretch before it. With a J cut that is
the outgoing clip's sound on the other audio track, so the J-cut take
could never lose its head. The stretch that closes is the one just after
the cut; the clip's own linked partners stay the caller's to trim.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
set_speed_group accepted NaN (speed <= 0.0 is false for NaN), and a tiny speed (1e-7) saturated the new duration to i64::MAX, after which the ripple shift overflowed (panic in debug, wraparound in release). Non-finite speeds are refused, the new duration is capped at what the timeline can hold, and the ripple shift uses checked arithmetic; a refused edit changes nothing (it works on a copy). Regression test with hostile speeds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: `edit.rippleDelete` on a clip whose linked sound has a
different edge than its picture (a J or an L cut) treated the two
ranges as two gaps and closed both on every affected track. With two or
more clips after it the later ones were moved up twice and the edit was
refused ("items overlap"). With one clip after it the delete went
through, and sequence markers after it were moved by both gaps (one
that landed inside the second gap was removed).
Solution: one set of stretches closes on every track: the time the
deleted clips covered, less whatever stays on the tracks that lose a
clip. Those tracks move up by the same amount, into time that is free
on all of them, so later clips stay in sync and nothing is overwritten.
For a J or an L cut that is the picture's range. Straight cuts close as
before, also with the sounds on alternating tracks. A track that is
locked loses nothing and does not count. `ripple_delete_items` returns
the stretches it closed and the command ripples the markers by those,
so the two cannot disagree.
Things to know. A gap can stay on one track when a clip that stays on
another of the tracks covers part of the deleted time: a sound shorter
than its picture with other sound right up against it, or unlinked
clips on different tracks deleted in one call. When nothing can close
at all the edit is refused and nothing changes; some such calls went
through before, moving clips by a gap another track did not have. A
selection wholly on locked tracks is refused as locked; before, it
deleted nothing and still shifted the sync-locked tracks.
Tests: an edit-crate test (a split on both sides, two neighbours,
clips apart in time, sounds on alternating tracks, a short sound with
and without neighbours, a locked track, the refusal); engine tests
through `edit.rippleDelete` for a J cut with a marker, undo and redo,
an L cut, two neighbours with a split between them, two neighbours with
sounds on alternating tracks, and a straight cut.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: `timeline.trim` with `mode: "ripple"` on the clip before a
split edit moved the later picture and left its linked sound behind.
With a J cut (the next clip's sound starts before its picture, on a
sync-locked track where the trimmed clip has no partner) a sound that
led its picture by 12 frames led by 20 after lengthening by 8, with no
error. Shortening was refused as a sync-lock conflict, because the
early sound sits in the stretch that closes.
Solution: in `ripple_trim_group`, a clip that starts before the cut,
reaches past it and is linked to a clip that shifts now shifts with it,
on the tracks that ripple (the members' tracks and sync-locked tracks).
It no longer counts as material in the way of a shortening. When it has
no room to follow (another clip right before it, or the sequence start)
the trim is refused with the sync-lock error and nothing changes (an
overlap on any track that ripples now reports that error instead of
the generic "items overlap"). A
linked clip that ends before the cut is left alone, as before, and so
are the trimmed clips' own partners. A transition into a follower from
a clip that stays behind would be left over a gap; it is removed.
Tests: an edit-crate test for longer, shorter, both refusals, the
clip that ends before the cut, the In edge, the trimmed clip's own
partners and the transition; engine tests through `timeline.trim` for
longer and shorter with undo and redo, and for both refusals.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: `clip.speedDuration` with `ripple: true` on a clip that has
linked sound set the speed on the picture and then on its sound, and
each one rippled the later material on its own. Slower (50%) moved the
later clips by twice the change and left a double gap. Faster (200%)
was refused ("items overlap") when more than one clip followed: the
second ripple pulled the farther clips onto the nearer ones.
Solution: `set_speed_group` changes a clip and its linked partners on
other tracks as one edit and ripples once, on the members' tracks and
on sync-locked tracks. The command groups the selected clips by link
(one clip per track in a group); clips that are not linked, or linked
clips of one track, still change one after the other. When the members'
lengths change by different amounts (a split edit), later material
moves by the largest change, so no member is overlapped and the tracks
stay in sync; the shorter member is followed by a gap. The group edit
works on a copy, so a failure changes nothing. `set_speed` keeps its
signature and behaviour for one clip.
Tests: an edit-crate test (slower, faster, a split edit both ways, two
clips of one track and a missing clip refused with nothing changed);
engine tests through `clip.speedDuration` for slower with undo and
redo, faster, two clips in one call, no ripple, picture and sound both
named with Linked Selection off, linked clips of one track, a split
edit, and a clip that is not in the sequence.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Commands: place/nest/sequence settings/import/multicam/subclip/audio channels/extract audio,
graphics reset duration, dynamic trim and Make Subsequence return errors instead of `expect`
when an item, sequence or track is missing (stale ids from a damaged project or a racing
agent); File ▸ Import detects caption/interchange documents without a second `expect`.
- `find_spec` (graphics) returns `UnknownCommand` instead of `expect`.
- Built-in effect lookups (presets, demo project) fall back to a bare instance instead of
`panic!`/`expect`; the demo project falls back to an empty one.
- Multicam record base, shortcut presets and field parsing without `expect`.
- edit: caption merge/move and transcript caption blocks without `expect`.
- Both crates deny unwrap/expect/panic/unimplemented/todo/unreachable outside tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>