Skip to content

Phase 2-B: MobjectGroup, layout helpers, and group animations - #12

Open
ronaldmannak wants to merge 5 commits into
mainfrom
claude/picomanim-phase2-02-groups
Open

Phase 2-B: MobjectGroup, layout helpers, and group animations#12
ronaldmannak wants to merge 5 commits into
mainfrom
claude/picomanim-phase2-02-groups

Conversation

@ronaldmannak

@ronaldmannak ronaldmannak commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Phase 2-B: MobjectGroup, layout helpers, and group animations

Scope

Manim's VGroup workflow: position mobjects relative to each other, treat a set of them as one unit, and animate them together (optionally staggered).

  1. Layout extension on MobjectboundingBox, width, height, center (visual bbox center), edge(_:) (Manim's critical point: each axis picks min/center/max by the direction component's sign, so diagonals name true corners), and nextTo(_:direction:gap:) (facing edge points placed gap apart along the direction).
  2. MobjectGroup — a value type wrapping [Mobject] with a union boundingBox, group center, and whole-group modifiers: shifted(by:), moved(to:), rotated(by:) / scaled(by:) about the group center, and arranged(direction:spacing:) (chained nextTo).
  3. Group animation factories on [ManimAnimation].create(group, lag:), .fadeIn/.fadeOut(group, lag:), .shift, .move, .rotate, .scale, and .transform(group, into:) (pairs by index; extra sources fade out, extra targets fade in). Move/rotate/scale resolve the group's center from the live scene state when played (not the captured group value), so sequential group animations act on wherever the group actually is.
  4. play shapesplay([ManimAnimation]...) for several groups, and play([ManimAnimation], ManimAnimation...) so the mixed shape scene.play(.create(group), .shift(solo, by: ...)) type-checks; arbitrary mixes concatenate arrays.
  5. New animation primitivesManimAnimation.delay (per-animation start offset inside a play group; what lag compiles to), rotateAbout/scaleAbout kinds (.rotate(m, by:, about:) orbits the pivot along a circular arc; .scale(m, by:, about:) moves radially), and group-relative kinds groupMove/groupRotate/groupScale that carry their siblings and are rewritten into concrete kinds by play after resolving the live group center.

Public API sketch

extension Mobject {
    public var boundingBox: (min: Vec2, max: Vec2)? { get }
    public var width: Double { get }
    public var height: Double { get }
    public var center: Vec2 { get }
    public func edge(_ direction: Vec2) -> Vec2          // Manim critical point
    public func nextTo(_ other: Mobject, direction: Vec2 = Vec2(1, 0), gap: Double = 0.25) -> Mobject
}

public struct MobjectGroup: Sendable {
    public var mobjects: [Mobject]
    public var boundingBox: (min: Vec2, max: Vec2)? { get }
    public var center: Vec2 { get }
    public func shifted(by: Vec2) -> MobjectGroup
    public func moved(to: Vec2) -> MobjectGroup
    public func rotated(by: Double) -> MobjectGroup      // about group center
    public func scaled(by: Double) -> MobjectGroup       // about group center
    public func arranged(direction: Vec2 = Vec2(1, 0), spacing: Double = 0.25) -> MobjectGroup
}

extension Array where Element == ManimAnimation {
    public static func create(_ group: MobjectGroup, duration: Double = 1, lag: Double = 0, ...) -> [ManimAnimation]
    // fadeIn / fadeOut / shift / move / rotate / scale / transform analogues;
    // move/rotate/scale resolve the group center from live scene state at play time
}

extension ManimAnimation {
    public var delay: Double   // seconds after the play group starts
    // Kind gains rotateAbout/scaleAbout and groupMove/groupRotate/groupScale
    // .rotate(_, by:, about:) and .scale(_, by:, about:) gain optional pivots
}

extension ManimScene {
    public mutating func play(_ animationGroups: [ManimAnimation]...)
    public mutating func play(_ animationGroup: [ManimAnimation], _ animations: ManimAnimation...)
}

Design notes

  • Why factories live on [ManimAnimation]: scene.play(.create(group)) resolves the leading dot against the parameter type [ManimAnimation], so the group factories must be declared in an extension of that array type — statics on ManimAnimation returning arrays would not be found by contextual lookup.
  • Live-center resolution: group move/rotate/scale emit kinds that carry their sibling mobjects; play resolves the union-bbox center from the current build cursor (falling back to the carried snapshots for unseen mobjects) and rewrites them into concrete shift/rotateAbout/scaleAbout before the entry is built. snapshot(at:) never sees group kinds.
  • delay vs nested timelines: a per-animation delay keeps snapshot(at:) a pure fold over flat entries. play appends a group's entries sorted by start time (stable on ties) and advances the build cursor in that same chronological order, so delayed siblings compose correctly with the documented "later wins" rule and state(of:) agrees with the rendered timeline.
  • Pivot animations interpolate along the arc, not linearly between endpoints — rotateAbout orbits, scaleAbout moves radially — matching Manim's Rotate(about_point:).

Test strategy

GroupTests.swift: layout math (nextTo/edge/arranged, including diagonal corners on non-square boxes), union bbox, group modifiers about the center, staggered lag timing, arc-orbit and radial-scale midpoints, pivot rotation, group transform with mismatched counts, mixed group + solo play (natural un-wrapped shape), live-center move/rotate after earlier animations, and delayed-sibling chronology incl. the build cursor.

Concurrency

Pure value types; no @unchecked Sendable.

Stack

Based on main. PRs 2-D (Axes, #14) and 2-E (Updaters, #15) stack on this branch — please review/merge this one first among the three.

claude added 2 commits July 7, 2026 01:47
The VGroup counterpart. Groups are a build-time convenience with no
runtime identity: group transformations return repositioned copies of
the children (identity preserved) and group animations expand into
per-child animations, so the timeline and renderer are untouched.

- Mobject layout helpers: boundingBox, width/height, visual center,
  edge(_:), and nextTo(_:direction:gap:) placement.
- MobjectGroup: union bounding box, shifted/moved/rotated/scaled about
  the group center, and arranged(direction:spacing:) row layout.
- Group animation factories (create/fadeIn/fadeOut/shift/move/rotate/
  scale/transform) returning [ManimAnimation]; a lag parameter staggers
  children via the new ManimAnimation.delay, and group transform pairs
  children by index, fading unmatched extras in or out.
- New animation kinds rotateAbout/scaleAbout (also exposed on the
  single-mobject rotate/scale factories via an optional `about` pivot):
  positions orbit the pivot along a circular arc / move radially,
  rather than cutting across the chord.
- ManimScene honors per-animation delay (entries start at group start
  plus delay; the group advances by the longest delay + duration) and
  gains a play overload accepting several animation lists at once.

Covered by a new GroupTests suite (layout, arrangement, union bounds,
staggered create, arc orbits, radial scaling, group morphs with
unmatched extras, mixed group/single play).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YGb3ukB5dQy4vgz7Z1hags
Leading-dot lookup resolves against the contextual type, and a play
call expecting [ManimAnimation] looks members up on Array - static
factories declared on ManimAnimation are invisible there, so
scene.play(.create(group)) would not compile. The group factories now
live in `extension Array where Element == ManimAnimation`, with inner
per-child calls fully qualified (leading-dot inside map closures has
no contextual base to infer from).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YGb3ukB5dQy4vgz7Z1hags

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4eff597f06

ℹ️ 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".

duration: Double = 1,
rate: RateFunction = .smooth
) -> [ManimAnimation] {
let delta = point - group.center

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Resolve group moves from the live group center

After any earlier animation has moved the children, this uses the original MobjectGroup value rather than the scene's current state to compute the delta. In a normal sequence such as scene.play(.shift(group, by: Vec2(10, 0))) followed by scene.play(.move(group, to: .zero)), ManimScene animates the live children from their shifted positions but this delta is still based on the old center, so the group ends offset instead of at the requested point. The same stale-center calculation is used for the rotate/scale pivots below.

Useful? React with 👍 / 👎.

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 30c1fb7. The move/rotate/scale factories no longer bake in a delta or pivot from the captured group value. They now emit group-relative kinds (groupMove/groupRotate/groupScale) that carry their sibling mobjects, and ManimScene.play resolves the group's bounding-box center from the live scene state (falling back to the carried snapshots for mobjects the scene hasn't met), rewriting them into concrete shift/rotateAbout/scaleAbout kinds before the entry is built — snapshot(at:) stays untouched. Tests added: groupMoveResolvesFromTheLiveCenter (shift then move-to-origin lands exactly at the origin) and groupRotateOrbitsTheLivePivot (half-turn after a shift swaps the dots about the live center instead of flinging them around the stale one).


Generated by Claude Code

@@ -110,6 +116,7 @@ public struct ManimScene: 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.

P2 Badge Sort delayed entries by their actual start time

Once animations can start after groupStart, entries are no longer guaranteed to be chronological, but snapshot(at:) still folds them in append order and completed entries keep applying their final value. If a delayed animation on a mobject is listed before an immediate sibling that drives the same property, the immediate sibling is applied after the delayed one even after it has completed, so the delayed motion is overwritten instead of taking effect when its delay elapses.

Useful? React with 👍 / 👎.

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 30c1fb7play now buffers the group's entries and appends them sorted by start time (stable on ties, preserving the documented "later listed wins" rule for simultaneous same-property animations). Since a play group's entries all start at or before the next group's start, sorting within the group keeps the whole timeline chronological, so the snapshot fold applies entries in the order they actually begin. Test added: delayedSiblingDrivesThePropertyAfterItsDelay (a delayed move listed before an immediate sibling on the same property takes over when its delay elapses).


Generated by Claude Code

public func edge(_ direction: Vec2) -> Vec2 {
guard let box = boundingBox else { return position }
let half = (box.max - box.min) / 2
return center + Vec2(direction.x * half.x, direction.y * half.y)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Normalize layout directions before applying extents

When callers pass a non-cardinal unit direction, such as Vec2.direction(.pi / 4), this multiplies the half-extents by about 0.707 and returns a point inside the box instead of on its edge. The same component-wise scaling is used by nextTo/arranged, so diagonal layouts with unit direction vectors place boxes too close and can overlap rather than sitting outside the previous mobject.

Useful? React with 👍 / 👎.

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 30c1fb7edge(_:) is now the point where the ray from the center exits the bounding box, so a unit diagonal and Manim-style Vec2(1, 1) both name the corner (direction magnitude no longer matters), and nextTo places this mobject's facing edge point gap away from the other's along the direction — cardinal placements are numerically unchanged, diagonals now meet corner-to-corner outside the box. Test added: diagonalEdgeAndNextToStayOnTheBox.


Generated by Claude Code

@gemini-code-assist

Copy link
Copy Markdown

Warning

Gemini encountered an error creating the review. You can try again by commenting /gemini review.

- Group move/rotate/scale factories now emit group-relative kinds
  (groupMove/groupRotate/groupScale) that carry their siblings; play()
  resolves the delta or pivot from the live scene state, so sequential
  group animations act on where the group actually is instead of the
  stale value the factory captured (Codex P1). Pinned by
  groupMoveResolvesFromTheLiveCenter and groupRotateOrbitsTheLivePivot.
- play() buffers a group's entries and appends them sorted by start
  time (stable on ties), so a delayed animation listed before an
  immediate sibling on the same property takes over when its delay
  elapses instead of being overwritten by the finished sibling (Codex).
  Pinned by delayedSiblingDrivesThePropertyAfterItsDelay.
- edge(_:) is now the ray-box exit point, so diagonal directions (unit
  or Manim-style (1,1)) name corners instead of interior points, and
  nextTo places facing edge points gap apart along the direction;
  cardinal behavior is numerically unchanged (Codex). Pinned by
  diagonalEdgeAndNextToStayOnTheBox.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 30c1fb7098

ℹ️ 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".

Comment on lines +215 to +218
let indexed = newEntries.enumerated().sorted {
($0.element.startTime, $0.offset) < ($1.element.startTime, $1.offset)
}
entries.append(contentsOf: indexed.map { $0.element })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Recompute the build cursor in start-time order

This sort only fixes the order used by snapshot(at:); currentStates has already been advanced in the original caller order in the loop above. When a delayed animation targeting the same property is listed before an immediate sibling, the rendered timeline can end at the delayed value, but state(of:) and any later play still start from the immediate sibling's value, causing follow-up animations to jump or use the wrong live state.

Useful? React with 👍 / 👎.

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 4853453 — the build cursor is now advanced after the group's entries are sorted, folding them in the same chronological order snapshot(at:) uses, so state(of:) and follow-up plays agree with the rendered timeline. The delayed-sibling test now also asserts scene.state(of: dot) ends at the delayed animation's value.


Generated by Claude Code

let ratioY = half.y > 0 ? abs(direction.y) / half.y : (direction.y == 0 ? 0 : .infinity)
let maxRatio = Swift.max(ratioX, ratioY)
guard maxRatio > 0, maxRatio.isFinite else { return center }
return center + direction / maxRatio

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Return diagonal corners for layout edges

The current diff still computes a ray/box intersection, which does not return the documented Manim-style diagonal corner for non-square boxes; for example a 10×2 rectangle's edge(Vec2(1, 1)) returns the top edge near (1, 1) instead of the upper-right corner (5, 1). nextTo can then overlap differently shaped rectangles in diagonal layouts because the two objects may anchor to different edge axes rather than opposite corners.

Useful? React with 👍 / 👎.

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 4853453edge(_:) now uses Manim's critical-point semantics: each axis independently picks the box's min/center/max by the sign of that direction component, so Vec2(1, 1) (or a unit diagonal) on a 10×2 rectangle returns the true corner (5, 1). diagonalEdgeAndNextToStayOnTheBox now pins exactly that wide-rectangle case.


Generated by Claude Code


/// Plays several animation lists (for example group animations, which
/// expand to one animation per child) together in one parallel group.
public mutating func play(_ animationGroups: [ManimAnimation]...) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Support solo animations in mixed group plays

This overload only accepts variadic animation arrays, while the existing overload only accepts variadic single animations, so the intended mixed call shape scene.play(.create(group), .shift(solo, by: ...)) has no matching overload: the first argument is [ManimAnimation] and the second is ManimAnimation. Users have to manually wrap every solo animation in an array, which makes the advertised group+solo mixing API fail to type-check in normal use.

Useful? React with 👍 / 👎.

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 4853453 — added play(_ animationGroup: [ManimAnimation], _ animations: ManimAnimation...), so scene.play(.create(group), .shift(solo, by: ...)) type-checks as advertised (group array first, any number of solo animations after). Arbitrary interleavings concatenate arrays (play(.create(a) + [b, c])), noted in the doc comment. The mixed test now uses the natural un-wrapped shape.


Generated by Claude Code

- edge(_:) uses Manim's critical-point semantics: each axis picks the
  box min/center/max by the sign of that direction component, so
  diagonals name true corners even for non-square boxes (Codex).
  Pinned by the wide-rectangle assertions in
  diagonalEdgeAndNextToStayOnTheBox.
- The build cursor now advances over the group's entries in the same
  chronological order the snapshot fold uses, so state(of:) and
  follow-up plays agree with the rendered timeline when delayed
  siblings drive the same property (Codex). Pinned in
  delayedSiblingDrivesThePropertyAfterItsDelay.
- New play overload ([ManimAnimation], ManimAnimation...) so the
  advertised mixed shape scene.play(.create(group), .shift(solo, ...))
  type-checks without wrapping solos in arrays (Codex). Pinned by
  mixedGroupAndSingleAnimationsPlayTogether.

Copy link
Copy Markdown
Contributor Author

@codex review

/gemini review


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 MobjectGroup (the counterpart to Manim's VGroup) along with layout helpers, group-relative animations, and staggered animation support. It also updates ManimScene to resolve group animations against live scene state and handle delayed animations chronologically. The review feedback highlights a performance bottleneck in group center resolution where the group center is redundantly recalculated for each member, resulting in $O(N^2)$ complexity. Caching the computed centers using a dictionary keyed by member IDs is suggested to optimize this to $O(N)$.

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.

Comment on lines +125 to +134
var newEntries: [Entry] = []

for animation in animations {
let id = animation.mobject.id
let entryStart = groupStart + max(0, animation.delay)
// Group-relative kinds carry their sibling mobjects; resolve the
// pivot or delta against the live scene state now, so a group
// animation acts on wherever the group actually is, not on the
// (possibly stale) group value the factory captured.
let kind = Self.resolvingGroupKind(animation.kind, states: groupStartStates)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Performance Bottleneck: $O(N^2)$ Complexity in Group Center Resolution

When playing a group animation with $N$ members, resolvingGroupKind is called for each member. Inside resolvingGroupKind, liveCenter is invoked, which maps all $N$ members and computes the bounding box center of the group. This results in $O(N^2)$ complexity for path transformations and bounding box calculations per play call.

For larger groups (such as text with many characters or complex grids), this can cause a severe rendering bottleneck. We can optimize this to $O(N)$ by caching the computed center of the group during the resolution phase using a dictionary keyed by the member IDs.

Suggested change
var newEntries: [Entry] = []
for animation in animations {
let id = animation.mobject.id
let entryStart = groupStart + max(0, animation.delay)
// Group-relative kinds carry their sibling mobjects; resolve the
// pivot or delta against the live scene state now, so a group
// animation acts on wherever the group actually is, not on the
// (possibly stale) group value the factory captured.
let kind = Self.resolvingGroupKind(animation.kind, states: groupStartStates)
var newEntries: [Entry] = []
var centerCache: [[Mobject.ID]: Vec2] = [:]
for animation in animations {
let id = animation.mobject.id
let entryStart = groupStart + max(0, animation.delay)
// Group-relative kinds carry their sibling mobjects; resolve the
// pivot or delta against the live scene state now, so a group
// animation acts on wherever the group actually is, not on the
// (possibly stale) group value the factory captured.
let kind = Self.resolvingGroupKind(animation.kind, states: groupStartStates, centerCache: &centerCache)

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 632fba3play now carries a groupCenterCache keyed by the members' id list and threads it through resolvingGroupKind, so a group's bounding-box center is computed once and reused for all N members instead of recomputed per member. The dominant cost (transforming every child's curves + sampling its bounding box) drops from O(N²) to O(N) per play call. Pinned by twoGroupsInOnePlayResolveIndependentCenters, which also guards the cache keys against collisions when two distinct groups animate in the same play.


Generated by Claude Code

Comment on lines +284 to +301
private static func resolvingGroupKind(
_ kind: ManimAnimation.Kind,
states: [Mobject.ID: Mobject]
) -> ManimAnimation.Kind {
func liveCenter(_ members: [Mobject]) -> Vec2 {
MobjectGroup(members.map { states[$0.id] ?? $0 }).center
}
switch kind {
case .groupMove(let point, let members):
return .shift(by: point - liveCenter(members))
case .groupRotate(let angle, let members):
return .rotateAbout(pivot: liveCenter(members), by: angle)
case .groupScale(let factor, let members):
return .scaleAbout(pivot: liveCenter(members), by: factor)
default:
return kind
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Performance Bottleneck: $O(N^2)$ Complexity in Group Center Resolution

Update resolvingGroupKind to accept and utilize the centerCache to avoid redundant bounding box calculations and path transformations for members of the same group.

    private static func resolvingGroupKind(
        _ kind: ManimAnimation.Kind,
        states: [Mobject.ID: Mobject],
        centerCache: inout [[Mobject.ID]: Vec2]
    ) -> ManimAnimation.Kind {
        func liveCenter(_ members: [Mobject]) -> Vec2 {
            let ids = members.map { $0.id }
            if let cached = centerCache[ids] {
                return cached
            }
            let center = MobjectGroup(members.map { states[$0.id] ?? $0 }).center
            centerCache[ids] = center
            return center
        }
        switch kind {
        case .groupMove(let point, let members):
            return .shift(by: point - liveCenter(members))
        case .groupRotate(let angle, let members):
            return .rotateAbout(pivot: liveCenter(members), by: angle)
        case .groupScale(let factor, let members):
            return .scaleAbout(pivot: liveCenter(members), by: factor)
        default:
            return kind
        }
    }

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 632fba3resolvingGroupKind now takes centerCache: inout [[Mobject.ID]: Vec2] and its liveCenter memoizes the computed center per members-id list, exactly as suggested, so members of the same group reuse one bounding-box computation. (Companion to the reply on the caller-side thread above.)


Generated by Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 48534530ab

ℹ️ 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".

resolvingGroupKind computed the group's bounding-box center once per
member, so a group animation with N members did O(N) work N times
(O(N^2) per play call — costly for large groups like multi-character
text). Cache the resolved center per members-id list within a play
call, dropping the dominant bounding-box cost to O(N) (Gemini). Pinned
by twoGroupsInOnePlayResolveIndependentCenters, which also guards the
cache keys against collisions between distinct groups in one play.

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. More of your lovely PRs please.

Reviewed commit: 632fba33cb

ℹ️ 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-B (MobjectGroup, layout helpers, group animations) is merge-ready.

  • Focused diffMobjectGroup + Mobject layout helpers, group animation factories, delay/rotateAbout/scaleAbout/group-relative kinds, and the play overloads (4 files, +762/−27).
  • CI greentest-macos and test-linux both pass on the head commit (632fba3); mergeable_state: clean against current main.
  • TestsGroupTests.swift: layout math incl. diagonal corners on non-square boxes, union bbox, group modifiers, staggered lag, arc-orbit/radial-scale midpoints, group transform with mismatched counts, mixed group+solo play, live-center move/rotate after earlier animations, delayed-sibling chronology (timeline + build cursor), and two-groups-in-one-play cache isolation.
  • Automated review resolved — Codex reviewed the head commit clean ("Didn't find any major issues"). Every Gemini/Codex comment answered individually across the rounds: live group-center resolution (P1), delayed-entry sort + build-cursor order, Manim critical-point edge/nextTo, mixed-play overload, and the O(N²) → O(N) group-center cache (high-priority).
  • PR description updated to match the implementation.

Based on main; base for #14 and #15. Suggested merge order: this PR first, then #13, #14, #15. Heads-up: #13 also touches the apply .create case in ManimScene.swift, so whichever of #12/#13 merges second will need a small rebase — I'll handle it. 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