Skip to content

Implement Clip Overlap Resolution - #134

Open
Sheerwin02 wants to merge 34 commits into
mainfrom
sheerwin/overlap-bug-fix
Open

Implement Clip Overlap Resolution#134
Sheerwin02 wants to merge 34 commits into
mainfrom
sheerwin/overlap-bug-fix

Conversation

@Sheerwin02

@Sheerwin02 Sheerwin02 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Overlapping clips no longer panic or corrupt

Two animations driving the same field of the same subject at the same time
used to panic in debug and silently corrupt the lane in release. This PR
makes that case defined behaviour: both clips are kept, and at any instant the
one later in its lane is the one on screen.

Why it broke

Sequence refused any clip that did not begin after the previous one ended.
That rejected genuine overlaps — and also rejected clips that merely arrived
out of order, which all/any/flow produce whenever a fragment listed
first is delayed past one listed later. Clips seconds apart could panic purely
on listing order.

Underneath, bake chained values: each clip opened on the previous clip's end.
That assumption is what made the rejection necessary in the first place.

What changed

Resolution moves to where it is observed. A new resolve_clip answers
"which clip is on screen at this time" for both baking and playback: the last
clip in the lane amon
last.

Baking opens on what is visible. Each clip now starts from the value the
lane is showing at clip.start, rather than from the previous clip's end. An
overlapping clip takes over seamlessly instead of teleporting.

Failures log instead of panicking. Both debug_asserts are removed.
Sequence emits error! when a clip starts before the one ahead of it, or
overlaps one already present — at build time, so the author hears about it
while writing the animation. Adds an optional tracing dependency, on by
default. The overlap test is strict on both ends, so chain stays quiet.

Track::duration now maxes over every clip rather than each lane's last with clips stored as listed, the last-stored clip isn't always the last to finish, so a lane could clamp the playhead before its own animation ended. (require review at (TrackFragment::compile, crates/motiongfx/src/track.rs:223) )

Other fix. compile() had field_offset = field_len where it meant
+=, so field lookups pointed at the wrong lanes from the third distinct
field onward.

Design decision: precedence

Where clips overlap, position in the lane decides — the clip listed later
wins while it covers the playhead. Listing order is what the author writes;
start times are derived by the combinators.

Compatibility

No animation that worked before changes behaviour. main rejected
overlapping lanes outright, so every lane it could represent is sequential —
and on a sequential lane the new resolution returns exactly what main's
binary_search_by returned, for clips, gaps, boundaries and the pre-lane
value alike. The new rule only decides territory main could not express.

Known limitations

  • Handing back jumps. When a covering clip ends, the clip underneath
    resumes at its own curve position rather than continuing from what was on
    screen. A clip has a single segment, so it cannot be reopened part way
    through; fixing it means splitting clips at coverage boundaries — a data
    model change beyond this PR.
  • Actions without interpolation open the next clip on a value the lane
    never displayed (TODO in pipeline.rs).

Jaghov and others added 17 commits July 10, 2026 22:04
## Objective
Resolves #107
Partially addresses : #89
## Solution
Basic timelineui implementation with:
- Scrubbing/seeking support
- Track segment visualisation
- Action segment visualisation

---------

Co-authored-by: Nixon <43715558+nixonyh@users.noreply.github.com>
Use pointer cursor on timeline. 
Also some minor visual changes.
<img width="1392" height="864" alt="image"
src="https://github.com/user-attachments/assets/84a034ee-080e-4c93-91ca-3e98de40d38c"
/>

- Added `motiongfx_editor_ui`
- Added `motiongfx_editor_ui_kernel`
- Moved `motiongfx_editor` into `editor/` folder, and make it a binary
instead of a lib.
- Removed `editor.rs` example and move its content to
`motiongfx_editor/src/main.rs` as a stub content until we have proper
serialization implementation.

Relates to #70 & #89
Support serialization of `motiongfx` via `motiongfx_scene`!
`motiongfx_scene` is backend agnostic, which means it's not just bevy,
it can work with any backend world/renderer/app you like!

This PR also made double confirmation on `no_std` support for
`motiongfx` & `motiongfx_scene` by compiling it using the
`thumbv7em-none-eabihf` target.
Follows the builder no longer accumulating tracks itself. `Tracks` wraps
a `NonEmpty<Track>`, so a timeline can't be built with zero tracks, and
`compile` takes `impl Into<Tracks>` with a `From<Track>` impl so the
single-track case stays `b.compile(track)`.

Multiple tracks go through `Tracks(nonempty![a, b])`, with `nonempty`
re-exported from `motiongfx` so callers don't depend on it directly.

`CompileError::EmptyTimeline` goes with it - the type now rules that
case out, leaving the variant unconstructible.
The goal here is to encapsulate ui elements and widgets into their own
modules so private `const`s, `fn`s, `struct`s stays private without
leaking into unnecessary places.

