Skip to content

Phase 2-E: Updaters (scene.always) - #15

Open
ronaldmannak wants to merge 2 commits into
claude/picomanim-phase2-02-groupsfrom
claude/picomanim-phase2-05-updaters
Open

Phase 2-E: Updaters (scene.always)#15
ronaldmannak wants to merge 2 commits into
claude/picomanim-phase2-02-groupsfrom
claude/picomanim-phase2-05-updaters

Conversation

@ronaldmannak

Copy link
Copy Markdown
Contributor

Phase 2-E: Updaters (scene.always)

Stacked on #12 (Phase 2-B) — base branch is claude/picomanim-phase2-02-groups; will be retargeted to main once #12 merges. Only the final commit is new here.

Scope

Manim's add_updater equivalent, redesigned to fit PicoManim's pure-snapshot model: a per-mobject function of absolute time that post-processes the animated state.

scene.always(dot) { time, state in
    state.moved(to: Vec2(cos(time), sin(time)))
}
scene.always(label, during: 2...5) { _, state in state.withOpacity(0.5) }

Public API

extension ManimScene {
    /// Registers an updater for `mobject`. From registration time on (or the
    /// `during` window), every snapshot applies `update(time, animatedState)`
    /// after animations resolve. Unseen mobjects are added automatically.
    public mutating func always(
        _ mobject: Mobject,
        during: ClosedRange<Double>? = nil,
        _ update: @escaping @Sendable (_ time: Double, _ state: Mobject) -> Mobject
    )
}

Design notes

  • Pure functions of time, not dt-stepping. Manim's dt updaters mutate state each frame, which breaks scrubbing and pure evaluation. Here an updater receives the absolute scene time and the fully-animated state, and returns a new state — snapshot(at:) stays deterministic and side-effect-free, so scrubbing/looping/offline render keep working. This is the one intentional semantic departure from Manim, called out in the docs.
  • Layering: updaters apply after the animation fold, in registration order (later updaters see earlier updaters' output) — pinned by a composition test.
  • Windowing: active from registration time (or during.lowerBound) until scene end (or during.upperBound).
  • Build cursor is unaffected: play targeting an updated mobject starts from the animated cursor state, not the updated one — updaters are a render-time overlay, so removing one never changes animation targets. Pinned by a test.
  • The closure is @Sendable and stored in a Sendable struct — real sendability, no @unchecked Sendable.

Test strategy

UpdaterTests.swift: purity (same t twice → same state), layering over a concurrent animation, registration-time start, during windowing, auto-add of unseen mobjects, registration-order composition, and cursor independence.


Generated by Claude Code

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces per-frame modifiers (updaters) to ManimScene, allowing pure functions of time to be layered on top of animated states. It also adds a comprehensive suite of unit tests to verify updater behaviors. Feedback on the changes suggests improving encapsulation by marking the internal Updater struct as private, and simplifying the optional mapping logic for endTime to make it more readable and idiomatic.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.


/// A registered per-frame modifier: a pure function of time applied to
/// the target's animated state during evaluation.
struct Updater: Sendable {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Since the updaters array is private, the Updater struct is only used internally within ManimScene. We should mark the Updater struct as private to enforce proper encapsulation.

Suggested change
struct Updater: Sendable {
private struct Updater: Sendable {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 56fcafbUpdater is now private struct Updater, matching the visibility of the updaters array that holds it.


Generated by Claude Code

}
// Updaters layer on top of the animated state, in registration order.
for updater in updaters {
guard t >= updater.startTime, updater.endTime.map({ t <= $0 }) ?? true else { continue }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using .map on an optional Double to perform a comparison can be hard to read and introduces minor overhead in the hot path of snapshot(at:).

We can simplify this by splitting the conditions into a straightforward guard and an optional binding check, which is more idiomatic in Swift.

            guard t >= updater.startTime else { continue }
            if let endTime = updater.endTime, t > endTime { continue }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 56fcafb — the window check is now a plain guard t >= updater.startTime else { continue } followed by if let endTime = updater.endTime, t > endTime { continue }, as suggested.


Generated by Claude Code

@ronaldmannak
ronaldmannak force-pushed the claude/picomanim-phase2-05-updaters branch 2 times, most recently from 56fcafb to e31549b Compare July 7, 2026 02:41

Copy link
Copy Markdown
Contributor Author

@codex review


Generated by Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: e31549bf85

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

claude added 2 commits July 9, 2026 03:58
scene.always(_:during:_:) registers a @sendable (time, state) -> state
closure that layers on top of the animated state during evaluation.
Because updaters are pure functions of time, snapshot(at:) stays pure:
scrubbing, looping, and offline rendering keep working unchanged.

Semantics, each pinned by a test:
- Active from the registration point in the timeline onward, or exactly
  over an explicit `during` window (outside it the underlying animated
  state shows again).
- Layers over animations (an updater sees the animated state at t) and
  multiple updaters compose in registration order.
- Evaluation-only: the build cursor and later animations' start poles
  see the un-updated state.
- Registering for a never-seen mobject shows it at the current point,
  like add.

Stacked on the MobjectGroup PR (shares the ManimScene surface).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YGb3ukB5dQy4vgz7Z1hags
- Updater is only ever used by ManimScene's private updaters array, so
  mark the struct itself private (Gemini).
- Replace the Optional.map window check in snapshot with a guard plus
  optional binding (Gemini).
@ronaldmannak
ronaldmannak force-pushed the claude/picomanim-phase2-05-updaters branch from e31549b to d12aca8 Compare July 9, 2026 03:58

Copy link
Copy Markdown
Contributor Author

@codex review


Generated by Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: d12aca8d90

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown
Contributor Author

Ready for human review/merge

Phase 2-E (updaters — scene.always) is merge-ready.

  • Focused diff — a per-mobject pure function of absolute time layered over the animated state, applied after the entry fold in registration order.
  • CI greentest-macos and test-linux both pass on the head commit (d12aca8).
  • TestsUpdaterTests.swift: purity, layering over a concurrent animation, registration-time start, during windowing, auto-add of unseen mobjects, registration-order composition, and build-cursor independence.
  • Automated review resolved — Codex reviewed the head commit clean ("Didn't find any major issues"); the two Gemini comments (private Updater struct, plainer window check) were both fixed.
  • PR description current.

Stacked on #12 (Phase 2-B) — base is claude/picomanim-phase2-02-groups; please merge #12 first, then this retargets to main. The rebase after #12's group-center-cache fix is already in this head. Not merging; awaiting your go-ahead.


Generated by Claude Code

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.

2 participants