diff --git a/examples/formula.py b/examples/formula.py index 1a532ff..e96ec1f 100644 --- a/examples/formula.py +++ b/examples/formula.py @@ -12,20 +12,16 @@ async def construct( self: Self ) -> None: factored_formula = Math( - "(a_0^2 + a_1^2) (b_0^2 + b_1^2 + b_2^2)" + "(a_0 + a_1) (b_0 + b_1 + b_2)" ).set_local_colors({ - "a": TEAL, - re.compile(r"(?<=a_)\d"): TEAL, - "b": ORANGE, - re.compile(r"(?<=b_)\d"): ORANGE + re.compile(r"a_\d"): TEAL, + re.compile(r"b_\d"): ORANGE }).shift(UP) expanded_formula = Math( - "a_0^2 b_0^2 + a_0^2 b_1^2 + a_0^2 b_2^2 + a_1^2 b_0^2 + a_1^2 b_1^2 + a_1^2 b_2^2" + "a_0 b_0 + a_0 b_1 + a_0 b_2 + a_1 b_0 + a_1 b_1 + a_1 b_2" ).set_local_colors({ - "a": TEAL, - re.compile(r"(?<=a_)\d"): TEAL, - "b": ORANGE, - re.compile(r"(?<=b_)\d"): ORANGE + re.compile(r"a_\d"): TEAL, + re.compile(r"b_\d"): ORANGE }).shift(DOWN) self.add(factored_formula) await self.wait() diff --git a/manim3/mobjects/string_mobjects/code_mobject.py b/manim3/mobjects/string_mobjects/code_mobject.py index e6677b0..40b4e7b 100644 --- a/manim3/mobjects/string_mobjects/code_mobject.py +++ b/manim3/mobjects/string_mobjects/code_mobject.py @@ -1,6 +1,8 @@ from __future__ import annotations +import itertools +import pathlib from typing import ( Self, Unpack @@ -20,11 +22,18 @@ class CodeKwargs(TypstMobjectKwargs, total=False): syntax: str + theme: str | pathlib.Path | None @attrs.frozen(kw_only=True) class CodeInputs(TypstMobjectInputs): - syntax: str = attrs.field(factory=lambda: Toplevel._get_config().code_syntax) + syntax: str = attrs.field( + factory=lambda: Toplevel._get_config().code_syntax + ) + theme: pathlib.Path | None = attrs.field( + factory=lambda: Toplevel._get_config().code_theme, + converter=lambda theme: None if theme is None else pathlib.Path(theme) if isinstance(theme, str) else theme + ) class Code(TypstMobject[CodeInputs]): @@ -37,10 +46,24 @@ def __init__( ) -> None: super().__init__(CodeInputs(string=string, **kwargs)) + @classmethod + def _get_preamble_from_inputs( + cls: type[Self], + inputs: CodeInputs, + temp_path: pathlib.Path + ) -> str: + return "\n".join(filter(None, ( + super()._get_preamble_from_inputs(inputs, temp_path), + f"""#set raw(theme: "{ + pathlib.Path("/".join(itertools.repeat("..", len(temp_path.parts) - 1))).joinpath(inputs.theme) + }")""" if inputs.theme is not None else "" + ))) + @classmethod def _get_environment_pair_from_inputs( cls: type[Self], - inputs: CodeInputs + inputs: CodeInputs, + temp_path: pathlib.Path ) -> tuple[str, str]: return f"```{inputs.syntax}\n", "\n```" diff --git a/manim3/mobjects/string_mobjects/math_mobject.py b/manim3/mobjects/string_mobjects/math_mobject.py index 2beaad5..e55c602 100644 --- a/manim3/mobjects/string_mobjects/math_mobject.py +++ b/manim3/mobjects/string_mobjects/math_mobject.py @@ -2,6 +2,7 @@ import itertools +import pathlib import re from typing import ( Self, @@ -25,7 +26,9 @@ class MathKwargs(TypstMobjectKwargs, total=False): @attrs.frozen(kw_only=True) class MathInputs(TypstMobjectInputs): - inline: bool = attrs.field(factory=lambda: Toplevel._get_config().math_inline) + inline: bool = attrs.field( + factory=lambda: Toplevel._get_config().math_inline + ) class Math(TypstMobject[MathInputs]): @@ -41,7 +44,8 @@ def __init__( @classmethod def _get_environment_pair_from_inputs( cls: type[Self], - inputs: MathInputs + inputs: MathInputs, + temp_path: pathlib.Path ) -> tuple[str, str]: if inputs.inline: return "$", "$" diff --git a/manim3/mobjects/string_mobjects/typst_mobject.py b/manim3/mobjects/string_mobjects/typst_mobject.py index 1c9cf77..d395ae2 100644 --- a/manim3/mobjects/string_mobjects/typst_mobject.py +++ b/manim3/mobjects/string_mobjects/typst_mobject.py @@ -64,12 +64,23 @@ def _docstring_trim( trimmed.pop(0) return "\n".join(trimmed) - string: str = attrs.field(converter=_docstring_trim) - preamble: str = attrs.field(factory=lambda: Toplevel._get_config().typst_preamble, converter=_docstring_trim) + string: str = attrs.field( + converter=_docstring_trim + ) + preamble: str = attrs.field( + factory=lambda: Toplevel._get_config().typst_preamble, + converter=_docstring_trim + ) concatenate: bool = False - align: str | None = attrs.field(factory=lambda: Toplevel._get_config().typst_align) - font: str | tuple[str, ...] | None = attrs.field(factory=lambda: Toplevel._get_config().typst_font) - color: ColorType | None = attrs.field(factory=lambda: Toplevel._get_config().default_color) + align: str | None = attrs.field( + factory=lambda: Toplevel._get_config().typst_align + ) + font: str | tuple[str, ...] | None = attrs.field( + factory=lambda: Toplevel._get_config().typst_font + ) + color: ColorType | None = attrs.field( + factory=lambda: Toplevel._get_config().default_color + ) class TypstMobject[TypstMobjectInputsT: TypstMobjectInputs](CachedMobject[TypstMobjectInputsT]): @@ -93,8 +104,8 @@ def _generate_shape_mobjects( inputs: TypstMobjectInputsT, temp_path: pathlib.Path ) -> tuple[ShapeMobject, ...]: - preamble = cls._get_preamble_from_inputs(inputs) - environment_begin, environment_end = cls._get_environment_pair_from_inputs(inputs) + preamble = cls._get_preamble_from_inputs(inputs, temp_path) + environment_begin, environment_end = cls._get_environment_pair_from_inputs(inputs, temp_path) content = "\n".join(filter(None, ( preamble, inputs.preamble, @@ -106,18 +117,14 @@ def _generate_shape_mobjects( typst_path.write_text(content, encoding="utf-8") try: - if (stdout := subprocess.check_output(( + subprocess.check_output(( "typst", "compile", + "--root", pathlib.Path(), typst_path, svg_path - ))): - error = OSError("Typst error") - error.add_note(stdout.decode()) - raise error - + ), stderr=subprocess.STDOUT) shape_mobjects = SVGMobject._generate_shape_mobjects_from_svg(svg_path) - finally: for path in (svg_path, typst_path): path.unlink(missing_ok=True) @@ -131,7 +138,8 @@ def _generate_shape_mobjects( @classmethod def _get_preamble_from_inputs( cls: type[Self], - inputs: TypstMobjectInputsT + inputs: TypstMobjectInputsT, + temp_path: pathlib.Path ) -> str: return "\n".join(filter(None, ( f"""#set align({ @@ -150,7 +158,8 @@ def _get_preamble_from_inputs( @classmethod def _get_environment_pair_from_inputs( cls: type[Self], - inputs: TypstMobjectInputsT + inputs: TypstMobjectInputsT, + temp_path: pathlib.Path ) -> tuple[str, str]: return "", "" @@ -163,16 +172,7 @@ def _get_labelled_inputs( ) -> TypstMobjectInputsT: pass - def _build_from_selector( - self: Self, - selector: SelectorType - ) -> ShapeMobject: - return ShapeMobject().add(*( - self._shape_mobjects[index] - for index in self._selector_to_indices_dict[selector] - )) - - def probe( + def _probe_indices_from_selectors( self: Self, selectors: tuple[SelectorType, ...] ) -> None: @@ -204,18 +204,27 @@ def probe( continue self._selector_to_indices_dict[label_to_selector_dict[label]].append(index) + def _build_from_selector( + self: Self, + selector: SelectorType + ) -> ShapeMobject: + return ShapeMobject().add(*( + self._shape_mobjects[index] + for index in self._selector_to_indices_dict[selector] + )) + def select( self: Self, selector: SelectorType ) -> ShapeMobject: - self.probe((selector,)) + self._probe_indices_from_selectors((selector,)) return self._build_from_selector(selector) def set_local_styles( self: Self, selector_to_kwargs_dict: dict[SelectorType, SetKwargs] ) -> Self: - self.probe(tuple(selector_to_kwargs_dict)) + self._probe_indices_from_selectors(tuple(selector_to_kwargs_dict)) for selector, kwargs in selector_to_kwargs_dict.items(): self._build_from_selector(selector).set(**kwargs) return self @@ -229,3 +238,11 @@ def set_local_colors( for selector, color in selector_to_color_dict.items() }) return self + + def set_local_color( + self: Self, + selector: SelectorType, + color: ColorType + ) -> Self: + self.set_local_colors({selector: color}) + return self diff --git a/manim3/toplevel/config.py b/manim3/toplevel/config.py index 537f932..e4eaa1d 100644 --- a/manim3/toplevel/config.py +++ b/manim3/toplevel/config.py @@ -46,6 +46,7 @@ class Config(ToplevelResource): typst_font: str | tuple[str, ...] | None = None math_inline: bool = False code_syntax: str = "py" + code_theme: pathlib.Path | None = None shader_search_dirs: tuple[pathlib.Path, ...] = ( pathlib.Path(),