This PR also deletes most of the marker components where it's become
useless because of the use of `moxie_ui_kernel` bindings & watchers.
Replace all the manual `ui.node(|world, entity| {
world.entity_mut(entity).insert(..) })` with `ui.bundle(..)` that
inserts the bundle immediately instead!
@Sheerwin02
Sheerwin02 requested a review from nixonyh August 6, 2026 14:56
@Sheerwin02 Sheerwin02 self-assigned this Aug 6, 2026
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Timelines now support overlapping clips, with later-starting clips taking playback precedence.
    • Playback resolves clips correctly across overlaps, gaps, boundaries, lane changes, and track transitions.
    • Sequence durations now reflect the latest-ending clip.
    • Overlapping animation states preserve interpolation and easing during playback and export.
  • Bug Fixes

    • Improved scrubbing, backward seeking, zero-duration clip handling, and track transitions.
    • Prevented finished clips from being unnecessarily reprocessed while playback is paused.

Walkthrough

The PR adds overlap-aware sequence merging, timeline sampling, track compilation, and pipeline baking. Clips remain ordered by start time. Later-starting clips take precedence during overlaps. Latest-finishing clips determine gap and track-end values.

Changes

Overlap-Aware Clip Playback

Layer / File(s) Summary
Sequence merging and track compilation
crates/motiongfx/src/sequence.rs, crates/motiongfx/src/track.rs
Sequences accept overlapping clips and merge them by start time. Track duration uses the latest clip end. Field offsets accumulate across fields.
Timeline overlap resolution
crates/motiongfx/src/timeline.rs
Timeline sampling resolves covering clips by lane position. It uses latest-finishing clips for gaps, track ends, and skips. Tests cover overlaps, boundaries, scrubbing, parked playheads, and lane isolation.
Overlap-aware pipeline baking
crates/motiongfx/src/pipeline.rs
Baking resolves visible lane state with interpolation and easing. It accumulates baked clips and segments, then writes them to the action table.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Sequence
  participant Track
  participant Timeline
  participant Pipeline
  Sequence->>Track: merge sorted overlapping clips
  Track->>Timeline: provide compiled clip timing
  Timeline->>Timeline: resolve visible clip for sample time
  Pipeline->>Timeline: resolve lane state during baking
  Timeline-->>Pipeline: return interpolated clip state
  Pipeline-->>Pipeline: accumulate and write baked segments
Loading

Possibly related PRs

Suggested reviewers: jaghov

Poem

I’m a rabbit hopping through overlapping time,
Sorting each clip in a neat little line.
The latest start shines when clips combine,
Furthest ends mark the finish sign.
Baked lanes now flow, precise and bright.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely identifies the main change: resolving overlapping animation clips.
Description check ✅ Passed The description directly explains overlapping clip behavior, precedence, baking changes, duration fixes, and the known limitation.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
crates/motiongfx/src/action/table.rs (1)

84-85: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Align the counter arithmetic with the documented invariant.

Line 38 documents next_order as monotonic. wrapping_add breaks that after 2^32 actions: a later-authored action then receives a lower order, and both resolve_overlaps (other.order > clip.order) and resolve_clip (max_by_key(|clip| clip.order)) invert their precedence. The count is unreachable in practice, so this is a robustness and documentation point only. saturating_add keeps the invariant in the degenerate case, at the cost of ties at the ceiling.

♻️ Proposed change
         let order = self.next_order;
-        self.next_order = self.next_order.wrapping_add(1);
+        self.next_order = self.next_order.saturating_add(1);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/motiongfx/src/action/table.rs` around lines 84 - 85, Update the
counter increment in the action-order assignment using saturating arithmetic
instead of wrapping arithmetic, so next_order remains monotonic at its maximum
value and preserves precedence in resolve_overlaps and resolve_clip.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/motiongfx/src/action/table.rs`:
- Around line 84-85: Update the counter increment in the action-order assignment
using saturating arithmetic instead of wrapping arithmetic, so next_order
remains monotonic at its maximum value and preserves precedence in
resolve_overlaps and resolve_clip.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e33393d2-74f4-4b1d-bf24-e30920d3196b

📥 Commits

Reviewing files that changed from the base of the PR and between 7203222 and 46db1b8.

📒 Files selected for processing (8)
  • crates/bevy_motiongfx/Cargo.toml
  • crates/bevy_motiongfx/src/manager.rs
  • crates/motiongfx/src/action.rs
  • crates/motiongfx/src/action/table.rs
  • crates/motiongfx/src/lib.rs
  • crates/motiongfx/src/sequence.rs
  • crates/motiongfx/src/timeline.rs
  • crates/motiongfx/src/track.rs

@nixonyh nixonyh left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is my first pass review. Generally I'm not happy with how overlaps is handled here. I would prefer the more naive and easier way to just store clips as is in the Sequence / TrackFragment / Track and only resolve it during baking (see motiongfx::pipeline::bake<..>()).

Comment thread crates/bevy_motiongfx/Cargo.toml
Comment thread crates/motiongfx/src/track.rs Outdated
Comment thread crates/motiongfx/src/track.rs Outdated
Comment thread crates/motiongfx/src/track.rs Outdated
Comment thread crates/motiongfx/src/action/table.rs Outdated
@Sheerwin02
Sheerwin02 marked this pull request as draft August 6, 2026 15:59
@Sheerwin02
Sheerwin02 marked this pull request as ready for review August 7, 2026 11:14
@Sheerwin02
Sheerwin02 requested a review from nixonyh August 7, 2026 11:14

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
crates/motiongfx/src/pipeline.rs (2)

223-248: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Optional: the gap fast path duplicates resolve_clip's fallback.

When max_end < clip.start, every clip in baked has already finished. resolve_clip(&baked, clip.start) then finds no covering clip and falls through to its max_by_key(clip.end()) branch, which returns the same index with SampleMode::End. max_by_key keeps the last maximum, and the >= test at line 303 keeps the last maximum too, so both paths break ties the same way today.

That leaves two independent tie-break rules that must stay in sync. If you drop the fast path, max_end and max_idx become unnecessary and the precedence rule lives in one place.

The behavior at a touching boundary does not change: max_end == clip.start already falls through to resolve_clip.

♻️ Proposed simplification
-        // The furthest end so far and which clip reached it. Ties go
-        // to the later clip, as in `resolve_clip`.
-        let mut max_end = Duration::ZERO;
-        let mut max_idx = 0;
-
         for clip in ctx.track.clips(*span) {
-                // Nothing is still running, so nothing covers this
-                // clip.
-                let (i, mode) = if max_end < clip.start {
-                    (max_idx, SampleMode::End)
-                } else {
-                    resolve_clip(&baked, clip.start)
-                };
+                // Whatever the lane shows at `clip.start`: the last
+                // covering clip, or the last one to finish.
+                let (i, mode) = resolve_clip(&baked, clip.start);
-            if clip.end() >= max_end {
-                max_end = clip.end();
-                max_idx = baked.len() - 1;
-            }
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/motiongfx/src/pipeline.rs` around lines 223 - 248, Remove the gap fast
path that checks max_end < clip.start in the clip-opening logic, and always call
resolve_clip(&baked, clip.start) for non-empty baked state. Then remove the
now-unused max_end and max_idx tracking and related updates, leaving
tie-breaking solely in resolve_clip while preserving the existing
touching-boundary behavior.

289-296: 🎯 Functional Correctness | 🔵 Trivial

TODO recorded for the missing-interpolation fallback.

An action with no interpolation opens the next clip on segment.end, a value the lane never displayed. The rest of the block is correct: the eased t is applied before interpolation, and the value is read from the local segments buffer rather than the table.

Do you want me to open an issue to track this fallback?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/motiongfx/src/pipeline.rs` around lines 289 - 296, Replace the `None
=> segment.end.clone()` fallback in the interpolation logic with behavior that
uses the value actually displayed by the lane for actions without interpolation,
rather than advancing to `segment.end`. Preserve the existing eased-`t`
interpolation and local `segments` buffer usage for interpolated actions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/motiongfx/src/pipeline.rs`:
- Around line 223-248: Remove the gap fast path that checks max_end < clip.start
in the clip-opening logic, and always call resolve_clip(&baked, clip.start) for
non-empty baked state. Then remove the now-unused max_end and max_idx tracking
and related updates, leaving tie-breaking solely in resolve_clip while
preserving the existing touching-boundary behavior.
- Around line 289-296: Replace the `None => segment.end.clone()` fallback in the
interpolation logic with behavior that uses the value actually displayed by the
lane for actions without interpolation, rather than advancing to `segment.end`.
Preserve the existing eased-`t` interpolation and local `segments` buffer usage
for interpolated actions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 49b3bdaa-49e2-4f44-8a66-65e1fd80af96

📥 Commits

Reviewing files that changed from the base of the PR and between cd85830 and 579088a.

📒 Files selected for processing (4)
  • crates/motiongfx/src/pipeline.rs
  • crates/motiongfx/src/sequence.rs
  • crates/motiongfx/src/timeline.rs
  • crates/motiongfx/src/track.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/motiongfx/src/sequence.rs

Comment thread crates/motiongfx/src/sequence.rs
@Sheerwin02
Sheerwin02 requested a review from nixonyh August 8, 2026 06:03

@nixonyh nixonyh left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, I dun think we should be sorting the clips. And some nits.

Comment thread crates/motiongfx/src/sequence.rs
Comment thread crates/motiongfx/src/sequence.rs Outdated
Comment on lines +10 to +13
/// Clips **may overlap**. Where they do, the one later in the list
/// wins. The list is sorted by start, so that is whichever began
/// most recently, not whichever was authored last. Clips it covers
/// show again when it ends, each at its own progress.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dun think we're sorting anything, but we have a guarantee that the [ActionClip::start] will be in order.

Comment thread crates/motiongfx/src/track.rs
Comment thread crates/motiongfx/src/track.rs Outdated
Comment thread crates/motiongfx/src/track.rs Outdated
Comment thread crates/motiongfx/src/track.rs Outdated
Comment thread crates/motiongfx/src/sequence.rs Outdated
Comment thread crates/motiongfx/src/track.rs Outdated
Comment thread crates/motiongfx/src/track.rs Outdated
Comment thread crates/motiongfx/src/track.rs Outdated
@Sheerwin02
Sheerwin02 marked this pull request as draft August 8, 2026 14:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants