manim icon indicating copy to clipboard operation
manim copied to clipboard

Proposal to remove `Mobject.submobjects`, allow only Groups to have `submobjects`, and define `get_submobjects()` to expose submobjects

Open chopan050 opened this issue 5 months ago • 3 comments

This proposal is related to the issue #4339 and attempts to solve the described problem in a different way:

Description of problem

Sometimes we store child mobjects of some composed mobject as attributes, while also .add-ing them to the self.submobjects list directly.

This is fine in principle, but has the odd side effect of producing "mismatches" when a user updates the attribute by reassinging it, because the actually for rendering relevant self.submobjects list still holds a reference to the former attribute value.

As an example:

sq = Square()
b = BraceText(sq, "hello")  # this stores the label in `b.label`
b.label = Text("new value")  # and here we reassign the label ...

This example still renders as a brace with label "hello", because the reference in the self.submobjects list is never updated.

Proposed solution

The idea is the same: to have a single reference to each submobject, a single source of truth for them.

It does not make a lot of sense that "basic" Mobjects such as Circle, Square, ImageMobject or ValueTracker have submobjects. Instead, it makes more sense to take a Circle and group it inside a Group or VGroup containing other Mobjects. This leads me to the idea that maybe only Groups should have submobjects as such.

Therefore, my idea is:

  • The base class Mobject does not have .submobjects. It also does not have methods such as .add(), .remove(), etc.
  • Only Group, VGroup and theirs subclasses have .submobjects, .add(), .remove(), etc.
  • Groups should be a special Mobject which is more clearly separated from the rest of Mobjects. For example, currently Axes is a subclass of VGroup and CoordinateSystem. It should not be a subclass of VGroup. Axes should have another way of exposing its submobjects and should not have .submobjects, .add(), .remove(), etc. See a more detailed description of how to achieve that below.

In this way, if only Groups have submobjects and you can only directly add submobjects to Groups, then we put a stricter limit to the unexpected behaviors which might arise from, say, adding a Mobject which already exists.

We still need Mobjects that contain other Mobjects without necessarily being Groups, like MathTex and Axes. Therefore, I propose defining a Mobject.get_submobjects() or Mobject.get_children() method.

  • For the base class Mobject, this should simply return [].
  • Other Mobjects which need children may override it. For example, Axes.get_children() would return [self.x_axis, self.y_axis]. NumberLine.get_children() would return something like [*super().get_children(), *self.ticks, *self.numbers].
  • Of course, Circle does not override the method, so it still returns [].
  • Group.get_children() and VGroup.get_children() simply return self.submobjects.

In this way, by getting rid of .submobjects where necessary and making .get_submobjects() or .get_children() return the already existing references to those submobjects, we ensure that there's effectively one single reference for each one of them, solving the described problem.

chopan050 avatar Aug 10 '25 18:08 chopan050

While it does constitute a severe breaking change in contrast to #4339, I still kind of like the idea. We should probably check

  • where in the library things are being .added to non-group mobjects,
  • and if all of these instances can easily be replaced.

I would assume that users primarily use VGroup to add/remove submobjects to (which would still be fine, if I understand your proposal correctly), but it would also be nice to try and get more input from perhaps the Discord or so.

behackl avatar Aug 11 '25 18:08 behackl

To reduce the gap with the current implementation, we could keep an internal list of additional submobjects:

class Mobject:
    _extra_mobjects: list[Mobject]

    @property
    def submobjects(self, /) -> _SubMobjectsAccessor:
        return _SubMobjectsAccessor(self)

    @submobjects.setter
    def submobjects(self, new_submobjects: Iterable[Mobject], /) -> None:
        self.submobjects.clear()
        self.submobjects.extend(new_submobjects)

where _SubMobjectsAccessor is a class with the same interface as list, but uses the new interface proposed above and the _extra_mobjects attribute for things like .append(...) and similar. Because it's a custom class, it can be smart about which Mobjects to add (to avoid, for example, including the same Mobject multiple times): I think this would make the new proposal almost (if not fully) backwards-compatible.

Edit: This would also allow a more incremental adoption of the new API in the whole library. And of course, once we've migrated most of the library to the new API, we can deprecate and then remove submobjects and _SubMobjectsAccessor.

RBerga06 avatar Aug 15 '25 14:08 RBerga06

I know graphs and matrices .add submobjects to themselves despite inheriting from VMobject.

nubDotDev avatar Aug 21 '25 11:08 nubDotDev