Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion docs/source/conduct.md

This file was deleted.

14 changes: 13 additions & 1 deletion example_scenes/basic.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python


from manim import *
from manim.mobject.three_d.implicit_surface import ImplicitSurface

# To watch one of these scenes, run the following:
# python --quality m manim -p example_scenes.py SquareToCircle
Expand Down Expand Up @@ -176,4 +176,16 @@ def construct(self):
self.add(grp)


class ExampleImplicitSurface(ThreeDScene):
def construct(self):
self.set_camera_orientation(phi=70 * DEGREES, theta=45 * DEGREES)
surface = ImplicitSurface(
lambda x, y, z: x**2 + y**2 + z**2 - 1,
resolution=30,
color=BLUE,
)
self.add(surface)
self.wait()


# See many more examples at https://docs.manim.community/en/stable/examples.html
77 changes: 77 additions & 0 deletions manim/mobject/three_d/implicit_surface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from __future__ import annotations

from collections.abc import Sequence
from typing import Any, Callable

import numpy as np
from skimage import measure

from manim import *

Check notice

Code scanning / CodeQL

'import *' may pollute namespace Note

Import pollutes the enclosing namespace, as the imported module
manim
does not define '__all__'.
from manim.mobject.three_d.three_dimensions import ThreeDVMobject
from manim.utils.color import ManimColor


class ImplicitSurface(ThreeDVMobject):
"""Render an implicit isosurface using the Marching Cubes algorithm.

Parameters
----------
func : Callable[[float | np.ndarray, float | np.ndarray, float | np.ndarray], float | np.ndarray]
Implicit function f(x, y, z). The surface is defined where f(x, y, z) = isolevel.
resolution : int, default=25
Number of divisions per axis.
isolevel : float, default=0.0
The isosurface level.
x_range, y_range, z_range : Sequence[float], default=(-2, 2)
Sampling ranges.
color : Color, default=BLUE
Color of the surface.
**kwargs
Additional arguments passed to ThreeDVMobject.
"""

def __init__(
self,
func: Callable[
[float | np.ndarray, float | np.ndarray, float | np.ndarray],
float | np.ndarray,
],
resolution: int = 25,
isolevel: float = 0.0,
x_range: Sequence[float] = (-2.0, 2.0),
y_range: Sequence[float] = (-2.0, 2.0),
z_range: Sequence[float] = (-2.0, 2.0),
color: ManimColor = BLUE,
**kwargs: Any,
) -> None:
super().__init__(**kwargs)

# Generates the 3D grid:
x = np.linspace(x_range[0], x_range[1], resolution)
y = np.linspace(y_range[0], y_range[1], resolution)
z = np.linspace(z_range[0], z_range[1], resolution)
X, Y, Z = np.meshgrid(x, y, z, indexing="ij")
values = func(X, Y, Z)

# Extracts the mesh:
verts, faces, _, _ = measure.marching_cubes(values, level=isolevel)

# Normalizes to the real domain:
scale_x = (x_range[1] - x_range[0]) / resolution
scale_y = (y_range[1] - y_range[0]) / resolution
scale_z = (z_range[1] - z_range[0]) / resolution
verts = np.array(
[
[
x_range[0] + v[0] * scale_x,
y_range[0] + v[1] * scale_y,
z_range[0] + v[2] * scale_z,
]
for v in verts
]
)

# Builds the polygons
for face in faces:
tri = [verts[i] for i in face]
self.add(Polygon(*tri, color=color, fill_opacity=1))
6 changes: 3 additions & 3 deletions manim/utils/docbuild/autoaliasattr_directive.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ def run(self) -> list[nodes.Element]:
# Parse the reST text into a fresh container
# https://www.sphinx-doc.org/en/master/extdev/markupapi.html#parsing-directive-content-as-rest
alias_container = nodes.container()
self.state.nested_parse(unparsed, 0, alias_container)
self.state.nested_parse(unparsed, 0, alias_container) # type: ignore[arg-type]
category_alias_container += alias_container

# then add the module TypeVars section
Expand Down Expand Up @@ -205,7 +205,7 @@ def run(self) -> list[nodes.Element]:
# Parse the reST text into a fresh container
# https://www.sphinx-doc.org/en/master/extdev/markupapi.html#parsing-directive-content-as-rest
typevar_container = nodes.container()
self.state.nested_parse(unparsed, 0, typevar_container)
self.state.nested_parse(unparsed, 0, typevar_container) # type: ignore[arg-type]
module_typevars_section += typevar_container

# Then, add the traditional "Module Attributes" section
Expand All @@ -228,7 +228,7 @@ def run(self) -> list[nodes.Element]:
# Parse the reST text into a fresh container
# https://www.sphinx-doc.org/en/master/extdev/markupapi.html#parsing-directive-content-as-rest
data_container = nodes.container()
self.state.nested_parse(unparsed, 0, data_container)
self.state.nested_parse(unparsed, 0, data_container) # type: ignore[arg-type]
module_attrs_section += data_container

return [content]
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ dependencies = [
"pydub>=0.20.0",
"pygments>=2.0.0",
"rich>=12.0.0",
"scikit-image",
"scipy>=1.13.0",
"scipy>=1.14.0 ; python_full_version >= '3.13'",
"screeninfo>=0.7",
Expand Down
12 changes: 12 additions & 0 deletions tests/test_implicit_surface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from manim import *
from manim.mobject.three_d.implicit_surface import ImplicitSurface


class TestSurfaceScene(ThreeDScene):
def construct(self):
self.set_camera_orientation(phi=70 * DEGREES, theta=45 * DEGREES)
surface = ImplicitSurface(
lambda x, y, z, r=0.7: x**2 + y**2 - r**2, resolution=20, color=WHITE
)
self.add(surface)
self.wait()
16 changes: 16 additions & 0 deletions tests/test_implicit_surface360.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from manim import *
from manim.mobject.three_d.implicit_surface import ImplicitSurface


class TestSurface360(ThreeDScene):
def construct(self):
self.set_camera_orientation(phi=70 * DEGREES, theta=0)

surface = ImplicitSurface(
lambda x, y, z: x**2 + y**2 + z**2 - 1, resolution=20, color=BLUE
)
self.add(surface)

self.begin_ambient_camera_rotation(rate=PI / 4)
self.wait(8)
self.stop_ambient_camera_rotation()
Loading