manim-simplex 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,214 @@
1
+ Metadata-Version: 2.4
2
+ Name: manim-simplex
3
+ Version: 0.2.0
4
+ Summary: Manim plugin: theme tokens, mobjects, slide hierarchy, deck manifest schema.
5
+ Project-URL: Changelog, https://github.com/shlomi-perles/manim-simplex/blob/main/CHANGELOG.md
6
+ Project-URL: Homepage, https://github.com/shlomi-perles/manim-simplex
7
+ Project-URL: Issues, https://github.com/shlomi-perles/manim-simplex/issues
8
+ Project-URL: Repository, https://github.com/shlomi-perles/manim-simplex
9
+ Author: Shlomi Perles
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: animation,computer-science,education,lecture,manim,manim-slides,math,presentation
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Education
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Education
22
+ Classifier: Topic :: Multimedia :: Graphics
23
+ Classifier: Topic :: Multimedia :: Video
24
+ Classifier: Topic :: Scientific/Engineering :: Visualization
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.13
27
+ Requires-Dist: manim-slides>=5.1.7
28
+ Requires-Dist: manim>=0.20.1
29
+ Requires-Dist: pydantic>=2.7
30
+ Requires-Dist: pygments>=2.18
31
+ Description-Content-Type: text/markdown
32
+
33
+ # manim-simplex
34
+
35
+ [![PyPI version](https://img.shields.io/pypi/v/manim-simplex.svg)](https://pypi.org/project/manim-simplex/)
36
+ [![Python](https://img.shields.io/pypi/pyversions/manim-simplex.svg)](https://pypi.org/project/manim-simplex/)
37
+ [![License](https://img.shields.io/pypi/l/manim-simplex.svg)](https://github.com/shlomi-perles/manim-simplex/blob/main/LICENSE)
38
+
39
+ The Manim plugin half of [Simplex](https://github.com/shlomi-perles/simplex):
40
+ theme tokens, reusable mobjects, slide hierarchy, deck manifest schema,
41
+ and the `manim.plugins` entry-point. Distributed on PyPI as
42
+ `manim-simplex`.
43
+
44
+ The lecture-portal platform (CLI, deck discovery, render orchestration,
45
+ web builder) lives in the sibling [`simplex`](https://github.com/shlomi-perles/simplex)
46
+ package; both contribute modules to the shared PEP 420 `simplex/`
47
+ namespace.
48
+
49
+ ## What ships here
50
+
51
+ | Module | Contents |
52
+ |---|---|
53
+ | `simplex.plugin` | `activate()` -- the `manim.plugins` entry-point. Applies the active theme to `manim.config`. |
54
+ | `simplex.section` | `SimplexSectionType` enum -- the slide-hierarchy strings written into Manim's sections JSON. Manim-free. |
55
+ | `simplex.manifest` | `DeckManifest`, `MainSlide`, `Subsection` Pydantic models -- the cross-package contract consumed by the `simplex` web builder. Manim-free. |
56
+ | `simplex.theme` | `Theme`, `Palette`, `Typography`, `Spacing`, `Motion`, `LatexProfile`, `WebPalette`, `active_theme`, `get_active_theme`, `presets`, `render_web_css`. |
57
+ | `simplex.engine` | Animation primitives -- `Region`, `Remove`, `clear_scene`, `exit_for`, `register_exit`, `set_exit_animation`, `HighlightResult`, `apply_theme_defaults`, plus the `glyph_map`, `ghost_fade`, `dynamics`, `geometry`, `code`, `text`, `scaling`, `debug` submodules. |
58
+ | `simplex.mobjects` | `Node`, `Edge`, `ArrayMob`, `ArrayEntry`, `ArrayPointer`, `OutlineProgressBar`. |
59
+ | `simplex.slides` | `BaseSlide`, `OutlineScene`, `OutlinePart`, `Chrome`, `make_chrome`. |
60
+
61
+ ## Install
62
+
63
+ ```bash
64
+ uv add manim-simplex
65
+ # or
66
+ pip install manim-simplex
67
+ ```
68
+
69
+ System dependencies (texlive, ffmpeg, cairo, pango) are the same as
70
+ Manim's -- see the [Manim install guide](https://docs.manim.community/en/stable/installation.html).
71
+
72
+ ## Quick start
73
+
74
+ ```ini
75
+ # decks/<your-deck>/manim.cfg
76
+ [CLI]
77
+ plugins = simplex
78
+ save_sections = True
79
+ ```
80
+
81
+ ```python
82
+ from manim import MathTex
83
+ from simplex.slides import BaseSlide, make_chrome
84
+ from simplex.theme import presets
85
+
86
+ class Hello(BaseSlide):
87
+ def setup(self) -> None:
88
+ super().setup()
89
+ chrome = make_chrome(presets.DASTIMATOR_DARK, self.region, header="Hello")
90
+ self.add_to_canvas(**chrome.mobjects)
91
+ self.region = chrome.body_region
92
+
93
+ def construct(self) -> None:
94
+ from manim import ORIGIN, Write
95
+ eq = MathTex(r"e^{i\pi} + 1 = 0")
96
+ self.region.place(eq, ORIGIN)
97
+ self.play(Write(eq))
98
+ self.next_slide(name="Hello")
99
+ ```
100
+
101
+ ```bash
102
+ uv run manim-slides render path/to/your_deck/scene.py Hello
103
+ ```
104
+
105
+ ## Slide hierarchy
106
+
107
+ `BaseSlide.next_slide` writes a `SimplexSectionType` value into Manim's
108
+ native section JSON. The web builder reconciles that with manim-slides'
109
+ `PresentationConfig` to build a main/sub tree.
110
+
111
+ - `self.next_slide(name="Title")` -> **MAIN** slide named `"Title"`.
112
+ - `self.next_slide()` as the *first* call -> **MAIN** slide
113
+ auto-named after the scene class with PascalCase boundaries spaced
114
+ out (``DFSLecture`` → ``"DFS Lecture"``; no warning).
115
+ - `self.next_slide()` after a named main -> **SUB** slide.
116
+ - `self.next_slide(..., loop=True)` -> the `LOOP` variant.
117
+ - `self.next_slide(..., section_type="simplex.main.skip")` -> explicit
118
+ override always wins.
119
+
120
+ ## Outline slides
121
+
122
+ `OutlineScene` composes typed `OutlinePart` objects into an animated
123
+ `BaseSlide` outline. Each part owns already-built Manim mobjects for its
124
+ feature title, compact label, and optional visual. Progress dots are
125
+ positioned with `self.region.linspace(RIGHT, n)` defaults, so edge
126
+ margins and inter-dot gaps are equal.
127
+
128
+ ```python
129
+ from manim import Circle, Square, Tex
130
+ from simplex.engine.text import Caption
131
+ from simplex.slides import OutlinePart, OutlineScene
132
+
133
+ class Outline(OutlineScene):
134
+ def __init__(self, **kwargs):
135
+ super().__init__(
136
+ parts=[
137
+ OutlinePart(Tex("Research Question"), Caption("Question"), Circle()),
138
+ OutlinePart(Tex("Algorithms"), Caption("Algorithms"), Square()),
139
+ ],
140
+ **kwargs,
141
+ )
142
+ ```
143
+
144
+ ## Theme
145
+
146
+ Themes are frozen Pydantic models -- the same instance produces:
147
+
148
+ 1. Manim defaults (via `apply_theme_defaults`, called by the plugin).
149
+ 2. A `TexTemplate` (via `LatexProfile.as_tex_template`).
150
+ 3. CSS variables for the web portal + RevealJS HTML (via
151
+ `render_web_css(theme.web_palette)`).
152
+ 4. The `darcula` Pygments style (registered by the plugin).
153
+
154
+ Switch themes per-scope with `active_theme`:
155
+
156
+ ```python
157
+ from simplex.theme import presets
158
+ from simplex.theme.context import active_theme
159
+
160
+ from manim import Tex
161
+
162
+ with active_theme(presets.ACADEMIC_LIGHT):
163
+ label = Tex("This Tex picks up the academic light palette.")
164
+ ```
165
+
166
+ ## Cross-package contract
167
+
168
+ `manim-simplex` owns the manifest schema; `simplex` imports it:
169
+
170
+ ```python
171
+ from simplex.manifest import DeckManifest, MainSlide, Subsection
172
+ ```
173
+
174
+ When the schema bumps `schema_version`, the web builder hard-fails on
175
+ unknown versions with a pointer at the `manim-simplex` upgrade. This
176
+ keeps the two repos honest about their contract.
177
+
178
+ ## Why a separate distribution?
179
+
180
+ The plugin surface (mobjects + theme + entry-point + manifest schema)
181
+ is reusable independently of the lecture-portal pipeline. Splitting
182
+ them lets the plugin be a thin dependency for users who want to render
183
+ slides without pulling in Typer, watchfiles, Jinja, and the web
184
+ builder stack.
185
+
186
+ Python's PEP 420 implicit namespace packages merge the two distributions
187
+ at import time. Neither wheel ships `src/simplex/__init__.py`, so
188
+ `from simplex.engine import Remove` resolves regardless of which wheel
189
+ contributed the module.
190
+
191
+ ## Development
192
+
193
+ Requires Python 3.13+ and [uv](https://docs.astral.sh/uv/).
194
+
195
+ ```bash
196
+ git clone https://github.com/shlomi-perles/manim-simplex.git
197
+ cd manim-simplex
198
+ uv sync --all-extras
199
+ uv run pre-commit install
200
+ uv run pytest -q
201
+ uv run ruff check .
202
+ uv run basedpyright
203
+ ```
204
+
205
+ Examples under `examples/` are runnable demo scenes; they double as
206
+ documentation and CI smoke tests:
207
+
208
+ ```bash
209
+ uv run manim -pql examples/hello_slide.py HelloSlide
210
+ ```
211
+
212
+ ## License
213
+
214
+ MIT.
@@ -0,0 +1,39 @@
1
+ simplex/manifest.py,sha256=g7pQoTJb0fD_FGvvXFmNkwMrNDR5mPTWm8GD4etEZzs,3646
2
+ simplex/plugin.py,sha256=aIMQhTgOAv8gv-ZvPTNPJ-NMM3W1OtOC8lvLZetLfF4,2990
3
+ simplex/section.py,sha256=tJfR5rVPAG-Wud0dM-wNeCexfDgRtu4fupGX-ojEFs4,1654
4
+ simplex/engine/README.md,sha256=rHMhErOWc9X6aq6k75I1aVjIyhjNFvrL9LSiYiTfCDk,2991
5
+ simplex/engine/__init__.py,sha256=WyGyuduPx1c3ESuDkHPs78JqXIRBavGYJcFV0_EuTVg,808
6
+ simplex/engine/animations.py,sha256=X-y1hA9o9woRJIPfMw3zqSS0ePn5sKCJtJwfjmuCNM0,5565
7
+ simplex/engine/code.py,sha256=t-ejp9aQrey6HD_eufx1xqD-hKstvSBIn0Mg7VCOH3o,15134
8
+ simplex/engine/debug.py,sha256=18u4udhT4XD8Sa7dXY9jxpz7jzkA08IvsQglEGTP2Mg,5636
9
+ simplex/engine/defaults.py,sha256=ZG8I0FUMwOnpe0QmB4m_vXFOSG_FuCTLOJA6S4eLVlE,1093
10
+ simplex/engine/dynamics.py,sha256=kxjlvGb00EKiYS0_lKvxeCjCAHSvEDKkGfkUIbrf1mc,3403
11
+ simplex/engine/geometry.py,sha256=kjbR3DRSV1rHlGMkRGM8OF38Qhw0M4odKjQGj_zAEdQ,8279
12
+ simplex/engine/ghost_fade.py,sha256=s3EG-Ope47gOY4miSfjdB7W-6zSIy4EiojKh1Uey6_c,2823
13
+ simplex/engine/glyph_map.py,sha256=Fz5SfapdC6bfRoZwZ4hWvraGYImUtmQ6h14VDjxsOnQ,12820
14
+ simplex/engine/region.py,sha256=qZvmSQhAchLEklnLwxrmpRF2_PDwH5uKWGcI49dVkGo,10821
15
+ simplex/engine/scaling.py,sha256=_7chCXatOA8WIm2w9qDbJNgdkyWZ-F9hK4oa4LmEmag,2456
16
+ simplex/engine/text.py,sha256=3O3TuNLiOCX5-Wgjx9gRRoBMFuc-UilnvsZqdBukb7w,3909
17
+ simplex/mobjects/README.md,sha256=_vOhB65LibwkV276SCbSwj3JPhgNpBUmMawSJhro-Wg,1168
18
+ simplex/mobjects/__init__.py,sha256=crKQGZN2ZwEH3kE5dAszsrVcruYkZScK6l7usnd2XFE,531
19
+ simplex/mobjects/array.py,sha256=C02Vavj8c7rK2Pa0HchP354MGwPAWi2Mir8Nuiovc2A,13700
20
+ simplex/mobjects/graph.py,sha256=HtuDYESlt7aJgCu0KbLy5NKqk81-Ow60b6Y-d_8DkIA,1983
21
+ simplex/mobjects/outline.py,sha256=3uQiyT6NCaDBx9jLu6vc7hl7yDYUNQhxKlVIKarMhiU,5065
22
+ simplex/slides/README.md,sha256=h2XjoR3FqwXDS43dFtBakUKwloUKWpqDlUVDFL-njrc,2018
23
+ simplex/slides/__init__.py,sha256=rbcsHN8BlImuEjpXzFPsAADX9dCoDXvdDhVzIgTAy8w,469
24
+ simplex/slides/base.py,sha256=9bpOtIdLhe3FYiE2pJo3XVdPCXbH1XRk6LkDmmYBM_U,4488
25
+ simplex/slides/chrome.py,sha256=PACBPP7IZ40RBoE6aXHLxGwSAX0RpkwnS64zjAt3ATw,2391
26
+ simplex/slides/outline.py,sha256=PPoAMKRCLB38MF65UWryFivRW-M-A3RFuIIxCkzZh0c,15380
27
+ simplex/theme/README.md,sha256=Ie94AOM3dC9dVdCSZo3eTXtVN8n_aRTqwIbqOtpetWQ,900
28
+ simplex/theme/__init__.py,sha256=nK8kxrSLAGsvciEs0wrVmmrfLgAr5nyk34PdQsNYjBI,540
29
+ simplex/theme/context.py,sha256=vDY_CtUQ6SxdnmGZFdHJ5X4vrnvbl4p8Pm53KRSpom0,787
30
+ simplex/theme/presets.py,sha256=pRT6S57coBUSyMQcZ4lTsMtmBfz66A81TPUV_crVQUo,2429
31
+ simplex/theme/pygments_style.py,sha256=sHkdMhwelJzZHpFDtOwy87El2SC2gUg7jp34Wc3SaGU,2789
32
+ simplex/theme/tokens.py,sha256=2UjMGE927gVAC7dcV51Qlb9s0FUCUyPY36TTZ-lATc8,3073
33
+ simplex/theme/web_css.py,sha256=VQURjkh038Iwu6_tt8wkfNVUD4aYcenvio3uHY1FsKs,1218
34
+ simplex/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
+ manim_simplex-0.2.0.dist-info/METADATA,sha256=VqT3nJpEpkqxTfcoZKIAmBKusme0vbwuPVOJf-N__lo,7857
36
+ manim_simplex-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
37
+ manim_simplex-0.2.0.dist-info/entry_points.txt,sha256=mEZB63zKWkjk7pWQSE9cwsO6msA9DizJwa_TV9OXnQU,50
38
+ manim_simplex-0.2.0.dist-info/licenses/LICENSE,sha256=nl59bQ87NqHIcm_46xQjWvu0Nv630wsBOK4I1-ou9SY,1070
39
+ manim_simplex-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [manim.plugins]
2
+ simplex = simplex.plugin:activate
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shlomi Perles
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,38 @@
1
+ # engine/
2
+
3
+ Small additive helpers that augment vanilla Manim. **Never wrap Manim's constructors.**
4
+
5
+ ## Public surface
6
+
7
+ - `apply_theme_defaults(theme)` -- calls `Mobject.set_default(...)` for Tex / MathTex / Text / Line / Dot / Arrow / Rectangle / Square (invoked by `simplex.plugin:activate`)
8
+ - `Region` -- mutable rectangular drawing area; default lives on `BaseSlide.region`
9
+ - `Remove(mob, **kw)` -- exit animation lookup; dispatches through `exit_for(mob)`
10
+ - `exit_for(mob, **kw)` -- per-instance override (WeakKeyDictionary) -> MRO match in defaults -> `FadeOut`
11
+ - `register_exit(mob_type, factory)` -- register a default exit for a Mobject class
12
+ - `set_exit_animation(mob, factory)` -- per-instance exit override (stored in a `WeakKeyDictionary`; no monkey-patching)
13
+ - `clear_scene(scene, *, exclude=())` -- free function used by `BaseSlide.clear_scene`
14
+ - `HighlightResult` -- typed return for `highlight_code_lines` (fade + optional indicate, iterable)
15
+
16
+ Cross-package types live one level up:
17
+
18
+ - `simplex.section.SimplexSectionType` -- enum encoded into Manim's section JSON
19
+ - `simplex.manifest.DeckManifest` / `MainSlide` / `Subsection` -- web builder contract
20
+
21
+ ## Submodules (import directly to keep `simplex.engine` cheap)
22
+
23
+ - `engine.text` -- `Caption`, `TexPage` (fixed-width minipage; `width_cm` kwarg / class attr); `color_tex(eq, t2c)`; `search_shape_in_text`. Body-sized paragraphs use plain `manim.Tex` -- `apply_theme_defaults` already sets `font_size=theme.typography.body`.
24
+ - `engine.code` -- `code_block`, `highlight_code_lines`, `code_explain`, `transform_code_lines`; `DarculaStyle`, `register_darcula`; `HighlightResult`
25
+ - `engine.geometry` -- `get_convex_hull_polygon`, `get_surrounding_rectangle`, `get_frame_center`, `Vcis`, `Arc3d`, `SurroundingRectangleUnion`
26
+ - `engine.glyph_map` -- `TransformByGlyphMap` (glyph-indexed Tex transitions)
27
+ - `engine.ghost_fade` -- `GhostSlideFade` (one-shot fade-in/drift/fade-out cue)
28
+ - `engine.dynamics` -- `VT` (`~`/`@`/`@=` over `ValueTracker`), `DN` (auto-tracking `DecimalNumber`), `keep_orientation`, `maintain_apparent_stroke_width`
29
+ - `engine.scaling` -- `scale_to_fit` (multi-axis fit + buff), `scale_to_fit_mobject`, `scale_with_stroke_width`
30
+ - `engine.debug` -- `bounding_box`, `indexx_labels` (multi-color), `debug_glyph(s)`
31
+
32
+ ## Don't
33
+
34
+ - Don't call `Mobject.set_default(...)` outside `apply_theme_defaults`.
35
+ - Don't subclass Manim Mobjects to inject defaults; use `set_default` via `apply_theme_defaults`.
36
+ - Don't reimplement what Manim ships: `ValueTracker` arithmetic ops, `index_labels`, `ConvexHull`, `Union`, `Polygon.round_corners`, `scale_to_fit_height/_width/_depth`, `BraceLabel`/`BraceText`, `Mobject.always` -- all already in 0.20.x.
37
+ - Don't import Manim at module load time from animations / region / defaults -- import inside the function so importing `simplex.engine` stays cheap.
38
+ - Don't monkey-patch Mobjects (no `_simplex_*` attributes). Use the `WeakKeyDictionary` registry in `animations.py` instead.
@@ -0,0 +1,31 @@
1
+ """Engine helpers that augment vanilla Manim.
2
+
3
+ The engine ships animation primitives, layout helpers, and small custom
4
+ mobjects -- everything that *isn't* a Slide and *isn't* a theme token.
5
+
6
+ Cross-package types like ``SimplexSectionType`` live at the package root
7
+ (``simplex.section``) so the web builder and CLI can import them without
8
+ touching Manim.
9
+ """
10
+
11
+ from simplex.engine.animations import (
12
+ Remove,
13
+ clear_scene,
14
+ exit_for,
15
+ register_exit,
16
+ set_exit_animation,
17
+ )
18
+ from simplex.engine.code import HighlightResult
19
+ from simplex.engine.defaults import apply_theme_defaults
20
+ from simplex.engine.region import Region
21
+
22
+ __all__ = [
23
+ "HighlightResult",
24
+ "Region",
25
+ "Remove",
26
+ "apply_theme_defaults",
27
+ "clear_scene",
28
+ "exit_for",
29
+ "register_exit",
30
+ "set_exit_animation",
31
+ ]
@@ -0,0 +1,160 @@
1
+ """Default-exit registry, ``Remove``, ``set_exit_animation``, and ``clear_scene``.
2
+
3
+ Each Mobject type has a default *exit* animation (e.g. ``Tex`` -> ``Unwrite``,
4
+ ``Circle`` -> ``ShrinkToCenter``). Callers can override per-instance via
5
+ ``set_exit_animation(mob, anim_cls_or_factory)``, or per-type via
6
+ ``register_exit(type, factory)``.
7
+
8
+ ``Remove(mob)`` and ``clear_scene(scene, exclude=...)`` both dispatch through
9
+ ``exit_for(mob)``, which checks instance overrides, then walks the MRO of
10
+ ``type(mob)`` against the type defaults, falling back to ``FadeOut``.
11
+
12
+ Implementation notes:
13
+
14
+ - The type defaults dict is wrapped in a ``_DefaultRegistry`` singleton with
15
+ a double-checked ``threading.Lock`` around its lazy init. Manim renders are
16
+ mostly single-threaded but plugins can be poked from setup hooks running
17
+ in parallel (e.g. test suites), so the lock is cheap insurance.
18
+ - Per-instance overrides are kept in a ``WeakKeyDictionary`` keyed by the
19
+ Mobject, *not* monkey-patched onto the Mobject as a ``_simplex_exit``
20
+ attribute. Manim's Mobject base is plain-class so both ``__hash__`` (id
21
+ identity) and ``__weakref__`` work; if a downstream subclass disables one
22
+ of these the override call fails fast at ``set_exit_animation`` time
23
+ rather than leaking through a swallowed ``setattr``.
24
+ """
25
+
26
+ import threading
27
+ from collections.abc import Callable, Iterable
28
+ from typing import Any
29
+ from weakref import WeakKeyDictionary
30
+
31
+ ExitFactory = Callable[..., Any]
32
+
33
+
34
+ class _DefaultRegistry:
35
+ """Type -> exit-factory map with double-checked locked lazy init."""
36
+
37
+ def __init__(self) -> None:
38
+ self._lock = threading.Lock()
39
+ self._map: dict[type, ExitFactory] | None = None
40
+
41
+ def get(self) -> dict[type, ExitFactory]:
42
+ if self._map is None:
43
+ with self._lock:
44
+ if self._map is None:
45
+ self._map = self._build()
46
+ return self._map
47
+
48
+ @staticmethod
49
+ def _build() -> dict[type, ExitFactory]:
50
+ from manim import (
51
+ DOWN,
52
+ Arrow,
53
+ Circle,
54
+ Code,
55
+ DashedLine,
56
+ Dot,
57
+ FadeOut,
58
+ Line,
59
+ MarkupText,
60
+ MathTex,
61
+ ShrinkToCenter,
62
+ Tex,
63
+ Text,
64
+ Uncreate,
65
+ Unwrite,
66
+ VMobject,
67
+ )
68
+
69
+ def fade_with_drift(m: Any, **kw: Any) -> Any:
70
+ return FadeOut(m, shift=0.1 * DOWN, **kw)
71
+
72
+ return {
73
+ Tex: Unwrite,
74
+ MathTex: Unwrite,
75
+ Text: Unwrite,
76
+ MarkupText: Unwrite,
77
+ Code: Unwrite,
78
+ Circle: ShrinkToCenter,
79
+ Dot: ShrinkToCenter,
80
+ Line: Uncreate,
81
+ Arrow: Uncreate,
82
+ DashedLine: Uncreate,
83
+ VMobject: fade_with_drift,
84
+ }
85
+
86
+
87
+ _REGISTRY = _DefaultRegistry()
88
+ _OVERRIDES: WeakKeyDictionary[Any, ExitFactory] = WeakKeyDictionary()
89
+
90
+
91
+ def register_exit(mob_type: type, factory: ExitFactory) -> None:
92
+ """Register a default exit animation for ``mob_type`` (and subclasses via MRO).
93
+
94
+ ``factory(mob, **kwargs)`` must return an ``Animation``. Most callers pass
95
+ an ``Animation`` class directly (``Unwrite``, ``FadeOut``, ...) since
96
+ classes are callable in this signature.
97
+ """
98
+ _REGISTRY.get()[mob_type] = factory
99
+
100
+
101
+ def set_exit_animation(mob: Any, factory: ExitFactory) -> Any:
102
+ """Stash a per-instance exit factory for ``mob``. Returns ``mob`` for chaining.
103
+
104
+ The override lives in a module-level ``WeakKeyDictionary`` -- it disappears
105
+ automatically when ``mob`` is garbage-collected. No attribute is set on
106
+ the Mobject itself.
107
+ """
108
+ try:
109
+ _OVERRIDES[mob] = factory
110
+ except TypeError as exc: # mob is not hashable or weakref-able
111
+ raise TypeError(
112
+ f"set_exit_animation: {type(mob).__name__} cannot be used as a "
113
+ "registry key (no __weakref__ or unhashable). Wrap the mobject "
114
+ "in a VGroup or pass a different mobject."
115
+ ) from exc
116
+ return mob
117
+
118
+
119
+ def exit_for(mob: Any, **kwargs: Any) -> Any:
120
+ """Return an ``Animation`` that exits ``mob``.
121
+
122
+ Resolution order:
123
+
124
+ 1. per-instance override registered by ``set_exit_animation``;
125
+ 2. exact type or MRO match in the default registry;
126
+ 3. ``FadeOut`` fallback.
127
+
128
+ Any ``**kwargs`` are forwarded to the resolved factory.
129
+ """
130
+ override = _OVERRIDES.get(mob)
131
+ if override is not None:
132
+ return override(mob, **kwargs) if kwargs else override(mob)
133
+ defaults = _REGISTRY.get()
134
+ for cls in type(mob).__mro__:
135
+ factory = defaults.get(cls)
136
+ if factory is not None:
137
+ return factory(mob, **kwargs) if kwargs else factory(mob)
138
+ from manim import FadeOut
139
+
140
+ return FadeOut(mob, **kwargs)
141
+
142
+
143
+ def Remove(mob: Any, **kwargs: Any) -> Any: # noqa: N802 -- mirrors Manim's PascalCase Animations
144
+ """Alias for ``exit_for(mob, **kwargs)`` -- spelled to match Manim animations."""
145
+ return exit_for(mob, **kwargs)
146
+
147
+
148
+ def clear_scene(scene: Any, *, exclude: Iterable[Any] = ()) -> None:
149
+ """Play exit animations for every Mobject not in ``exclude``.
150
+
151
+ Uses ``scene.mobjects_without_canvas`` when available (so the
152
+ manim-slides canvas survives) and dispatches through ``exit_for`` so
153
+ per-instance and per-type overrides apply.
154
+ """
155
+ skip = set(exclude)
156
+ pool = getattr(scene, "mobjects_without_canvas", None) or scene.mobjects
157
+ targets = [m for m in pool if m not in skip]
158
+ if not targets:
159
+ return
160
+ scene.play(*(exit_for(m) for m in targets))