mirror of
https://github.com/ManimCommunity/manim.git
synced 2026-06-22 10:01:47 +00:00
Compare commits
43 commits
experiment
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e83f4b09a |
||
|
|
f7fd708276 |
||
|
|
4c4622df54 |
||
|
|
66d5a4937a |
||
|
|
c07137dbed |
||
|
|
71ab85f960 |
||
|
|
16f0a3de3e |
||
|
|
537a134360 |
||
|
|
b7bf2ea90f |
||
|
|
bdaf4497b7 |
||
|
|
037b376ec2 |
||
|
|
e9639c2697 |
||
|
|
1b2d5ce72b |
||
|
|
c94a7ea9fc |
||
|
|
bb1be6ef8a |
||
|
|
516c8c8ba7 |
||
|
|
ccee37a614 |
||
|
|
31db147222 |
||
|
|
34124c3f60 |
||
|
|
d999d422c9 |
||
|
|
561de9d72a |
||
|
|
05b3042ab0 |
||
|
|
2ece488b2c |
||
|
|
852ebd1c60 |
||
|
|
82522795f1 |
||
|
|
12c5640a32 |
||
|
|
33424fe43d |
||
|
|
1b3390073c |
||
|
|
56f7eb2a1f |
||
|
|
cfb5c684b7 |
||
|
|
429f25328d |
||
|
|
c45724989d |
||
|
|
af70b6fef2 |
||
|
|
4b32312dd1 |
||
|
|
90141df105 |
||
|
|
82f93b6c3c |
||
|
|
752b46a003 |
||
|
|
21cf9998cc |
||
|
|
ebb230f6f1 |
||
|
|
46177d247e |
||
|
|
468929889b |
||
|
|
98c458b6b2 |
||
|
|
d4af5b2baa |
52 changed files with 3023 additions and 543 deletions
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
|
|
@ -104,8 +104,8 @@ jobs:
|
|||
oriPath=$PATH
|
||||
sudo mkdir -p $PWD/macos-cache
|
||||
echo "Install TinyTeX"
|
||||
sudo curl -L -o "/tmp/TinyTeX.tgz" "https://github.com/yihui/tinytex-releases/releases/download/daily/TinyTeX-1.tgz"
|
||||
sudo tar zxf "/tmp/TinyTeX.tgz" -C "$PWD/macos-cache"
|
||||
sudo curl -L -o "/tmp/TinyTeX.tar.xz" "https://github.com/rstudio/tinytex-releases/releases/download/daily/TinyTeX-1-darwin.tar.xz"
|
||||
sudo tar xJf "/tmp/TinyTeX.tar.xz" -C "$PWD/macos-cache"
|
||||
export PATH="$PWD/macos-cache/TinyTeX/bin/universal-darwin:$PATH"
|
||||
sudo tlmgr update --self
|
||||
for i in "${ttp[@]}"; do
|
||||
|
|
@ -129,7 +129,7 @@ jobs:
|
|||
path: ${{ github.workspace }}\ManimCache
|
||||
key: ${{ runner.os }}-dependencies-tinytex-${{ hashFiles('.github/manimdependency.json') }}-${{ steps.cache-vars.outputs.date }}-1
|
||||
|
||||
- uses: ssciwr/setup-mesa-dist-win@v2
|
||||
- uses: ssciwr/setup-mesa-dist-win@v3
|
||||
|
||||
- name: Install system dependencies (Windows)
|
||||
if: runner.os == 'Windows' && steps.cache-windows.outputs.cache-hit != 'true'
|
||||
|
|
@ -137,8 +137,8 @@ jobs:
|
|||
$tinyTexPackages = $(python -c "import json;print(' '.join(json.load(open('.github/manimdependency.json'))['windows']['tinytex']))") -Split ' '
|
||||
$OriPath = $env:PATH
|
||||
echo "Install Tinytex"
|
||||
Invoke-WebRequest "https://github.com/yihui/tinytex-releases/releases/download/daily/TinyTeX-1.zip" -OutFile "$($env:TMP)\TinyTex.zip"
|
||||
Expand-Archive -LiteralPath "$($env:TMP)\TinyTex.zip" -DestinationPath "$($PWD)\ManimCache\LatexWindows"
|
||||
Invoke-WebRequest "https://github.com/rstudio/tinytex-releases/releases/download/daily/TinyTeX-1-windows.exe" -OutFile "$($env:TMP)\TinyTex.exe"
|
||||
.$env:TMP\TinyTex.exe -o"$($PWD)\ManimCache\LatexWindows"
|
||||
$env:Path = "$($PWD)\ManimCache\LatexWindows\TinyTeX\bin\windows;$($env:PATH)"
|
||||
tlmgr update --self
|
||||
tlmgr install $tinyTexPackages
|
||||
|
|
|
|||
16
.github/workflows/publish-docker.yml
vendored
16
.github/workflows/publish-docker.yml
vendored
|
|
@ -13,19 +13,19 @@ jobs:
|
|||
if: github.event_name != 'release'
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
platforms: linux/arm64,linux/amd64
|
||||
push: true
|
||||
|
|
@ -38,13 +38,13 @@ jobs:
|
|||
if: github.event_name == 'release'
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
|
@ -61,7 +61,7 @@ jobs:
|
|||
print(f"tag_name={ref_tag}", file=f)
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
platforms: linux/arm64,linux/amd64
|
||||
push: true
|
||||
|
|
|
|||
2
.github/workflows/python-publish.yml
vendored
2
.github/workflows/python-publish.yml
vendored
|
|
@ -33,7 +33,7 @@ jobs:
|
|||
uv publish
|
||||
|
||||
- name: Store artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: dist/*.tar.gz
|
||||
name: manim.tar.gz
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ jobs:
|
|||
babel-english ctex doublestroke dvisvgm frcursive fundus-calligra jknapltx \
|
||||
mathastext microtype physics preview ragged2e relsize rsfs setspace standalone \
|
||||
wasy wasysym
|
||||
uv sync
|
||||
uv sync --extra typst
|
||||
|
||||
- name: Build and package documentation
|
||||
run: |
|
||||
|
|
@ -43,7 +43,7 @@ jobs:
|
|||
tar -czvf ../html-docs.tar.gz *
|
||||
|
||||
- name: Store artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: ${{ github.workspace }}/docs/build/html-docs.tar.gz
|
||||
name: html-docs.tar.gz
|
||||
|
|
|
|||
382
agents/typst_selector.md
Normal file
382
agents/typst_selector.md
Normal file
|
|
@ -0,0 +1,382 @@
|
|||
# Design: Sub-Expression Selection for `Typst` / `TypstMath`
|
||||
|
||||
## Problem Statement
|
||||
|
||||
Users need to interact with individual parts of a Typst-rendered expression:
|
||||
color a variable, animate the numerator of a fraction, morph one sub-expression
|
||||
into another, etc. The `MathTex` class solves this with:
|
||||
|
||||
1. **`{{ ... }}` double-brace notation** — splits the TeX string into named
|
||||
submobject groups at compile time.
|
||||
2. **`substrings_to_isolate` / `get_part_by_tex`** — identifies submobjects
|
||||
whose TeX source matches a given string.
|
||||
|
||||
Both mechanisms ultimately rely on injecting `\special{dvisvgm:raw <g id='...'>}`
|
||||
markers into the LaTeX source so that the resulting SVG contains `<g>` elements
|
||||
with known `id` attributes, which SVGMobject's parser maps to `VGroup`
|
||||
sub-trees via `id_to_vgroup_dict`.
|
||||
|
||||
We need an analogous mechanism for Typst.
|
||||
|
||||
## Key Discovery: `data-typst-label` in SVG Output
|
||||
|
||||
Typst's SVG renderer (`typst-svg` crate) already emits a `data-typst-label`
|
||||
attribute on `<g>` elements whenever a `GroupItem` (hard frame) carries a
|
||||
label. The relevant code path:
|
||||
|
||||
```rust
|
||||
// typst-svg/src/lib.rs — render_group()
|
||||
if let Some(label) = group.label {
|
||||
svg.init().attr("data-typst-label", label.resolve());
|
||||
}
|
||||
```
|
||||
|
||||
A **hard frame** is created by the `box` element (and `block`, etc.). Crucially,
|
||||
`box` can be used inline inside math mode, and labels can be attached to it.
|
||||
|
||||
### Proof of Concept
|
||||
|
||||
The following Typst helper wraps content in a labeled `box`:
|
||||
|
||||
```typst
|
||||
#let grp(lbl, body) = [#box(body) #label(lbl)]
|
||||
```
|
||||
|
||||
When used in math:
|
||||
|
||||
```typst
|
||||
$ #grp("numerator", $a + b$) / #grp("denom", $c - d$) = #grp("result", $x$) $
|
||||
```
|
||||
|
||||
The compiled SVG contains:
|
||||
|
||||
```xml
|
||||
<g class="typst-group" ... data-typst-label="numerator">
|
||||
<!-- glyphs for a + b -->
|
||||
</g>
|
||||
<g class="typst-group" ... data-typst-label="denom">
|
||||
<!-- glyphs for c - d -->
|
||||
</g>
|
||||
<g class="typst-group" ... data-typst-label="result">
|
||||
<!-- glyph for x -->
|
||||
</g>
|
||||
```
|
||||
|
||||
**Nesting works.** A `grp` wrapping a fraction that itself contains `grp`-ed
|
||||
sub-parts produces nested `data-typst-label` groups:
|
||||
|
||||
```typst
|
||||
$ #grp("whole-frac", $frac(#grp("numerator", $a + b$), #grp("denom", $c - d$))$) $
|
||||
```
|
||||
|
||||
SVG output:
|
||||
|
||||
```xml
|
||||
<g ... data-typst-label="whole-frac">
|
||||
<g ... data-typst-label="numerator"> ... </g>
|
||||
<g ... data-typst-label="denom"> ... </g>
|
||||
<path class="typst-shape" ... /> <!-- fraction bar -->
|
||||
</g>
|
||||
```
|
||||
|
||||
### SVG Parser Compatibility
|
||||
|
||||
Manim uses `svgelements` to parse SVGs. The library preserves
|
||||
`data-typst-label` in the `values` dictionary of `Group` objects, and it
|
||||
propagates to child elements. Manim's `SVGMobject.get_mobjects_from()` already
|
||||
iterates over groups and builds `id_to_vgroup_dict` keyed by the `id` attribute.
|
||||
Extending this to also key by `data-typst-label` is straightforward.
|
||||
|
||||
## Proposed Interface
|
||||
|
||||
### 1. Explicit Groups via `{{ ... }}` Notation (Compile-Time)
|
||||
|
||||
Mirror the `MathTex` double-brace convention. Users write:
|
||||
|
||||
```python
|
||||
eq = TypstMath("{{ a + b }} / {{ c - d }} = {{ x }}")
|
||||
```
|
||||
|
||||
The pre-processor splits on `{{ ... }}` (reusing the same whitespace-guard
|
||||
rules as `MathTex._split_double_braces`) and wraps each group in a labeled
|
||||
`box` call:
|
||||
|
||||
```typst
|
||||
$ #box[$a + b$] <_grp-0> / #box[$c - d$] <_grp-1> = #box[$x$] <_grp-2> $
|
||||
```
|
||||
|
||||
Each group gets an auto-generated label (`_grp-0`, `_grp-1`, ...).
|
||||
The `data-typst-label` attributes then appear in the SVG, and
|
||||
`SVGMobject.get_mobjects_from()` can map them to `VGroup` entries in
|
||||
`label_to_vgroup_dict` (or reuse `id_to_vgroup_dict`).
|
||||
|
||||
These groups become sub-mobjects of the `TypstMath` instance, accessible by
|
||||
index:
|
||||
|
||||
```python
|
||||
eq[0] # VGroup for "a + b"
|
||||
eq[1] # VGroup for "c - d"
|
||||
eq[2] # VGroup for "x"
|
||||
```
|
||||
|
||||
(Non-group content between groups — like `/` and `=` — also becomes
|
||||
its own submobject, mirroring `MathTex` behavior.)
|
||||
|
||||
**For `Typst` (text mode):** the same `{{ ... }}` notation applies, but the
|
||||
wrapper is `#box[...]` without math delimiters.
|
||||
|
||||
### 2. Named Groups via Labels
|
||||
|
||||
Users can also assign explicit label names for retrieval by name:
|
||||
|
||||
```python
|
||||
eq = Typst(
|
||||
r"$ #box[$a + b$] <numerator> / #box[$c - d$] <denom> $"
|
||||
)
|
||||
eq.select("numerator").set_color(RED)
|
||||
eq.select("denom").set_color(BLUE)
|
||||
```
|
||||
|
||||
Alternatively, an even more ergonomic approach that hides the `box` boilerplate
|
||||
and uses the `{{ ... : label }}` notation:
|
||||
|
||||
```python
|
||||
eq = TypstMath("{{ a + b : numerator }} / {{ c - d : denom }}")
|
||||
eq.select("numerator").set_color(RED)
|
||||
```
|
||||
|
||||
Here the pre-processor recognizes `{{ content : label }}` and emits
|
||||
`#box[$content$] <label>` in the Typst source.
|
||||
|
||||
### 3. The `.select()` Method
|
||||
|
||||
```python
|
||||
def select(self, key: str | int) -> VGroup:
|
||||
"""Select a labeled sub-expression.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
key
|
||||
Either a label name (string) matching a ``data-typst-label``
|
||||
in the SVG, or an integer index into the auto-numbered
|
||||
``{{ ... }}`` groups.
|
||||
|
||||
Returns
|
||||
-------
|
||||
VGroup
|
||||
The sub-mobjects corresponding to the selected group.
|
||||
|
||||
Raises
|
||||
------
|
||||
KeyError
|
||||
If no group with the given label/index exists.
|
||||
"""
|
||||
```
|
||||
|
||||
This returns a `VGroup` containing exactly the submobjects (paths) that
|
||||
were rendered inside the corresponding `<g data-typst-label="...">` in the SVG.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Step 1: Extend `SVGMobject.get_mobjects_from()` to Track Labels
|
||||
|
||||
In `manim/mobject/svg/svg_mobject.py`, the group-walking loop already checks
|
||||
for `id` attributes. Add a parallel check for `data-typst-label`:
|
||||
|
||||
```python
|
||||
try:
|
||||
group_name = str(element.values["id"])
|
||||
except Exception:
|
||||
# Fall back to data-typst-label if available
|
||||
label = element.values.get("data-typst-label")
|
||||
if label:
|
||||
group_name = f"typst-label:{label}"
|
||||
else:
|
||||
group_name = f"numbered_group_{group_id_number}"
|
||||
group_id_number += 1
|
||||
```
|
||||
|
||||
This automatically populates `id_to_vgroup_dict` with label-keyed entries.
|
||||
|
||||
### Step 2: Pre-Processing `{{ ... }}` in Typst Source
|
||||
|
||||
Add a `_split_and_label_groups()` method that:
|
||||
|
||||
1. Scans the input for `{{ ... }}` or `{{ ... : label }}` patterns
|
||||
(using the same whitespace-guard rules as `MathTex._split_double_braces`).
|
||||
2. Replaces each group with `#box[$content$] <label>` (math mode) or
|
||||
`#box[content] <label>` (text mode).
|
||||
3. Records the mapping from label → original source string for later lookup.
|
||||
|
||||
### Step 3: `Typst.select()` / Index Access
|
||||
|
||||
- Store the ordered list of group labels and their source strings.
|
||||
- `select(label_or_index)` looks up the corresponding `VGroup` from
|
||||
`id_to_vgroup_dict` (using the `typst-label:...` key).
|
||||
- `__getitem__(int)` returns the *n*-th group's `VGroup`.
|
||||
|
||||
### Step 4: Compatibility with `TransformMatchingTex` (future)
|
||||
|
||||
`TransformMatchingTex` (and its successor `TransformMatchingShapes`) works by
|
||||
matching submobjects between two `MathTex` instances by their TeX string keys.
|
||||
The same approach extends to `Typst` if each `{{ ... }}` group carries its
|
||||
original source string as metadata. A `TransformMatchingTypst` animation could
|
||||
match groups by label name or by source string equality.
|
||||
|
||||
## Open Design Questions
|
||||
|
||||
### Q1: Context-Aware Wrapping — Math vs. Text Mode
|
||||
|
||||
The `box` + `label` mechanism works identically in math and text mode, but the
|
||||
**wrapping** of group content must match the surrounding context:
|
||||
|
||||
- **In text mode:** `{{ Hello : greeting }}` → `#box[Hello] <greeting>`
|
||||
- **In math mode:** `{{ y^2 : second }}` → `#box[$y^2$] <second>`
|
||||
|
||||
Getting this wrong is not a silent error — it produces visually broken output.
|
||||
Wrapping math content with `#box[y^2]` (no `$...$`) renders `y^2` as literal
|
||||
text in the body font instead of as a math superscript.
|
||||
|
||||
This is a real problem for `Typst()`, where a single source string can mix text
|
||||
and math freely:
|
||||
|
||||
```python
|
||||
Typst("hello world, here is a formula: $x^2 + {{ y^2 : second }} = z^2$")
|
||||
```
|
||||
|
||||
Here `{{ y^2 : second }}` is inside a `$ ... $` block, so it needs the
|
||||
math-mode wrapper, but the pre-processor has no way to know this unless it
|
||||
tracks `$` delimiters.
|
||||
|
||||
### The `#` prefix problem and math calls
|
||||
|
||||
A natural idea is to translate `{{ content }}` into a Typst function call like
|
||||
`grp("lbl", content)`. However, this has a subtle but critical context
|
||||
sensitivity: Typst has two different call conventions depending on context:
|
||||
|
||||
- **Math call** (no `#` prefix): `$ grp("lbl", a^2 + b) $` — arguments are
|
||||
parsed **in math mode**. The content `a^2 + b` is math. ✓
|
||||
- **Code call** (`#` prefix): `$ #grp("lbl", a^2 + b) $` — arguments are
|
||||
parsed **in code mode**. `a^2` is a syntax error in code! ✗
|
||||
|
||||
So in math mode, the function MUST be called without `#` for args to stay in
|
||||
math mode. In text/markup mode, the function MUST be called WITH `#` (that's
|
||||
how you invoke code from markup), and content arguments need `[...]` wrapping:
|
||||
|
||||
```typst
|
||||
// Text context: #grp("lbl", [Hello world])
|
||||
// Math context: grp("lbl", a^2 + b)
|
||||
```
|
||||
|
||||
The function definition is the same either way:
|
||||
```typst
|
||||
#let grp(lbl, body) = [#box(body) #label(lbl)]
|
||||
```
|
||||
|
||||
This means the function call approach has **exactly the same context problem**
|
||||
as the raw `#box` approach: the pre-processor must know whether it's in math or
|
||||
text to emit the right calling convention.
|
||||
|
||||
### Further complication: string literals and content blocks
|
||||
|
||||
Even inside `TypstMath` (where everything is math), the scanner must avoid
|
||||
`{{ }}` matches inside string literals or content blocks:
|
||||
|
||||
```python
|
||||
TypstMath('x^2 + y^2 =_("Hello {{ world }}") z^2')
|
||||
```
|
||||
|
||||
Here `{{ world }}` is inside a `"..."` string literal — it should NOT be
|
||||
processed. Similarly, content blocks `[...]` inside math switch back to text
|
||||
mode.
|
||||
|
||||
### Options
|
||||
|
||||
**A. `TypstMath`: math calls with simple string-aware scanning.**
|
||||
For `TypstMath`, the entire body is math, so `{{ content }}` always becomes
|
||||
`grp("_grp-N", content)` (no `#`, no `$...$`). The scanner just needs to
|
||||
skip `"..."` string literals and `[...]` content blocks — no `$` tracking
|
||||
needed. This is clean and robust.
|
||||
|
||||
**B. `Typst`: context-aware scanning (full parser).**
|
||||
For the general `Typst` class, the scanner must additionally track `$...$`
|
||||
math blocks (toggling a mode flag on unescaped `$`) to choose between
|
||||
`grp(...)` (in math) and `#grp("lbl", [...])` (in text). It must also handle
|
||||
string literals and content blocks inside math that switch context back. This
|
||||
is doable but non-trivial — essentially a mini Typst lexer.
|
||||
|
||||
**C. `Typst`: no `{{ }}`, manual groups only.**
|
||||
For the general `Typst` class, don't support `{{ }}` at all. Users write
|
||||
`grp(...)` / `#grp(...)` themselves (with the helper injected into the
|
||||
preamble). `{{ }}` is only available on `TypstMath`. This is simpler and
|
||||
avoids the parsing complexity, at the cost of ergonomics for mixed-mode
|
||||
documents.
|
||||
|
||||
**Recommendation:** Start with A (TypstMath only) and C (manual for Typst).
|
||||
Upgrade to B later if demand warrants it — the function call infrastructure
|
||||
is already in place, it's only the scanner that needs upgrading.
|
||||
|
||||
### Q2: What about "unlabeled" content between groups?
|
||||
|
||||
Like `MathTex`, the pieces of content *between* `{{ ... }}` groups should also
|
||||
become their own submobjects (auto-labeled with sequential indices). For
|
||||
example:
|
||||
|
||||
```python
|
||||
TypstMath("{{ a }} + {{ b }} = {{ c }}")
|
||||
# group-0: "a"
|
||||
# group-1: "+" (auto-group for inter-group content)
|
||||
# group-2: "b"
|
||||
# group-3: "=" (auto-group for inter-group content)
|
||||
# group-4: "c"
|
||||
```
|
||||
|
||||
Each segment (group or inter-group) gets wrapped in its own labeled `box`.
|
||||
|
||||
### Q3: What happens with `box` and baseline alignment?
|
||||
|
||||
`box` is an inline element in Typst, and when used inside math mode it
|
||||
participates in math layout. Testing confirms that fractions, superscripts, and
|
||||
other constructs render correctly when their children are `box`-wrapped.
|
||||
However, `box` creates a "hard frame" boundary which may subtly affect spacing
|
||||
in edge cases (e.g., math operator spacing around a boxed expression). This
|
||||
needs further testing; if issues arise, we could explore `block(breakable: false)`
|
||||
or invisible `rect` wrappers as alternatives.
|
||||
|
||||
### Q4: Can we avoid the `#grp(...)` / `#box[...] <label>` verbosity?
|
||||
|
||||
Yes — the `{{ ... }}` double-brace notation is purely syntactic sugar that gets
|
||||
pre-processed by Manim before the source reaches the Typst compiler. Users never
|
||||
need to write raw `#box` or `#label()` calls unless they want finer control.
|
||||
|
||||
### Q5: String-based selection without explicit groups?
|
||||
|
||||
A future enhancement could support:
|
||||
|
||||
```python
|
||||
eq = TypstMath(r"a + b = c")
|
||||
eq.select("a") # finds submobjects corresponding to the glyph "a"
|
||||
```
|
||||
|
||||
This is hard to do reliably because:
|
||||
- Typst SVGs embed glyphs as `<use xlink:href="#gXXX">` references; there's no
|
||||
text content in the SVG itself.
|
||||
- A single variable in Typst may span multiple glyphs (e.g., `"alpha"` → one
|
||||
glyph) or identical glyphs may appear multiple times.
|
||||
|
||||
A possible approach: at pre-processing time, wrap every "token" in the Typst
|
||||
math source in its own labeled `box`. This would require a Typst math tokenizer
|
||||
and is better suited for a v2 implementation.
|
||||
|
||||
## Summary: What Typst Gives Us
|
||||
|
||||
| Mechanism | How it works | SVG output |
|
||||
|---|---|---|
|
||||
| `#box(body) <label>` | Creates a hard-frame `GroupItem` with a `Label` | `<g data-typst-label="label">...</g>` |
|
||||
| `#metadata(val) <label>` | Invisible; queryable via `typst query` CLI | No visual output (useful for CLI queries, not SVG) |
|
||||
| Show rules on labels | `#show <label>: ...` | Transforms visual output but no automatic SVG grouping |
|
||||
| `context query(<label>)` | Document introspection (positions, counters) | In-document only; not available from Python |
|
||||
|
||||
The `box` + `label` mechanism is the **only** one that produces identifiable
|
||||
groups in the SVG output, making it the correct tool for sub-expression
|
||||
selection in Manim.
|
||||
|
|
@ -5,3 +5,4 @@ sphinx-copybutton
|
|||
sphinxext-opengraph
|
||||
sphinx-design
|
||||
sphinx-reredirects
|
||||
typst>=0.14
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
jupyterlab
|
||||
sphinxcontrib-programoutput
|
||||
typst>=0.14
|
||||
|
|
|
|||
|
|
@ -2,17 +2,24 @@
|
|||
Rendering Text and Formulas
|
||||
###########################
|
||||
|
||||
There are two different ways by which you can render **Text** in videos:
|
||||
There are three different ways by which you can render **Text** in videos:
|
||||
|
||||
1. Using Pango (:mod:`~.text_mobject`)
|
||||
2. Using LaTeX (:mod:`~.tex_mobject`)
|
||||
3. Using Typst (:mod:`~.typst_mobject`)
|
||||
|
||||
If you want to render simple text, you should use either :class:`~.Text` or
|
||||
:class:`~.MarkupText`, or one of its derivatives like :class:`~.Paragraph`.
|
||||
Manim's Pango-based text classes include :class:`~.Text`,
|
||||
:class:`~.MarkupText`, and derivatives such as :class:`~.Paragraph`.
|
||||
See :ref:`using-text-objects` for more information.
|
||||
|
||||
LaTeX should be used when you need mathematical typesetting. See
|
||||
:ref:`rendering-with-latex` for more information.
|
||||
LaTeX rendering is available via :class:`~.Tex` and
|
||||
:class:`~.MathTex`. See :ref:`rendering-with-latex` for more
|
||||
information.
|
||||
|
||||
Typst support is available via :class:`~.Typst` and
|
||||
:class:`~.TypstMath`. It offers both general markup and mathematical
|
||||
typesetting through the Typst compiler without requiring a TeX
|
||||
distribution. See :ref:`typst-mobjects` for more information.
|
||||
|
||||
.. _using-text-objects:
|
||||
|
||||
|
|
@ -291,6 +298,54 @@ and further references about PangoMarkup.
|
|||
)
|
||||
self.add(text)
|
||||
|
||||
.. _rendering-with-typst:
|
||||
|
||||
Text With Typst
|
||||
***************
|
||||
|
||||
Manim also supports rendering text and formulas with Typst via
|
||||
:class:`~.Typst` and :class:`~.TypstMath`.
|
||||
|
||||
.. important::
|
||||
|
||||
Typst support requires the optional ``typst`` dependency. Install it with
|
||||
``pip install manim[typst]``.
|
||||
|
||||
Typst mobjects compile Typst markup directly to SVG and import the result as
|
||||
vector graphics. This works both for general markup and for mathematical
|
||||
expressions.
|
||||
|
||||
.. manim:: HelloTypst
|
||||
:save_last_frame:
|
||||
:ref_classes: Typst
|
||||
|
||||
class HelloTypst(Scene):
|
||||
def construct(self):
|
||||
text = Typst(r"*Hello* from _Typst!_", font_size=96)
|
||||
self.add(text)
|
||||
|
||||
For mathematical expressions, use :class:`~.TypstMath`:
|
||||
|
||||
.. manim:: HelloTypstMath
|
||||
:save_last_frame:
|
||||
:ref_classes: TypstMath
|
||||
|
||||
class HelloTypstMath(Scene):
|
||||
def construct(self):
|
||||
equation = TypstMath(r"sum_(k=1)^n k = (n(n + 1)) / 2", font_size=72)
|
||||
self.add(equation)
|
||||
|
||||
Typst also supports selecting subexpressions via labels in the Typst source,
|
||||
or via Manim's ``{{ ... }}`` shorthand in :class:`~.TypstMath`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
eq = TypstMath("{{ a + b : lhs }} = {{ c }}")
|
||||
eq.select("lhs").set_color(BLUE)
|
||||
eq.select(0).set_color(YELLOW)
|
||||
|
||||
See :ref:`typst-mobjects` for more details and additional examples.
|
||||
|
||||
.. _rendering-with-latex:
|
||||
|
||||
Text With LaTeX
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ Positioning ``Mobject``\s
|
|||
|
||||
Next, let's go over some basic techniques for positioning ``Mobject``\s.
|
||||
|
||||
1. Open ``scene.py``, and add the following code snippet below the ``SquareToCircle`` method:
|
||||
1. Open ``scene.py``, and add the following code snippet below the ``SquareToCircle`` class:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ from .mobject.text.code_mobject import *
|
|||
from .mobject.text.numbers import *
|
||||
from .mobject.text.tex_mobject import *
|
||||
from .mobject.text.text_mobject import *
|
||||
from .mobject.text.typst_mobject import *
|
||||
from .mobject.three_d.polyhedra import *
|
||||
from .mobject.three_d.three_d_utils import *
|
||||
from .mobject.three_d.three_dimensions import *
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ class Animation:
|
|||
suspend_mobject_updating: bool = True,
|
||||
introducer: bool = False,
|
||||
*,
|
||||
_on_finish: Callable[[], None] = lambda _: None,
|
||||
_on_finish: Callable[[Scene], None] = lambda _: None,
|
||||
use_override: bool = True, # included here to avoid TypeError if passed from a subclass' constructor
|
||||
) -> None:
|
||||
self._typecheck_input(mobject)
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ from ..constants import (
|
|||
RendererType,
|
||||
)
|
||||
from ..mobject.mobject import Group, Mobject
|
||||
from ..mobject.types.vectorized_mobject import VGroup
|
||||
from ..utils.paths import path_along_arc, path_along_circles
|
||||
from ..utils.rate_functions import smooth, squish_rate_func
|
||||
|
||||
|
|
@ -735,10 +736,13 @@ class CyclicReplace(Transform):
|
|||
def __init__(
|
||||
self, *mobjects: Mobject, path_arc: float = 90 * DEGREES, **kwargs
|
||||
) -> None:
|
||||
self.group = Group(*mobjects)
|
||||
if len(mobjects) == 1 and isinstance(mobjects[0], (Group, VGroup)):
|
||||
self.group = mobjects[0]
|
||||
else:
|
||||
self.group = Group(*mobjects)
|
||||
super().__init__(self.group, path_arc=path_arc, **kwargs)
|
||||
|
||||
def create_target(self) -> Group:
|
||||
def create_target(self) -> Group | VGroup:
|
||||
target = self.group.copy()
|
||||
cycled_targets = [target[-1], *target[:-1]]
|
||||
for m1, m2 in zip(cycled_targets, self.group, strict=True):
|
||||
|
|
@ -747,7 +751,21 @@ class CyclicReplace(Transform):
|
|||
|
||||
|
||||
class Swap(CyclicReplace):
|
||||
pass # Renaming, more understandable for two entries
|
||||
"""Another name for :class:`~.CyclicReplace`, which is more understandable for two entries.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. manim :: SwapExample
|
||||
|
||||
class SwapExample(Scene):
|
||||
def construct(self):
|
||||
text_a = Text("A").move_to(LEFT)
|
||||
text_b = Text("B").move_to(RIGHT)
|
||||
text_group = Group(text_a, text_b)
|
||||
self.play(FadeIn(text_group))
|
||||
self.play(Swap(text_group))
|
||||
self.wait()
|
||||
"""
|
||||
|
||||
|
||||
# TODO, this may be deprecated...worth reimplementing?
|
||||
|
|
@ -835,7 +853,14 @@ class FadeTransform(Transform):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, mobject, target_mobject, stretch=True, dim_to_match=1, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
target_mobject: Mobject,
|
||||
stretch: bool = True,
|
||||
dim_to_match: int = 1,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.to_add_on_completion = target_mobject
|
||||
self.stretch = stretch
|
||||
self.dim_to_match = dim_to_match
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@ from __future__ import annotations
|
|||
|
||||
__all__ = ["TransformMatchingShapes", "TransformMatchingTex"]
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLGroup, OpenGLMobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup, OpenGLVMobject
|
||||
from manim.mobject.text.tex_mobject import MathTexPart
|
||||
|
||||
from .._config import config
|
||||
from ..constants import RendererType
|
||||
|
|
@ -74,10 +75,10 @@ class TransformMatchingAbstractBase(AnimationGroup):
|
|||
transform_mismatches: bool = False,
|
||||
fade_transform_mismatches: bool = False,
|
||||
key_map: dict | None = None,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
if isinstance(mobject, OpenGLVMobject):
|
||||
group_type = OpenGLVGroup
|
||||
group_type: type[OpenGLVGroup | OpenGLGroup | VGroup | Group] = OpenGLVGroup
|
||||
elif isinstance(mobject, OpenGLMobject):
|
||||
group_type = OpenGLGroup
|
||||
elif isinstance(mobject, VMobject):
|
||||
|
|
@ -141,7 +142,7 @@ class TransformMatchingAbstractBase(AnimationGroup):
|
|||
self.to_add = target_mobject
|
||||
|
||||
def get_shape_map(self, mobject: Mobject) -> dict:
|
||||
shape_map = {}
|
||||
shape_map: dict[int | str, VGroup | OpenGLVGroup] = {}
|
||||
for sm in self.get_mobject_parts(mobject):
|
||||
key = self.get_mobject_key(sm)
|
||||
if key not in shape_map:
|
||||
|
|
@ -149,23 +150,25 @@ class TransformMatchingAbstractBase(AnimationGroup):
|
|||
shape_map[key] = OpenGLVGroup()
|
||||
else:
|
||||
shape_map[key] = VGroup()
|
||||
shape_map[key].add(sm)
|
||||
# error: Argument 1 to "add" of "OpenGLVGroup" has incompatible type "Mobject"; expected "OpenGLVMobject" [arg-type]
|
||||
shape_map[key].add(sm) # type: ignore[arg-type]
|
||||
return shape_map
|
||||
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
# Interpolate all animations back to 0 to ensure source mobjects remain unchanged.
|
||||
for anim in self.animations:
|
||||
anim.interpolate(0)
|
||||
scene.remove(self.mobject)
|
||||
# error: Argument 1 to "remove" of "Scene" has incompatible type "OpenGLMobject"; expected "Mobject" [arg-type]
|
||||
scene.remove(self.mobject) # type: ignore[arg-type]
|
||||
scene.remove(*self.to_remove)
|
||||
scene.add(self.to_add)
|
||||
|
||||
@staticmethod
|
||||
def get_mobject_parts(mobject: Mobject):
|
||||
def get_mobject_parts(mobject: Mobject) -> list[Mobject]:
|
||||
raise NotImplementedError("To be implemented in subclass.")
|
||||
|
||||
@staticmethod
|
||||
def get_mobject_key(mobject: Mobject):
|
||||
def get_mobject_key(mobject: Mobject) -> int | str:
|
||||
raise NotImplementedError("To be implemented in subclass.")
|
||||
|
||||
|
||||
|
|
@ -205,7 +208,7 @@ class TransformMatchingShapes(TransformMatchingAbstractBase):
|
|||
transform_mismatches: bool = False,
|
||||
fade_transform_mismatches: bool = False,
|
||||
key_map: dict | None = None,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(
|
||||
mobject,
|
||||
|
|
@ -269,7 +272,7 @@ class TransformMatchingTex(TransformMatchingAbstractBase):
|
|||
transform_mismatches: bool = False,
|
||||
fade_transform_mismatches: bool = False,
|
||||
key_map: dict | None = None,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(
|
||||
mobject,
|
||||
|
|
@ -294,4 +297,5 @@ class TransformMatchingTex(TransformMatchingAbstractBase):
|
|||
|
||||
@staticmethod
|
||||
def get_mobject_key(mobject: Mobject) -> str:
|
||||
assert isinstance(mobject, MathTexPart)
|
||||
return mobject.tex_string
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ __all__ = [
|
|||
|
||||
import inspect
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -29,6 +29,9 @@ if TYPE_CHECKING:
|
|||
from manim.animation.animation import Animation
|
||||
|
||||
|
||||
M = TypeVar("M", bound=Mobject)
|
||||
|
||||
|
||||
def assert_is_mobject_method(method: Callable) -> None:
|
||||
assert inspect.ismethod(method)
|
||||
mobject = method.__self__
|
||||
|
|
@ -43,7 +46,7 @@ def always(method: Callable, *args, **kwargs) -> Mobject:
|
|||
return mobject
|
||||
|
||||
|
||||
def f_always(method: Callable[[Mobject], None], *arg_generators, **kwargs) -> Mobject:
|
||||
def f_always(method: Callable[[M], None], *arg_generators, **kwargs) -> M:
|
||||
"""
|
||||
More functional version of always, where instead
|
||||
of taking in args, it takes in functions which output
|
||||
|
|
@ -61,7 +64,7 @@ def f_always(method: Callable[[Mobject], None], *arg_generators, **kwargs) -> Mo
|
|||
return mobject
|
||||
|
||||
|
||||
def always_redraw(func: Callable[[], Mobject]) -> Mobject:
|
||||
def always_redraw(func: Callable[[], M]) -> M:
|
||||
"""Redraw the mobject constructed by a function every frame.
|
||||
|
||||
This function returns a mobject with an attached updater that
|
||||
|
|
@ -107,8 +110,8 @@ def always_redraw(func: Callable[[], Mobject]) -> Mobject:
|
|||
|
||||
|
||||
def always_shift(
|
||||
mobject: Mobject, direction: np.ndarray[np.float64] = RIGHT, rate: float = 0.1
|
||||
) -> Mobject:
|
||||
mobject: M, direction: np.ndarray[np.float64] = RIGHT, rate: float = 0.1
|
||||
) -> M:
|
||||
"""A mobject which is continuously shifted along some direction
|
||||
at a certain rate.
|
||||
|
||||
|
|
@ -145,7 +148,7 @@ def always_shift(
|
|||
return mobject
|
||||
|
||||
|
||||
def always_rotate(mobject: Mobject, rate: float = 20 * DEGREES, **kwargs) -> Mobject:
|
||||
def always_rotate(mobject: M, rate: float = 20 * DEGREES, **kwargs) -> M:
|
||||
"""A mobject which is continuously rotated at a certain rate.
|
||||
|
||||
Parameters
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from __future__ import annotations
|
|||
__all__ = ["MovingCamera"]
|
||||
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
from typing import Any, Literal, overload
|
||||
|
||||
from cairo import Context
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ from .. import config
|
|||
from ..camera.camera import Camera
|
||||
from ..constants import DOWN, LEFT, RIGHT, UP
|
||||
from ..mobject.frame import ScreenRectangle
|
||||
from ..mobject.mobject import Mobject
|
||||
from ..mobject.mobject import Mobject, _AnimationBuilder
|
||||
from ..utils.color import WHITE, ManimColor
|
||||
|
||||
|
||||
|
|
@ -166,13 +166,31 @@ class MovingCamera(Camera):
|
|||
"""
|
||||
return [self.frame]
|
||||
|
||||
@overload
|
||||
def auto_zoom(
|
||||
self,
|
||||
mobjects: Iterable[Mobject],
|
||||
margin: float,
|
||||
only_mobjects_in_frame: bool,
|
||||
animate: Literal[False],
|
||||
) -> Mobject: ...
|
||||
|
||||
@overload
|
||||
def auto_zoom(
|
||||
self,
|
||||
mobjects: Iterable[Mobject],
|
||||
margin: float,
|
||||
only_mobjects_in_frame: bool,
|
||||
animate: Literal[True],
|
||||
) -> _AnimationBuilder: ...
|
||||
|
||||
def auto_zoom(
|
||||
self,
|
||||
mobjects: Iterable[Mobject],
|
||||
margin: float = 0,
|
||||
only_mobjects_in_frame: bool = False,
|
||||
animate: bool = True,
|
||||
) -> Mobject:
|
||||
) -> _AnimationBuilder | Mobject:
|
||||
"""Zooms on to a given array of mobjects (or a singular mobject)
|
||||
and automatically resizes to frame all the mobjects.
|
||||
|
||||
|
|
|
|||
|
|
@ -15,14 +15,15 @@ from manim.mobject.geometry.shape_matchers import (
|
|||
BackgroundRectangle,
|
||||
SurroundingRectangle,
|
||||
)
|
||||
from manim.mobject.text.tex_mobject import MathTex, Tex
|
||||
from manim.mobject.text.tex_mobject import MathTex
|
||||
from manim.mobject.text.text_mobject import Text
|
||||
from manim.mobject.text.typst_mobject import Typst
|
||||
from manim.mobject.types.vectorized_mobject import VGroup
|
||||
from manim.utils.color import WHITE
|
||||
from manim.utils.polylabel import polylabel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.typing import Point3DLike_Array
|
||||
from manim.typing import ManimTextLabel, Point3DLike_Array
|
||||
|
||||
|
||||
class Label(VGroup):
|
||||
|
|
@ -61,7 +62,7 @@ class Label(VGroup):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
label: str | Tex | MathTex | Text,
|
||||
label: str | ManimTextLabel,
|
||||
label_config: dict[str, Any] | None = None,
|
||||
box_config: dict[str, Any] | None = None,
|
||||
frame_config: dict[str, Any] | None = None,
|
||||
|
|
@ -94,13 +95,15 @@ class Label(VGroup):
|
|||
frame_config = default_frame_config | (frame_config or {})
|
||||
|
||||
# Determine the type of label and instantiate the appropriate object
|
||||
self.rendered_label: MathTex | Tex | Text
|
||||
self.rendered_label: ManimTextLabel
|
||||
if isinstance(label, str):
|
||||
self.rendered_label = MathTex(label, **label_config)
|
||||
elif isinstance(label, (MathTex, Tex, Text)):
|
||||
elif isinstance(label, (MathTex, Text, Typst)):
|
||||
self.rendered_label = label
|
||||
else:
|
||||
raise TypeError("Unsupported label type. Must be MathTex, Tex, or Text.")
|
||||
raise TypeError(
|
||||
"Unsupported label type. Must be MathTex, Tex, Text, Typst, or TypstMath."
|
||||
)
|
||||
|
||||
# Add a background box
|
||||
self.background_rect = BackgroundRectangle(self.rendered_label, **box_config)
|
||||
|
|
@ -155,7 +158,7 @@ class LabeledLine(Line):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
label: str | Tex | MathTex | Text,
|
||||
label: str | ManimTextLabel,
|
||||
label_position: float = 0.5,
|
||||
label_config: dict[str, Any] | None = None,
|
||||
box_config: dict[str, Any] | None = None,
|
||||
|
|
@ -343,7 +346,7 @@ class LabeledPolygram(Polygram):
|
|||
def __init__(
|
||||
self,
|
||||
*vertex_groups: Point3DLike_Array,
|
||||
label: str | Tex | MathTex | Text,
|
||||
label: str | ManimTextLabel,
|
||||
precision: float = 0.01,
|
||||
label_config: dict[str, Any] | None = None,
|
||||
box_config: dict[str, Any] | None = None,
|
||||
|
|
|
|||
|
|
@ -14,14 +14,14 @@ __all__ = [
|
|||
"RightAngle",
|
||||
]
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim import config
|
||||
from manim.constants import *
|
||||
from manim.mobject.geometry.arc import Arc, ArcBetweenPoints, Dot, TipableVMobject
|
||||
from manim.mobject.geometry.tips import ArrowTriangleFilledTip
|
||||
from manim.mobject.geometry.tips import ArrowTip, ArrowTriangleFilledTip
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
|
|
@ -648,9 +648,11 @@ class Arrow(Line):
|
|||
self._set_stroke_width_from_length()
|
||||
|
||||
if has_tip:
|
||||
self.add_tip(tip=old_tips[0])
|
||||
# error: Argument "tip" to "add_tip" of "TipableVMobject" has incompatible type "VMobject"; expected "ArrowTip | None" [arg-type]
|
||||
self.add_tip(tip=cast(ArrowTip, old_tips[0]))
|
||||
if has_start_tip:
|
||||
self.add_tip(tip=old_tips[1], at_start=True)
|
||||
# error: Argument "tip" to "add_tip" of "TipableVMobject" has incompatible type "VMobject"; expected "ArrowTip | None" [arg-type]
|
||||
self.add_tip(tip=cast(ArrowTip, old_tips[1]), at_start=True)
|
||||
return self
|
||||
|
||||
def get_normal_vector(self) -> Vector3D:
|
||||
|
|
|
|||
|
|
@ -669,8 +669,30 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
"""Helper method for populating the edges of the graph."""
|
||||
raise NotImplementedError("To be implemented in concrete subclasses")
|
||||
|
||||
def __getitem__(self: Graph, v: Hashable) -> Mobject:
|
||||
return self.vertices[v]
|
||||
def __getitem__(self: Graph, k: Hashable | tuple[Hashable, Hashable]) -> Mobject:
|
||||
"""Get a vertex or edge by its name/identifier.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
k
|
||||
A vertex name (hashable) or an edge tuple ``(u, v)``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Mobject
|
||||
The :class:`~.Mobject` corresponding to the given vertex or edge.
|
||||
|
||||
Raises
|
||||
------
|
||||
KeyError
|
||||
If ``k`` is not a valid vertex or edge.
|
||||
"""
|
||||
if k in self.vertices:
|
||||
return self.vertices[k]
|
||||
elif k in self.edges:
|
||||
return self.edges[k]
|
||||
else:
|
||||
raise ValueError(f"Could not find {k} in vertices or edges")
|
||||
|
||||
def _create_vertex(
|
||||
self,
|
||||
|
|
@ -1342,6 +1364,11 @@ class Graph(GenericGraph):
|
|||
g[2].animate.move_to([-1, 1, 0]),
|
||||
g[3].animate.move_to([1, -1, 0]),
|
||||
g[4].animate.move_to([-1, -1, 0]))
|
||||
self.play(LaggedStart(Wiggle(g[(1, 2)]),
|
||||
Wiggle(g[(2, 3)]),
|
||||
Wiggle(g[(3, 4)]),
|
||||
Wiggle(g[(1, 3)]),
|
||||
Wiggle(g[(1, 4)])))
|
||||
self.wait()
|
||||
|
||||
There are several automatic positioning algorithms to choose from:
|
||||
|
|
|
|||
|
|
@ -8,14 +8,14 @@ from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
|
|||
__all__ = ["NumberLine", "UnitInterval"]
|
||||
|
||||
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from typing import TYPE_CHECKING
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Self
|
||||
from typing import Self
|
||||
|
||||
from manim.mobject.geometry.tips import ArrowTip
|
||||
from manim.typing import Point3D, Point3DLike, Vector3D
|
||||
from manim.typing import ManimTextLabel, Point3D, Point3DLike, Vector3D
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -23,9 +23,9 @@ from manim import config
|
|||
from manim.constants import *
|
||||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.graphing.scale import LinearBase, _ScaleBase
|
||||
from manim.mobject.text.numbers import DecimalNumber, Integer
|
||||
from manim.mobject.text.tex_mobject import MathTex, Tex
|
||||
from manim.mobject.text.text_mobject import Text
|
||||
from manim.mobject.text.numbers import DecimalNumber
|
||||
from manim.mobject.text.tex_mobject import MathTex, SingleStringMathTex, Tex
|
||||
from manim.mobject.text.typst_mobject import Typst, TypstMath
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.utils.bezier import interpolate
|
||||
from manim.utils.config_ops import merge_dicts_recursively
|
||||
|
|
@ -161,7 +161,7 @@ class NumberLine(Line):
|
|||
include_numbers: bool = False,
|
||||
font_size: float = 36,
|
||||
label_direction: Point3DLike = DOWN,
|
||||
label_constructor: type[MathTex] = MathTex,
|
||||
label_constructor: type[ManimTextLabel] = MathTex,
|
||||
scaling: _ScaleBase = LinearBase(),
|
||||
line_to_number_buff: float = MED_SMALL_BUFF,
|
||||
decimal_number_config: dict | None = None,
|
||||
|
|
@ -450,7 +450,7 @@ class NumberLine(Line):
|
|||
direction: Vector3D | None = None,
|
||||
buff: float | None = None,
|
||||
font_size: float | None = None,
|
||||
label_constructor: type[MathTex] | None = None,
|
||||
label_constructor: type[SingleStringMathTex] | None = None,
|
||||
**number_config: dict[str, Any],
|
||||
) -> VMobject:
|
||||
"""Generates a positioned :class:`~.DecimalNumber` mobject
|
||||
|
|
@ -487,7 +487,7 @@ class NumberLine(Line):
|
|||
if font_size is None:
|
||||
font_size = self.font_size
|
||||
if label_constructor is None:
|
||||
label_constructor = self.label_constructor
|
||||
label_constructor = cast(type[SingleStringMathTex], self.label_constructor)
|
||||
|
||||
num_mob = DecimalNumber(
|
||||
x,
|
||||
|
|
@ -515,7 +515,7 @@ class NumberLine(Line):
|
|||
x_values: Iterable[float] | None = None,
|
||||
excluding: Iterable[float] | None = None,
|
||||
font_size: float | None = None,
|
||||
label_constructor: type[MathTex] | None = None,
|
||||
label_constructor: type[SingleStringMathTex] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Self:
|
||||
"""Adds :class:`~.DecimalNumber` mobjects representing their position
|
||||
|
|
@ -547,7 +547,7 @@ class NumberLine(Line):
|
|||
font_size = self.font_size
|
||||
|
||||
if label_constructor is None:
|
||||
label_constructor = self.label_constructor
|
||||
label_constructor = cast(type[SingleStringMathTex], self.label_constructor)
|
||||
|
||||
numbers = VGroup()
|
||||
for x in x_values:
|
||||
|
|
@ -571,7 +571,7 @@ class NumberLine(Line):
|
|||
direction: Point3DLike | None = None,
|
||||
buff: float | None = None,
|
||||
font_size: float | None = None,
|
||||
label_constructor: type[MathTex] | None = None,
|
||||
label_constructor: type[ManimTextLabel] | None = None,
|
||||
) -> Self:
|
||||
"""Adds specifically positioned labels to the :class:`~.NumberLine` using a ``dict``.
|
||||
The labels can be accessed after creation via ``self.labels``.
|
||||
|
|
@ -609,14 +609,18 @@ class NumberLine(Line):
|
|||
# TODO: remove this check and ability to call
|
||||
# this method via CoordinateSystem.add_coordinates()
|
||||
# must be explicitly called
|
||||
if isinstance(label, str) and label_constructor is MathTex:
|
||||
label = Tex(label)
|
||||
if isinstance(label, str):
|
||||
if label_constructor is MathTex:
|
||||
label = Tex(label)
|
||||
elif label_constructor is TypstMath:
|
||||
label = Typst(label)
|
||||
else:
|
||||
label = self._create_label_tex(label, label_constructor)
|
||||
else:
|
||||
label = self._create_label_tex(label, label_constructor)
|
||||
|
||||
if hasattr(label, "font_size"):
|
||||
assert isinstance(label, (MathTex, Tex, Text, Integer)), label
|
||||
label.font_size = font_size
|
||||
cast(Any, label).font_size = font_size
|
||||
else:
|
||||
raise AttributeError(f"{label} is not compatible with add_labels.")
|
||||
label.next_to(self.number_to_point(x), direction=direction, buff=buff)
|
||||
|
|
@ -629,7 +633,7 @@ class NumberLine(Line):
|
|||
def _create_label_tex(
|
||||
self,
|
||||
label_tex: str | float | VMobject,
|
||||
label_constructor: Callable | None = None,
|
||||
label_constructor: type[ManimTextLabel] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> VMobject:
|
||||
"""Checks if the label is a :class:`~.VMobject`, otherwise, creates a
|
||||
|
|
|
|||
|
|
@ -207,13 +207,11 @@ class SampleSpace(Rectangle):
|
|||
if hasattr(parts, subattr):
|
||||
self.add(getattr(parts, subattr))
|
||||
|
||||
def __getitem__(self, index: int) -> SampleSpace:
|
||||
def __getitem__(self, index: int) -> VMobject:
|
||||
if hasattr(self, "horizontal_parts"):
|
||||
val: SampleSpace = self.horizontal_parts[index]
|
||||
return val
|
||||
return self.horizontal_parts[index]
|
||||
elif hasattr(self, "vertical_parts"):
|
||||
val = self.vertical_parts[index]
|
||||
return val
|
||||
return self.vertical_parts[index]
|
||||
return self.split()[index]
|
||||
|
||||
|
||||
|
|
@ -373,7 +371,7 @@ class BarChart(Axes):
|
|||
# to accommodate negative bars, the label may need to be
|
||||
# below or above the x_axis depending on the value of the bar
|
||||
direction = UP if self.values[i] < 0 else DOWN
|
||||
bar_name_label: MathTex = self.x_axis.label_constructor(bar_name)
|
||||
bar_name_label = self.x_axis.label_constructor(bar_name)
|
||||
|
||||
bar_name_label.font_size = self.x_axis.font_size
|
||||
bar_name_label.next_to(
|
||||
|
|
|
|||
|
|
@ -40,15 +40,15 @@ __all__ = [
|
|||
|
||||
|
||||
import itertools as it
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import Any, Self
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.text.numbers import DecimalNumber, Integer
|
||||
from manim.mobject.text.tex_mobject import MathTex, Tex
|
||||
from manim.typing import Vector2DLike, Vector3DLike
|
||||
|
||||
from ..constants import *
|
||||
from ..mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
|
|
@ -164,16 +164,16 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
matrix: Iterable,
|
||||
matrix: Iterable[Iterable[Any] | Vector2DLike],
|
||||
v_buff: float = 0.8,
|
||||
h_buff: float = 1.3,
|
||||
bracket_h_buff: float = MED_SMALL_BUFF,
|
||||
bracket_v_buff: float = MED_SMALL_BUFF,
|
||||
add_background_rectangles_to_entries: bool = False,
|
||||
include_background_rectangle: bool = False,
|
||||
element_to_mobject: type[Mobject] | Callable[..., Mobject] = MathTex,
|
||||
element_to_mobject_config: dict = {},
|
||||
element_alignment_corner: Sequence[float] = DR,
|
||||
element_to_mobject: type[VMobject] | Callable[..., VMobject] = MathTex,
|
||||
element_to_mobject_config: dict[str, Any] = {},
|
||||
element_alignment_corner: Vector3DLike = DR,
|
||||
left_bracket: str = "[",
|
||||
right_bracket: str = "]",
|
||||
stretch_brackets: bool = True,
|
||||
|
|
@ -206,7 +206,9 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
|
|||
if self.include_background_rectangle:
|
||||
self.add_background_rectangle()
|
||||
|
||||
def _matrix_to_mob_matrix(self, matrix: np.ndarray) -> list[list[Mobject]]:
|
||||
def _matrix_to_mob_matrix(
|
||||
self, matrix: Iterable[Iterable[Any]]
|
||||
) -> list[list[VMobject]]:
|
||||
return [
|
||||
[
|
||||
self.element_to_mobject(item, **self.element_to_mobject_config)
|
||||
|
|
@ -215,7 +217,7 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
|
|||
for row in matrix
|
||||
]
|
||||
|
||||
def _organize_mob_matrix(self, matrix: list[list[Mobject]]) -> Self:
|
||||
def _organize_mob_matrix(self, matrix: list[list[VMobject]]) -> Self:
|
||||
for i, row in enumerate(matrix):
|
||||
for j, _ in enumerate(row):
|
||||
mob = matrix[i][j]
|
||||
|
|
@ -401,7 +403,7 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
|
|||
mob.add_background_rectangle()
|
||||
return self
|
||||
|
||||
def get_mob_matrix(self) -> list[list[Mobject]]:
|
||||
def get_mob_matrix(self) -> list[list[VMobject]]:
|
||||
"""Return the underlying mob matrix mobjects.
|
||||
|
||||
Returns
|
||||
|
|
@ -483,8 +485,8 @@ class DecimalMatrix(Matrix):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
matrix: Iterable,
|
||||
element_to_mobject: type[Mobject] = DecimalNumber,
|
||||
matrix: Iterable[Iterable[Any]],
|
||||
element_to_mobject: type[VMobject] | Callable[..., VMobject] = DecimalNumber,
|
||||
element_to_mobject_config: dict[str, Any] = {"num_decimal_places": 1},
|
||||
**kwargs: Any,
|
||||
):
|
||||
|
|
@ -528,8 +530,8 @@ class IntegerMatrix(Matrix):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
matrix: Iterable,
|
||||
element_to_mobject: type[Mobject] = Integer,
|
||||
matrix: Iterable[Iterable[Any]],
|
||||
element_to_mobject: type[VMobject] | Callable[..., VMobject] = Integer,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""
|
||||
|
|
@ -566,8 +568,8 @@ class MobjectMatrix(Matrix):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
matrix: Iterable,
|
||||
element_to_mobject: type[Mobject] | Callable[..., Mobject] = lambda m: m,
|
||||
matrix: Iterable[Iterable[Any]],
|
||||
element_to_mobject: type[VMobject] | Callable[..., VMobject] = lambda m: m,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(matrix, element_to_mobject=element_to_mobject, **kwargs)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -6,6 +6,7 @@ import itertools as it
|
|||
import random
|
||||
import sys
|
||||
import types
|
||||
import warnings
|
||||
from collections.abc import Callable, Iterable, Iterator, Sequence
|
||||
from functools import partialmethod, wraps
|
||||
from math import ceil
|
||||
|
|
@ -1222,7 +1223,7 @@ class OpenGLMobject:
|
|||
) -> Sequence[Vector3D]:
|
||||
if str_alignments is None:
|
||||
# Use cell_alignment as fallback
|
||||
return [cast(Vector3D, cell_alignment * direction)] * num
|
||||
return [cast("Vector3D", cell_alignment * direction)] * num
|
||||
if len(str_alignments) != num:
|
||||
raise ValueError(f"{name}_alignments has a mismatching size.")
|
||||
return [mapping[letter] for letter in str_alignments]
|
||||
|
|
@ -2134,26 +2135,33 @@ class OpenGLMobject:
|
|||
return self
|
||||
|
||||
def put_start_and_end_on(self, start: Point3DLike, end: Point3DLike) -> Self:
|
||||
curr_start, curr_end = self.get_start_and_end()
|
||||
curr_vect = curr_end - curr_start
|
||||
if np.all(curr_vect == 0):
|
||||
raise Exception("Cannot position endpoints of closed loop")
|
||||
target_vect = np.array(end) - np.array(start)
|
||||
current_start, current_end = self.get_start_and_end()
|
||||
current_vector = current_end - current_start
|
||||
if np.all(current_vector == 0):
|
||||
warnings.warn(
|
||||
"put_start_and_end_on has been called on a closed loop or zero-length mobject. "
|
||||
f"{type(self).__name__} will be shifted to start point instead.",
|
||||
stacklevel=2,
|
||||
)
|
||||
self.shift(np.asarray(start) - current_start)
|
||||
return self
|
||||
|
||||
target_vector = np.asarray(end) - np.asarray(start)
|
||||
axis = (
|
||||
normalize(np.cross(curr_vect, target_vect))
|
||||
if np.linalg.norm(np.cross(curr_vect, target_vect)) != 0
|
||||
normalize(np.cross(current_vector, target_vector))
|
||||
if np.linalg.norm(np.cross(current_vector, target_vector)) != 0
|
||||
else OUT
|
||||
)
|
||||
self.scale(
|
||||
float(np.linalg.norm(target_vect) / np.linalg.norm(curr_vect)),
|
||||
about_point=curr_start,
|
||||
np.linalg.norm(target_vector) / np.linalg.norm(current_vector),
|
||||
about_point=current_start,
|
||||
)
|
||||
self.rotate(
|
||||
angle_between_vectors(curr_vect, target_vect),
|
||||
about_point=curr_start,
|
||||
angle_between_vectors(current_vector, target_vector),
|
||||
about_point=current_start,
|
||||
axis=axis,
|
||||
)
|
||||
self.shift(start - curr_start)
|
||||
self.shift(np.asarray(start) - current_start)
|
||||
return self
|
||||
|
||||
# Color functions
|
||||
|
|
@ -3037,7 +3045,7 @@ class OpenGLPoint(OpenGLMobject):
|
|||
return self.artificial_height
|
||||
|
||||
def get_location(self) -> Point3D:
|
||||
return cast(Point3D, self.points[0]).copy()
|
||||
return cast("Point3D", self.points[0]).copy()
|
||||
|
||||
@override
|
||||
def get_bounding_box_point(self, *args: object, **kwargs: Any) -> Point3D:
|
||||
|
|
|
|||
|
|
@ -1227,7 +1227,9 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
def get_nth_subpath(path_list, n):
|
||||
if n >= len(path_list):
|
||||
# Create a null path at the very end
|
||||
return [path_list[-1][-1]] * nppc
|
||||
if len(path_list) == 0:
|
||||
return np.tile(np.zeros(3), (nppc, 1))
|
||||
return np.tile(path_list[-1][-1], (nppc, 1))
|
||||
path = path_list[n]
|
||||
# Check for useless points at the end of the path and remove them
|
||||
# https://github.com/ManimCommunity/manim/issues/1959
|
||||
|
|
|
|||
|
|
@ -104,6 +104,8 @@ class Table(VGroup):
|
|||
Horizontal buffer passed to :meth:`~.Mobject.arrange_in_grid`, by default 1.3.
|
||||
include_outer_lines
|
||||
``True`` if the table should include outer lines, by default False.
|
||||
include_inner_lines
|
||||
``True`` if the table should include inner lines, by default True.
|
||||
add_background_rectangles_to_entries
|
||||
``True`` if background rectangles should be added to entries, by default ``False``.
|
||||
entries_background_color
|
||||
|
|
@ -193,6 +195,7 @@ class Table(VGroup):
|
|||
v_buff: float = 0.8,
|
||||
h_buff: float = 1.3,
|
||||
include_outer_lines: bool = False,
|
||||
include_inner_lines: bool = True,
|
||||
add_background_rectangles_to_entries: bool = False,
|
||||
entries_background_color: ParsableManimColor = BLACK,
|
||||
include_background_rectangle: bool = False,
|
||||
|
|
@ -214,6 +217,7 @@ class Table(VGroup):
|
|||
self.v_buff = v_buff
|
||||
self.h_buff = h_buff
|
||||
self.include_outer_lines = include_outer_lines
|
||||
self.include_inner_lines = include_inner_lines
|
||||
self.add_background_rectangles_to_entries = add_background_rectangles_to_entries
|
||||
self.entries_background_color = ManimColor(entries_background_color)
|
||||
self.include_background_rectangle = include_background_rectangle
|
||||
|
|
@ -349,15 +353,19 @@ class Table(VGroup):
|
|||
)
|
||||
line_group.add(line)
|
||||
self.add(line)
|
||||
for k in range(len(self.mob_table) - 1):
|
||||
anchor = self.get_rows()[k + 1].get_top()[1] + 0.5 * (
|
||||
self.get_rows()[k].get_bottom()[1] - self.get_rows()[k + 1].get_top()[1]
|
||||
)
|
||||
line = Line(
|
||||
[anchor_left, anchor, 0], [anchor_right, anchor, 0], **self.line_config
|
||||
)
|
||||
line_group.add(line)
|
||||
self.add(line)
|
||||
if self.include_inner_lines:
|
||||
for k in range(len(self.mob_table) - 1):
|
||||
anchor = self.get_rows()[k + 1].get_top()[1] + 0.5 * (
|
||||
self.get_rows()[k].get_bottom()[1]
|
||||
- self.get_rows()[k + 1].get_top()[1]
|
||||
)
|
||||
line = Line(
|
||||
[anchor_left, anchor, 0],
|
||||
[anchor_right, anchor, 0],
|
||||
**self.line_config,
|
||||
)
|
||||
line_group.add(line)
|
||||
self.add(line)
|
||||
self.horizontal_lines = line_group
|
||||
return self
|
||||
|
||||
|
|
@ -379,16 +387,19 @@ class Table(VGroup):
|
|||
)
|
||||
line_group.add(line)
|
||||
self.add(line)
|
||||
for k in range(len(self.mob_table[0]) - 1):
|
||||
anchor = self.get_columns()[k + 1].get_left()[0] + 0.5 * (
|
||||
self.get_columns()[k].get_right()[0]
|
||||
- self.get_columns()[k + 1].get_left()[0]
|
||||
)
|
||||
line = Line(
|
||||
[anchor, anchor_bottom, 0], [anchor, anchor_top, 0], **self.line_config
|
||||
)
|
||||
line_group.add(line)
|
||||
self.add(line)
|
||||
if self.include_inner_lines:
|
||||
for k in range(len(self.mob_table[0]) - 1):
|
||||
anchor = self.get_columns()[k + 1].get_left()[0] + 0.5 * (
|
||||
self.get_columns()[k].get_right()[0]
|
||||
- self.get_columns()[k + 1].get_left()[0]
|
||||
)
|
||||
line = Line(
|
||||
[anchor, anchor_bottom, 0],
|
||||
[anchor, anchor_top, 0],
|
||||
**self.line_config,
|
||||
)
|
||||
line_group.add(line)
|
||||
self.add(line)
|
||||
self.vertical_lines = line_group
|
||||
return self
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""Mobjects used to display Text using Pango or LaTeX.
|
||||
"""Mobjects used to display Text using Pango, LaTeX, or Typst.
|
||||
|
||||
Modules
|
||||
=======
|
||||
|
|
@ -10,4 +10,5 @@ Modules
|
|||
~numbers
|
||||
~tex_mobject
|
||||
~text_mobject
|
||||
~typst_mobject
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -139,7 +139,10 @@ class Code(VMobject, metaclass=ConvertToOpenGL):
|
|||
if code_file is not None:
|
||||
code_file = Path(code_file)
|
||||
code_string = code_file.read_text(encoding="utf-8")
|
||||
lexer = guess_lexer_for_filename(code_file.name, code_string)
|
||||
if language is not None:
|
||||
lexer = get_lexer_by_name(language)
|
||||
else:
|
||||
lexer = guess_lexer_for_filename(code_file.name, code_string)
|
||||
elif code_string is not None:
|
||||
if language is not None:
|
||||
lexer = get_lexer_by_name(language)
|
||||
|
|
|
|||
|
|
@ -587,7 +587,7 @@ class MathTex(SingleStringMathTex):
|
|||
self.id_to_vgroup_dict[match[1]].set_color(color)
|
||||
return self
|
||||
|
||||
def index_of_part(self, part: MathTex) -> int:
|
||||
def index_of_part(self, part: VMobject) -> int:
|
||||
split_self = self.split()
|
||||
if part not in split_self:
|
||||
raise ValueError("Trying to get index of part not in MathTex")
|
||||
|
|
|
|||
|
|
@ -166,9 +166,12 @@ class Paragraph(VGroup):
|
|||
lines_str_list = lines_str.split("\n")
|
||||
self.chars = self._gen_chars(lines_str_list)
|
||||
|
||||
self.lines = [list(self.chars), [self.alignment] * len(self.chars)]
|
||||
self.lines_initial_positions = [line.get_center() for line in self.lines[0]]
|
||||
self.add(*self.lines[0])
|
||||
# TODO: If possible get rid of self.lines_chars, as it seems to be a
|
||||
# listified duplicate of self.chars.
|
||||
self.lines_chars = list(self.chars)
|
||||
self.lines_alignments = [self.alignment] * len(self.chars)
|
||||
self.lines_initial_positions = [line.get_center() for line in self.lines_chars]
|
||||
self.add(*self.lines_chars)
|
||||
self.move_to(np.array([0, 0, 0]))
|
||||
if self.alignment:
|
||||
self._set_all_lines_alignments(self.alignment)
|
||||
|
|
@ -221,7 +224,7 @@ class Paragraph(VGroup):
|
|||
alignment
|
||||
Defines the alignment of paragraph. Possible values are "left", "right", "center".
|
||||
"""
|
||||
for line_no in range(len(self.lines[0])):
|
||||
for line_no in range(len(self.lines_chars)):
|
||||
self._change_alignment_for_a_line(alignment, line_no)
|
||||
return self
|
||||
|
||||
|
|
@ -240,8 +243,8 @@ class Paragraph(VGroup):
|
|||
|
||||
def _set_all_lines_to_initial_positions(self) -> Paragraph:
|
||||
"""Set all lines to their initial positions."""
|
||||
self.lines[1] = [None] * len(self.lines[0])
|
||||
for line_no in range(len(self.lines[0])):
|
||||
self.lines_alignments = [None] * len(self.lines_chars)
|
||||
for line_no in range(len(self.lines_chars)):
|
||||
self[line_no].move_to(
|
||||
self.get_center() + self.lines_initial_positions[line_no],
|
||||
)
|
||||
|
|
@ -255,7 +258,7 @@ class Paragraph(VGroup):
|
|||
line_no
|
||||
Defines the line number for which we want to set given alignment.
|
||||
"""
|
||||
self.lines[1][line_no] = None
|
||||
self.lines_alignments[line_no] = None
|
||||
self[line_no].move_to(self.get_center() + self.lines_initial_positions[line_no])
|
||||
return self
|
||||
|
||||
|
|
@ -269,12 +272,12 @@ class Paragraph(VGroup):
|
|||
line_no
|
||||
Defines the line number for which we want to set given alignment.
|
||||
"""
|
||||
self.lines[1][line_no] = alignment
|
||||
if self.lines[1][line_no] == "center":
|
||||
self.lines_alignments[line_no] = alignment
|
||||
if self.lines_alignments[line_no] == "center":
|
||||
self[line_no].move_to(
|
||||
np.array([self.get_center()[0], self[line_no].get_center()[1], 0]),
|
||||
)
|
||||
elif self.lines[1][line_no] == "right":
|
||||
elif self.lines_alignments[line_no] == "right":
|
||||
self[line_no].move_to(
|
||||
np.array(
|
||||
[
|
||||
|
|
@ -284,7 +287,7 @@ class Paragraph(VGroup):
|
|||
],
|
||||
),
|
||||
)
|
||||
elif self.lines[1][line_no] == "left":
|
||||
elif self.lines_alignments[line_no] == "left":
|
||||
self[line_no].move_to(
|
||||
np.array(
|
||||
[
|
||||
|
|
|
|||
818
manim/mobject/text/typst_mobject.py
Normal file
818
manim/mobject/text/typst_mobject.py
Normal file
|
|
@ -0,0 +1,818 @@
|
|||
"""Mobjects representing text rendered using Typst.
|
||||
|
||||
.. _typst-mobjects:
|
||||
|
||||
.. important::
|
||||
|
||||
The ``typst`` Python package must be installed to use these classes.
|
||||
Install it via ``pip install typst>=0.14`` or add the ``typst`` optional
|
||||
dependency group (``pip install manim[typst]``).
|
||||
|
||||
Typst mobjects compile Typst markup directly to SVG using the ``typst``
|
||||
Python package and then import the result through :class:`~.SVGMobject`.
|
||||
Use :class:`~.Typst` for general Typst markup and :class:`~.TypstMath`
|
||||
for display-style math.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Basic text and math
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. manim:: TypstTextReferenceExample
|
||||
:save_last_frame:
|
||||
:ref_classes: Typst
|
||||
|
||||
class TypstTextReferenceExample(Scene):
|
||||
def construct(self):
|
||||
text = Typst(
|
||||
r"*Hello* from _Typst!_",
|
||||
color=YELLOW,
|
||||
font_size=72,
|
||||
)
|
||||
self.add(text)
|
||||
|
||||
.. manim:: TypstMathReferenceExample
|
||||
:save_last_frame:
|
||||
:ref_classes: TypstMath
|
||||
|
||||
class TypstMathReferenceExample(Scene):
|
||||
def construct(self):
|
||||
equation = TypstMath(
|
||||
r"sum_(k=1)^n k = frac(n(n + 1), 2)",
|
||||
font_size=72,
|
||||
)
|
||||
self.add(equation)
|
||||
|
||||
Selecting subexpressions
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Typst mobjects expose label-based selection via :meth:`~.Typst.select`.
|
||||
There are two common ways to create selectable groups:
|
||||
|
||||
- use ordinary Typst labels in :class:`~.Typst`
|
||||
- use Manim's ``{{ ... }}`` shorthand in :class:`~.TypstMath`
|
||||
|
||||
.. note::
|
||||
|
||||
The ``{{ ... }}`` shorthand is currently only supported by
|
||||
:class:`~.TypstMath`. For :class:`~.Typst`, create labels directly in the
|
||||
Typst source, for example with ``#box[body] <label>``.
|
||||
|
||||
.. manim:: TypstLabelSelectionExample
|
||||
:save_last_frame:
|
||||
:ref_classes: Typst
|
||||
:ref_methods: Typst.select
|
||||
|
||||
class TypstLabelSelectionExample(Scene):
|
||||
def construct(self):
|
||||
text = Typst(
|
||||
r'''
|
||||
#box[
|
||||
*Typst* labels also work in regular markup.
|
||||
] <headline>
|
||||
|
||||
#let pick(body) = [#box(body) <picked>]
|
||||
We can highlight #pick[multiple] #pick[fragments] at once.
|
||||
''',
|
||||
font_size=42,
|
||||
)
|
||||
text.select("headline").set_color(BLUE)
|
||||
text.select("picked").set_color(YELLOW)
|
||||
self.add(text)
|
||||
|
||||
.. manim:: TypstMathSelectionExample
|
||||
:save_last_frame:
|
||||
:ref_classes: TypstMath
|
||||
:ref_methods: Typst.select
|
||||
|
||||
class TypstMathSelectionExample(Scene):
|
||||
def construct(self):
|
||||
equation = TypstMath(
|
||||
"{{ a^2 + b^2 : lhs }} = {{ c^2 }}",
|
||||
font_size=72,
|
||||
)
|
||||
equation.select("lhs").set_color(BLUE)
|
||||
equation.select(0).set_color(YELLOW)
|
||||
self.add(equation)
|
||||
|
||||
Inspecting baseline frames
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
For debugging or alignment tasks, Typst mobjects can optionally track a
|
||||
per-element baseline frame. Enable this with ``track_baselines=True`` and
|
||||
query either :attr:`~.Typst.baseline_frames` for all tracked leaf elements or
|
||||
:meth:`~.Typst.get_baseline_frame` for a specific selected submobject.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
text = Typst("Ggf", track_baselines=True)
|
||||
orig, right, up = text.baseline_frames[0]
|
||||
|
||||
eq = TypstMath("{{ a^2 + b^2 : lhs }} = c^2", track_baselines=True)
|
||||
for part in eq.select("lhs"):
|
||||
orig, right, up = eq.get_baseline_frame(part)
|
||||
print(orig, right, up)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = [
|
||||
"Typst",
|
||||
"TypstMath",
|
||||
]
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
import numpy as np
|
||||
import svgelements as se
|
||||
|
||||
from manim import config
|
||||
from manim.constants import DEFAULT_FONT_SIZE, SCALE_FACTOR_PER_FONT_POINT, RendererType
|
||||
from manim.mobject.svg.svg_mobject import SVGMobject
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.utils.color import BLACK, ParsableManimColor
|
||||
from manim.utils.typst_file_writing import typst_to_svg_file
|
||||
|
||||
_MANIMGRP_PREAMBLE = "#let manimgrp(lbl, body) = [#box(body) #label(lbl)]"
|
||||
|
||||
# Pattern for the label part of {{ content : label }}.
|
||||
# The label must be a valid Typst label identifier.
|
||||
_LABEL_RE = re.compile(r"^(.*)\s*:\s*([a-zA-Z_][a-zA-Z0-9_-]*)\s*$", re.DOTALL)
|
||||
_INTERNAL_TYPST_ID_RE = re.compile(r"g[0-9A-Fa-f]+")
|
||||
_DUPLICATE_LABEL_SUFFIX = "__manim_typst_dup_"
|
||||
# Empirical correction so Typst-authored SVG strokes (fraction bars,
|
||||
# underlines, etc.) visually match the weight of TeX-derived geometry more
|
||||
# closely after import into Manim's pixel-based stroke model.
|
||||
_TYPST_SVG_STROKE_WIDTH_SCALE = 0.5
|
||||
|
||||
|
||||
class Typst(SVGMobject):
|
||||
"""A mobject rendered from a Typst markup string.
|
||||
|
||||
The Typst source is compiled to SVG via the ``typst`` Python package
|
||||
(a self-contained Rust binary extension — no system-level install
|
||||
required) and then imported through :class:`~.SVGMobject`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
typst_code
|
||||
Raw Typst markup to be compiled. This string is placed verbatim
|
||||
into the body of a minimal Typst document.
|
||||
font_size
|
||||
Font size in Manim font-size units (default: ``DEFAULT_FONT_SIZE``,
|
||||
i.e. 48). The actual scaling is applied *after* SVG import, matching
|
||||
the approach used by :class:`~.SingleStringMathTex`.
|
||||
typst_preamble
|
||||
Extra Typst code inserted before the body. Useful for ``#import``,
|
||||
``#set``, or ``#show`` rules. Default: ``""``.
|
||||
color
|
||||
The color of the mobject. By default the standard VMobject color
|
||||
(white in dark mode). Overrides the Typst text fill color.
|
||||
stroke_width
|
||||
SVG stroke width override. If ``None`` (default), the stroke widths
|
||||
from Typst's SVG output are preserved.
|
||||
font_paths
|
||||
Optional list of additional font directories passed to the Typst
|
||||
compiler (e.g. for custom fonts not installed system-wide).
|
||||
track_baselines
|
||||
Whether to keep enough per-element reference data to recover the
|
||||
current Typst baseline frame for each imported submobject.
|
||||
When enabled, :attr:`baseline_frames` and
|
||||
:meth:`get_baseline_frame` can be used to retrieve the current
|
||||
``(orig, right, up)`` positions for the imported SVG elements.
|
||||
should_center
|
||||
Whether to center the mobject after import (default ``True``).
|
||||
height
|
||||
Target height of the mobject. If ``None`` (default), the height is
|
||||
determined by ``font_size``.
|
||||
**kwargs
|
||||
Forwarded to :class:`~.SVGMobject`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. manim:: TypstExample
|
||||
:ref_classes: Typst
|
||||
|
||||
class TypstExample(Scene):
|
||||
def construct(self):
|
||||
formula = Typst(r"$ integral_a^b f(x) dif x $")
|
||||
self.play(Write(formula))
|
||||
|
||||
.. manim:: TypstTextExample
|
||||
:save_last_frame:
|
||||
:ref_classes: Typst
|
||||
|
||||
class TypstTextExample(Scene):
|
||||
def construct(self):
|
||||
text = Typst(
|
||||
r"*Hello* from _Typst!_",
|
||||
font_size=72,
|
||||
)
|
||||
self.add(text)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
typst_code: str,
|
||||
*,
|
||||
font_size: float = DEFAULT_FONT_SIZE,
|
||||
typst_preamble: str = "",
|
||||
color: ParsableManimColor | None = None,
|
||||
stroke_width: float | None = None,
|
||||
font_paths: list[str | Path] | None = None,
|
||||
track_baselines: bool = False,
|
||||
should_center: bool = True,
|
||||
height: float | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
if color is None:
|
||||
color = VMobject().color
|
||||
|
||||
self._font_size = font_size
|
||||
self.typst_code = typst_code
|
||||
self.typst_preamble = typst_preamble
|
||||
self.track_baselines = track_baselines
|
||||
self._preserve_svg_stroke_widths = stroke_width is None
|
||||
self._baseline_tracked_submobjects: list[VMobject] = []
|
||||
self._stroke_width_tracked_submobjects: list[VMobject] = []
|
||||
self._label_aliases: dict[str, list[str]] = {}
|
||||
|
||||
file_name = typst_to_svg_file(
|
||||
typst_code,
|
||||
preamble=typst_preamble,
|
||||
font_paths=font_paths,
|
||||
)
|
||||
super().__init__(
|
||||
file_name=file_name,
|
||||
should_center=should_center,
|
||||
stroke_width=stroke_width,
|
||||
height=height,
|
||||
color=color,
|
||||
path_string_config={
|
||||
"should_subdivide_sharp_curves": True,
|
||||
"should_remove_null_curves": True,
|
||||
},
|
||||
**kwargs,
|
||||
)
|
||||
self._rebuild_label_aliases()
|
||||
self._refresh_svg_stroke_widths()
|
||||
self.init_colors()
|
||||
|
||||
# Used for scaling via font_size property (mirrors SingleStringMathTex).
|
||||
self.initial_height = self.height
|
||||
|
||||
if height is None:
|
||||
self.font_size = self._font_size
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{type(self).__name__}({self.typst_code!r})"
|
||||
|
||||
@property
|
||||
def hash_seed(self) -> tuple:
|
||||
"""Include baseline tracking in the SVG cache key."""
|
||||
return (*super().hash_seed, self.track_baselines)
|
||||
|
||||
# -- font_size property (same approach as SingleStringMathTex) -----------
|
||||
|
||||
@property
|
||||
def font_size(self) -> float:
|
||||
"""The font size of the Typst mobject."""
|
||||
return self.height / self.initial_height / SCALE_FACTOR_PER_FONT_POINT
|
||||
|
||||
@font_size.setter
|
||||
def font_size(self, val: float) -> None:
|
||||
if val <= 0:
|
||||
raise ValueError("font_size must be greater than 0.")
|
||||
if self.height > 0:
|
||||
self.scale(val / self.font_size)
|
||||
|
||||
def scale(
|
||||
self,
|
||||
scale_factor: float,
|
||||
scale_stroke: bool = False,
|
||||
*,
|
||||
about_point: np.ndarray | None = None,
|
||||
about_edge: np.ndarray | None = None,
|
||||
) -> Typst:
|
||||
result = super().scale(
|
||||
scale_factor,
|
||||
scale_stroke=scale_stroke,
|
||||
about_point=about_point,
|
||||
about_edge=about_edge,
|
||||
)
|
||||
self._refresh_svg_stroke_widths()
|
||||
return result
|
||||
|
||||
def _refresh_svg_stroke_widths(self) -> None:
|
||||
"""Refresh pixel stroke widths for Typst-authored SVG strokes.
|
||||
|
||||
SVG stroke widths are specified in the SVG's local coordinate system,
|
||||
while Manim stroke widths are pixel-based. For Typst-authored strokes
|
||||
such as fraction bars or underlines, rescale them according to the
|
||||
current geometric scale of the imported element so their visual weight
|
||||
stays proportional to the rest of the expression.
|
||||
"""
|
||||
if not self._preserve_svg_stroke_widths:
|
||||
return
|
||||
|
||||
pixels_per_unit = config.pixel_width / config.frame_width
|
||||
for submobject in self._stroke_width_tracked_submobjects:
|
||||
submobject_any = cast(Any, submobject)
|
||||
reference_size = cast(float, submobject_any._typst_reference_size)
|
||||
source_stroke_width = cast(
|
||||
float,
|
||||
submobject_any._typst_source_stroke_width,
|
||||
)
|
||||
current_size = max(submobject.width, submobject.height)
|
||||
if reference_size <= 0:
|
||||
continue
|
||||
current_stroke_width = source_stroke_width * current_size / reference_size
|
||||
submobject.set_stroke(
|
||||
width=current_stroke_width
|
||||
* pixels_per_unit
|
||||
* _TYPST_SVG_STROKE_WIDTH_SCALE,
|
||||
family=False,
|
||||
)
|
||||
|
||||
# -- baseline frame tracking ---------------------------------------------
|
||||
|
||||
def get_mob_from_shape_element(self, shape: se.SVGElement) -> VMobject | None:
|
||||
"""Attach Typst-specific metadata to imported shape mobjects."""
|
||||
mob = super().get_mob_from_shape_element(shape)
|
||||
if mob is None or not mob.has_points():
|
||||
return mob
|
||||
|
||||
if self._preserve_svg_stroke_widths and shape.stroke_width not in (None, 0):
|
||||
reference_size = max(mob.width, mob.height)
|
||||
if reference_size > 0:
|
||||
mob_any = cast(Any, mob)
|
||||
mob_any._typst_reference_size = reference_size
|
||||
mob_any._typst_source_stroke_width = shape.stroke_width
|
||||
self._stroke_width_tracked_submobjects.append(mob)
|
||||
|
||||
if not self.track_baselines:
|
||||
return mob
|
||||
|
||||
baseline_marks = self._get_reference_baseline_frame(shape)
|
||||
if baseline_marks is None:
|
||||
return mob
|
||||
|
||||
reference_points = mob.points.copy()
|
||||
reference_xy = np.column_stack(
|
||||
[
|
||||
reference_points[:, 0],
|
||||
reference_points[:, 1],
|
||||
np.ones(len(reference_points)),
|
||||
],
|
||||
)
|
||||
if np.linalg.matrix_rank(reference_xy) < 3:
|
||||
return mob
|
||||
|
||||
mob_any = cast(Any, mob)
|
||||
mob_any._typst_reference_points = reference_points
|
||||
mob_any._typst_reference_baseline_frame = baseline_marks
|
||||
self._baseline_tracked_submobjects.append(mob)
|
||||
return mob
|
||||
|
||||
@staticmethod
|
||||
def _get_reference_baseline_frame(
|
||||
shape: se.SVGElement,
|
||||
) -> np.ndarray | None:
|
||||
"""Return the reference ``(orig, right, up)`` frame for a Typst SVG element.
|
||||
|
||||
The frame is expressed in the same pre-centering coordinate system as the
|
||||
imported submobject points after the element's own SVG transform has been
|
||||
applied.
|
||||
"""
|
||||
if not isinstance(shape, se.Transformable):
|
||||
return None
|
||||
|
||||
matrix = shape.transform if shape.apply else se.Matrix()
|
||||
return np.array(
|
||||
[
|
||||
[matrix.e, matrix.f, 0.0],
|
||||
[matrix.a + matrix.e, matrix.b + matrix.f, 0.0],
|
||||
[matrix.c + matrix.e, matrix.d + matrix.f, 0.0],
|
||||
],
|
||||
)
|
||||
|
||||
def get_baseline_frame(
|
||||
self, submobject: VMobject
|
||||
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
"""Return the current Typst baseline frame for a tracked submobject.
|
||||
|
||||
The returned tuple contains the current positions of ``(orig, right, up)``.
|
||||
These are recovered from the stored reference frame and the submobject's
|
||||
current affine position in the scene.
|
||||
"""
|
||||
try:
|
||||
submobject_any = cast(Any, submobject)
|
||||
reference_points = cast(
|
||||
np.ndarray,
|
||||
submobject_any._typst_reference_points,
|
||||
)
|
||||
reference_frame = cast(
|
||||
np.ndarray,
|
||||
submobject_any._typst_reference_baseline_frame,
|
||||
)
|
||||
except AttributeError as err:
|
||||
raise ValueError(
|
||||
"No tracked Typst baseline frame is available for this submobject. "
|
||||
"Construct the Typst mobject with track_baselines=True.",
|
||||
) from err
|
||||
|
||||
reference_xy = np.column_stack(
|
||||
[
|
||||
reference_points[:, 0],
|
||||
reference_points[:, 1],
|
||||
np.ones(len(reference_points)),
|
||||
],
|
||||
)
|
||||
if np.linalg.matrix_rank(reference_xy) < 3:
|
||||
raise ValueError(
|
||||
"The stored Typst reference geometry is degenerate, so its baseline "
|
||||
"frame cannot be recovered.",
|
||||
)
|
||||
|
||||
transform, _, _, _ = np.linalg.lstsq(
|
||||
reference_xy, submobject.points, rcond=None
|
||||
)
|
||||
frame_xy = np.column_stack(
|
||||
[
|
||||
reference_frame[:, 0],
|
||||
reference_frame[:, 1],
|
||||
np.ones(len(reference_frame)),
|
||||
],
|
||||
)
|
||||
current_frame = frame_xy @ transform
|
||||
return tuple(
|
||||
cast(tuple[np.ndarray, np.ndarray, np.ndarray], tuple(current_frame))
|
||||
)
|
||||
|
||||
@property
|
||||
def baseline_frames(self) -> list[tuple[np.ndarray, np.ndarray, np.ndarray]]:
|
||||
"""Current Typst baseline frames for all tracked leaf submobjects."""
|
||||
if not self.track_baselines:
|
||||
return []
|
||||
return [
|
||||
self.get_baseline_frame(submobject)
|
||||
for submobject in self._baseline_tracked_submobjects
|
||||
]
|
||||
|
||||
def _rebuild_label_aliases(self) -> None:
|
||||
"""Rebuild user-facing label aliases from imported SVG ids."""
|
||||
aliases: dict[str, list[str]] = {}
|
||||
for key in self.id_to_vgroup_dict:
|
||||
if key == "root" or key.startswith("numbered_group_"):
|
||||
continue
|
||||
if _INTERNAL_TYPST_ID_RE.fullmatch(key) is not None:
|
||||
continue
|
||||
|
||||
base_label = key
|
||||
if _DUPLICATE_LABEL_SUFFIX in key:
|
||||
base_label, _, _ = key.partition(_DUPLICATE_LABEL_SUFFIX)
|
||||
aliases.setdefault(base_label, []).append(key)
|
||||
self._label_aliases = aliases
|
||||
|
||||
def _select_label(self, label: str) -> VGroup:
|
||||
if label not in self._label_aliases:
|
||||
raise KeyError(
|
||||
f"No group with label {label!r} found. "
|
||||
f"Available labels: {self._user_label_keys()}"
|
||||
)
|
||||
|
||||
result = VGroup()
|
||||
seen_ids: set[int] = set()
|
||||
for group_id in self._label_aliases[label]:
|
||||
for submobject in self.id_to_vgroup_dict[group_id]:
|
||||
submobject_id = id(submobject)
|
||||
if submobject_id in seen_ids:
|
||||
continue
|
||||
seen_ids.add(submobject_id)
|
||||
result.add(submobject)
|
||||
return result
|
||||
|
||||
# -- SVG post-processing -------------------------------------------------
|
||||
|
||||
def modify_xml_tree(self, element_tree: ET.ElementTree) -> ET.ElementTree:
|
||||
"""Convert ``data-typst-label`` attributes to ``id`` before parsing.
|
||||
|
||||
Typst's SVG renderer emits ``data-typst-label`` on ``<g>`` elements
|
||||
that carry a label (created via ``#box(body) <label>``). The
|
||||
``svgelements`` library propagates custom ``data-*`` attributes from
|
||||
parent groups to all children, making them unusable as unique group
|
||||
keys. ``id`` attributes, on the other hand, are *not* inherited.
|
||||
|
||||
This method walks the XML tree and promotes every
|
||||
``data-typst-label`` to ``id`` (on ``<g>`` elements only), so that
|
||||
:meth:`~.SVGMobject.get_mobjects_from` can pick them up via its
|
||||
existing ``id``-based grouping logic.
|
||||
"""
|
||||
# Let the base class inject default style wrappers first.
|
||||
element_tree = super().modify_xml_tree(element_tree)
|
||||
|
||||
# Walk all elements regardless of namespace — ElementTree
|
||||
# qualifies tags with the namespace URI, so a bare ``"g"``
|
||||
# won't match ``{http://www.w3.org/2000/svg}g``.
|
||||
label_counts: dict[str, int] = {}
|
||||
for element in element_tree.iter():
|
||||
label = element.get("data-typst-label")
|
||||
if label is not None:
|
||||
count = label_counts.get(label, 0)
|
||||
label_counts[label] = count + 1
|
||||
svg_id = label
|
||||
if count > 0:
|
||||
svg_id = f"{label}{_DUPLICATE_LABEL_SUFFIX}{count}"
|
||||
element.set("id", svg_id)
|
||||
del element.attrib["data-typst-label"]
|
||||
|
||||
return element_tree
|
||||
|
||||
# -- sub-expression selection --------------------------------------------
|
||||
|
||||
def select(self, key: str | int) -> VGroup:
|
||||
"""Select a labeled sub-expression.
|
||||
|
||||
Labels are created in the Typst source either manually via the
|
||||
``manimgrp`` helper or automatically through the ``{{ }}``
|
||||
double-brace notation in :class:`TypstMath`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
key
|
||||
A label name (``str``) matching a ``data-typst-label`` in the
|
||||
SVG, or an integer index into the auto-numbered ``{{ }}``
|
||||
groups (``_grp-0``, ``_grp-1``, …).
|
||||
|
||||
Returns
|
||||
-------
|
||||
VGroup
|
||||
The submobjects corresponding to the selected group.
|
||||
|
||||
Raises
|
||||
------
|
||||
KeyError
|
||||
If no group with the given label exists.
|
||||
IndexError
|
||||
If an integer index is out of range.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. manim:: TypstSelectExample
|
||||
:save_last_frame:
|
||||
:ref_classes: TypstMath
|
||||
:ref_methods: Typst.select
|
||||
|
||||
class TypstSelectExample(Scene):
|
||||
def construct(self):
|
||||
eq = TypstMath(
|
||||
"{{ a + b : num }} / {{ c : den }} = {{ lambda }} {{ x }}"
|
||||
)
|
||||
eq.select("num").set_color(RED) # "a + b"
|
||||
eq.select("den").set_color(BLUE) # "c"
|
||||
eq.select(0).set_color(YELLOW) # "lambda" (auto-numbered: "grp-0")
|
||||
eq.select(1).set_color(GREEN) # "x" (auto-numbered: "grp-1")
|
||||
|
||||
self.add(eq)
|
||||
"""
|
||||
if isinstance(key, int):
|
||||
label = f"_grp-{key}"
|
||||
if label not in self._label_aliases:
|
||||
raise IndexError(
|
||||
f"Group index {key} out of range. "
|
||||
f"Available labels: {self._user_label_keys()}"
|
||||
)
|
||||
return self._select_label(label)
|
||||
|
||||
return self._select_label(key)
|
||||
|
||||
def _user_label_keys(self) -> list[str]:
|
||||
"""Return the label keys that were created from ``data-typst-label``
|
||||
attributes (filtering out internal Typst group IDs and auto-numbered
|
||||
groups).
|
||||
"""
|
||||
return list(self._label_aliases)
|
||||
|
||||
# -- color handling ------------------------------------------------------
|
||||
|
||||
def init_colors(self, propagate_colors: bool = True) -> Typst:
|
||||
"""Recolor black submobjects to ``self.color``.
|
||||
|
||||
Typst renders text in black (``fill="#000000"``) by default.
|
||||
This mirrors the approach of :meth:`SingleStringMathTex.init_colors`:
|
||||
any submobject whose color is black is recolored to ``self.color``,
|
||||
while explicitly colored submobjects (non-black) are preserved.
|
||||
"""
|
||||
for submobject in self.submobjects:
|
||||
if submobject.color != BLACK:
|
||||
continue
|
||||
submobject.color = self.color
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
submobject.init_colors()
|
||||
elif config.renderer == RendererType.CAIRO:
|
||||
submobject.init_colors(propagate_colors=propagate_colors)
|
||||
return self
|
||||
|
||||
|
||||
class TypstMath(Typst):
|
||||
r"""Convenience wrapper: wraps the input in Typst math delimiters.
|
||||
|
||||
The expression is rendered as a display-level equation
|
||||
(``$ ... $`` with surrounding spaces).
|
||||
|
||||
Supports the ``{{ ... }}`` double-brace notation for grouping
|
||||
sub-expressions. Each ``{{ content }}`` is wrapped in a labeled
|
||||
``manimgrp`` call so that the resulting SVG contains identifiable
|
||||
groups accessible via :meth:`~.Typst.select`.
|
||||
|
||||
Groups can optionally be given explicit labels:
|
||||
``{{ content : label }}``. Without a label, groups are
|
||||
auto-numbered (``_grp-0``, ``_grp-1``, …).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
math_expression
|
||||
Typst math-mode content **without** the ``$ ... $`` delimiters.
|
||||
May contain ``{{ ... }}`` groups.
|
||||
**kwargs
|
||||
Forwarded to :class:`Typst`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. manim:: DisplayMath
|
||||
:save_last_frame:
|
||||
:ref_classes: TypstMath
|
||||
|
||||
class DisplayMath(Scene):
|
||||
def construct(self):
|
||||
eq = TypstMath(r"sum_(k=0)^n k = (n(n+1)) / 2")
|
||||
self.add(eq)
|
||||
|
||||
.. manim:: GroupedMath
|
||||
:save_last_frame:
|
||||
:ref_classes: TypstMath
|
||||
:ref_methods: Typst.select
|
||||
|
||||
class GroupedMath(Scene):
|
||||
def construct(self):
|
||||
eq = TypstMath("{{ a^2 + b^2 : lhs }} = {{ c^2 }}")
|
||||
eq.select("lhs").set_color(RED) # "a^2 + b^2"
|
||||
eq.select(0).set_color(BLUE) # "c^2" (auto-numbered: "grp-0")
|
||||
self.add(eq)
|
||||
"""
|
||||
|
||||
def __init__(self, math_expression: str, **kwargs: Any):
|
||||
processed, labels = self._preprocess_groups(math_expression)
|
||||
self._group_labels = labels
|
||||
|
||||
# Inject the manimgrp helper when groups are present.
|
||||
if labels:
|
||||
preamble = kwargs.get("typst_preamble", "")
|
||||
if _MANIMGRP_PREAMBLE not in preamble:
|
||||
preamble = (
|
||||
f"{_MANIMGRP_PREAMBLE}\n{preamble}"
|
||||
if preamble
|
||||
else _MANIMGRP_PREAMBLE
|
||||
)
|
||||
kwargs["typst_preamble"] = preamble
|
||||
|
||||
super().__init__(f"$ {processed} $", **kwargs)
|
||||
|
||||
# -- double-brace preprocessor -------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _preprocess_groups(math_expr: str) -> tuple[str, list[str]]:
|
||||
"""Replace ``{{ ... }}`` groups with ``manimgrp(...)`` calls.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
math_expr
|
||||
The raw math expression (without ``$ ... $`` delimiters).
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[str, list[str]]
|
||||
The processed expression and an ordered list of group labels.
|
||||
"""
|
||||
result: list[str] = []
|
||||
labels: list[str] = []
|
||||
auto_index = 0
|
||||
i = 0
|
||||
n = len(math_expr)
|
||||
outer_in_string = False
|
||||
outer_bracket_depth = 0
|
||||
|
||||
while i < n:
|
||||
ch = math_expr[i]
|
||||
|
||||
# Track string literals at the outer level.
|
||||
if outer_in_string:
|
||||
result.append(ch)
|
||||
if ch == "\\" and i + 1 < n:
|
||||
result.append(math_expr[i + 1])
|
||||
i += 2
|
||||
continue
|
||||
if ch == '"':
|
||||
outer_in_string = False
|
||||
i += 1
|
||||
continue
|
||||
if ch == '"':
|
||||
outer_in_string = True
|
||||
result.append(ch)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Track [...] content blocks at the outer level.
|
||||
if ch == "[":
|
||||
outer_bracket_depth += 1
|
||||
result.append(ch)
|
||||
i += 1
|
||||
continue
|
||||
if ch == "]" and outer_bracket_depth > 0:
|
||||
outer_bracket_depth -= 1
|
||||
result.append(ch)
|
||||
i += 1
|
||||
continue
|
||||
if outer_bracket_depth > 0:
|
||||
result.append(ch)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Look for opening {{ (not a single {)
|
||||
if i + 1 < n and ch == "{" and math_expr[i + 1] == "{":
|
||||
i += 2 # skip {{
|
||||
content_start = i
|
||||
depth = 1
|
||||
in_string = False
|
||||
bracket_depth = 0
|
||||
|
||||
while i < n and depth > 0:
|
||||
ch = math_expr[i]
|
||||
|
||||
if in_string:
|
||||
if ch == "\\" and i + 1 < n:
|
||||
i += 2
|
||||
continue
|
||||
if ch == '"':
|
||||
in_string = False
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if ch == '"':
|
||||
in_string = True
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if ch == "[":
|
||||
bracket_depth += 1
|
||||
i += 1
|
||||
continue
|
||||
if ch == "]" and bracket_depth > 0:
|
||||
bracket_depth -= 1
|
||||
i += 1
|
||||
continue
|
||||
if bracket_depth > 0:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if ch == "{" and i + 1 < n and math_expr[i + 1] == "{":
|
||||
depth += 1
|
||||
i += 2
|
||||
continue
|
||||
if ch == "}" and i + 1 < n and math_expr[i + 1] == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
content = math_expr[content_start:i]
|
||||
i += 2 # skip }}
|
||||
break
|
||||
i += 2
|
||||
continue
|
||||
|
||||
i += 1
|
||||
else:
|
||||
# Unclosed {{ — emit literally and stop.
|
||||
result.append("{{")
|
||||
result.append(math_expr[content_start:])
|
||||
return "".join(result), labels
|
||||
|
||||
# Check for optional `: label` suffix.
|
||||
m = _LABEL_RE.match(content)
|
||||
if m is not None:
|
||||
body = m.group(1).strip()
|
||||
label = m.group(2)
|
||||
else:
|
||||
body = content.strip()
|
||||
label = f"_grp-{auto_index}"
|
||||
auto_index += 1
|
||||
|
||||
labels.append(label)
|
||||
result.append(f'manimgrp("{label}", {body})')
|
||||
else:
|
||||
result.append(math_expr[i])
|
||||
i += 1
|
||||
|
||||
return "".join(result), labels
|
||||
|
|
@ -149,6 +149,7 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
|
|||
self.pre_function_handle_to_anchor_scale_factor = (
|
||||
pre_function_handle_to_anchor_scale_factor
|
||||
)
|
||||
self.list_of_faces: list[ThreeDVMobject] = []
|
||||
self._func = func
|
||||
self._setup_in_uv_space()
|
||||
self.apply_function(lambda p: func(p[0], p[1]))
|
||||
|
|
@ -172,6 +173,7 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
|
|||
def _setup_in_uv_space(self) -> None:
|
||||
u_values, v_values = self._get_u_values_and_v_values()
|
||||
faces = VGroup()
|
||||
self.list_of_faces = []
|
||||
for i in range(len(u_values) - 1):
|
||||
for j in range(len(v_values) - 1):
|
||||
u1, u2 = u_values[i : i + 2]
|
||||
|
|
@ -193,6 +195,7 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
|
|||
face.u2 = u2
|
||||
face.v1 = v1
|
||||
face.v2 = v2
|
||||
self.list_of_faces.append(face)
|
||||
faces.set_fill(color=self.fill_color, opacity=self.fill_opacity)
|
||||
faces.set_stroke(
|
||||
color=self.stroke_color,
|
||||
|
|
@ -223,7 +226,7 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
|
|||
The parametric surface with an alternating pattern.
|
||||
"""
|
||||
n_colors = len(colors)
|
||||
for face in self:
|
||||
for face in self.list_of_faces:
|
||||
c_index = (face.u_index + face.v_index) % n_colors
|
||||
face.set_fill(colors[c_index], opacity=opacity)
|
||||
return self
|
||||
|
|
@ -376,13 +379,7 @@ class Sphere(Surface):
|
|||
class ExampleSphere(ThreeDScene):
|
||||
def construct(self):
|
||||
self.set_camera_orientation(phi=PI / 6, theta=PI / 6)
|
||||
sphere1 = Sphere(
|
||||
center=(3, 0, 0),
|
||||
radius=1,
|
||||
resolution=(20, 20),
|
||||
u_range=[0.001, PI - 0.001],
|
||||
v_range=[0, TAU]
|
||||
)
|
||||
sphere1 = Sphere(center=(3, 0, 0), radius=1, resolution=(20, 20))
|
||||
sphere1.set_color(RED)
|
||||
self.add(sphere1)
|
||||
sphere2 = Sphere(center=(-1, -3, 0), radius=2, resolution=(18, 18))
|
||||
|
|
@ -391,6 +388,57 @@ class Sphere(Surface):
|
|||
sphere3 = Sphere(center=(-1, 2, 0), radius=2, resolution=(16, 16))
|
||||
sphere3.set_color(BLUE)
|
||||
self.add(sphere3)
|
||||
|
||||
This example shows that overlapping spheres can intersect with rough transitions.
|
||||
|
||||
.. manim:: ExampleSphereOverlap
|
||||
:save_last_frame:
|
||||
|
||||
class ExampleSphereOverlap(ThreeDScene):
|
||||
def construct(self):
|
||||
self.set_camera_orientation(phi=PI / 4, theta=PI / 4)
|
||||
sphere1 = Sphere(center=(0, 0, 0), radius=1, resolution=(20, 20))
|
||||
sphere1.set_color(RED)
|
||||
self.add(sphere1)
|
||||
sphere2 = Sphere(center=(-0.5, -1, 0.5), radius=1.2, resolution=(20, 20))
|
||||
sphere2.set_color(GREEN)
|
||||
self.add(sphere2)
|
||||
sphere3 = Sphere(center=(1, -1, 0), radius=1.1, resolution=(20, 20))
|
||||
sphere3.set_color(BLUE)
|
||||
self.add(sphere3)
|
||||
|
||||
In this example, by modifying ``u_range`` (the range of the azimuthal angle) and
|
||||
``v_range`` (the range of the polar angle), it is possible to obtain a portion of a
|
||||
sphere:
|
||||
|
||||
.. manim:: ExamplePartialSpheres
|
||||
:save_last_frame:
|
||||
|
||||
class ExamplePartialSpheres(ThreeDScene):
|
||||
def construct(self):
|
||||
self.set_camera_orientation(phi=PI / 4)
|
||||
sphere1 = Sphere(
|
||||
center=(-3, 0, 0),
|
||||
resolution=(10, 20),
|
||||
u_range=[TAU / 4, 3 * TAU / 4],
|
||||
)
|
||||
sphere1.set_color(RED)
|
||||
self.add(sphere1)
|
||||
sphere2 = Sphere(
|
||||
center=(0, 0, 0),
|
||||
resolution=(20, 10),
|
||||
v_range=[0, TAU / 4],
|
||||
)
|
||||
sphere2.set_color(GREEN)
|
||||
self.add(sphere2)
|
||||
sphere3 = Sphere(
|
||||
center=(3, 0, 0),
|
||||
resolution=(5, 10),
|
||||
u_range=[3 * TAU / 4, TAU],
|
||||
v_range=[TAU / 4, TAU / 2],
|
||||
)
|
||||
sphere3.set_color(BLUE)
|
||||
self.add(sphere3)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ class AbstractImageMobject(Mobject):
|
|||
def get_pixel_array(self) -> PixelArray:
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_color( # type: ignore[override]
|
||||
def set_color(
|
||||
self,
|
||||
color: ParsableManimColor = YELLOW_C,
|
||||
alpha: Any = None,
|
||||
|
|
@ -217,7 +217,7 @@ class ImageMobject(AbstractImageMobject):
|
|||
"""A simple getter method."""
|
||||
return self.pixel_array
|
||||
|
||||
def set_color( # type: ignore[override]
|
||||
def set_color(
|
||||
self,
|
||||
color: ParsableManimColor = YELLOW_C,
|
||||
alpha: Any = None,
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ from manim.utils.iterables import (
|
|||
from manim.utils.space_ops import rotate_vector, shoelace_direction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator
|
||||
from typing import Self
|
||||
|
||||
import numpy.typing as npt
|
||||
|
|
@ -103,6 +104,7 @@ class VMobject(Mobject):
|
|||
"""
|
||||
|
||||
sheen_factor = 0.0
|
||||
target: VMobject
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -153,7 +155,7 @@ class VMobject(Mobject):
|
|||
self.shade_in_3d: bool = shade_in_3d
|
||||
self.tolerance_for_point_equality: float = tolerance_for_point_equality
|
||||
self.n_points_per_cubic_curve: int = n_points_per_cubic_curve
|
||||
self._bezier_t_values: npt.NDArray[float] = np.linspace(
|
||||
self._bezier_t_values: npt.NDArray[np.float64] = np.linspace(
|
||||
0, 1, n_points_per_cubic_curve
|
||||
)
|
||||
self.cap_style: CapStyleType = cap_style
|
||||
|
|
@ -172,6 +174,9 @@ class VMobject(Mobject):
|
|||
def _assert_valid_submobjects(self, submobjects: Iterable[VMobject]) -> Self:
|
||||
return self._assert_valid_submobjects_internal(submobjects, VMobject)
|
||||
|
||||
def __iter__(self) -> Iterator[VMobject]:
|
||||
return iter(self.split())
|
||||
|
||||
# OpenGL compatibility
|
||||
@property
|
||||
def n_points_per_curve(self) -> int:
|
||||
|
|
@ -495,8 +500,10 @@ class VMobject(Mobject):
|
|||
will shrink, and for :math:`|\alpha| > 1` it will grow. Furthermore,
|
||||
if :math:`\alpha < 0`, the mobject is also flipped.
|
||||
scale_stroke
|
||||
Boolean determining if the object's outline is scaled when the object is scaled.
|
||||
If enabled, and object with 2px outline is scaled by a factor of .5, it will have an outline of 1px.
|
||||
Boolean determining if each submobject's outline is scaled when the object
|
||||
is scaled. If enabled, each submobject keeps its relative stroke width (for
|
||||
example, a submobject with a 2px outline scaled by a factor of .5 will have
|
||||
a 1px outline, while a submobject with 0px stroke remains at 0px).
|
||||
kwargs
|
||||
Additional keyword arguments passed to
|
||||
:meth:`~.Mobject.scale`.
|
||||
|
|
@ -533,11 +540,17 @@ class VMobject(Mobject):
|
|||
|
||||
"""
|
||||
if scale_stroke:
|
||||
self.set_stroke(width=abs(scale_factor) * self.get_stroke_width())
|
||||
self.set_stroke(
|
||||
width=abs(scale_factor) * self.get_stroke_width(background=True),
|
||||
background=True,
|
||||
)
|
||||
for mob in self.get_family():
|
||||
if isinstance(mob, VMobject):
|
||||
mob.set_stroke(
|
||||
width=abs(scale_factor) * mob.get_stroke_width(),
|
||||
family=False,
|
||||
)
|
||||
mob.set_stroke(
|
||||
width=abs(scale_factor) * mob.get_stroke_width(background=True),
|
||||
background=True,
|
||||
family=False,
|
||||
)
|
||||
super().scale(scale_factor, about_point=about_point, about_edge=about_edge)
|
||||
return self
|
||||
|
||||
|
|
@ -630,6 +643,17 @@ class VMobject(Mobject):
|
|||
|
||||
color: ManimColor = property(get_color, set_color)
|
||||
|
||||
def nonempty_submobjects(self) -> Sequence[VMobject]:
|
||||
return [
|
||||
submob
|
||||
for submob in self.submobjects
|
||||
if len(submob.submobjects) != 0 or len(submob.points) != 0
|
||||
]
|
||||
|
||||
def split(self) -> list[VMobject]:
|
||||
result: list[VMobject] = [self] if len(self.points) > 0 else []
|
||||
return result + self.submobjects
|
||||
|
||||
def set_sheen_direction(self, direction: Vector3DLike, family: bool = True) -> Self:
|
||||
"""Sets the direction of the applied sheen.
|
||||
|
||||
|
|
@ -1769,7 +1793,9 @@ class VMobject(Mobject):
|
|||
def get_nth_subpath(path_list, n):
|
||||
if n >= len(path_list):
|
||||
# Create a null path at the very end
|
||||
return [path_list[-1][-1]] * nppcc
|
||||
if len(path_list) == 0:
|
||||
return np.tile(np.zeros(3), (nppcc, 1))
|
||||
return np.tile(path_list[-1][-1], (nppcc, 1))
|
||||
path = path_list[n]
|
||||
# Check for useless points at the end of the path and remove them
|
||||
# https://github.com/ManimCommunity/manim/issues/1959
|
||||
|
|
@ -2303,6 +2329,11 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
|
|||
self._assert_valid_submobjects(tuplify(value))
|
||||
self.submobjects[key] = value
|
||||
|
||||
def __getitem__(self, key: int | slice) -> VMobject:
|
||||
if isinstance(key, slice):
|
||||
return VGroup(self.submobjects[key])
|
||||
return self.submobjects[key]
|
||||
|
||||
|
||||
class VDict(VMobject, metaclass=ConvertToOpenGL):
|
||||
"""A VGroup-like class, also offering submobject access by
|
||||
|
|
|
|||
|
|
@ -428,8 +428,8 @@ class ThreeDScene(Scene):
|
|||
which have the same meaning as the parameters in set_camera_orientation.
|
||||
"""
|
||||
config = dict(
|
||||
self.default_camera_orientation_kwargs,
|
||||
) # Where doe this come from?
|
||||
self.default_angled_camera_orientation_kwargs,
|
||||
)
|
||||
config.update(kwargs)
|
||||
self.set_camera_orientation(**config)
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ if TYPE_CHECKING:
|
|||
from typing import Self
|
||||
|
||||
from manim.typing import (
|
||||
ManimTextLabel,
|
||||
MappingFunction,
|
||||
Point3D,
|
||||
Point3DLike,
|
||||
|
|
@ -281,15 +282,19 @@ class VectorScene(Scene):
|
|||
color (str),
|
||||
label_scale_factor=VECTOR_LABEL_SCALE_FACTOR (int, float),
|
||||
"""
|
||||
i_hat, j_hat = self.get_basis_vectors()
|
||||
i_hat = self.get_basis_vectors().submobjects[0]
|
||||
j_hat = self.get_basis_vectors().submobjects[1]
|
||||
return VGroup(
|
||||
*(
|
||||
self.get_vector_label(
|
||||
vect, label, color=color, label_scale_factor=1, **kwargs
|
||||
)
|
||||
for vect, label, color in [
|
||||
(i_hat, "\\hat{\\imath}", X_COLOR),
|
||||
(j_hat, "\\hat{\\jmath}", Y_COLOR),
|
||||
# Casting i_hat and j_hat to Vector, as the VGroup from
|
||||
# self.get_basis_vectors() contains two vectors, but the
|
||||
# type checker is currently not aware of that.
|
||||
(cast(Vector, i_hat), "\\hat{\\imath}", X_COLOR),
|
||||
(cast(Vector, j_hat), "\\hat{\\jmath}", Y_COLOR),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
|
@ -297,13 +302,13 @@ class VectorScene(Scene):
|
|||
def get_vector_label(
|
||||
self,
|
||||
vector: Vector,
|
||||
label: MathTex | str,
|
||||
label: ManimTextLabel | str,
|
||||
at_tip: bool = False,
|
||||
direction: str = "left",
|
||||
rotate: bool = False,
|
||||
color: ParsableManimColor | None = None,
|
||||
label_scale_factor: float = LARGE_BUFF - 0.2,
|
||||
) -> MathTex:
|
||||
) -> ManimTextLabel:
|
||||
"""
|
||||
Returns naming labels for the passed vector.
|
||||
|
||||
|
|
@ -326,19 +331,18 @@ class VectorScene(Scene):
|
|||
|
||||
Returns
|
||||
-------
|
||||
MathTex
|
||||
The MathTex of the label.
|
||||
:class:`~.ManimTextLabel`
|
||||
The rendered label mobject.
|
||||
"""
|
||||
if not isinstance(label, MathTex):
|
||||
if isinstance(label, str):
|
||||
if len(label) == 1:
|
||||
label = "\\vec{\\textbf{%s}}" % label # noqa: UP031
|
||||
label = rf"\vec{{\textbf{{{label}}}}}"
|
||||
label = MathTex(label)
|
||||
if color is None:
|
||||
prepared_color: ParsableManimColor = vector.get_color()
|
||||
else:
|
||||
prepared_color = color
|
||||
label.set_color(prepared_color)
|
||||
assert isinstance(label, MathTex)
|
||||
label.scale(label_scale_factor)
|
||||
label.add_background_rectangle()
|
||||
|
||||
|
|
@ -361,8 +365,12 @@ class VectorScene(Scene):
|
|||
return label
|
||||
|
||||
def label_vector(
|
||||
self, vector: Vector, label: MathTex | str, animate: bool = True, **kwargs: Any
|
||||
) -> MathTex:
|
||||
self,
|
||||
vector: Vector,
|
||||
label: ManimTextLabel | str,
|
||||
animate: bool = True,
|
||||
**kwargs: Any,
|
||||
) -> ManimTextLabel:
|
||||
"""
|
||||
Shortcut method for creating, and animating the addition of
|
||||
a label for the vector.
|
||||
|
|
@ -373,7 +381,7 @@ class VectorScene(Scene):
|
|||
The vector for which the label must be added.
|
||||
|
||||
label
|
||||
The MathTex/string of the label.
|
||||
The rendered label mobject or the string used to create one.
|
||||
|
||||
animate
|
||||
Whether or not to animate the labelling w/ Write
|
||||
|
|
@ -383,8 +391,8 @@ class VectorScene(Scene):
|
|||
|
||||
Returns
|
||||
-------
|
||||
:class:`~.MathTex`
|
||||
The MathTex of the label.
|
||||
:class:`~.ManimTextLabel`
|
||||
The rendered label mobject.
|
||||
"""
|
||||
mathtex_label = self.get_vector_label(vector, label, **kwargs)
|
||||
if animate:
|
||||
|
|
@ -517,7 +525,9 @@ class VectorScene(Scene):
|
|||
y_line = Line(x_line.get_end(), arrow.get_end())
|
||||
x_line.set_color(X_COLOR)
|
||||
y_line.set_color(Y_COLOR)
|
||||
x_coord, y_coord = cast(VGroup, array.get_entries())
|
||||
temp = array.get_entries()
|
||||
x_coord = temp.submobjects[0]
|
||||
y_coord = temp.submobjects[1]
|
||||
x_coord_start = self.position_x_coordinate(x_coord.copy(), x_line, vector)
|
||||
y_coord_start = self.position_y_coordinate(y_coord.copy(), y_line, vector)
|
||||
brackets = array.get_brackets()
|
||||
|
|
@ -697,7 +707,7 @@ class LinearTransformationScene(VectorScene):
|
|||
self.foreground_mobjects: list[Mobject] = []
|
||||
self.transformable_mobjects: list[Mobject] = []
|
||||
self.moving_vectors: list[Mobject] = []
|
||||
self.transformable_labels: list[MathTex] = []
|
||||
self.transformable_labels: list[Any] = []
|
||||
self.moving_mobjects: list[Mobject] = []
|
||||
|
||||
self.background_plane = NumberPlane(**self.background_plane_kwargs)
|
||||
|
|
@ -965,16 +975,17 @@ class LinearTransformationScene(VectorScene):
|
|||
"""
|
||||
# TODO: Clear up types in this function. This is currently a mess.
|
||||
label_mob = self.label_vector(vector, label, **kwargs)
|
||||
label_mob_any = cast(Any, label_mob)
|
||||
if new_label:
|
||||
label_mob.target_text = new_label # type: ignore[attr-defined]
|
||||
label_mob_any.target_text = new_label
|
||||
else:
|
||||
label_mob.target_text = ( # type: ignore[attr-defined]
|
||||
label_mob_any.target_text = (
|
||||
f"{transformation_name}({label_mob.get_tex_string()})"
|
||||
)
|
||||
label_mob.vector = vector # type: ignore[attr-defined]
|
||||
label_mob.kwargs = kwargs # type: ignore[attr-defined]
|
||||
if "animate" in label_mob.kwargs:
|
||||
label_mob.kwargs.pop("animate")
|
||||
label_mob_any.vector = vector
|
||||
label_mob_any.kwargs = kwargs
|
||||
if "animate" in label_mob_any.kwargs:
|
||||
label_mob_any.kwargs.pop("animate")
|
||||
self.transformable_labels.append(label_mob)
|
||||
return cast(MathTex, label_mob)
|
||||
|
||||
|
|
@ -1143,11 +1154,12 @@ class LinearTransformationScene(VectorScene):
|
|||
for label in self.transformable_labels:
|
||||
# TODO: This location and lines 933 and 335 are the only locations in
|
||||
# the code where the target_text property is referenced.
|
||||
target_text: MathTex | str = label.target_text # type: ignore[assignment]
|
||||
label_any = cast(Any, label)
|
||||
target_text: MathTex | str = label_any.target_text
|
||||
label.target = self.get_vector_label(
|
||||
label.vector.target, # type: ignore[attr-defined]
|
||||
label_any.vector.target,
|
||||
target_text,
|
||||
**label.kwargs, # type: ignore[arg-type]
|
||||
**label_any.kwargs,
|
||||
)
|
||||
return self.get_piece_movement(self.transformable_labels)
|
||||
|
||||
|
|
|
|||
|
|
@ -22,11 +22,16 @@ from __future__ import annotations
|
|||
|
||||
from collections.abc import Callable, Sequence
|
||||
from os import PathLike
|
||||
from typing import TypeAlias
|
||||
from typing import TYPE_CHECKING, TypeAlias
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.text.tex_mobject import MathTex
|
||||
from manim.mobject.text.text_mobject import Text
|
||||
from manim.mobject.text.typst_mobject import Typst
|
||||
|
||||
__all__ = [
|
||||
"ManimFloat",
|
||||
"ManimInt",
|
||||
|
|
@ -108,6 +113,7 @@ __all__ = [
|
|||
"PathFuncType",
|
||||
"MappingFunction",
|
||||
"MultiMappingFunction",
|
||||
"ManimTextLabel",
|
||||
"PixelArray",
|
||||
"GrayscalePixelArray",
|
||||
"RGBPixelArray",
|
||||
|
|
@ -931,6 +937,19 @@ MultiMappingFunction: TypeAlias = Callable[[Point3D_Array], Point3D_Array]
|
|||
:class:`.Point3D_Array`.
|
||||
"""
|
||||
|
||||
"""
|
||||
[CATEGORY]
|
||||
Text mobject types
|
||||
"""
|
||||
|
||||
ManimTextLabel: TypeAlias = "Text | MathTex | Typst"
|
||||
"""Text-like label mobjects commonly used across Manim.
|
||||
|
||||
This includes :class:`~.Text`, :class:`~.MathTex`, and :class:`~.Typst`.
|
||||
Subtype-specific variants like :class:`~.Tex` and :class:`~.TypstMath` are
|
||||
covered implicitly through inheritance.
|
||||
"""
|
||||
|
||||
"""
|
||||
[CATEGORY]
|
||||
Image types
|
||||
|
|
|
|||
|
|
@ -250,6 +250,9 @@ def deprecated(
|
|||
|
||||
if type(func).__name__ != "function":
|
||||
deprecate_docs(func)
|
||||
# The following line raises this mypy error:
|
||||
# Accessing "__init__" on an instance is unsound, since instance.__init__
|
||||
# could be from an incompatible subclass [misc]</pre>
|
||||
func.__init__ = decorate(func.__init__, deprecate)
|
||||
return func
|
||||
|
||||
|
|
|
|||
|
|
@ -33,8 +33,10 @@ class TexTemplate:
|
|||
_body: str = field(default="", init=False)
|
||||
"""A custom body, can be set from a file."""
|
||||
|
||||
tex_compiler: str = "latex"
|
||||
"""The TeX compiler to be used, e.g. ``latex``, ``pdflatex`` or ``lualatex``."""
|
||||
tex_compiler: str | list[str] = "latex"
|
||||
"""The TeX compiler(s) to be used. Can be a single compiler (e.g. ``"latex"``,
|
||||
``"pdflatex"``, ``"lualatex"``) or a list of compilers to compile in order
|
||||
(e.g. ``["lualatex", "pdflatex"]``)."""
|
||||
|
||||
description: str = ""
|
||||
"""A description of the template"""
|
||||
|
|
|
|||
|
|
@ -178,7 +178,9 @@ def insight_package_not_found_error(matching: Match[str]) -> Generator[str]:
|
|||
yield f"Install {matching[1]} it using your LaTeX package manager, or check for typos."
|
||||
|
||||
|
||||
def compile_tex(tex_file: Path, tex_compiler: str, output_format: str) -> Path:
|
||||
def compile_tex(
|
||||
tex_file: Path, tex_compiler: str | list[str], output_format: str
|
||||
) -> Path:
|
||||
"""Compiles a tex_file into a .dvi or a .xdv or a .pdf
|
||||
|
||||
Parameters
|
||||
|
|
@ -186,7 +188,8 @@ def compile_tex(tex_file: Path, tex_compiler: str, output_format: str) -> Path:
|
|||
tex_file
|
||||
File name of TeX file to be typeset.
|
||||
tex_compiler
|
||||
String containing the compiler to be used, e.g. ``pdflatex`` or ``lualatex``
|
||||
The TeX compiler(s) to be used.
|
||||
Can be a single compiler (e.g. ``"latex"``, ``"pdflatex"`` ``"lualatex"``) or a list of compilers to compile in order (e.g. ``["lualatex", "pdflatex"]``).
|
||||
output_format
|
||||
String containing the output format generated by the compiler, e.g. ``.dvi`` or ``.pdf``
|
||||
|
||||
|
|
@ -197,22 +200,26 @@ def compile_tex(tex_file: Path, tex_compiler: str, output_format: str) -> Path:
|
|||
"""
|
||||
result = tex_file.with_suffix(output_format)
|
||||
tex_dir = config.get_dir("tex_dir")
|
||||
tex_compilers = [tex_compiler] if isinstance(tex_compiler, str) else tex_compiler
|
||||
if not result.exists():
|
||||
command = make_tex_compilation_command(
|
||||
tex_compiler,
|
||||
output_format,
|
||||
tex_file,
|
||||
tex_dir,
|
||||
)
|
||||
cp = subprocess.run(command, stdout=subprocess.DEVNULL)
|
||||
if cp.returncode != 0:
|
||||
log_file = tex_file.with_suffix(".log")
|
||||
print_all_tex_errors(log_file, tex_compiler, tex_file)
|
||||
raise ValueError(
|
||||
f"{tex_compiler} error converting to"
|
||||
f" {output_format[1:]}. See log output above or"
|
||||
f" the log file: {log_file}",
|
||||
for i, compiler in enumerate(tex_compilers, start=1):
|
||||
if len(tex_compilers) > 1:
|
||||
logger.info(f"Compiling {i} of {len(tex_compilers)}: {compiler}")
|
||||
command = make_tex_compilation_command(
|
||||
compiler,
|
||||
output_format,
|
||||
tex_file,
|
||||
tex_dir,
|
||||
)
|
||||
cp = subprocess.run(command, stdout=subprocess.DEVNULL)
|
||||
if cp.returncode != 0:
|
||||
log_file = tex_file.with_suffix(".log")
|
||||
print_all_tex_errors(log_file, compiler, tex_file)
|
||||
raise ValueError(
|
||||
f"{compiler} error converting to"
|
||||
f" {output_format[1:]}. See log output above or"
|
||||
f" the log file: {log_file}",
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
|
|
|
|||
106
manim/utils/typst_file_writing.py
Normal file
106
manim/utils/typst_file_writing.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
"""Interface for writing, compiling, and converting ``.typ`` files via the ``typst`` Python package.
|
||||
|
||||
.. SEEALSO::
|
||||
|
||||
:mod:`.mobject.text.typst_mobject`
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
from manim import config, logger
|
||||
|
||||
__all__ = ["typst_to_svg_file"]
|
||||
|
||||
# Use 10pt instead of Typst's 11pt default so that the post-import scaling
|
||||
# based on Manim's font_size property matches TeX / MathTex more closely.
|
||||
TYPST_COMPILATION_FONT_SIZE = 10 # pt
|
||||
|
||||
TYPST_TEMPLATE = """\
|
||||
#set page(width: auto, height: auto, margin: 0pt, fill: none)
|
||||
#set text(size: {text_size}pt)
|
||||
{preamble}
|
||||
{body}
|
||||
"""
|
||||
|
||||
|
||||
def _typst_hash(content: str) -> str:
|
||||
"""Return a truncated SHA-256 hex digest of *content*."""
|
||||
return hashlib.sha256(content.encode()).hexdigest()[:16]
|
||||
|
||||
|
||||
def typst_to_svg_file(
|
||||
typst_code: str,
|
||||
preamble: str = "",
|
||||
text_size: float = TYPST_COMPILATION_FONT_SIZE,
|
||||
font_paths: list[str | Path] | None = None,
|
||||
) -> Path:
|
||||
"""Compile a Typst string to SVG via the ``typst`` Python package.
|
||||
|
||||
The compiled SVG and the intermediate ``.typ`` source are cached
|
||||
under :func:`config.get_dir("tex_dir") <manim.ManimConfig.get_dir>`
|
||||
using a content-hash filename scheme (identical to the LaTeX pipeline).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
typst_code
|
||||
The body of the Typst document (user-supplied markup).
|
||||
preamble
|
||||
Extra Typst code inserted between the ``#set`` rules and the body.
|
||||
Useful for ``#import``, ``#set``, or ``#show`` rules.
|
||||
text_size
|
||||
Font size in Typst points used during compilation.
|
||||
font_paths
|
||||
Optional list of additional font directories passed to the Typst
|
||||
compiler.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`Path`
|
||||
Path to the generated SVG file.
|
||||
|
||||
Raises
|
||||
------
|
||||
ImportError
|
||||
If the ``typst`` Python package is not installed.
|
||||
"""
|
||||
try:
|
||||
import typst as typst_compiler
|
||||
except ImportError as err:
|
||||
raise ImportError(
|
||||
"TypstMobject requires the 'typst' Python package. "
|
||||
"Install it with: pip install typst>=0.14"
|
||||
) from err
|
||||
|
||||
full_source = TYPST_TEMPLATE.format(
|
||||
text_size=text_size,
|
||||
preamble=preamble,
|
||||
body=typst_code,
|
||||
)
|
||||
content_hash = _typst_hash(full_source)
|
||||
typst_dir = config.get_dir("tex_dir")
|
||||
typst_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
typ_file = typst_dir / f"{content_hash}.typ"
|
||||
svg_file = typst_dir / f"{content_hash}.svg"
|
||||
|
||||
if svg_file.exists():
|
||||
return svg_file
|
||||
|
||||
typ_file.write_text(full_source, encoding="utf-8")
|
||||
|
||||
logger.info(
|
||||
"Compiling Typst source %(path)s ...",
|
||||
{"path": f"{typ_file}"},
|
||||
)
|
||||
|
||||
svg_bytes = typst_compiler.compile(
|
||||
str(typ_file),
|
||||
format="svg",
|
||||
font_paths=font_paths or [],
|
||||
)
|
||||
svg_file.write_bytes(svg_bytes)
|
||||
return svg_file
|
||||
9
mypy.ini
9
mypy.ini
|
|
@ -64,9 +64,6 @@ ignore_errors = True
|
|||
[mypy-manim.animation.speedmodifier]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.animation.transform_matching_parts]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.animation.transform]
|
||||
ignore_errors = True
|
||||
|
||||
|
|
@ -85,9 +82,6 @@ ignore_errors = True
|
|||
[mypy-manim.mobject.logo]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.mobject]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.opengl.opengl_point_cloud_mobject]
|
||||
ignore_errors = True
|
||||
|
||||
|
|
@ -100,6 +94,9 @@ ignore_errors = True
|
|||
[mypy-manim.mobject.table]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.types.point_cloud_mobject]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.types.vectorized_mobject]
|
||||
ignore_errors = True
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ documentation = "https://docs.manim.community/"
|
|||
homepage = "https://www.manim.community/"
|
||||
"Bug Tracker" = "https://github.com/ManimCommunity/manim/issues"
|
||||
"Changelog" = "https://docs.manim.community/en/stable/changelog.html"
|
||||
"X / Twitter" = "https://x.com/manim_community"
|
||||
"X / Twitter" = "https://x.com/manimcommunity"
|
||||
"Bluesky" = "https://bsky.app/profile/manim.community"
|
||||
"Discord" = "https://www.manim.community/discord/"
|
||||
|
||||
|
|
@ -74,6 +74,9 @@ jupyterlab = [
|
|||
"jupyterlab>=4.3.4",
|
||||
"notebook>=7.3.2",
|
||||
]
|
||||
typst = [
|
||||
"typst>=0.14",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
|
|
|
|||
|
|
@ -129,4 +129,6 @@ def test_start_and_end_at_same_point():
|
|||
line = DashedLine(np.zeros(3), np.zeros(3))
|
||||
line.put_start_and_end_on(np.zeros(3), np.array([0, 0, 0]))
|
||||
|
||||
np.testing.assert_array_equal(np.round(np.zeros(3), 4), np.round(line.points, 4))
|
||||
np.testing.assert_array_equal(
|
||||
np.round(line.points, 4), np.round(np.zeros((4, 3)), 4)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,20 @@ from __future__ import annotations
|
|||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from manim import DL, PI, UR, Circle, Mobject, Rectangle, Square, Triangle, VGroup
|
||||
from manim import (
|
||||
DL,
|
||||
DR,
|
||||
PI,
|
||||
UL,
|
||||
UR,
|
||||
Circle,
|
||||
Mobject,
|
||||
Rectangle,
|
||||
Square,
|
||||
Triangle,
|
||||
VGroup,
|
||||
VMobject,
|
||||
)
|
||||
|
||||
|
||||
def test_mobject_add():
|
||||
|
|
@ -135,22 +148,24 @@ def test_mobject_dimensions_nested_mobjects():
|
|||
assert is_close(vg.depth, 0.775), vg.depth
|
||||
|
||||
|
||||
def test_mobject_dimensions_mobjects_with_no_points_are_at_origin():
|
||||
rect = Rectangle(width=2, height=3)
|
||||
rect.move_to([-4, -5, 0])
|
||||
outer_group = VGroup(rect)
|
||||
def test_mobject_dimensions_mobjects_with_no_points():
|
||||
empty_mob = VMobject()
|
||||
assert empty_mob.width == 0
|
||||
assert empty_mob.height == 0
|
||||
|
||||
# This is as one would expect
|
||||
assert outer_group.width == 2
|
||||
assert outer_group.height == 3
|
||||
for direction in [DL, DR, UL, UR]:
|
||||
rect = Rectangle(width=2, height=3)
|
||||
rect.move_to(direction * 10)
|
||||
outer_group = VGroup(rect)
|
||||
|
||||
# Adding a mobject with no points has a quirk of adding a "point"
|
||||
# to [0, 0, 0] (the origin). This changes the size of the outer
|
||||
# group because now the bottom left corner is at [-5, -6.5, 0]
|
||||
# but the upper right corner is [0, 0, 0] instead of [-3, -3.5, 0]
|
||||
outer_group.add(VGroup())
|
||||
assert outer_group.width == 5
|
||||
assert outer_group.height == 6.5
|
||||
# This is as one would expect
|
||||
assert outer_group.width == 2
|
||||
assert outer_group.height == 3
|
||||
|
||||
# Adding a submobject with no points does not change the group size
|
||||
outer_group.add(empty_mob)
|
||||
assert outer_group.width == 2
|
||||
assert outer_group.height == 3
|
||||
|
||||
|
||||
def test_mobject_dimensions_has_points_and_children():
|
||||
|
|
|
|||
|
|
@ -76,6 +76,19 @@ def test_graph_add_edges():
|
|||
assert set(G._graph.edges()) == set(G.edges.keys())
|
||||
|
||||
|
||||
def test_graph_getitem():
|
||||
vertices = [1, 2, 3, 4]
|
||||
edges = [(1, 2), (2, 3), (3, 4), (4, 1)]
|
||||
G = Graph(vertices, edges)
|
||||
# Vertex access
|
||||
assert G[1] is G.vertices[1]
|
||||
# Edge access via tuple key
|
||||
assert G[(1, 2)] is G.edges[(1, 2)]
|
||||
# DiGraph edge access
|
||||
DG = DiGraph(vertices, edges)
|
||||
assert DG[(1, 2)] is DG.edges[(1, 2)]
|
||||
|
||||
|
||||
def test_graph_remove_edges():
|
||||
G = Graph([1, 2, 3, 4, 5], [(1, 2), (2, 3), (3, 4), (4, 5), (1, 5)])
|
||||
removed_mobjects = G.remove_edges((1, 2))
|
||||
|
|
|
|||
|
|
@ -17,3 +17,27 @@ def test_highlighted_cell_color_access():
|
|||
# Should not raise RecursionError
|
||||
color = rect.color
|
||||
assert color == GREEN
|
||||
|
||||
|
||||
def test_table_include_inner_lines_false():
|
||||
"""Verify that inner lines can be disabled while outer lines remain."""
|
||||
table = Table(
|
||||
[["A", "B"], ["C", "D"]],
|
||||
include_outer_lines=True,
|
||||
include_inner_lines=False,
|
||||
)
|
||||
|
||||
assert len(table.get_horizontal_lines()) == 2
|
||||
assert len(table.get_vertical_lines()) == 2
|
||||
|
||||
|
||||
def test_table_include_inner_lines_true():
|
||||
"""Verify that inner lines are present by default."""
|
||||
table = Table(
|
||||
[["A", "B"], ["C", "D"]],
|
||||
include_outer_lines=True,
|
||||
include_inner_lines=True,
|
||||
)
|
||||
|
||||
assert len(table.get_horizontal_lines()) == 3
|
||||
assert len(table.get_vertical_lines()) == 3
|
||||
|
|
|
|||
339
tests/module/mobject/text/test_typst_mobject.py
Normal file
339
tests/module/mobject/text/test_typst_mobject.py
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from manim import (
|
||||
RIGHT,
|
||||
Label,
|
||||
MathTex,
|
||||
NumberLine,
|
||||
Tex,
|
||||
Typst,
|
||||
TypstMath,
|
||||
Vector,
|
||||
VectorScene,
|
||||
)
|
||||
|
||||
|
||||
def test_Typst(config):
|
||||
"""Basic Typst creation produces an SVG file."""
|
||||
m = Typst(r"$ x^2 $")
|
||||
assert m.height > 0
|
||||
assert m.width > 0
|
||||
assert len(m.submobjects) > 0
|
||||
|
||||
|
||||
def test_TypstMath(config):
|
||||
"""TypstMath wraps the expression in math delimiters."""
|
||||
m = TypstMath(r"alpha + beta")
|
||||
assert m.typst_code == "$ alpha + beta $"
|
||||
assert m.height > 0
|
||||
|
||||
|
||||
def test_typst_default_font_size(config):
|
||||
"""Default font_size is 48 (DEFAULT_FONT_SIZE)."""
|
||||
m = Typst(r"$ a + b $")
|
||||
assert np.isclose(m.font_size, 48)
|
||||
|
||||
|
||||
def test_typst_custom_font_size(config):
|
||||
"""Passing a custom font_size scales the mobject accordingly."""
|
||||
m = Typst(r"$ a + b $", font_size=72)
|
||||
assert np.isclose(m.font_size, 72)
|
||||
|
||||
|
||||
def test_typst_font_size_property_setter(config):
|
||||
"""Setting font_size after creation rescales correctly."""
|
||||
m = Typst(r"$ a + b $")
|
||||
original_height = m.height
|
||||
m.font_size = 96
|
||||
assert np.isclose(m.font_size, 96)
|
||||
assert m.height > original_height
|
||||
|
||||
|
||||
def test_typst_font_size_scaling_also_scales_svg_strokes(config):
|
||||
"""Typst-authored stroke widths scale together with font_size."""
|
||||
m = TypstMath("frac(a,b)", font_size=48, use_svg_cache=False)
|
||||
original_stroke_width = max(submobject.stroke_width for submobject in m.submobjects)
|
||||
|
||||
m.font_size = 96
|
||||
scaled_stroke_width = max(submobject.stroke_width for submobject in m.submobjects)
|
||||
|
||||
assert np.isclose(scaled_stroke_width, 2 * original_stroke_width)
|
||||
|
||||
|
||||
def test_typst_font_size_error(config):
|
||||
"""Setting font_size to a non-positive value raises ValueError."""
|
||||
m = Typst(r"$ a + b $")
|
||||
with pytest.raises(ValueError, match="font_size must be greater than 0"):
|
||||
m.font_size = -1
|
||||
|
||||
|
||||
def test_typst_caching(config):
|
||||
"""Compiling the same source twice uses the cached SVG."""
|
||||
m1 = Typst(r"$ e^{i pi} + 1 = 0 $")
|
||||
m2 = Typst(r"$ e^{i pi} + 1 = 0 $")
|
||||
assert np.isclose(m1.height, m2.height)
|
||||
assert np.isclose(m1.width, m2.width)
|
||||
|
||||
|
||||
def test_typst_preamble(config):
|
||||
"""A custom preamble is accepted without error."""
|
||||
m = Typst(
|
||||
r"$ x^2 $",
|
||||
typst_preamble='#set text(font: "New Computer Modern")',
|
||||
)
|
||||
assert m.height > 0
|
||||
|
||||
|
||||
def test_typst_repr(config):
|
||||
"""__repr__ includes the Typst source."""
|
||||
m = Typst("hello")
|
||||
assert repr(m) == "Typst('hello')"
|
||||
|
||||
m2 = TypstMath("x")
|
||||
assert repr(m2) == "TypstMath('$ x $')"
|
||||
|
||||
|
||||
def test_typst_text_rendering(config):
|
||||
"""Non-math Typst markup renders correctly."""
|
||||
m = Typst(r"*Bold* and _italic_")
|
||||
assert m.height > 0
|
||||
assert len(m.submobjects) > 0
|
||||
|
||||
|
||||
def test_typst_preserves_svg_stroke_widths_by_default(config):
|
||||
"""Default stroke_width=None preserves Typst-authored SVG strokes."""
|
||||
m = Typst("#underline[abc]", use_svg_cache=False)
|
||||
assert any(submobject.stroke_width > 0 for submobject in m.submobjects)
|
||||
|
||||
|
||||
def test_typst_baseline_frames_empty_without_tracking(config):
|
||||
"""Baseline frames are only collected when requested."""
|
||||
m = Typst("Ggf", use_svg_cache=False)
|
||||
assert m.baseline_frames == []
|
||||
|
||||
|
||||
def test_typst_baseline_frames_track_scene_positions(config):
|
||||
"""Tracked baseline frames follow ordinary affine transformations."""
|
||||
m = Typst("Ggf", track_baselines=True, use_svg_cache=False)
|
||||
assert m.baseline_frames
|
||||
|
||||
orig, right, up = m.baseline_frames[0]
|
||||
delta = 2 * RIGHT
|
||||
m.shift(delta)
|
||||
shifted_orig, shifted_right, shifted_up = m.baseline_frames[0]
|
||||
|
||||
assert np.allclose(shifted_orig - orig, delta)
|
||||
assert np.allclose(shifted_right - right, delta)
|
||||
assert np.allclose(shifted_up - up, delta)
|
||||
|
||||
|
||||
def test_typst_text_font_size_matches_tex_closely(config):
|
||||
"""Typst text is calibrated close to Tex for the same font_size."""
|
||||
tex = Tex("Hello", font_size=48)
|
||||
typst = Typst("Hello", font_size=48, use_svg_cache=False)
|
||||
assert np.isclose(typst.height, tex.height, rtol=0.02)
|
||||
assert np.isclose(typst.width, tex.width, rtol=0.02)
|
||||
|
||||
|
||||
def test_typstmath_font_size_matches_mathtex_closely(config):
|
||||
"""Typst math is calibrated close to MathTex for the same font_size."""
|
||||
mathtex = MathTex(r"\frac{a}{b}", font_size=48)
|
||||
typstmath = TypstMath("frac(a,b)", font_size=48, use_svg_cache=False)
|
||||
assert np.isclose(typstmath.height, mathtex.height, rtol=0.02)
|
||||
assert np.isclose(typstmath.width, mathtex.width, rtol=0.02)
|
||||
|
||||
|
||||
# -- data-typst-label → id mapping tests ------------------------------------
|
||||
|
||||
MANIMGRP_PREAMBLE = "#let manimgrp(lbl, body) = [#box(body) #label(lbl)]"
|
||||
|
||||
|
||||
def test_typst_labels_mapped_to_vgroups(config):
|
||||
"""data-typst-label attributes are promoted to id and appear in id_to_vgroup_dict."""
|
||||
m = Typst(
|
||||
'$ #manimgrp("numerator", $a + b$) / #manimgrp("denom", $c - d$) $',
|
||||
typst_preamble=MANIMGRP_PREAMBLE,
|
||||
use_svg_cache=False,
|
||||
)
|
||||
assert "numerator" in m.id_to_vgroup_dict
|
||||
assert "denom" in m.id_to_vgroup_dict
|
||||
# a, +, b → 3 submobjects; c, -, d → 3 submobjects
|
||||
assert len(m.id_to_vgroup_dict["numerator"]) == 3
|
||||
assert len(m.id_to_vgroup_dict["denom"]) == 3
|
||||
|
||||
|
||||
def test_typst_nested_labels(config):
|
||||
"""Nested labeled boxes produce nested VGroups without cross-contamination."""
|
||||
m = Typst(
|
||||
'$ #manimgrp("outer", $#manimgrp("inner", $a$) + b$) $',
|
||||
typst_preamble=MANIMGRP_PREAMBLE,
|
||||
use_svg_cache=False,
|
||||
)
|
||||
assert "outer" in m.id_to_vgroup_dict
|
||||
assert "inner" in m.id_to_vgroup_dict
|
||||
# "inner" contains only "a" (1 submobject)
|
||||
assert len(m.id_to_vgroup_dict["inner"]) == 1
|
||||
# "outer" contains everything: a, +, b (3 submobjects)
|
||||
assert len(m.id_to_vgroup_dict["outer"]) == 3
|
||||
# The inner submobject is a subset of the outer one
|
||||
inner_mob = m.id_to_vgroup_dict["inner"][0]
|
||||
assert inner_mob in m.id_to_vgroup_dict["outer"]
|
||||
|
||||
|
||||
def test_typst_no_labels_no_extra_keys(config):
|
||||
"""Without labeled boxes, no extra label keys appear."""
|
||||
m = Typst(r"$ a + b $", use_svg_cache=False)
|
||||
label_keys = [
|
||||
k
|
||||
for k in m.id_to_vgroup_dict
|
||||
if not k.startswith(("numbered_group", "root", "g"))
|
||||
]
|
||||
assert label_keys == []
|
||||
|
||||
|
||||
def test_typst_select(config):
|
||||
"""select() returns the correct VGroup for a given label."""
|
||||
m = Typst(
|
||||
'$ #manimgrp("lhs", $a + b$) = #manimgrp("rhs", $c$) $',
|
||||
typst_preamble=MANIMGRP_PREAMBLE,
|
||||
use_svg_cache=False,
|
||||
)
|
||||
lhs = m.select("lhs")
|
||||
rhs = m.select("rhs")
|
||||
assert len(lhs) == 3 # a, +, b
|
||||
assert len(rhs) == 1 # c
|
||||
|
||||
|
||||
def test_typst_select_collects_duplicate_labels(config):
|
||||
"""Repeated Typst labels are combined into one selectable group."""
|
||||
m = Typst(
|
||||
'$ #manimgrp("picked", $a$) + #manimgrp("picked", $b$) $',
|
||||
typst_preamble=MANIMGRP_PREAMBLE,
|
||||
use_svg_cache=False,
|
||||
)
|
||||
picked = m.select("picked")
|
||||
assert len(picked) == 2
|
||||
|
||||
|
||||
def test_typst_get_baseline_frame_for_selected_submobjects(config):
|
||||
"""Tracked frames can be queried for submobjects returned by select()."""
|
||||
m = Typst(
|
||||
'$ #manimgrp("lhs", $a + b$) = c $',
|
||||
typst_preamble=MANIMGRP_PREAMBLE,
|
||||
track_baselines=True,
|
||||
use_svg_cache=False,
|
||||
)
|
||||
lhs = m.select("lhs")
|
||||
frames = [m.get_baseline_frame(submobject) for submobject in lhs]
|
||||
|
||||
assert len(frames) == len(lhs)
|
||||
for orig, right, up in frames:
|
||||
assert orig.shape == (3,)
|
||||
assert right.shape == (3,)
|
||||
assert up.shape == (3,)
|
||||
|
||||
|
||||
def test_typst_select_keyerror(config):
|
||||
"""select() raises KeyError for a nonexistent label."""
|
||||
m = Typst(r"$ a + b $", use_svg_cache=False)
|
||||
with pytest.raises(KeyError, match="No group with label 'missing'"):
|
||||
m.select("missing")
|
||||
|
||||
|
||||
def test_typst_select_keyerror_lists_labels_starting_with_g(config):
|
||||
"""Error messages keep user labels even when they start with ``g``."""
|
||||
m = Typst(
|
||||
'$ #manimgrp("gamma", $a$) $',
|
||||
typst_preamble=MANIMGRP_PREAMBLE,
|
||||
use_svg_cache=False,
|
||||
)
|
||||
with pytest.raises(KeyError, match="gamma"):
|
||||
m.select("missing")
|
||||
|
||||
|
||||
# -- {{ }} double-brace preprocessor tests ----------------------------------
|
||||
|
||||
|
||||
def test_typstmath_double_brace_auto_numbered(config):
|
||||
"""{{ }} groups are auto-numbered and selectable by index."""
|
||||
eq = TypstMath("{{ a + b }} / {{ c - d }} = {{ x }}", use_svg_cache=False)
|
||||
assert eq._group_labels == ["_grp-0", "_grp-1", "_grp-2"]
|
||||
assert len(eq.select(0)) == 3 # a, +, b
|
||||
assert len(eq.select(1)) == 3 # c, -, d
|
||||
assert len(eq.select(2)) == 1 # x
|
||||
|
||||
|
||||
def test_typstmath_double_brace_named(config):
|
||||
"""{{ content : label }} assigns an explicit label."""
|
||||
eq = TypstMath("{{ a + b : numerator }} / {{ c - d : denom }}", use_svg_cache=False)
|
||||
assert "numerator" in eq._group_labels
|
||||
assert "denom" in eq._group_labels
|
||||
assert len(eq.select("numerator")) == 3
|
||||
assert len(eq.select("denom")) == 3
|
||||
|
||||
|
||||
def test_typstmath_double_brace_mixed_named_auto(config):
|
||||
"""Named and auto-numbered groups can coexist."""
|
||||
eq = TypstMath("{{ a : lhs }} = {{ b }}", use_svg_cache=False)
|
||||
assert eq._group_labels == ["lhs", "_grp-0"]
|
||||
assert len(eq.select("lhs")) == 1
|
||||
assert len(eq.select(0)) == 1
|
||||
|
||||
|
||||
def test_typstmath_no_braces_no_preamble(config):
|
||||
"""Without {{ }}, the manimgrp preamble is not injected."""
|
||||
eq = TypstMath("a + b", use_svg_cache=False)
|
||||
assert eq._group_labels == []
|
||||
assert "manimgrp" not in eq.typst_preamble
|
||||
|
||||
|
||||
def test_typstmath_select_index_error(config):
|
||||
"""select(int) raises IndexError for out-of-range index."""
|
||||
eq = TypstMath("{{ a }}", use_svg_cache=False)
|
||||
with pytest.raises(IndexError, match="out of range"):
|
||||
eq.select(1)
|
||||
|
||||
|
||||
def test_typstmath_preprocessor_skips_strings():
|
||||
"""{{ }} inside string literals are not processed."""
|
||||
processed, labels = TypstMath._preprocess_groups('x =_("{{ not a group }}") z')
|
||||
assert labels == []
|
||||
assert "manimgrp" not in processed
|
||||
|
||||
|
||||
def test_typstmath_preprocessor_skips_content_blocks():
|
||||
"""{{ }} inside [...] content blocks are not processed."""
|
||||
processed, labels = TypstMath._preprocess_groups("[text {{ here }}] {{ real }}")
|
||||
assert labels == ["_grp-0"]
|
||||
assert processed.count("manimgrp") == 1
|
||||
|
||||
|
||||
# -- integration tests for existing APIs ------------------------------------
|
||||
|
||||
|
||||
def test_label_accepts_typst(config):
|
||||
"""Label accepts a prebuilt Typst mobject."""
|
||||
rendered = Typst("hello", use_svg_cache=False)
|
||||
label = Label(rendered)
|
||||
assert label.rendered_label is rendered
|
||||
|
||||
|
||||
def test_numberline_add_labels_with_typstmath_constructor_uses_typst(config):
|
||||
"""String labels use Typst text mode when label_constructor is TypstMath."""
|
||||
number_line = NumberLine(x_range=[-1, 1])
|
||||
number_line.add_labels({0: "origin"}, label_constructor=TypstMath)
|
||||
assert len(number_line.labels) == 1
|
||||
assert isinstance(number_line.labels[0], Typst)
|
||||
assert not isinstance(number_line.labels[0], TypstMath)
|
||||
|
||||
|
||||
def test_vector_scene_get_vector_label_accepts_typst(config):
|
||||
"""VectorScene accepts a prebuilt Typst label mobject."""
|
||||
scene = VectorScene()
|
||||
vector = Vector(RIGHT)
|
||||
label = Typst("v", use_svg_cache=False)
|
||||
returned = scene.get_vector_label(vector, label)
|
||||
assert returned is label
|
||||
|
|
@ -61,3 +61,33 @@ def test_background_stroke_scale():
|
|||
b.scale(0.5, scale_stroke=True)
|
||||
assert a.get_stroke_width(background=True) == 50
|
||||
assert b.get_stroke_width(background=True) == 25
|
||||
|
||||
|
||||
def test_stroke_scale_preserves_relative_widths_in_compound_mobjects():
|
||||
"""Regression test for fix 429f25328 (PR #4694).
|
||||
|
||||
When ``scale(..., scale_stroke=True)`` is called on a compound VMobject
|
||||
whose submobjects have different stroke widths, the buggy version called
|
||||
``self.set_stroke(width=abs(scale_factor) * self.get_stroke_width())``,
|
||||
which uses the *parent's* stroke width and then propagates that single
|
||||
scaled value to the whole family — overwriting each submobject's own
|
||||
width. In particular, a submobject with zero stroke would gain non-zero
|
||||
stroke after scaling.
|
||||
|
||||
The fix iterates over ``self.get_family()`` and scales each submobject's
|
||||
stroke individually with ``family=False`` so the relative widths are
|
||||
preserved.
|
||||
"""
|
||||
from manim import VGroup
|
||||
|
||||
inner_with_stroke = VMobject()
|
||||
inner_with_stroke.set_stroke(width=4)
|
||||
inner_zero_stroke = VMobject()
|
||||
inner_zero_stroke.set_stroke(width=0)
|
||||
compound = VGroup(inner_with_stroke, inner_zero_stroke)
|
||||
|
||||
compound.scale(0.5, scale_stroke=True)
|
||||
|
||||
# Post-fix: each submob's width is scaled by 0.5 of its OWN value.
|
||||
assert inner_with_stroke.get_stroke_width() == 2
|
||||
assert inner_zero_stroke.get_stroke_width() == 0
|
||||
|
|
|
|||
|
|
@ -96,6 +96,29 @@ def test_vmobject_add_points_as_corners():
|
|||
np.testing.assert_allclose(obj1.points, obj3.points)
|
||||
|
||||
|
||||
def test_add_points_as_corners_single_point_connects_to_existing_path():
|
||||
"""Regression test for #4218 / fix f6cdb547 (PR #4219).
|
||||
|
||||
When ``add_points_as_corners`` is called with a single new point on a
|
||||
VMobject whose last subpath is complete (so ``has_new_path_started()``
|
||||
returns False), the buggy version silently dropped the new point — the
|
||||
``else`` branch computed ``start_corners = points[:-1]`` which is empty
|
||||
for a one-point input. The fix unifies the two branches so the existing
|
||||
path's last point is always used as the start corner.
|
||||
"""
|
||||
v = VMobject()
|
||||
v.start_new_path(np.array([0.0, 0.0, 0.0]))
|
||||
v.add_line_to(np.array([1.0, 0.0, 0.0]))
|
||||
assert not v.has_new_path_started()
|
||||
n_before = len(v.points)
|
||||
|
||||
v.add_points_as_corners([[2.0, 0.0, 0.0]])
|
||||
|
||||
# Post-fix: a cubic from [1, 0, 0] to [2, 0, 0] is appended.
|
||||
assert len(v.points) > n_before
|
||||
np.testing.assert_array_equal(v.points[-1], [2.0, 0.0, 0.0])
|
||||
|
||||
|
||||
def test_vmobject_point_from_proportion():
|
||||
obj = VMobject()
|
||||
|
||||
|
|
@ -528,6 +551,63 @@ def test_proportion_from_point():
|
|||
np.testing.assert_allclose(props, [0, 1 / 3, 2 / 3])
|
||||
|
||||
|
||||
def test_align_points_handles_vmobject_with_no_complete_cubic_curves():
|
||||
"""Regression test for #3569 / #4629 (fix 21cf9998 / PR #4630).
|
||||
|
||||
When ``align_points`` encounters a VMobject whose points array is
|
||||
non-empty but holds fewer than ``n_points_per_cubic_curve`` points,
|
||||
``get_subpaths()`` returns ``[]`` while ``has_no_points()`` returns
|
||||
``False`` — so the pre-loop sanitization that would normally add a
|
||||
null curve is skipped. The buggy ``get_nth_subpath`` closure then
|
||||
indexed ``path_list[-1]`` on the empty list and raised
|
||||
``IndexError: list index out of range``.
|
||||
|
||||
The fix returns a zero-valued null path in that case and ensures the
|
||||
closure always returns a NumPy array (the previous list return type
|
||||
broke downstream ``reshape`` calls).
|
||||
"""
|
||||
target = VMobject()
|
||||
target.set_points(
|
||||
np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0], [3.0, 0.0, 0.0]])
|
||||
)
|
||||
|
||||
sub_cubic = VMobject()
|
||||
sub_cubic.set_points(np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]]))
|
||||
assert sub_cubic.get_subpaths() == []
|
||||
assert not sub_cubic.has_no_points()
|
||||
|
||||
# Pre-fix: raises IndexError. Post-fix: completes; points are ndarray.
|
||||
target.align_points(sub_cubic)
|
||||
assert isinstance(target.points, np.ndarray)
|
||||
assert isinstance(sub_cubic.points, np.ndarray)
|
||||
|
||||
|
||||
def test_pointwise_become_partial_preserves_target_when_source_has_no_curves():
|
||||
"""Regression test for #4255 / fix 3d029c12 (PR #4320).
|
||||
|
||||
When ``pointwise_become_partial`` is called with a source ``VMobject`` that
|
||||
has zero cubic curves (e.g. an empty ``VMobject`` or a ``VectorizedPoint``
|
||||
holding a single point), the buggy version called ``self.clear_points()``
|
||||
on the *target*, zeroing out its data. The fix removes that call.
|
||||
|
||||
This bug surfaced as ``Arrow3D.get_start()`` / ``get_end()`` returning
|
||||
``[0, 0, 0]`` after a ``Create`` animation, because the arrow's
|
||||
``end_point`` sub-mobject has 1 point but no cubic curves.
|
||||
"""
|
||||
target = VMobject()
|
||||
original_points = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
|
||||
target.set_points(original_points)
|
||||
|
||||
empty_source = VMobject()
|
||||
assert empty_source.get_num_curves() == 0
|
||||
|
||||
# Choose a, b so the `(a <= 0 and b >= 1)` early-return is skipped
|
||||
# and the `num_curves == 0` branch is exercised.
|
||||
target.pointwise_become_partial(empty_source, 0.0, 0.5)
|
||||
|
||||
np.testing.assert_array_equal(target.points, original_points)
|
||||
|
||||
|
||||
def test_pointwise_become_partial_where_vmobject_is_self():
|
||||
sq = Square()
|
||||
sq.pointwise_become_partial(vmobject=sq, a=0.2, b=0.7)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from manim import Circle, Square, ThreeDScene
|
||||
from manim import DEGREES, Circle, Square, ThreeDScene
|
||||
|
||||
|
||||
def test_fixed_mobjects():
|
||||
|
|
@ -15,3 +15,12 @@ def test_fixed_mobjects():
|
|||
assert set(scene.camera.fixed_orientation_mobjects) == {s}
|
||||
scene.remove_fixed_orientation_mobjects(s)
|
||||
assert len(scene.camera.fixed_orientation_mobjects) == 0
|
||||
|
||||
|
||||
def test_set_to_default_angled_camera_orientation():
|
||||
scene = ThreeDScene()
|
||||
|
||||
scene.set_to_default_angled_camera_orientation(phi=45 * DEGREES)
|
||||
|
||||
assert scene.camera.get_phi() == 45 * DEGREES
|
||||
assert scene.camera.get_theta() == -135 * DEGREES
|
||||
|
|
|
|||
264
tests/module/utils/test_color_helpers.py
Normal file
264
tests/module/utils/test_color_helpers.py
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import numpy.testing as nt
|
||||
import pytest
|
||||
|
||||
from manim.utils.color import (
|
||||
BLACK,
|
||||
BLUE,
|
||||
GREEN,
|
||||
RED,
|
||||
WHITE,
|
||||
YELLOW,
|
||||
ManimColor,
|
||||
)
|
||||
from manim.utils.color.core import (
|
||||
RandomColorGenerator,
|
||||
average_color,
|
||||
color_gradient,
|
||||
color_to_int_rgb,
|
||||
color_to_int_rgba,
|
||||
color_to_rgb,
|
||||
color_to_rgba,
|
||||
hex_to_rgb,
|
||||
interpolate_color,
|
||||
invert_color,
|
||||
random_bright_color,
|
||||
random_color,
|
||||
rgb_to_color,
|
||||
rgb_to_hex,
|
||||
rgba_to_color,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parsing — one case per linearly independent input branch in ManimColor.__init__
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize( # : PT006
|
||||
("color_input", "expected_rgb"),
|
||||
[
|
||||
("#FF0000", (1.0, 0.0, 0.0)),
|
||||
("#F00", (1.0, 0.0, 0.0)),
|
||||
("RED", (0xFC / 255, 0x62 / 255, 0x55 / 255)),
|
||||
(0xFF0000, (1.0, 0.0, 0.0)),
|
||||
((255, 0, 0), (1.0, 0.0, 0.0)),
|
||||
((1.0, 0.0, 0.0), (1.0, 0.0, 0.0)),
|
||||
(RED, (0xFC / 255, 0x62 / 255, 0x55 / 255)),
|
||||
],
|
||||
ids=[
|
||||
"hex_long",
|
||||
"hex_short",
|
||||
"name",
|
||||
"packed_int",
|
||||
"int_tuple",
|
||||
"float_tuple",
|
||||
"ManimColor",
|
||||
],
|
||||
)
|
||||
def test_color_to_rgb_accepts_all_parsable_forms(color_input, expected_rgb) -> None:
|
||||
nt.assert_allclose(color_to_rgb(color_input), expected_rgb)
|
||||
|
||||
|
||||
def test_color_to_rgb_returns_a_float64_array_of_length_3() -> None:
|
||||
rgb = color_to_rgb("#123456")
|
||||
assert isinstance(rgb, np.ndarray)
|
||||
assert rgb.shape == (3,)
|
||||
assert rgb.dtype == np.float64
|
||||
|
||||
|
||||
def test_color_to_rgb_unknown_name_raises() -> None:
|
||||
with pytest.raises(ValueError, match="Color TOMATO not found"):
|
||||
color_to_rgb("TOMATO")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Alpha & int conversions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_color_to_rgba_default_alpha_is_opaque() -> None:
|
||||
nt.assert_array_equal(color_to_rgba("#FF0000"), (1.0, 0.0, 0.0, 1.0))
|
||||
|
||||
|
||||
def test_color_to_rgba_uses_alpha_argument() -> None:
|
||||
nt.assert_array_equal(color_to_rgba("#FF0000", alpha=0.25), (1.0, 0.0, 0.0, 0.25))
|
||||
|
||||
|
||||
def test_color_to_int_rgb_returns_signed_ints_in_0_255_range() -> None:
|
||||
int_rgb = color_to_int_rgb("#FF8040")
|
||||
nt.assert_array_equal(int_rgb, (0xFF, 0x80, 0x40))
|
||||
assert int_rgb.dtype.kind == "i"
|
||||
|
||||
|
||||
def test_color_to_int_rgba_default_alpha_is_fully_opaque_byte() -> None:
|
||||
# Pins the default alpha=1.0 in the signature (without this, mutations
|
||||
# to the default value silently survive).
|
||||
nt.assert_array_equal(color_to_int_rgba("#FF8040"), (0xFF, 0x80, 0x40, 255))
|
||||
|
||||
|
||||
def test_color_to_int_rgba_appends_alpha_byte() -> None:
|
||||
nt.assert_array_equal(
|
||||
color_to_int_rgba("#FF8040", alpha=0.5),
|
||||
(0xFF, 0x80, 0x40, int(0.5 * 255)),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Inverse direction — rgb_to_color / rgba_to_color route through from_rgb /
|
||||
# from_rgba, a different code path from ManimColor(value).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_rgb_to_color_normalizes_int_input_to_floats() -> None:
|
||||
assert rgb_to_color((255, 128, 0)) == ManimColor((1.0, 128 / 255, 0.0))
|
||||
|
||||
|
||||
def test_rgba_to_color_preserves_alpha() -> None:
|
||||
assert rgba_to_color((1.0, 0.0, 0.0, 0.25)) == ManimColor((1.0, 0.0, 0.0, 0.25))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hex ↔ RGB
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_rgb_to_hex_format_is_uppercase_with_hash() -> None:
|
||||
nt.assert_equal(rgb_to_hex((1.0, 0.0, 0xA0 / 255)), "#FF00A0")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"hex_input",
|
||||
["#000000", "#FFFFFF", "#FF0000", "#FF00A0"],
|
||||
)
|
||||
def test_hex_rgb_roundtrip_is_lossless_for_8bit_aligned_values(hex_input) -> None:
|
||||
nt.assert_equal(rgb_to_hex(hex_to_rgb(hex_input)), hex_input)
|
||||
|
||||
|
||||
def test_rgb_hex_roundtrip_drift_is_under_one_byte_per_channel() -> None:
|
||||
rgb = np.array([0.42, 0.18, 0.93])
|
||||
nt.assert_allclose(hex_to_rgb(rgb_to_hex(rgb)), rgb, atol=1 / 255)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# invert_color
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_invert_color_flips_white_to_black() -> None:
|
||||
# Anchors the actual semantic (1 - x), independent of the involution property.
|
||||
assert invert_color(WHITE) == BLACK
|
||||
|
||||
|
||||
@pytest.mark.parametrize("color", [RED, GREEN, BLUE, YELLOW])
|
||||
def test_invert_color_is_an_involution(color) -> None:
|
||||
# ManimColor.__eq__ uses np.allclose, absorbing the machine-epsilon drift.
|
||||
assert invert_color(invert_color(color)) == color
|
||||
|
||||
|
||||
def test_invert_color_preserves_alpha_by_default() -> None:
|
||||
c = ManimColor("#FF0000", alpha=0.3)
|
||||
nt.assert_equal(invert_color(c)._internal_value[3], 0.3)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# interpolate_color — three samples of a linear function in alpha
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize( # : PT006
|
||||
("alpha", "expected"),
|
||||
[
|
||||
(0.0, BLACK),
|
||||
(0.5, ManimColor((0.5, 0.5, 0.5))),
|
||||
(1.0, WHITE),
|
||||
],
|
||||
ids=["start", "midpoint", "end"],
|
||||
)
|
||||
def test_interpolate_color_between_black_and_white(alpha, expected) -> None:
|
||||
assert interpolate_color(BLACK, WHITE, alpha) == expected
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# average_color
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_average_color_of_black_and_white_is_mid_gray() -> None:
|
||||
assert average_color(BLACK, WHITE) == ManimColor((0.5, 0.5, 0.5))
|
||||
|
||||
|
||||
def test_average_color_always_returns_alpha_one() -> None:
|
||||
# average_color drops input alpha per its docstring contract.
|
||||
avg = average_color(
|
||||
ManimColor("#FF0000", alpha=0.1), ManimColor("#FF0000", alpha=0.9)
|
||||
)
|
||||
nt.assert_equal(avg._internal_value[3], 1.0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# color_gradient
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_color_gradient_zero_length_returns_empty_list() -> None:
|
||||
assert color_gradient([RED], 0) == []
|
||||
|
||||
|
||||
def test_color_gradient_empty_reference_with_positive_length_raises() -> None:
|
||||
with pytest.raises(ValueError, match="Expected 1 or more reference colors"):
|
||||
color_gradient([], 5)
|
||||
|
||||
|
||||
def test_color_gradient_single_reference_is_repeated_n_times() -> None:
|
||||
gradient = color_gradient([RED], 5)
|
||||
assert len(gradient) == 5
|
||||
assert all(color == RED for color in gradient)
|
||||
|
||||
|
||||
def test_color_gradient_interpolates_endpoints_and_respects_length() -> None:
|
||||
gradient = color_gradient([BLACK, WHITE], 7)
|
||||
assert len(gradient) == 7
|
||||
assert gradient[0] == BLACK
|
||||
assert gradient[-1] == WHITE
|
||||
|
||||
|
||||
def test_color_gradient_passes_through_each_of_four_reference_colors() -> None:
|
||||
# With >= 4 reference colors the internal `num_colors - 2` bookkeeping
|
||||
# diverges from `num_colors % 2`; pins the former.
|
||||
refs = [BLACK, RED, BLUE, WHITE]
|
||||
gradient = color_gradient(refs, 4)
|
||||
assert len(gradient) == 4
|
||||
assert gradient[0] == BLACK
|
||||
assert gradient[1] == RED
|
||||
assert gradient[2] == BLUE
|
||||
assert gradient[3] == WHITE
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Random color machinery
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_random_color_returns_a_manim_color() -> None:
|
||||
assert isinstance(random_color(), ManimColor)
|
||||
|
||||
|
||||
def test_random_bright_color_has_every_channel_at_or_above_half() -> None:
|
||||
# By construction: 0.5 * (random_rgb + 1) => each channel >= 0.5.
|
||||
assert (random_bright_color().to_rgb() >= 0.5).all()
|
||||
|
||||
|
||||
def test_random_color_generator_is_deterministic_under_a_fixed_seed() -> None:
|
||||
a = RandomColorGenerator(seed=42)
|
||||
b = RandomColorGenerator(seed=42)
|
||||
for _ in range(5):
|
||||
assert a.next() == b.next()
|
||||
|
||||
|
||||
def test_random_color_generator_only_samples_from_custom_palette() -> None:
|
||||
palette = [RED, GREEN, BLUE]
|
||||
gen = RandomColorGenerator(seed=1, sample_colors=palette)
|
||||
for _ in range(10):
|
||||
assert gen.next() in palette
|
||||
282
uv.lock
generated
282
uv.lock
generated
|
|
@ -263,14 +263,14 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "bleach"
|
||||
version = "6.3.0"
|
||||
version = "6.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "webencodings" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/48/3c/e12ac860709702bd5ebeb9b56a4fe334f1001246ee1b8f2b7ee28912df7d/bleach-6.4.0.tar.gz", hash = "sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452", size = 204857, upload-time = "2026-06-05T13:01:13.734Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6", size = 164437, upload-time = "2025-10-27T17:57:37.538Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl", hash = "sha256:4b6b6a54fff2e69a3dde9d21cc6301220bee3c3cb792187d11403fd795031081", size = 165109, upload-time = "2026-06-05T13:01:12.504Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
|
|
@ -952,11 +952,11 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
version = "3.15"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1203,7 +1203,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "jupyter-server"
|
||||
version = "2.17.0"
|
||||
version = "2.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
|
|
@ -1226,9 +1226,9 @@ dependencies = [
|
|||
{ name = "traitlets" },
|
||||
{ name = "websocket-client" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/ac/e040ec363d7b6b1f11304cc9f209dac4517ece5d5e01821366b924a64a50/jupyter_server-2.17.0.tar.gz", hash = "sha256:c38ea898566964c888b4772ae1ed58eca84592e88251d2cfc4d171f81f7e99d5", size = 731949, upload-time = "2025-08-21T14:42:54.042Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6b/dc/db3a582633170186f8c8b31298d7eb26ad0eb031a1f53476c258b64eed05/jupyter_server-2.20.0.tar.gz", hash = "sha256:b5778ba337d8015a3dc2b80803ecdd5ac18d3797fddf61a50ea5fb472b4ebe14", size = 756523, upload-time = "2026-06-17T12:09:09.435Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl", hash = "sha256:e8cb9c7db4251f51ed307e329b81b72ccf2056ff82d50524debde1ee1870e13f", size = 388221, upload-time = "2025-08-21T14:42:52.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/71/8c002223e873a870f5c41dc69b0a7c922301123e4a31d5d01ecb700aef77/jupyter_server-2.20.0-py3-none-any.whl", hash = "sha256:c3b67c93c471e947c18b5026f04f21614218adb706df8f48227d3ee8e0a7cdcc", size = 393143, upload-time = "2026-06-17T12:09:07.234Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1246,7 +1246,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "jupyterlab"
|
||||
version = "4.5.2"
|
||||
version = "4.5.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "async-lru" },
|
||||
|
|
@ -1263,9 +1263,9 @@ dependencies = [
|
|||
{ name = "tornado" },
|
||||
{ name = "traitlets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/93/dc/2c8c4ff1aee27ac999ba04c373c5d0d7c6c181b391640d7b916b884d5985/jupyterlab-4.5.2.tar.gz", hash = "sha256:c80a6b9f6dace96a566d590c65ee2785f61e7cd4aac5b4d453dcc7d0d5e069b7", size = 23990371, upload-time = "2026-01-12T12:27:08.493Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/52/a8d4895bef501ffeb6af448e8bf7079541c7772978211963aa653518c2d9/jupyterlab-4.5.9.tar.gz", hash = "sha256:dd79a073fecae7a39066ea99e4627ed6c76269ac926e95a810e1e1df6358d865", size = 23994445, upload-time = "2026-06-17T15:42:16.406Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/78/7e455920f104ef2aa94a4c0d2b40e5b44334ee7057eae1aa1fb97b9631ad/jupyterlab-4.5.2-py3-none-any.whl", hash = "sha256:76466ebcfdb7a9bb7e2fbd6459c0e2c032ccf75be673634a84bee4b3e6b13ab6", size = 12385807, upload-time = "2026-01-12T12:27:03.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/bb/2f9b425062416fba58f580c9b89c3b07277ccdf0a292501fedbca8ea00ea/jupyterlab-4.5.9-py3-none-any.whl", hash = "sha256:5ff0f908e8ac0afbed32b106fdef360f101c0a6654d1bf4a81e98a293ae1b336", size = 12449803, upload-time = "2026-06-17T15:42:12.18Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1435,6 +1435,9 @@ jupyterlab = [
|
|||
{ name = "jupyterlab" },
|
||||
{ name = "notebook" },
|
||||
]
|
||||
typst = [
|
||||
{ name = "typst" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
|
|
@ -1490,9 +1493,10 @@ requires-dist = [
|
|||
{ name = "svgelements", specifier = ">=1.9.0" },
|
||||
{ name = "tqdm", specifier = ">=4.21.0" },
|
||||
{ name = "typing-extensions", specifier = ">=4.12.0" },
|
||||
{ name = "typst", marker = "extra == 'typst'", specifier = ">=0.14" },
|
||||
{ name = "watchdog", specifier = ">=2.0.0" },
|
||||
]
|
||||
provides-extras = ["gui", "jupyterlab"]
|
||||
provides-extras = ["gui", "jupyterlab", "typst"]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
|
|
@ -1781,11 +1785,11 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "mistune"
|
||||
version = "3.2.0"
|
||||
version = "3.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9d/55/d01f0c4b45ade6536c51170b9043db8b2ec6ddf4a35c7ea3f5f559ac935b/mistune-3.2.0.tar.gz", hash = "sha256:708487c8a8cdd99c9d90eb3ed4c3ed961246ff78ac82f03418f5183ab70e398a", size = 95467, upload-time = "2025-12-23T11:36:34.994Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/84/620cc3f7e3adf6f5067e10f4dbae71295d8f9e16d5d3f9ef97c40f2f592c/mistune-3.2.1.tar.gz", hash = "sha256:7c8e5501d38bac1582e067e46c8343f17d57ea1aaa735823f3aba1fd59c88a28", size = 98003, upload-time = "2026-05-03T14:33:22.312Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/7f/a946aa4f8752b37102b41e64dca18a1976ac705c3a0d1dfe74d820a02552/mistune-3.2.1-py3-none-any.whl", hash = "sha256:78cdb0ba5e938053ccf63651b352508d2efa9411dc8810bfb05f2dc5140c0048", size = 53749, upload-time = "2026-05-03T14:33:20.551Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1873,7 +1877,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "nbconvert"
|
||||
version = "7.17.0"
|
||||
version = "7.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "beautifulsoup4" },
|
||||
|
|
@ -1891,9 +1895,9 @@ dependencies = [
|
|||
{ name = "pygments" },
|
||||
{ name = "traitlets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/38/47/81f886b699450d0569f7bc551df2b1673d18df7ff25cc0c21ca36ed8a5ff/nbconvert-7.17.0.tar.gz", hash = "sha256:1b2696f1b5be12309f6c7d707c24af604b87dfaf6d950794c7b07acab96dda78", size = 862855, upload-time = "2026-01-29T16:37:48.478Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/b1/708e53fe2e429c103c6e6e159106bcf0357ac41aa4c28772bd8402339051/nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2", size = 865311, upload-time = "2026-04-08T00:44:14.914Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl", hash = "sha256:4f99a63b337b9a23504347afdab24a11faa7d86b405e5c8f9881cd313336d518", size = 261510, upload-time = "2026-01-29T16:37:46.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8", size = 261927, upload-time = "2026-04-08T00:44:12.845Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1940,7 +1944,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "notebook"
|
||||
version = "7.5.2"
|
||||
version = "7.5.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jupyter-server" },
|
||||
|
|
@ -1949,9 +1953,9 @@ dependencies = [
|
|||
{ name = "notebook-shim" },
|
||||
{ name = "tornado" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3b/b6/6b2c653570b02e4ec2a94c0646a4a25132be0749617776d0b72a2bcedb9b/notebook-7.5.2.tar.gz", hash = "sha256:83e82f93c199ca730313bea1bb24bc279ea96f74816d038a92d26b6b9d5f3e4a", size = 14059605, upload-time = "2026-01-12T14:56:53.483Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/c2/cf59bd2e6f2c8b976b52477e3e53bf6f97bc714ed046a51821afb428eaee/notebook-7.5.6.tar.gz", hash = "sha256:621174aade80108f0020b0f00738000b215f75fa3cd90771ad7aa0f24536a4e1", size = 14170814, upload-time = "2026-04-30T11:46:26.613Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/32/55/b754cd51c6011d90ef03e3f06136f1ebd44658b9529dbcf0c15fc0d6a0b7/notebook-7.5.2-py3-none-any.whl", hash = "sha256:17d078a98603d70d62b6b4b3fcb67e87d7a68c398a7ae9b447eb2d7d9aec9979", size = 14468915, upload-time = "2026-01-12T14:56:47.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/d6/1fd0646b9bbd9efbb0b8ae21b2325fbef515769a5621c03e31d8eb8da587/notebook-7.5.6-py3-none-any.whl", hash = "sha256:4dde3f8fb55fa8fb7946d58c6e869ce9baf46d00fc070664f62604569d0faca0", size = 14581730, upload-time = "2026-04-30T11:46:22.342Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2095,89 +2099,89 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "12.1.1"
|
||||
version = "12.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2399,11 +2403,11 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
version = "2.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2448,7 +2452,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.2"
|
||||
version = "9.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
|
|
@ -2457,9 +2461,9 @@ dependencies = [
|
|||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2667,7 +2671,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.5"
|
||||
version = "2.33.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
|
|
@ -2675,9 +2679,9 @@ dependencies = [
|
|||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3295,21 +3299,19 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "tornado"
|
||||
version = "6.5.4"
|
||||
version = "6.5.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/37/1d/0a336abf618272d53f62ebe274f712e213f5a03c0b2339575430b8362ef2/tornado-6.5.4.tar.gz", hash = "sha256:a22fa9047405d03260b483980635f0b041989d8bcc9a313f8fe18b411d84b1d7", size = 513632, upload-time = "2025-12-15T19:21:03.836Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/64/24/95ec527ad67b76d59299e5465b3935d05e4294b7e0290a3924b7487df30b/tornado-6.5.7.tar.gz", hash = "sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2", size = 519252, upload-time = "2026-06-08T17:34:51.232Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/a9/e94a9d5224107d7ce3cc1fab8d5dc97f5ea351ccc6322ee4fb661da94e35/tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d6241c1a16b1c9e4cc28148b1cda97dd1c6cb4fb7068ac1bedc610768dff0ba9", size = 443909, upload-time = "2025-12-15T19:20:48.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/7e/f7b8d8c4453f305a51f80dbb49014257bb7d28ccb4bbb8dd328ea995ecad/tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2d50f63dda1d2cac3ae1fa23d254e16b5e38153758470e9956cbc3d813d40843", size = 442163, upload-time = "2025-12-15T19:20:49.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/b5/206f82d51e1bfa940ba366a8d2f83904b15942c45a78dd978b599870ab44/tornado-6.5.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cf66105dc6acb5af613c054955b8137e34a03698aa53272dbda4afe252be17", size = 445746, upload-time = "2025-12-15T19:20:51.491Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/9d/1a3338e0bd30ada6ad4356c13a0a6c35fbc859063fa7eddb309183364ac1/tornado-6.5.4-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50ff0a58b0dc97939d29da29cd624da010e7f804746621c78d14b80238669335", size = 445083, upload-time = "2025-12-15T19:20:52.778Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/d4/e51d52047e7eb9a582da59f32125d17c0482d065afd5d3bc435ff2120dc5/tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fb5e04efa54cf0baabdd10061eb4148e0be137166146fff835745f59ab9f7f", size = 445315, upload-time = "2025-12-15T19:20:53.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/07/2273972f69ca63dbc139694a3fc4684edec3ea3f9efabf77ed32483b875c/tornado-6.5.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9c86b1643b33a4cd415f8d0fe53045f913bf07b4a3ef646b735a6a86047dda84", size = 446003, upload-time = "2025-12-15T19:20:56.101Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/83/41c52e47502bf7260044413b6770d1a48dda2f0246f95ee1384a3cd9c44a/tornado-6.5.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:6eb82872335a53dd063a4f10917b3efd28270b56a33db69009606a0312660a6f", size = 445412, upload-time = "2025-12-15T19:20:57.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/c7/bc96917f06cbee182d44735d4ecde9c432e25b84f4c2086143013e7b9e52/tornado-6.5.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6076d5dda368c9328ff41ab5d9dd3608e695e8225d1cd0fd1e006f05da3635a8", size = 445392, upload-time = "2025-12-15T19:20:58.692Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/1a/d7592328d037d36f2d2462f4bc1fbb383eec9278bc786c1b111cbbd44cfa/tornado-6.5.4-cp39-abi3-win32.whl", hash = "sha256:1768110f2411d5cd281bac0a090f707223ce77fd110424361092859e089b38d1", size = 446481, upload-time = "2025-12-15T19:21:00.008Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/6d/c69be695a0a64fd37a97db12355a035a6d90f79067a3cf936ec2b1dc38cd/tornado-6.5.4-cp39-abi3-win_amd64.whl", hash = "sha256:fa07d31e0cd85c60713f2b995da613588aa03e1303d75705dca6af8babc18ddc", size = 446886, upload-time = "2025-12-15T19:21:01.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/49/8dc3fd90902f70084bd2cd059d576ddb4f8bb44c2c7c0e33a11422acb17e/tornado-6.5.4-cp39-abi3-win_arm64.whl", hash = "sha256:053e6e16701eb6cbe641f308f4c1a9541f91b6261991160391bfc342e8a551a1", size = 445910, upload-time = "2025-12-15T19:21:02.571Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163", size = 448543, upload-time = "2026-06-08T17:34:38.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/4f/090b1431e5a43df696feceffc268c5383cc079ecb5f08ce58f917109aafe/tornado-6.5.7-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100", size = 446707, upload-time = "2026-06-08T17:34:39.594Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/d8/ef374952fd5da67d4463122c2b8e5a96536ec10b4b339254c6dcde81d01c/tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972", size = 449774, upload-time = "2026-06-08T17:34:41.204Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/37/d434c73f4c6e014b745b9b37085f34f40c022f007efff3d7fe65991899f3/tornado-6.5.7-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b", size = 450745, upload-time = "2026-06-08T17:34:42.531Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/2b/56b9aff361d7f1ab728a805ec7d7ea835f8807afa9f5cc690ea0e630efb9/tornado-6.5.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92", size = 450578, upload-time = "2026-06-08T17:34:43.787Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/30/a7444fb23aa76860a14198fab96ac79f1866b0a6e19e26c4381b0938e50f/tornado-6.5.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5", size = 449985, upload-time = "2026-06-08T17:34:45.326Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/42/5f0e56c01e8d9d36f4e23f367b85ae6cae0c1ecddd5e6977d8388ad27488/tornado-6.5.7-cp39-abi3-win32.whl", hash = "sha256:f8de3bf12d3efdd0cbe7c8887868198f8a91415e3f29fcf258d9b8eb7b1d9ae4", size = 451047, upload-time = "2026-06-08T17:34:46.784Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/a4/b393076ffb21b469eec5b328a0534cf03a3b90bfc6b1f09507cdd075d938/tornado-6.5.7-cp39-abi3-win_amd64.whl", hash = "sha256:de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4", size = 451485, upload-time = "2026-06-08T17:34:48.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/2e/7b1c769803121b809112cf9a00681c472eae1d80e32d7ec0e0bd61d0d0e1/tornado-6.5.7-cp39-abi3-win_arm64.whl", hash = "sha256:ff934fce95643af5f11efdae618eaa73d469dc588641e5c8d19295a0c65c4796", size = 450506, upload-time = "2026-06-08T17:34:49.702Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3381,6 +3383,30 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typst"
|
||||
version = "0.14.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0e/17/011059074fe6c51ed775991d5066c73443f17d49b3d4ab9c1a969dcdb5cb/typst-0.14.8.tar.gz", hash = "sha256:8ffb8d5896aa6a20a7b88ae3fa1dfcf062fdd09b5b6a0a164f92f78ad1a2d8cd", size = 62369, upload-time = "2026-02-08T02:31:21.753Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/67/af5551e95261fc425f6dbf241ec08bf1172fd10ef239787ff6e009bb2f08/typst-0.14.8-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:4697b9de12d7b1bc85209960e1ef7e2c4947cffd7d6ef68201aea03597cf38bd", size = 22935370, upload-time = "2026-02-08T02:30:30.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/93/cbb32c7e830a806105ee0f6d9b6c780f2736a9c75d8121602e7842a316d2/typst-0.14.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ecb523ff7e3eb68667ad693ff4c460ac58aedfbeb6514054efce2718e7563f", size = 22624078, upload-time = "2026-02-08T02:30:33.762Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/38/070c068442a8be93125366b27e5cf1a6b1dd62c85dab62bd6d4355643d29/typst-0.14.8-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9db137ca037bd12c0ebbbbfa1190fffaa75a2043d04adacb273cc98f0265a32", size = 26894087, upload-time = "2026-02-08T02:30:36.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/32/8754413c4cdf631c51e16690775dcfd28e783c1ccc0efc71d92ef73e0db3/typst-0.14.8-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d8c4ac751c3480b0fcfc7fce273025bb7392654db5a3aa65904f8678192c54f8", size = 26489748, upload-time = "2026-02-08T02:30:40.169Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/2b/3b1256033c7b971d0c79af41fadff552c1df7a9f9774a540f1a2ede97937/typst-0.14.8-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37da60ec4afcd82b55664612aab10cac11a8ebc075686057705261de9e901523", size = 28023293, upload-time = "2026-02-08T02:30:43.222Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/1b/8769c89998299525e4b04fddce1b15977d18051695c65760203b55f7ed47/typst-0.14.8-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1cfbc313ba3b883da8c45233506766a503da307057a5d8d39e360023733c463", size = 27109055, upload-time = "2026-02-08T02:30:46.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/97/b1f43e29051401289b6ef37398eb83d78584f52e0b213f8675b9b10b0c0b/typst-0.14.8-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b509e7a599dd07e36e18495f0258511de527f5e0dc145622025d204c84db5246", size = 26017464, upload-time = "2026-02-08T02:30:49.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/ca/44732fc1e486be822ba65ee9a02f0bc5f28d1cb08284c9dd1286d975f9ce/typst-0.14.8-cp314-cp314t-win_amd64.whl", hash = "sha256:10710c58dbc8820a954970ba5d0af5611c7c57f8ddacfebb1a85ddb6449f01eb", size = 21471708, upload-time = "2026-02-08T02:30:54.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/cb/e49219a75d39ce866ae5d64e0a1d8d712b394ed3a1e7de3a8f4a35cde78e/typst-0.14.8-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f47fe029f6ebe907f981ce0cb5208eab27eaf7342e319e6c798ac1dbae976f58", size = 22936285, upload-time = "2026-02-08T02:30:57.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/6b/d36f312c32b70303abd88d0abe6ffb50f8f7fcc0b457c914c78d791ed934/typst-0.14.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:aba11243463f6994ca1140b8515e70be1a98fd3025ae3211b84103499b0c5a5a", size = 22632767, upload-time = "2026-02-08T02:31:00.454Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/b4/87d2d24078b94645ba8788c8b4a5bbab6a3c779370141c31a02e2003ee0f/typst-0.14.8-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:544fcd9ce55b140115d7442b3661c45897778650c307e2eb0749efed29bbfcea", size = 26907232, upload-time = "2026-02-08T02:31:03.528Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/e8/3efdebcf37639daa4799e7a4c833a280f14685f7e6058fed576c6fb2e722/typst-0.14.8-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a296f85bf0d27043b031d1d2d74a34802e4876a8936f70784fbe99021b0dad4d", size = 26501791, upload-time = "2026-02-08T02:31:06.898Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/28/094d4b9f0ff4ee81f88eee2df00dbcfbd961070df981973bc385a1544ff8/typst-0.14.8-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e3891a2e5551017c9030dd6de31587a29b97c18464df6bcff05f30f7cdab677", size = 28028881, upload-time = "2026-02-08T02:31:10.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/a1/15cd399dfc5ce0ea9e05d5bbc274c95f8ecababc04b4210bae8d583fe454/typst-0.14.8-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a19cf938607c73fd8c5245a7cb32c94af413080a3d747fcf7e16df88713c686", size = 27128399, upload-time = "2026-02-08T02:31:13.597Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/6f/ff1c58dac9245d4c355bfced006090b14a2f17497e9cf79a84d9d720663a/typst-0.14.8-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12766a83e390377008722a8c80afdd9195a297261fd3c9d1f3720f9aecd2b19", size = 26026753, upload-time = "2026-02-08T02:31:16.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/42/db15d775c09f0da92191ea1b50cee056e46b599bb5524e2d8ff51f973765/typst-0.14.8-cp38-abi3-win_amd64.whl", hash = "sha256:66eb2ebfe13275cf2a63ed7ff261eb5af3da5293077a5d6ca16e27a96d0d2f5e", size = 21475900, upload-time = "2026-02-08T02:31:19.484Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2025.3"
|
||||
|
|
@ -3401,11 +3427,11 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.6.3"
|
||||
version = "2.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue