[
  {
    "path": ".github/manimdependency.json",
    "content": "{\n    \"windows\": {\n        \"tinytex\": [\n            \"standalone\",\n            \"preview\",\n            \"doublestroke\",\n            \"ms\",\n            \"everysel\",\n            \"setspace\",\n            \"rsfs\",\n            \"relsize\",\n            \"ragged2e\",\n            \"fundus-calligra\",\n            \"microtype\",\n            \"wasysym\",\n            \"physics\",\n            \"dvisvgm\",\n            \"jknapltx\",\n            \"wasy\",\n            \"cm-super\",\n            \"babel-english\",\n            \"gnu-freefont\",\n            \"mathastext\",\n            \"cbfonts-fd\"\n        ]\n    },\n    \"macos\": {\n        \"tinytex\": [\n            \"standalone\",\n            \"preview\",\n            \"doublestroke\",\n            \"ms\",\n            \"everysel\",\n            \"setspace\",\n            \"rsfs\",\n            \"relsize\",\n            \"ragged2e\",\n            \"fundus-calligra\",\n            \"microtype\",\n            \"wasysym\",\n            \"physics\",\n            \"dvisvgm\",\n            \"jknapltx\",\n            \"wasy\",\n            \"cm-super\",\n            \"babel-english\",\n            \"gnu-freefont\",\n            \"mathastext\",\n            \"cbfonts-fd\"\n        ]\n    }\n}\n"
  },
  {
    "path": ".github/scripts/ci_build_cairo.py",
    "content": "# Logic is as follows:\n# 1. Download cairo source code: https://cairographics.org/releases/cairo-<version>.tar.xz\n# 2. Verify the downloaded file using the sha256sums file: https://cairographics.org/releases/cairo-<version>.tar.xz.sha256sum\n# 3. Extract the downloaded file.\n# 4. Create a virtual environment and install meson and ninja.\n# 5. Run meson build in the extracted directory. Also, set required prefix.\n# 6. Run meson compile -C build.\n# 7. Run meson install -C build.\n\nimport hashlib\nimport logging\nimport os\nimport subprocess\nimport sys\nimport tarfile\nimport tempfile\nimport typing\nimport urllib.request\nfrom contextlib import contextmanager\nfrom pathlib import Path\nfrom sys import stdout\n\nCAIRO_VERSION = \"1.18.0\"\nCAIRO_URL = f\"https://cairographics.org/releases/cairo-{CAIRO_VERSION}.tar.xz\"\nCAIRO_SHA256_URL = f\"{CAIRO_URL}.sha256sum\"\n\nVENV_NAME = \"meson-venv\"\nBUILD_DIR = \"build\"\nINSTALL_PREFIX = Path(__file__).parent.parent.parent / \"third_party\" / \"cairo\"\n\nlogging.basicConfig(level=logging.INFO, format=\"%(asctime)s %(levelname)s %(message)s\")\nlogger = logging.getLogger(__name__)\n\n\ndef is_ci():\n    return os.getenv(\"CI\", None) is not None\n\n\ndef download_file(url, path):\n    logger.info(f\"Downloading {url} to {path}\")\n    block_size = 1024 * 1024\n    with urllib.request.urlopen(url) as response, open(path, \"wb\") as file:\n        while True:\n            data = response.read(block_size)\n            if not data:\n                break\n            file.write(data)\n\n\ndef verify_sha256sum(path, sha256sum):\n    with open(path, \"rb\") as file:\n        file_hash = hashlib.sha256(file.read()).hexdigest()\n    if file_hash != sha256sum:\n        raise Exception(\"SHA256SUM does not match\")\n\n\ndef extract_tar_xz(path, directory):\n    with tarfile.open(path) as file:\n        file.extractall(directory)\n\n\ndef run_command(command, cwd=None, env=None):\n    process = subprocess.Popen(command, cwd=cwd, env=env)\n    process.communicate()\n    if process.returncode != 0:\n        raise Exception(\"Command failed\")\n\n\n@contextmanager\ndef gha_group(title: str) -> typing.Generator:\n    if not is_ci():\n        yield\n        return\n    print(f\"\\n::group::{title}\")\n    stdout.flush()\n    try:\n        yield\n    finally:\n        print(\"::endgroup::\")\n        stdout.flush()\n\n\ndef set_env_var_gha(name: str, value: str) -> None:\n    if not is_ci():\n        return\n    env_file = os.getenv(\"GITHUB_ENV\", None)\n    if env_file is None:\n        return\n    with open(env_file, \"a\") as file:\n        file.write(f\"{name}={value}\\n\")\n    stdout.flush()\n\n\ndef get_ld_library_path(prefix: Path) -> str:\n    # given a prefix, the ld library path can be found at\n    # <prefix>/lib/* or sometimes just <prefix>/lib\n    # this function returns the path to the ld library path\n\n    # first, check if the ld library path exists at <prefix>/lib/*\n    ld_library_paths = list(prefix.glob(\"lib/*\"))\n    if len(ld_library_paths) == 1:\n        return ld_library_paths[0].absolute().as_posix()\n\n    # if the ld library path does not exist at <prefix>/lib/*,\n    # return <prefix>/lib\n    ld_library_path = prefix / \"lib\"\n    if ld_library_path.exists():\n        return ld_library_path.absolute().as_posix()\n    return \"\"\n\n\ndef main():\n    if sys.platform == \"win32\":\n        logger.info(\"Skipping build on windows\")\n        return\n\n    with tempfile.TemporaryDirectory() as tmpdir:\n        with gha_group(\"Downloading and Extracting Cairo\"):\n            logger.info(f\"Downloading cairo version {CAIRO_VERSION}\")\n            download_file(CAIRO_URL, os.path.join(tmpdir, \"cairo.tar.xz\"))\n\n            logger.info(\"Downloading cairo sha256sum\")\n            download_file(CAIRO_SHA256_URL, os.path.join(tmpdir, \"cairo.sha256sum\"))\n\n            logger.info(\"Verifying cairo sha256sum\")\n            with open(os.path.join(tmpdir, \"cairo.sha256sum\")) as file:\n                sha256sum = file.read().split()[0]\n            verify_sha256sum(os.path.join(tmpdir, \"cairo.tar.xz\"), sha256sum)\n\n            logger.info(\"Extracting cairo\")\n            extract_tar_xz(os.path.join(tmpdir, \"cairo.tar.xz\"), tmpdir)\n\n        with gha_group(\"Installing meson and ninja\"):\n            logger.info(\"Creating virtual environment\")\n            run_command([sys.executable, \"-m\", \"venv\", os.path.join(tmpdir, VENV_NAME)])\n\n            logger.info(\"Installing meson and ninja\")\n            run_command(\n                [\n                    os.path.join(tmpdir, VENV_NAME, \"bin\", \"pip\"),\n                    \"install\",\n                    \"meson\",\n                    \"ninja\",\n                ]\n            )\n\n        env_vars = {\n            # add the venv bin directory to PATH so that meson can find ninja\n            \"PATH\": f\"{os.path.join(tmpdir, VENV_NAME, 'bin')}{os.pathsep}{os.environ['PATH']}\",\n        }\n\n        with gha_group(\"Building and Installing Cairo\"):\n            logger.info(\"Running meson setup\")\n            run_command(\n                [\n                    os.path.join(tmpdir, VENV_NAME, \"bin\", \"meson\"),\n                    \"setup\",\n                    BUILD_DIR,\n                    f\"--prefix={INSTALL_PREFIX.absolute().as_posix()}\",\n                    \"--buildtype=release\",\n                    \"-Dtests=disabled\",\n                ],\n                cwd=os.path.join(tmpdir, f\"cairo-{CAIRO_VERSION}\"),\n                env=env_vars,\n            )\n\n            logger.info(\"Running meson compile\")\n            run_command(\n                [\n                    os.path.join(tmpdir, VENV_NAME, \"bin\", \"meson\"),\n                    \"compile\",\n                    \"-C\",\n                    BUILD_DIR,\n                ],\n                cwd=os.path.join(tmpdir, f\"cairo-{CAIRO_VERSION}\"),\n                env=env_vars,\n            )\n\n            logger.info(\"Running meson install\")\n            run_command(\n                [\n                    os.path.join(tmpdir, VENV_NAME, \"bin\", \"meson\"),\n                    \"install\",\n                    \"-C\",\n                    BUILD_DIR,\n                ],\n                cwd=os.path.join(tmpdir, f\"cairo-{CAIRO_VERSION}\"),\n                env=env_vars,\n            )\n\n        logger.info(f\"Successfully built cairo and installed it to {INSTALL_PREFIX}\")\n\n\nif __name__ == \"__main__\":\n    if \"--set-env-vars\" in sys.argv:\n        with gha_group(\"Setting environment variables\"):\n            # append the pkgconfig directory to PKG_CONFIG_PATH\n            set_env_var_gha(\n                \"PKG_CONFIG_PATH\",\n                f\"{Path(get_ld_library_path(INSTALL_PREFIX), 'pkgconfig').as_posix()}{os.pathsep}\"\n                f'{os.getenv(\"PKG_CONFIG_PATH\", \"\")}',\n            )\n            set_env_var_gha(\n                \"LD_LIBRARY_PATH\",\n                f\"{get_ld_library_path(INSTALL_PREFIX)}{os.pathsep}\"\n                f'{os.getenv(\"LD_LIBRARY_PATH\", \"\")}',\n            )\n        sys.exit(0)\n    main()\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\nconcurrency:\n  group: ${{ github.ref }}\n  cancel-in-progress: true\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\njobs:\n  test:\n    runs-on: ${{ matrix.os }}\n    env:\n      DISPLAY: :0\n      PYTEST_ADDOPTS: \"--color=yes\" # colors in pytest\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-22.04, macos-latest, windows-latest]\n        python: [\"3.9\", \"3.10\", \"3.11\", \"3.12\"]\n\n    steps:\n      - name: Checkout the repository\n        uses: actions/checkout@v4\n\n      - name: Install Poetry\n        run: |\n          pipx install \"poetry==1.7.*\"\n          poetry config virtualenvs.prefer-active-python true\n\n      - name: Setup Python ${{ matrix.python }}\n        uses: actions/setup-python@v5\n        with:\n          python-version: ${{ matrix.python }}\n          cache: \"poetry\"\n\n      - name: Setup macOS PATH\n        if: runner.os == 'macOS'\n        run: |\n          echo \"$HOME/.local/bin\" >> $GITHUB_PATH\n\n      - name: Setup cache variables\n        shell: bash\n        id: cache-vars\n        run: |\n          echo \"date=$(/bin/date -u \"+%m%w%Y\")\" >> $GITHUB_OUTPUT\n\n      - name: Install and cache ffmpeg (all OS)\n        uses: FedericoCarboni/setup-ffmpeg@v2\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n        id: setup-ffmpeg\n\n      - name: Install system dependencies (Linux)\n        if: runner.os == 'Linux'\n        uses: awalsh128/cache-apt-pkgs-action@latest\n        with:\n          packages: python3-opengl libpango1.0-dev xvfb freeglut3-dev\n          version: 1.0\n\n      - name: Install Texlive (Linux)\n        if: runner.os == 'Linux'\n        uses: teatimeguest/setup-texlive-action@v3\n        with:\n          cache: true\n          packages: scheme-basic fontspec inputenc fontenc tipa mathrsfs calligra xcolor standalone preview doublestroke ms everysel setspace rsfs relsize ragged2e fundus-calligra microtype wasysym physics dvisvgm jknapltx wasy cm-super babel-english gnu-freefont mathastext cbfonts-fd xetex\n\n      - name: Start virtual display (Linux)\n        if: runner.os == 'Linux'\n        run: |\n          # start xvfb in background\n          sudo /usr/bin/Xvfb $DISPLAY -screen 0 1280x1024x24 &\n\n      - name: Setup Cairo Cache\n        uses: actions/cache@v3\n        id: cache-cairo\n        if: runner.os == 'Linux' || runner.os == 'macOS'\n        with:\n          path: ${{ github.workspace }}/third_party\n          key: ${{ runner.os }}-dependencies-cairo-${{ hashFiles('.github/scripts/ci_build_cairo.py') }}\n\n      - name: Build and install Cairo (Linux and macOS)\n        if: (runner.os == 'Linux' || runner.os == 'macOS') && steps.cache-cairo.outputs.cache-hit != 'true'\n        run: python .github/scripts/ci_build_cairo.py\n\n      - name: Set env vars for Cairo (Linux and macOS)\n        if: runner.os == 'Linux' || runner.os == 'macOS'\n        run: python .github/scripts/ci_build_cairo.py --set-env-vars\n\n      - name: Setup macOS cache\n        uses: actions/cache@v3\n        id: cache-macos\n        if: runner.os == 'macOS'\n        with:\n          path: ${{ github.workspace }}/macos-cache\n          key: ${{ runner.os }}-dependencies-tinytex-${{ hashFiles('.github/manimdependency.json') }}-${{ steps.cache-vars.outputs.date }}-1\n\n      - name: Install system dependencies (MacOS)\n        if: runner.os == 'macOS' && steps.cache-macos.outputs.cache-hit != 'true'\n        run: |\n          tinyTexPackages=$(python -c \"import json;print(' '.join(json.load(open('.github/manimdependency.json'))['macos']['tinytex']))\")\n          IFS=' '\n          read -a ttp <<< \"$tinyTexPackages\"\n          oriPath=$PATH\n          sudo mkdir -p $PWD/macos-cache\n          echo \"Install TinyTeX\"\n          sudo curl -L -o \"/tmp/TinyTeX.tgz\" \"https://github.com/yihui/tinytex-releases/releases/download/daily/TinyTeX-1.tgz\"\n          sudo tar zxf \"/tmp/TinyTeX.tgz\" -C \"$PWD/macos-cache\"\n          export PATH=\"$PWD/macos-cache/TinyTeX/bin/universal-darwin:$PATH\"\n          sudo tlmgr update --self\n          for i in \"${ttp[@]}\"; do\n            sudo tlmgr install \"$i\"\n          done\n          export PATH=\"$oriPath\"\n          echo \"Completed TinyTeX\"\n\n      - name: Add macOS dependencies to PATH\n        if: runner.os == 'macOS'\n        shell: bash\n        run: |\n          echo \"/Library/TeX/texbin\" >> $GITHUB_PATH\n          echo \"$HOME/.poetry/bin\" >> $GITHUB_PATH\n          echo \"$PWD/macos-cache/TinyTeX/bin/universal-darwin\" >> $GITHUB_PATH\n\n      - name: Setup Windows cache\n        id: cache-windows\n        if: runner.os == 'Windows'\n        uses: actions/cache@v3\n        with:\n          path: ${{ github.workspace }}\\ManimCache\n          key: ${{ runner.os }}-dependencies-tinytex-${{ hashFiles('.github/manimdependency.json') }}-${{ steps.cache-vars.outputs.date }}-1\n\n      - uses: ssciwr/setup-mesa-dist-win@v1\n\n      - name: Install system dependencies (Windows)\n        if: runner.os == 'Windows' && steps.cache-windows.outputs.cache-hit != 'true'\n        run: |\n          $tinyTexPackages = $(python -c \"import json;print(' '.join(json.load(open('.github/manimdependency.json'))['windows']['tinytex']))\") -Split ' '\n          $OriPath = $env:PATH\n          echo \"Install Tinytex\"\n          Invoke-WebRequest \"https://github.com/yihui/tinytex-releases/releases/download/daily/TinyTeX-1.zip\" -OutFile \"$($env:TMP)\\TinyTex.zip\"\n          Expand-Archive -LiteralPath \"$($env:TMP)\\TinyTex.zip\" -DestinationPath \"$($PWD)\\ManimCache\\LatexWindows\"\n          $env:Path = \"$($PWD)\\ManimCache\\LatexWindows\\TinyTeX\\bin\\windows;$($env:PATH)\"\n          tlmgr update --self\n          foreach ($c in $tinyTexPackages){\n            $c=$c.Trim()\n            tlmgr install $c\n          }\n          $env:PATH=$OriPath\n          echo \"Completed Latex\"\n\n      - name: Add Windows dependencies to PATH\n        if: runner.os == 'Windows'\n        run: |\n          $env:Path += \";\" + \"$($PWD)\\ManimCache\\LatexWindows\\TinyTeX\\bin\\windows\"\n          $env:Path = \"$env:USERPROFILE\\.poetry\\bin;$($env:PATH)\"\n          echo \"$env:Path\" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append\n\n      - name: Install manim-physics\n        run: |\n          poetry config installer.modern-installation false\n          poetry install --with dev\n\n      - name: Run tests\n        run: |\n          poetry run python -m pytest\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\npip-wheel-metadata/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\n\n*.log\n\n# Sphinx documentation\ndocs/_build/\ndocs/build/\ndocs/source/_autosummary/\ndocs/source/reference/\ndocs/source/_build/\nrendering_times.csv\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\njupyter/\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n.python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# PyCharm\n/.idea/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n*.pyc\n*.bak\n.DS_Store\n\n.floo\n.flooignore\n.vscode\n.vs\n*.xml\n*.iml\nmedia\n.eggs/\nbuild/\ndist/\n\n/media_dir.txt\n# ^TODO: Remove the need for this with a proper config file"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "default_stages: [commit, push]\nfail_fast: false\nrepos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v4.4.0\n    hooks:\n      - id: mixed-line-ending\n  - repo: https://github.com/psf/black\n    rev: 23.3.0\n    hooks:\n      - id: black\n  - repo: https://github.com/asottile/blacken-docs\n    rev: 1.13.0\n    hooks:\n    -   id: blacken-docs\n        additional_dependencies:\n          - black==23.3.0\n  - repo: https://github.com/codespell-project/codespell\n    rev: v2.2.4\n    hooks:\n      - id: codespell\n"
  },
  {
    "path": ".readthedocs.yml",
    "content": "version: 2\r\nbuild:\r\n  os: ubuntu-22.04\r\n\r\n  tools:\r\n    python: \"3.10\"\r\n\r\n  apt_packages:\r\n    - libpango1.0-dev\r\n    - ffmpeg\r\n    - graphviz\r\n\r\npython:\r\n   install:\r\n      - requirements: docs/rtd-requirements.txt\r\n      - requirements: docs/requirements.txt\r\n      - method: pip\r\n        path: .\r\n"
  },
  {
    "path": "README.md",
    "content": "# manim-physics \n## Introduction\nThis is a 2D physics simulation plugin that allows you to generate complicated\nscenes in various branches of Physics such as rigid mechanics,\nelectromagnetism, wave etc. **Due to some reason, I (Matheart) may not have\ntime to maintain this repo, if you want to contribute please seek help from\nother contributors.**\n\nOfficial Documentation: https://manim-physics.readthedocs.io/en/latest/\n\nContributors: \n- [**pdcxs**](https://github.com/pdcxs)\n- [**Matheart**](https://github.com/Matheart)\n- [**icedcoffeeee**](https://github.com/icedcoffeeee)\n\n# Installation\n`manim-physics` is a package on pypi, and can be directly installed using pip:\n```bash\npip install manim-physics\n```\n"
  },
  {
    "path": "docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the environment for the first two.\nSPHINXOPTS    ?=\nSPHINXBUILD   ?= sphinx-build\n\n# Path base is the source directory\nSOURCEDIR     = .\nBUILDDIR      = ../build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@(cd source; $(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O))\n\n.PHONY: help Makefile i18n\n\n# All the code is executed as if everything was launched in one shell.\n.ONESHELL:\n# Like make clean but also remove files generated by autosummary and\n# rendered videos.\ncleanall: clean\n\t@rm source/reference/*\n\t@rm -rf source/media\n\t@rm -f rendering_times.csv\ni18n:\n\t@(cd source; $(SPHINXBUILD) -M gettext \"$(SOURCEDIR)\" ../i18n/ -t skip-manim $(SPHINXOPTS) $(O);cd ../i18n;bash stripUntranslatable.sh)\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@(cd source; $(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O))\n"
  },
  {
    "path": "docs/make.bat",
    "content": "@ECHO OFF\r\n\r\npushd %~dp0\\source\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\n\r\nREM The paths are taken from the source directory\r\nset SOURCEDIR=.\r\nset BUILDDIR=..\\build\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\n%SPHINXBUILD% >NUL 2>NUL\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.http://sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\r\ngoto end\r\n\r\n:help\r\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\r\n\r\n:end\r\npopd\r\n"
  },
  {
    "path": "docs/requirements.txt",
    "content": "furo\nmyst-parser\nsphinx<5.1\nsphinx-copybutton\nsphinxext-opengraph\n"
  },
  {
    "path": "docs/rtd-requirements.txt",
    "content": "jupyterlab\nsphinxcontrib-programoutput\n"
  },
  {
    "path": "docs/source/_static/custom.css",
    "content": "@media (prefers-color-scheme: dark) {\r\n    span.nc {\r\n        text-decoration: none !important;\r\n    }\r\n}\r\n\r\n.admonition-manim-example {\r\n    padding: 0;\r\n    display: flex;\r\n    flex-direction: column;\r\n}\r\n\r\n.admonition-manim-example p.admonition-title {\r\n    font-weight: 600;\r\n    font-size: 0.925rem;\r\n    margin: 0;\r\n}\r\n\r\n.admonition-manim-example .highlight-python {\r\n    margin: 0;\r\n}\r\n\r\n.admonition-manim-example .highlight {\r\n    border-radius: 0;\r\n}\r\n\r\n.admonition-manim-example .highlight pre {\r\n    font-size: 15px;\r\n}\r\n\r\n.manim-video {\r\n    width: 100%;\r\n    padding: 8px 0;\r\n    outline: 0;\r\n}\r\n\r\n.admonition-manim-example .manim-video {\r\n    padding: 0;\r\n}\r\n\r\n.admonition-manim-example img {\r\n    margin-bottom: 0;\r\n}\r\n\r\n.admonition-manim-example p:last-child {\r\n    margin-top: 0;\r\n    padding-left: 0.5rem;\r\n    padding-bottom: 0.15rem;\r\n    font-size: 15px;\r\n}\r\n\r\n.admonition-manim-example .copybtn {\r\n    margin-right: 6px;\r\n    font-size: 18px;\r\n}\r\n\r\n.admonition-manim-example .copybtn:hover {\r\n    cursor: pointer;\r\n}\r\n\r\np.rubric{\r\n    text-transform: capitalize;\r\n    font-size: 1.25rem;\r\n    font-weight: bold;\r\n}\r\n\r\n.sig-param{\r\n    color: var(--color-content-foreground);\r\n}\r\n\r\ndl.c .field-list dt, dl.cpp .field-list dt, dl.js .field-list dt, dl.py .field-list dt {\r\n    text-transform: capitalize;\r\n    font-weight: bold;\r\n    font-size: var(--font-size--normal);\r\n}\r\n\r\nh4, h5, h6{\r\n    text-transform: none;\r\n}\r\n\r\n/* yikes-ish attempt at bugfix for navbar on some browsers */\r\n.sidebar-tree a.internal.reference {\r\n    display: table-cell;\r\n}\r\n"
  },
  {
    "path": "docs/source/_static/responsiveSvg.js",
    "content": "window.addEventListener(\"load\", function () {\r\n    const styleElements = []\r\n    const colorSchemeQuery = window.matchMedia('(prefers-color-scheme: dark)');\r\n    const diagrams = document.querySelectorAll(\"object.inheritance.graphviz\");\r\n\r\n    for (let diagram of diagrams) {\r\n        style = document.createElement('style');\r\n        styleElements.push(style);\r\n        console.log(diagram);\r\n        diagram.contentDocument.firstElementChild.appendChild(style);\r\n    }\r\n\r\n    function setColorScheme(e) {\r\n        let colors, additions = \"\";\r\n        if (e.matches) {\r\n            // Dark\r\n            colors = {\r\n                text: \"#e07a5f\",\r\n                box: \"#383838\",\r\n                edge: \"#d0d0d0\",\r\n                background: \"#131416\"\r\n            };\r\n        } else {\r\n            // Light\r\n            colors = {\r\n                text: \"#e07a5f\",\r\n                box: \"#fff\",\r\n                edge: \"#413c3c\",\r\n                background: \"#ffffff\"\r\n            };\r\n            additions = `\r\n            .node polygon {\r\n                filter: drop-shadow(0 1px 3px #0002);\r\n            }\r\n            `\r\n        }\r\n        for (let style of styleElements) {\r\n            style.innerHTML = `\r\n                svg {\r\n                    background-color: ${colors.background};\r\n                }\r\n\r\n                .node text {\r\n                    fill: ${colors.text};\r\n                }\r\n\r\n                .node polygon {\r\n                    fill: ${colors.box};\r\n                }\r\n\r\n                .edge polygon {\r\n                    fill: ${colors.edge};\r\n                    stroke: ${colors.edge};\r\n                }\r\n\r\n                .edge path {\r\n                    stroke: ${colors.edge};\r\n                }\r\n                ${additions}\r\n            `;\r\n        }\r\n    }\r\n\r\n    setColorScheme(colorSchemeQuery);\r\n    colorSchemeQuery.addEventListener(\"change\", setColorScheme);\r\n});\r\n"
  },
  {
    "path": "docs/source/_templates/autosummary/class.rst",
    "content": "{{ name | escape | underline}}\n\nQualified name: ``{{ fullname | escape }}``\n\n.. currentmodule:: {{ module }}\n\n.. autoclass:: {{ objname }}\n   :show-inheritance:\n   :members:\n\n   {% block methods %}\n   {%- if methods %}\n   .. rubric:: {{ _('Methods') }}\n\n   .. autosummary::\n      :nosignatures:\n      {% for item in methods if item != '__init__' and item not in inherited_members %}\n      ~{{ name }}.{{ item }}\n      {%- endfor %}\n   {%- endif %}\n   {%- endblock %}\n\n   {% block attributes %}\n   {%- if attributes %}\n   .. rubric:: {{ _('Attributes') }}\n\n   .. autosummary::\n     {% for item in attributes %}\n     ~{{ name }}.{{ item }}\n     {%- endfor %}\n   {%- endif %}\n   {% endblock %}\n"
  },
  {
    "path": "docs/source/_templates/autosummary/module.rst",
    "content": "{{ name | escape | underline }}\n\n.. currentmodule:: {{ fullname }}\n\n.. automodule:: {{ fullname }}\n\n   {% block attributes %}\n   {% if attributes %}\n   .. rubric:: Module Attributes\n\n   .. autosummary::\n   {% for item in attributes %}\n      {{ item }}\n   {%- endfor %}\n   {% endif %}\n   {% endblock %}\n\n   {% block classes %}\n   {% if classes %}\n   .. rubric:: Classes\n\n   .. autosummary::\n      :toctree: .\n      :nosignatures:\n      {% for class in classes %}\n        {{ class }}\n      {% endfor %}\n   {% endif %}\n   {% endblock %}\n\n   {% block functions %}\n   {% if functions %}\n   .. rubric:: {{ _('Functions') }}\n\n   {% for item in functions %}\n   .. autofunction:: {{ item }}\n   {%- endfor %}\n   {% endif %}\n   {% endblock %}\n\n   {% block exceptions %}\n   {% if exceptions %}\n   .. rubric:: {{ _('Exceptions') }}\n\n   .. autosummary::\n   {% for item in exceptions %}\n      {{ item }}\n   {%- endfor %}\n   {% endif %}\n   {% endblock %}\n\n{% block modules %}\n{% if modules %}\n.. rubric:: Modules\n\n.. autosummary::\n   :toctree:\n   :recursive:\n{% for item in modules %}\n   {{ item }}\n{%- endfor %}\n{% endif %}\n{% endblock %}\n"
  },
  {
    "path": "docs/source/changelog.rst",
    "content": "=========\r\nChangelog\r\n=========\r\n\r\n**v0.4.1**\r\n==========\r\nBugfix\r\n------\r\n- `#34 <https://github.com/Matheart/manim-physics/pull/35>`_ : Magnetic fields\r\n  now accept multiple wires\r\n\r\n**v0.4.0**\r\n==========\r\nBreaking Changes\r\n----------------\r\n- Supported Python versions include 3.9 to 3.12\r\n- Updated manim version\r\n- Updated dependency versions\r\n\r\n**v0.3.0**\r\n==========\r\nBreaking Changes\r\n----------------\r\n- Huge library refactor.\r\n\r\n  - :class:`~.MagneticField` now takes a :class:`~.Wire` parameter. This allows\r\n    for a 3D field.\r\n  - Optimized field functions for both :class:`~.ElectricField` and\r\n    :class:`~.MagneticField`.\r\n\r\n**v0.2.5**\r\n==========\r\nBugfixes\r\n--------\r\n- ``VGroup`` s can be whole rigid bodies. Support for ``SVGMobject`` s\r\n\r\n**v0.2.4**\r\n==========\r\n2021.12.25\r\n\r\nNew Features\r\n------------\r\n- Hosted `official documentation\r\n  <https://manim-physics.readthedocs.io/en/latest/>`_ on\r\n  readthedocs. The readme might be restructured due to redundancy.\r\n- New ``lensing`` module: Mobjects including ``Lens`` and ``Ray`` \r\n- ``SpaceScene`` can now specify the gravity vector.\r\n- Fixed ``ConvertToOpenGL`` import error for ``manim v0.15.0``.\r\n\r\nImprovements\r\n-------------\r\n- Combined ``BarMagneticField`` with ``CurrentMagneticField`` into\r\n  ``MagneticField``.\r\n- Improved the updaters for ``pendulum`` module. Frame rate won't show any\r\n  lagging in the pendulum rods.\r\n\r\nBugfixes\r\n---------\r\n- Updated deprecated parameters in the ``wave`` module.\r\n\r\n**v0.2.3**\r\n==========\r\n2021.07.14\r\n\r\nBugfixes\r\n--------\r\n- Fix the small arrow bug in ``ElectricField``\r\n\r\n**v0.2.2**\r\n==========\r\n2021.07.06\r\n\r\nNew objects\r\n-----------\r\n- **Rigid Mechanics**: Pendulum\r\n\r\nBugfixes\r\n--------\r\n- Fix the ``__all__`` bug, now ``rigid_mechanics.py`` can run normally.\r\n\r\nImprovements\r\n------------\r\n- Rewrite README.md to improve its readability\r\n\r\n**v0.2.1**\r\n==========\r\n2021.07.03\r\n\r\nNew objects\r\n-----------\r\n- **Electromagnetism**: Charge, ElectricField, Current, CurrentMagneticField,\r\n  BarMagnet, and BarMagnetField\r\n- **Wave**: LinearWave, RadialWave, StandingWave\r\n\r\nBugfixes\r\n--------\r\n- Fix typo\r\n\r\nImprovements\r\n------------\r\n- Simplify rigid-mechanics\r\n\r\n**v0.2.0**\r\n==========\r\n2021.07.01\r\n\r\nBreaking Changes\r\n----------------\r\n- Objects in the manim-physics plugin are classified into several **main\r\n  branches** including rigid mechanics simulation, electromagnetism and wave.\r\n"
  },
  {
    "path": "docs/source/conf.py",
    "content": "# Configuration file for the Sphinx documentation builder.\n#\n# This file only contains a selection of the most common options. For a full\n# list see the documentation:\n# https://www.sphinx-doc.org/en/master/usage/configuration.html\n\nfrom __future__ import annotations\n\nimport os\nimport sys\n\n# -- Path setup --------------------------------------------------------------\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n\n\nsys.path.insert(0, os.path.abspath(\".\"))\n\n\n# -- Project information -----------------------------------------------------\n\nproject = \"Manim Physics\"\ncopyright = \"2020-2024, The Manim Physics Dev Team\"\nauthor = \"The Manim Physics Dev Team\"\n\n\n# -- General configuration ---------------------------------------------------\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = [\n    \"sphinx.ext.autodoc\",\n    \"sphinx_copybutton\",\n    \"sphinx.ext.napoleon\",\n    \"sphinx.ext.autosummary\",\n    \"sphinx.ext.doctest\",\n    \"sphinx.ext.extlinks\",\n    \"sphinx.ext.viewcode\",\n    \"manim.utils.docbuild.manim_directive\",\n    \"sphinxcontrib.programoutput\",\n    \"myst_parser\",\n]\n\n# Automatically generate stub pages when using the .. autosummary directive\nautosummary_generate = True\n\n# generate documentation from type hints\nautodoc_typehints = \"description\"\nautoclass_content = \"both\"\n\n# controls whether functions documented by the autofunction directive\n# appear with their full module names\nadd_module_names = False\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = [\"_templates\"]\n\n# Custom section headings in our documentation\nnapoleon_custom_sections = [\"Tests\", (\"Test\", \"Tests\")]\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This pattern also affects html_static_path and html_extra_path.\nexclude_patterns: list[str] = []\n\n# -- Options for HTML output -------------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\n\nhtml_theme = \"furo\"\n# html_favicon = str(Path(\"_static/favicon.ico\"))\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\n# html_static_path = [\"_static\"]\n\nhtml_theme_options = {\n    \"source_repository\": \"https://github.com/Matheart/manim-physics/\",\n    \"source_branch\": \"main\",\n    \"source_directory\": \"docs/source/\",\n    \"top_of_page_button\": None,\n    \"light_css_variables\": {\n        \"color-content-foreground\": \"#000000\",\n        \"color-background-primary\": \"#ffffff\",\n        \"color-background-border\": \"#ffffff\",\n        \"color-sidebar-background\": \"#f8f9fb\",\n        \"color-brand-content\": \"#1c00e3\",\n        \"color-brand-primary\": \"#192bd0\",\n        \"color-link\": \"#c93434\",\n        \"color-link--hover\": \"#5b0000\",\n        \"color-inline-code-background\": \"#f6f6f6;\",\n        \"color-foreground-secondary\": \"#000\",\n    },\n    \"dark_css_variables\": {\n        \"color-content-foreground\": \"#ffffffd9\",\n        \"color-background-primary\": \"#131416\",\n        \"color-background-border\": \"#303335\",\n        \"color-sidebar-background\": \"#1a1c1e\",\n        \"color-brand-content\": \"#2196f3\",\n        \"color-brand-primary\": \"#007fff\",\n        \"color-link\": \"#51ba86\",\n        \"color-link--hover\": \"#9cefc6\",\n        \"color-inline-code-background\": \"#262626\",\n        \"color-foreground-secondary\": \"#ffffffd9\",\n    },\n}\nhtml_title = f\"Manim Physics v0.4.0\"\n\n# This specifies any additional css files that will override the theme's\nhtml_css_files = [\"custom.css\"]\n\n\n# external links\nextlinks = {\n    \"issue\": (\"https://github.com/Matheart/manim-physics/issues/%s\", \"#%s\"),\n    \"pr\": (\"https://github.com/Matheart/manim-physics/pull/%s\", \"#%s\"),\n}\n\n# opengraph settings\nogp_site_name = \"Manim Physics | Documentation\"\n\nhtml_js_files = [\n    \"responsiveSvg.js\",\n]\n"
  },
  {
    "path": "docs/source/contributing.rst",
    "content": "Contributing\r\n============\r\n\r\nContributions are welcome! The repository owner (Matheart) might not be\r\navailable, any pull requests or issues will be attended by other developers.\r\n\r\nThere's three parts in a contribution:\r\n\r\n1. The proposed addition/improvement.\r\n2. Documentation for the new addition.\r\n3. Tests for the addition.\r\n\r\nNearly all contributing guidelines here are identical to `Manim's Contributing\r\nGuidelines <https://docs.manim.community/en/stable/contributing.html>`_.\r\n"
  },
  {
    "path": "docs/source/index.rst",
    "content": "Manim Physics\r\n-------------\r\n\r\nWelcome to the `manim-physics` official documentation!\r\n\r\nInstallation\r\n++++++++++++\r\n``manim-physics`` is a package on pypi, and can be directly installed using\r\npip:\r\n\r\n.. code-block:: powershell\r\n\r\n    pip install manim-physics\r\n\r\n.. warning::\r\n\r\n   Please do not directly clone the github repo! The repo is still under\r\n   development and it is not a stable version, download manim-physics through\r\n   pypi.\r\n\r\nUsage\r\n+++++\r\n\r\nInclude the import at the top of the .py file\r\n\r\n.. code-block::\r\n\r\n    from manim_physics import *\r\n\r\nIndex\r\n+++++\r\n\r\n.. toctree::\r\n   :maxdepth: 2\r\n\r\n   reference\r\n   changelog\r\n   contributing\r\n"
  },
  {
    "path": "docs/source/reference.rst",
    "content": "Reference\r\n---------\r\n\r\n\r\n.. toctree::\r\n   :maxdepth: 2\r\n\r\n   reference_index/electromagnetism\r\n   reference_index/optics\r\n   reference_index/rigid_mechanics\r\n   reference_index/wave\r\n"
  },
  {
    "path": "docs/source/reference_index/electromagnetism.rst",
    "content": "Electromagnetism\r\n================\r\n\r\n.. currentmodule:: manim_physics\r\n\r\n.. autosummary::\r\n   :toctree: ../reference\r\n\r\n   ~electromagnetism.electrostatics\r\n   ~electromagnetism.magnetostatics\r\n"
  },
  {
    "path": "docs/source/reference_index/optics.rst",
    "content": "Optics\r\n------\r\n\r\n\r\n.. currentmodule:: manim_physics\r\n\r\n.. autosummary::\r\n   :toctree: ../reference\r\n\r\n    ~optics.lenses\r\n    ~optics.rays\r\n    \r\n"
  },
  {
    "path": "docs/source/reference_index/rigid_mechanics.rst",
    "content": "Rigid Mechanics\r\n---------------\r\n\r\n.. currentmodule:: manim_physics\r\n\r\n.. autosummary::\r\n   :toctree: ../reference\r\n\r\n   ~rigid_mechanics.rigid_mechanics\r\n   ~rigid_mechanics.pendulum\r\n"
  },
  {
    "path": "docs/source/reference_index/wave.rst",
    "content": "Waves\r\n========\r\n\r\n.. currentmodule:: manim_physics\r\n\r\n.. autosummary::\r\n   :toctree: ../reference\r\n\r\n   ~wave\r\n\r\n"
  },
  {
    "path": "example.py",
    "content": "from manim_physics import *\r\n\r\n\r\nclass MagneticFieldExample(ThreeDScene):\r\n    def construct(self):\r\n        wire = Wire(Circle(2).rotate(PI / 2, UP))\r\n        mag_field = MagneticField(wire)\r\n        self.set_camera_orientation(PI / 3, PI / 4)\r\n        self.add(wire, mag_field)\r\n"
  },
  {
    "path": "manim_physics/__init__.py",
    "content": "__version__ = \"0.2.3\"\n\nfrom manim import *\n\nfrom .electromagnetism.electrostatics import *\nfrom .electromagnetism.magnetostatics import *\nfrom .optics.lenses import *\nfrom .optics.rays import *\nfrom .rigid_mechanics.pendulum import *\nfrom .rigid_mechanics.rigid_mechanics import *\nfrom .wave import *\n"
  },
  {
    "path": "manim_physics/electromagnetism/__init__.py",
    "content": ""
  },
  {
    "path": "manim_physics/electromagnetism/electrostatics.py",
    "content": "\"\"\"Electrostatics module\"\"\"\n\nfrom __future__ import annotations\nfrom typing import Iterable\n\nfrom manim import normalize\nfrom manim.constants import ORIGIN, TAU\nfrom manim.mobject.geometry.arc import Arc, Dot\nfrom manim.mobject.geometry.polygram import Rectangle\nfrom manim.mobject.types.vectorized_mobject import VGroup\nfrom manim.mobject.vector_field import ArrowVectorField\nfrom manim.utils.color import BLUE, RED, RED_A, RED_D, color_gradient\nimport numpy as np\n\n\n__all__ = [\n    \"Charge\",\n    \"ElectricField\",\n]\n\n\nclass Charge(VGroup):\n    def __init__(\n        self,\n        magnitude: float = 1,\n        point: np.ndarray = ORIGIN,\n        add_glow: bool = True,\n        **kwargs,\n    ) -> None:\n        \"\"\"An electrostatic charge object to produce an :class:`~ElectricField`.\n\n        Parameters\n        ----------\n        magnitude\n            The strength of the electrostatic charge.\n        point\n            The position of the charge.\n        add_glow\n            Whether to add a glowing effect. Adds rings of\n            varying opacities to simulate glowing effect.\n        kwargs\n            Additional parameters to be passed to ``VGroup``.\n        \"\"\"\n        VGroup.__init__(self, **kwargs)\n        self.magnitude = magnitude\n        self.point = point\n        self.radius = (abs(magnitude) * 0.4 if abs(magnitude) < 2 else 0.8) * 0.3\n\n        if magnitude > 0:\n            label = VGroup(\n                Rectangle(width=0.32 * 1.1, height=0.006 * 1.1).set_z_index(1),\n                Rectangle(width=0.006 * 1.1, height=0.32 * 1.1).set_z_index(1),\n            )\n            color = RED\n            layer_colors = [RED_D, RED_A]\n            layer_radius = 4\n        else:\n            label = Rectangle(width=0.27, height=0.003)\n            color = BLUE\n            layer_colors = [\"#3399FF\", \"#66B2FF\"]\n            layer_radius = 2\n\n        if add_glow:  # use many arcs to simulate glowing\n            layer_num = 80\n            color_list = color_gradient(layer_colors, layer_num)\n            opacity_func = lambda t: 1500 * (1 - abs(t - 0.009) ** 0.0001)\n            rate_func = lambda t: t**2\n\n            for i in range(layer_num):\n                self.add(\n                    Arc(\n                        radius=layer_radius * rate_func((0.5 + i) / layer_num),\n                        angle=TAU,\n                        color=color_list[i],\n                        stroke_width=101\n                        * (rate_func((i + 1) / layer_num) - rate_func(i / layer_num))\n                        * layer_radius,\n                        stroke_opacity=opacity_func(rate_func(i / layer_num)),\n                    ).shift(point)\n                )\n\n        self.add(Dot(point=self.point, radius=self.radius, color=color))\n        self.add(label.scale(self.radius / 0.3).shift(point))\n        for mob in self:\n            mob.set_z_index(1)\n\n\nclass ElectricField(ArrowVectorField):\n    def __init__(self, *charges: Charge, **kwargs) -> None:\n        \"\"\"An electric field.\n\n        Parameters\n        ----------\n        charges\n            The charges affecting the electric field.\n        kwargs\n            Additional parameters to be passed to ``ArrowVectorField``.\n\n        Examples\n        --------\n        .. manim:: ElectricFieldExampleScene\n            :save_last_frame:\n\n            from manim_physics import *\n\n            class ElectricFieldExampleScene(Scene):\n                def construct(self):\n                    charge1 = Charge(-1, LEFT + DOWN)\n                    charge2 = Charge(2, RIGHT + DOWN)\n                    charge3 = Charge(-1, UP)\n                    field = ElectricField(charge1, charge2, charge3)\n                    self.add(charge1, charge2, charge3)\n                    self.add(field)\n        \"\"\"\n        self.charges = charges\n        positions = []\n        magnitudes = []\n        for charge in charges:\n            positions.append(charge.get_center())\n            magnitudes.append(charge.magnitude)\n        super().__init__(lambda p: self._field_func(p, positions, magnitudes), **kwargs)\n\n    def _field_func(\n        self,\n        p: np.ndarray,\n        positions: Iterable[np.ndarray],\n        magnitudes: Iterable[float],\n    ) -> np.ndarray:\n        field_vect = np.zeros(3)\n        for p0, mag in zip(positions, magnitudes):\n            r = p - p0\n            dist = np.linalg.norm(r)\n            if dist < 0.1:\n                return np.zeros(3)\n            field_vect += mag / dist**2 * normalize(r)\n        return field_vect\n"
  },
  {
    "path": "manim_physics/electromagnetism/magnetostatics.py",
    "content": "\"\"\"Magnetostatics module\"\"\"\r\n\r\nfrom __future__ import annotations\r\nimport itertools as it\r\nfrom typing import Iterable, Tuple\r\n\r\nfrom manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL\r\nfrom manim.mobject.types.vectorized_mobject import VMobject\r\nfrom manim.mobject.vector_field import ArrowVectorField\r\nimport numpy as np\r\n\r\n\r\n__all__ = [\"Wire\", \"MagneticField\"]\r\n\r\n\r\nclass Wire(VMobject, metaclass=ConvertToOpenGL):\r\n    \"\"\"An abstract class denoting a current carrying wire to produce a\r\n    :class:`~MagneticField`.\r\n\r\n    Parameters\r\n    ----------\r\n    stroke\r\n        The original wire ``VMobject``. The resulting wire takes its form.\r\n    current\r\n        The magnitude of current flowing in the wire.\r\n    samples\r\n        The number of segments of the wire used to create the\r\n        :class:`~MagneticField`.\r\n    kwargs\r\n        Additional parameters passed to ``VMobject``.\r\n\r\n\r\n    .. note::\r\n\r\n        See :class:`~MagneticField` for examples.\r\n\r\n    \"\"\"\r\n\r\n    def __init__(\r\n        self,\r\n        stroke: VMobject,\r\n        current: float = 1,\r\n        samples: int = 16,\r\n        **kwargs,\r\n    ):\r\n        self.current = current\r\n        self.samples = samples\r\n\r\n        super().__init__(**kwargs)\r\n        self.set_points(stroke.points)\r\n\r\n\r\nclass MagneticField(ArrowVectorField):\r\n    \"\"\"A magnetic field.\r\n\r\n    Parameters\r\n    ----------\r\n    wires\r\n        All wires contributing to the total field.\r\n    kwargs\r\n        Additional parameters to be passed to ``ArrowVectorField``.\r\n\r\n    Example\r\n    -------\r\n    .. manim:: MagneticFieldExample\r\n        :save_last_frame:\r\n\r\n        from manim_physics import *\r\n\r\n        class MagneticFieldExample(ThreeDScene):\r\n            def construct(self):\r\n                wire = Wire(Circle(2).rotate(PI / 2, UP))\r\n                mag_field = MagneticField(\r\n                    wire,\r\n                    x_range=[-4, 4],\r\n                    y_range=[-4, 4],\r\n                )\r\n                self.set_camera_orientation(PI / 3, PI / 4)\r\n                self.add(wire, mag_field)\r\n\r\n    \"\"\"\r\n\r\n    def __init__(self, *wires: Wire, **kwargs):\r\n        dls = []\r\n        currents = []\r\n        for wire in wires:\r\n            points = [\r\n                wire.point_from_proportion(i)\r\n                for i in np.linspace(0, 1, wire.samples + 1)\r\n            ]\r\n            dls.append(list(zip(points, points[1:])))\r\n            currents.append(wire.current)\r\n        super().__init__(\r\n            lambda p: MagneticField._field_func(p, dls, currents), **kwargs\r\n        )\r\n\r\n    @staticmethod\r\n    def _field_func(\r\n        p: np.ndarray,\r\n        dls: Iterable[Tuple[np.ndarray, np.ndarray]],\r\n        currents: Iterable[float],\r\n    ):\r\n        B_field = np.zeros(3)\r\n        for dl in dls:\r\n            for (r0, r1), I in it.product(dl, currents):\r\n                dr = r1 - r0\r\n                r = p - r0\r\n                dist = np.linalg.norm(r)\r\n                if dist < 0.1:\r\n                    return np.zeros(3)\r\n                B_field += np.cross(dr, r) * I / dist**4\r\n        return B_field\r\n"
  },
  {
    "path": "manim_physics/optics/__init__.py",
    "content": "\"\"\"A lensing module.\r\n\r\nCurrently only shows refraction in lenses and not\r\ntotal internal reflection.\r\n\"\"\"\r\n"
  },
  {
    "path": "manim_physics/optics/lenses.py",
    "content": "\"\"\"Lenses for refracting Rays.\n\"\"\"\nfrom __future__ import annotations\nfrom typing import Iterable, Tuple\n\nfrom manim import config\nfrom manim.constants import LEFT, RIGHT\nfrom manim.mobject.geometry.arc import Circle\nfrom manim.mobject.geometry.boolean_ops import Difference, Intersection\nfrom manim.mobject.geometry.polygram import Square\nfrom manim.mobject.types.vectorized_mobject import VMobject, VectorizedPoint\nimport numpy as np\nfrom shapely import geometry as gm\n\n\n__all__ = [\"Lens\"]\n\n\ntry:\n    # For manim < 0.15.0\n    from manim.mobject.opengl_compatibility import ConvertToOpenGL\nexcept ModuleNotFoundError:\n    # For manim >= 0.15.0\n    from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL\n\n\ndef intersection(vmob1: VMobject, vmob2: VMobject) -> Iterable[Iterable[float]]:\n    \"\"\"intersection points of 2 curves\"\"\"\n    a = gm.LineString(vmob1.points)\n    b = gm.LineString(vmob2.points)\n    intersects: gm.GeometryCollection = a.intersection(b)\n    try:  # for intersections > 1\n        return np.array(\n            [[[x, y, z] for x, y, z in m.coords][0] for m in intersects.geoms]\n        )\n    except:  # else\n        return np.array([[x, y, z] for x, y, z in intersects.coords])\n\n\ndef snell(i_ang: float, n: float) -> float:\n    \"\"\"accepts radians, returns radians\"\"\"\n    return np.arcsin(np.sin(i_ang) / n)\n\n\ndef antisnell(r_ang: float, n: float) -> float:\n    \"\"\"accepts radians, returns radians\"\"\"\n    return np.arcsin(np.sin(r_ang) * n)\n\n\nclass Lens(VMobject, metaclass=ConvertToOpenGL):\n    def __init__(self, f: float, d: float, n: float = 1.52, **kwargs) -> None:\n        \"\"\"A lens. Commonly used with :class:`~Ray` .\n\n        Parameters\n        ----------\n        f\n            Focal length. This does not correspond correctly\n            to the point of focus (Known issue). Positive f\n            returns a convex lens, negative for concave.\n        d\n            Lens thickness\n        n\n            Refractive index. By default, glass.\n        kwargs\n            Additional parameters to be passed to :class:`~VMobject` .\n        \"\"\"\n        super().__init__(**kwargs)\n        self.f = f\n        f *= 50 / 7 * f if f > 0 else -50 / 7 * f  # this is odd, but it works\n        if f > 0:\n            r = ((n - 1) ** 2 * f * d / n) ** 0.5\n        else:\n            r = ((n - 1) ** 2 * -f * d / n) ** 0.5\n        self.d = d\n        self.n = n\n        self.r = r\n        if f > 0:\n            self.set_points(\n                Intersection(\n                    a := Circle(r).shift(RIGHT * (r - d / 2)),\n                    b := Circle(r).shift(LEFT * (r - d / 2)),\n                )\n                .insert_n_curves(50)\n                .points\n            )\n        else:\n            self.set_points(\n                Difference(\n                    Difference(\n                        Square(2 * 0.7 * r),\n                        a := Circle(r).shift(LEFT * (r + d / 2)),\n                    ),\n                    b := Circle(r).shift(RIGHT * (r + d / 2)),\n                )\n                .insert_n_curves(50)\n                .points\n            )\n        self.add(VectorizedPoint(a.get_center()), VectorizedPoint(b.get_center()))\n\n    @property\n    def C(self) -> Tuple[Iterable[float]]:\n        \"\"\"Returns a tuple of two points corresponding to the centers of curvature.\"\"\"\n        i = 0\n        i += 1 if config.renderer != \"opengl\" else 0\n        return self[i].points[0], self[i + 1].points[0]  # why is this confusing\n"
  },
  {
    "path": "manim_physics/optics/rays.py",
    "content": "\"\"\"Rays of light. Refracted by Lenses.\"\"\"\r\n\r\nfrom __future__ import annotations\r\nfrom typing import Iterable\r\n\r\nfrom manim import config\r\nfrom manim.mobject.geometry.line import Line\r\nfrom manim.utils.space_ops import angle_of_vector, rotate_vector\r\nimport numpy as np\r\n\r\nfrom .lenses import Lens, antisnell, intersection, snell\r\n\r\n__all__ = [\r\n    \"Ray\",\r\n]\r\n\r\n\r\nclass Ray(Line):\r\n    def __init__(\r\n        self,\r\n        start: Iterable[float],\r\n        direction: Iterable[float],\r\n        init_length: float = 5,\r\n        propagate: Iterable[Lens] | None = None,\r\n        **kwargs,\r\n    ) -> None:\r\n        \"\"\"A light ray.\r\n\r\n        Parameters\r\n        ----------\r\n        start\r\n            The start point of the ray\r\n        direction\r\n            The direction of the ray\r\n        init_length\r\n            The initial length of the ray. Once propagated,\r\n            the length are lengthened to showcase lensing.\r\n        propagate\r\n            A list of lenses to propagate through.\r\n\r\n        Example\r\n        -------\r\n        .. manim:: RayExampleScene\r\n            :save_last_frame:\r\n\r\n            from manim_physics import *\r\n\r\n            class RayExampleScene(Scene):\r\n                def construct(self):\r\n                    lens_style = {\"fill_opacity\": 0.5, \"color\": BLUE}\r\n                    a = Lens(-5, 1, **lens_style).shift(LEFT)\r\n                    a2 = Lens(5, 1, **lens_style).shift(RIGHT)\r\n                    b = [\r\n                        Ray(LEFT * 5 + UP * i, RIGHT, 8, [a, a2], color=RED)\r\n                        for i in np.linspace(-2, 2, 10)\r\n                    ]\r\n                    self.add(a, a2, *b)\r\n        \"\"\"\r\n        self.init_length = init_length\r\n        self.propagated = False\r\n        super().__init__(start, start + direction * init_length, **kwargs)\r\n        if propagate:\r\n            self.propagate(*propagate)\r\n\r\n    def propagate(self, *lenses: Lens) -> None:\r\n        \"\"\"Let the ray propagate through the list\r\n        of lenses passed.\r\n\r\n        Parameters\r\n        ----------\r\n        lenses\r\n            All the lenses for the ray to propagate through\r\n        \"\"\"\r\n        # TODO: make modular(?) Clean up logic\r\n        sorted_lens = self._sort_lens(lenses)\r\n        for lens in sorted_lens:\r\n            intersects = intersection(lens, self)\r\n            if len(intersects) == 0:\r\n                continue\r\n            intersects = self._sort_intersections(intersects)\r\n            if not self.propagated:\r\n                self.put_start_and_end_on(\r\n                    self.start,\r\n                    intersects[1],\r\n                )\r\n            else:\r\n                nppcc = (\r\n                    self.n_points_per_cubic_curve\r\n                    if config.renderer != \"opengl\"\r\n                    else self.n_points_per_curve\r\n                )\r\n                self.points = self.points[:-nppcc]\r\n                self.add_line_to(intersects[1])\r\n            self.end = intersects[1]\r\n            i_ang = angle_of_vector(self.end - lens.C[0])\r\n            i_ang -= angle_of_vector(self.start - self.end)\r\n            r_ang = snell(i_ang, lens.n)\r\n            r_ang *= -1 if lens.f > 0 else 1\r\n            ref_ray = rotate_vector(lens.C[0] - self.end, r_ang)\r\n            intersects = intersection(\r\n                lens,\r\n                Line(\r\n                    self.end - ref_ray * self.init_length,\r\n                    self.end + ref_ray * self.init_length,\r\n                ),\r\n            )\r\n            intersects = self._sort_intersections(intersects)\r\n            self.add_line_to(intersects[1])\r\n            self.start = self.end\r\n            self.end = intersects[1]\r\n            i_ang = angle_of_vector(self.end - lens.C[1])\r\n            i_ang -= angle_of_vector(self.start - self.end)\r\n            if np.abs(np.sin(i_ang)) < 1 / lens.n:\r\n                r_ang = antisnell(i_ang, lens.n)\r\n                r_ang *= -1 if lens.f < 0 else 1\r\n                ref_ray = rotate_vector(lens.C[1] - self.end, r_ang)\r\n                ref_ray *= -1 if lens.f > 0 else 1\r\n                self.add_line_to(self.end + ref_ray * self.init_length)\r\n                self.start = self.end\r\n                self.end = self.get_end()\r\n            self.propagated = True\r\n\r\n    def _sort_lens(self, lenses: Iterable[Lens]) -> Iterable[Lens]:\r\n        dists = []\r\n        for lens in lenses:\r\n            try:\r\n                dists += [\r\n                    [np.linalg.norm(intersection(self, lens)[0] - self.start), lens]\r\n                ]\r\n            except:\r\n                dists += [[np.inf, lens]]\r\n        dists.sort(key=lambda x: x[0])\r\n        return np.array(dists, dtype=object)[:, 1]\r\n\r\n    def _sort_intersections(\r\n        self, intersections: Iterable[Iterable[float]]\r\n    ) -> Iterable[Iterable[float]]:\r\n        result = []\r\n        for inters in intersections:\r\n            result.append([np.linalg.norm(inters - self.end), inters])\r\n        result.sort(key=lambda x: x[0])\r\n        return np.array(result, dtype=object)[:, 1]\r\n"
  },
  {
    "path": "manim_physics/rigid_mechanics/__init__.py",
    "content": ""
  },
  {
    "path": "manim_physics/rigid_mechanics/pendulum.py",
    "content": "r\"\"\"Pendulums.\n\n:class:`~MultiPendulum` and :class:`~Pendulum` both stem from the\n:py:mod:`~rigid_mechanics` feature.\n\n\"\"\"\n\nfrom __future__ import annotations\nfrom typing import Iterable\n\nfrom manim.constants import DOWN, RIGHT, UP\nfrom manim.mobject.geometry.arc import Circle\nfrom manim.mobject.geometry.line import Line\nfrom manim.mobject.mobject import Mobject\nfrom manim.mobject.types.vectorized_mobject import VGroup\nfrom manim.utils.color import ORANGE\nimport numpy as np\nimport pymunk\n\nfrom .rigid_mechanics import SpaceScene\n\n__all__ = [\n    \"Pendulum\",\n    \"MultiPendulum\",\n    \"SpaceScene\",\n]\n\n\nclass MultiPendulum(VGroup):\n    def __init__(\n        self,\n        *bobs: Iterable[np.ndarray],\n        pivot_point: np.ndarray = UP * 2,\n        rod_style: dict = {},\n        bob_style: dict = {\n            \"radius\": 0.1,\n            \"color\": ORANGE,\n            \"fill_opacity\": 1,\n        },\n        **kwargs,\n    ) -> None:\n        \"\"\"A multipendulum.\n\n        Parameters\n        ----------\n        bobs\n            Positions of pendulum bobs.\n        pivot_point\n            Position of the pivot.\n        rod_style\n            Parameters for ``Line``.\n        bob_style\n            Parameters for ``Circle``.\n        kwargs\n            Additional parameters for ``VGroup``.\n\n        Examples\n        --------\n        .. manim:: MultiPendulumExample\n            :quality: low\n\n            from manim_physics import *\n\n            class MultiPendulumExample(SpaceScene):\n                def construct(self):\n                    p = MultiPendulum(RIGHT, LEFT)\n                    self.add(p)\n                    self.make_rigid_body(*p.bobs)\n                    p.start_swinging()\n                    self.add(TracedPath(p.bobs[-1].get_center, stroke_color=BLUE))\n                    self.wait(10)\n        \"\"\"\n        self.pivot_point = pivot_point\n        self.bobs = VGroup(*[Circle(**bob_style).move_to(i) for i in bobs])\n        self.pins = [pivot_point]\n        self.pins += bobs\n        self.rods = VGroup()\n        self.rods += Line(self.pivot_point, self.bobs[0].get_center(), **rod_style)\n        self.rods.add(\n            *(\n                Line(\n                    self.bobs[i].get_center(),\n                    self.bobs[i + 1].get_center(),\n                    **rod_style,\n                )\n                for i in range(len(bobs) - 1)\n            )\n        )\n\n        super().__init__(**kwargs)\n        self.add(self.rods, self.bobs)\n\n    def _make_joints(\n        self, mob1: Mobject, mob2: Mobject, spacescene: SpaceScene\n    ) -> None:\n        a = mob1.body\n        if type(mob2) == np.ndarray:\n            b = pymunk.Body(body_type=pymunk.Body.STATIC)\n            b.position = mob2[0], mob2[1]\n        else:\n            b = mob2.body\n        joint = pymunk.PinJoint(a, b)\n        spacescene.space.space.add(joint)\n\n    def _redraw_rods(self, mob: Line, pins, i):\n        try:\n            x, y, _ = pins[i]\n        except:\n            x, y = pins[i].body.position\n        x1, y1 = pins[i + 1].body.position\n        mob.put_start_and_end_on(\n            RIGHT * x + UP * y,\n            RIGHT * x1 + UP * y1,\n        )\n\n    def start_swinging(self) -> None:\n        \"\"\"Start swinging.\"\"\"\n        spacescene: SpaceScene = self.bobs[0].spacescene\n        pins = [self.pivot_point]\n        pins += self.bobs\n\n        for i in range(len(pins) - 1):\n            self._make_joints(pins[i + 1], pins[i], spacescene)\n            self.rods[i].add_updater(lambda mob, i=i: self._redraw_rods(mob, pins, i))\n\n    def end_swinging(self) -> None:\n        \"\"\"Stop swinging.\"\"\"\n        spacescene = self.bobs[0].spacescene\n        spacescene.stop_rigidity(self.bobs)\n\n\nclass Pendulum(MultiPendulum):\n    def __init__(\n        self,\n        length=3.5,\n        initial_theta=0.3,\n        pivot_point=UP * 2,\n        rod_style={},\n        bob_style={\n            \"radius\": 0.25,\n            \"color\": ORANGE,\n            \"fill_opacity\": 1,\n        },\n        **kwargs,\n    ):\n        \"\"\"A pendulum.\n\n        Parameters\n        ----------\n        length\n            The length of the pendulum.\n        initial_theta\n            The initial angle of deviation.\n        rod_style\n            Parameters for ``Line``.\n        bob_style\n            Parameters for ``Circle``.\n        kwargs\n            Additional parameters for ``VGroup``.\n\n        Examples\n        --------\n        .. manim:: PendulumExample\n            :quality: low\n\n            from manim_physics import *\n            class PendulumExample(SpaceScene):\n                def construct(self):\n                    pends = VGroup(*[Pendulum(i) for i in np.linspace(1, 5, 7)])\n                    self.add(pends)\n                    for p in pends:\n                        self.make_rigid_body(*p.bobs)\n                        p.start_swinging()\n                    self.wait(10)\n        \"\"\"\n        self.length = length\n        self.pivot_point = pivot_point\n\n        point = self.pivot_point + (\n            RIGHT * np.sin(initial_theta) * length\n            + DOWN * np.cos(initial_theta) * length\n        )\n        super().__init__(\n            point,\n            pivot_point=self.pivot_point,\n            rod_style=rod_style,\n            bob_style=bob_style,\n            **kwargs,\n        )\n"
  },
  {
    "path": "manim_physics/rigid_mechanics/rigid_mechanics.py",
    "content": "\"\"\"A gravity simulation space.\n\nMost objects can be made into a rigid body (moves according to gravity\nand collision) or a static body (stays still within the scene).\n\nTo use this feature, the :class:`~SpaceScene` must be used, to access\nthe specific functions of the space.\n\n.. note::\n    *   This feature utilizes the pymunk package. Although unnecessary,\n        it might make it easier if you knew a few things on how to use it.\n\n        `Official Documentation <http://www.pymunk.org/en/latest/pymunk.html>`_\n\n        `Youtube Tutorial <https://youtu.be/pRk---rdrbo>`_\n\n    *   A low frame rate might cause some objects to pass static objects as\n        they don't register collisions finely enough. Trying to increase the\n        config frame rate might solve the problem.\n\nExamples\n--------\n.. manim:: TwoObjectsFalling\n        \n        from manim_physics import *\n        # use a SpaceScene to utilize all specific rigid-mechanics methods\n        class TwoObjectsFalling(SpaceScene):\n            def construct(self):\n                circle = Circle().shift(UP)\n                circle.set_fill(RED, 1)\n                circle.shift(DOWN + RIGHT)\n\n                rect = Square().shift(UP)\n                rect.rotate(PI / 4)\n                rect.set_fill(YELLOW_A, 1)\n                rect.shift(UP * 2)\n                rect.scale(0.5)\n\n                ground = Line([-4, -3.5, 0], [4, -3.5, 0])\n                wall1 = Line([-4, -3.5, 0], [-4, 3.5, 0])\n                wall2 = Line([4, -3.5, 0], [4, 3.5, 0])\n                walls = VGroup(ground, wall1, wall2)\n                self.add(walls)\n\n                self.play(\n                    DrawBorderThenFill(circle),\n                    DrawBorderThenFill(rect),\n                )\n                self.make_rigid_body(rect, circle)  # Mobjects will move with gravity\n                self.make_static_body(walls)  # Mobjects will stay in place\n                self.wait(5)\n                # during wait time, the circle and rect would move according to the simulate updater\n\"\"\"\n\nfrom __future__ import annotations\nfrom typing import Tuple\n\nfrom manim.constants import RIGHT, UP\nfrom manim.mobject.geometry.arc import Circle\nfrom manim.mobject.geometry.line import Line\nfrom manim.mobject.geometry.polygram import Polygon, Polygram, Rectangle\nfrom manim.mobject.mobject import Group, Mobject\nfrom manim.mobject.types.vectorized_mobject import VGroup, VMobject\nfrom manim.scene.scene import Scene\nfrom manim.utils.space_ops import angle_between_vectors\nimport numpy as np\nimport pymunk\n\n__all__ = [\n    \"Space\",\n    \"_step\",\n    \"_simulate\",\n    \"get_shape\",\n    \"get_angle\",\n    \"SpaceScene\",\n]\n\n\ntry:\n    # For manim < 0.15.0\n    from manim.mobject.opengl_compatibility import ConvertToOpenGL\nexcept ModuleNotFoundError:\n    # For manim >= 0.15.0\n    from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL\n\n\nclass Space(Mobject, metaclass=ConvertToOpenGL):\n    def __init__(self, gravity: Tuple[float, float] = (0, -9.81), **kwargs):\n        \"\"\"An Abstract object for gravity.\n\n        Parameters\n        ----------\n        gravity\n            The direction and strength of gravity.\n        \"\"\"\n        super().__init__(**kwargs)\n        self.space = pymunk.Space()\n        self.space.gravity = gravity\n        self.space.sleep_time_threshold = 5\n\n\nclass SpaceScene(Scene):\n    GRAVITY: Tuple[float, float] = 0, -9.81\n\n    def __init__(self, renderer=None, **kwargs):\n        \"\"\"A basis scene for all of rigid mechanics. The gravity vector\n        can be adjusted with ``self.GRAVITY``.\n        \"\"\"\n        self.space = Space(gravity=self.GRAVITY)\n        super().__init__(renderer=renderer, **kwargs)\n\n    def setup(self):\n        \"\"\"Used internally\"\"\"\n        self.add(self.space)\n        self.space.add_updater(_step)\n\n    def add_body(self, body: Mobject):\n        \"\"\"Bodies refer to pymunk's object.\n        This method ties Mobjects to their Bodies.\n        \"\"\"\n        if body.body != self.space.space.static_body:\n            self.space.space.add(body.body)\n        self.space.space.add(body.shape)\n\n    def make_rigid_body(\n        self,\n        *mobs: Mobject,\n        elasticity: float = 0.8,\n        density: float = 1,\n        friction: float = 0.8,\n    ):\n        \"\"\"Make any mobject movable by gravity.\n        Equivalent to ``Scene``'s ``add`` function.\n\n        Parameters\n        ----------\n        mobs\n            The mobs to be made rigid.\n        elasticity\n        density\n        friction\n            The attributes of the mobjects in regards to\n            interacting with other rigid and static objects.\n        \"\"\"\n        for mob in mobs:\n            if not hasattr(mob, \"body\"):\n                self.add(mob)\n                mob.body = pymunk.Body()\n                mob.body.position = mob.get_x(), mob.get_y()\n                get_angle(mob)\n                if not hasattr(mob, \"angle\"):\n                    mob.angle = 0\n                mob.body.angle = mob.angle\n                get_shape(mob)\n                mob.shape.density = density\n                mob.shape.elasticity = elasticity\n                mob.shape.friction = friction\n                mob.spacescene = self\n\n                self.add_body(mob)\n                mob.add_updater(_simulate)\n\n            else:\n                if mob.body.is_sleeping:\n                    mob.body.activate()\n\n    def make_static_body(\n        self, *mobs: Mobject, elasticity: float = 1, friction: float = 0.8\n    ) -> None:\n        \"\"\"Make any mobject interactable by rigid objects.\n\n        Parameters\n        ----------\n        mobs\n            The mobs to be made static.\n        elasticity\n        friction\n            The attributes of the mobjects in regards to\n            interacting with rigid objects.\n        \"\"\"\n        for mob in mobs:\n            if isinstance(mob, VGroup or Group):\n                return self.make_static_body(*mob)\n            mob.body = self.space.space.static_body\n            get_shape(mob)\n            mob.shape.elasticity = elasticity\n            mob.shape.friction = friction\n            self.add_body(mob)\n\n    def stop_rigidity(self, *mobs: Mobject) -> None:\n        \"\"\"Stop the mobjects rigidity\"\"\"\n        for mob in mobs:\n            if isinstance(mob, VGroup or Group):\n                self.stop_rigidity(*mob)\n            if hasattr(mob, \"body\"):\n                mob.body.sleep()\n\n\ndef _step(space, dt):\n    space.space.step(dt)\n\n\ndef _simulate(b):\n    x, y = b.body.position\n    b.move_to(x * RIGHT + y * UP)\n    b.rotate(b.body.angle - b.angle)\n    b.angle = b.body.angle\n\n\ndef get_shape(mob: VMobject) -> None:\n    \"\"\"Obtains the shape of the body from the mobject\"\"\"\n    if isinstance(mob, Circle):\n        mob.shape = pymunk.Circle(body=mob.body, radius=mob.radius)\n    elif isinstance(mob, Line):\n        mob.shape = pymunk.Segment(\n            mob.body,\n            (mob.get_start()[0], mob.get_start()[1]),\n            (mob.get_end()[0], mob.get_end()[1]),\n            mob.stroke_width - 3.95,\n        )\n    elif issubclass(type(mob), Rectangle):\n        width = np.linalg.norm(mob.get_vertices()[1] - mob.get_vertices()[0])\n        height = np.linalg.norm(mob.get_vertices()[2] - mob.get_vertices()[1])\n        mob.shape = pymunk.Poly.create_box(mob.body, (width, height))\n    elif issubclass(type(mob), Polygram):\n        vertices = [(a, b) for a, b, _ in mob.get_vertices() - mob.get_center()]\n        mob.shape = pymunk.Poly(mob.body, vertices)\n    else:\n        mob.shape = pymunk.Poly.create_box(mob.body, (mob.width, mob.height))\n\n\ndef get_angle(mob: VMobject) -> None:\n    \"\"\"Obtains the angle of the body from the mobject.\n    Used internally for updaters.\n    \"\"\"\n    if issubclass(type(mob), Polygon):\n        vec1 = mob.get_vertices()[0] - mob.get_vertices()[1]\n        vec2 = type(mob)().get_vertices()[0] - type(mob)().get_vertices()[1]\n        mob.angle = angle_between_vectors(vec1, vec2)\n    elif isinstance(mob, Line):\n        mob.angle = mob.get_angle()\n"
  },
  {
    "path": "manim_physics/wave.py",
    "content": "\"\"\"3D and 2D Waves module.\"\"\"\n\nfrom __future__ import annotations\nfrom typing import Iterable, Optional\n\nfrom manim import *\n\n__all__ = [\n    \"LinearWave\",\n    \"RadialWave\",\n    \"StandingWave\",\n]\n\n\ntry:\n    # For manim < 0.15.0\n    from manim.mobject.opengl_compatibility import ConvertToOpenGL\nexcept ModuleNotFoundError:\n    # For manim >= 0.15.0\n    from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL\n\n\nclass RadialWave(Surface, metaclass=ConvertToOpenGL):\n    def __init__(\n        self,\n        *sources: Optional[np.ndarray],\n        wavelength: float = 1,\n        period: float = 1,\n        amplitude: float = 0.1,\n        x_range: Iterable[float] = [-5, 5],\n        y_range: Iterable[float] = [-5, 5],\n        **kwargs,\n    ) -> None:\n        \"\"\"A 3D Surface with waves moving radially.\n\n        Parameters\n        ----------\n        sources\n            The sources of disturbance.\n        wavelength\n            The wavelength of the wave.\n        period\n            The period of the wave.\n        amplitude\n            The amplitude of the wave.\n        x_range\n            The range of the wave in the x direction.\n        y_range\n            The range of the wave in the y direction.\n        kwargs\n            Additional parameters to be passed to :class:`~Surface`.\n\n        Examples\n        --------\n        .. manim:: RadialWaveExampleScene\n\n            class RadialWaveExampleScene(ThreeDScene):\n                def construct(self):\n                    self.set_camera_orientation(60 * DEGREES, -45 * DEGREES)\n                    wave = RadialWave(\n                        LEFT * 2 + DOWN * 5,  # Two source of waves\n                        RIGHT * 2 + DOWN * 5,\n                        checkerboard_colors=[BLUE_D],\n                        stroke_width=0,\n                    )\n                    self.add(wave)\n                    wave.start_wave()\n                    self.wait()\n                    wave.stop_wave()\n        \"\"\"\n        self.wavelength = wavelength\n        self.period = period\n        self.amplitude = amplitude\n        self.time = 0\n        self.kwargs = kwargs\n        self.sources = sources\n\n        super().__init__(\n            lambda u, v: np.array([u, v, self._wave_z(u, v, sources)]),\n            u_range=x_range,\n            v_range=y_range,\n            **kwargs,\n        )\n\n    def _wave_z(self, u: float, v: float, sources: Iterable[np.ndarray]) -> float:\n        z = 0\n        for source in sources:\n            x0, y0, _ = source\n            z += self.amplitude * np.sin(\n                (2 * PI / self.wavelength) * ((u - x0) ** 2 + (v - y0) ** 2) ** 0.5\n                - 2 * PI * self.time / self.period\n            )\n        return z\n\n    def _update_wave(self, mob: Mobject, dt: float) -> None:\n        self.time += dt\n        mob.match_points(\n            Surface(\n                lambda u, v: np.array([u, v, self._wave_z(u, v, self.sources)]),\n                u_range=self.u_range,\n                v_range=self.v_range,\n                **self.kwargs,\n            )\n        )\n\n    def start_wave(self):\n        \"\"\"Animate the wave propagation.\"\"\"\n        self.add_updater(self._update_wave)\n\n    def stop_wave(self):\n        \"\"\"Stop animating the wave propagation.\"\"\"\n        self.remove_updater(self._update_wave)\n\n\nclass LinearWave(RadialWave):\n    def __init__(\n        self,\n        wavelength: float = 1,\n        period: float = 1,\n        amplitude: float = 0.1,\n        x_range: Iterable[float] = [-5, 5],\n        y_range: Iterable[float] = [-5, 5],\n        **kwargs,\n    ) -> None:\n        \"\"\"A 3D Surface with waves in one direction.\n\n        Parameters\n        ----------\n        wavelength\n            The wavelength of the wave.\n        period\n            The period of the wave.\n        amplitude\n            The amplitude of the wave.\n        x_range\n            The range of the wave in the x direction.\n        y_range\n            The range of the wave in the y direction.\n        kwargs\n            Additional parameters to be passed to :class:`~Surface`.\n\n        Examples\n        --------\n        .. manim:: LinearWaveExampleScene\n\n            class LinearWaveExampleScene(ThreeDScene):\n                def construct(self):\n                    self.set_camera_orientation(60 * DEGREES, -45 * DEGREES)\n                    wave = LinearWave()\n                    self.add(wave)\n                    wave.start_wave()\n                    self.wait()\n                    wave.stop_wave()\n        \"\"\"\n        super().__init__(\n            ORIGIN,\n            wavelength=wavelength,\n            period=period,\n            amplitude=amplitude,\n            x_range=x_range,\n            y_range=y_range,\n            **kwargs,\n        )\n\n    def _wave_z(self, u: float, v: float, sources: Iterable[np.ndarray]) -> float:\n        return self.amplitude * np.sin(\n            (2 * PI / self.wavelength) * u - 2 * PI * self.time / self.period\n        )\n\n\nclass StandingWave(ParametricFunction):\n    def __init__(\n        self,\n        n: int = 2,\n        length: float = 4,\n        period: float = 1,\n        amplitude: float = 1,\n        **kwargs,\n    ) -> None:\n        \"\"\"A 2D standing wave.\n\n        Parameters\n        ----------\n        n\n            Harmonic number.\n        length\n            The length of the wave.\n        period\n            The time taken for one full oscillation.\n        amplitude\n            The maximum height of the wave.\n        kwargs\n            Additional parameters to be passed to :class:`~ParametricFunction`.\n\n        Examples\n        --------\n        .. manim:: StandingWaveExampleScene\n\n            from manim_physics import *\n\n            class StandingWaveExampleScene(Scene):\n                def construct(self):\n                    wave1 = StandingWave(1)\n                    wave2 = StandingWave(2)\n                    wave3 = StandingWave(3)\n                    wave4 = StandingWave(4)\n                    waves = VGroup(wave1, wave2, wave3, wave4)\n                    waves.arrange(DOWN).move_to(ORIGIN)\n                    self.add(waves)\n                    for wave in waves:\n                        wave.start_wave()\n                    self.wait()\n        \"\"\"\n        self.n = n\n        self.length = length\n        self.period = period\n        self.amplitude = amplitude\n        self.time = 0\n        self.kwargs = {**kwargs}\n\n        super().__init__(\n            lambda t: np.array([t, amplitude * np.sin(n * PI * t / length), 0]),\n            t_range=[0, length],\n            **kwargs,\n        )\n        self.shift([-self.length / 2, 0, 0])\n\n    def _update_wave(self, mob: Mobject, dt: float) -> None:\n        self.time += dt\n        mob.become(\n            ParametricFunction(\n                lambda t: np.array(\n                    [\n                        t,\n                        self.amplitude\n                        * np.sin(self.n * PI * t / self.length)\n                        * np.cos(2 * PI * self.time / self.period),\n                        0,\n                    ]\n                ),\n                t_range=[0, self.length],\n                **self.kwargs,\n            ).shift(self.wave_center + [-self.length / 2, 0, 0])\n        )\n\n    def start_wave(self):\n        self.wave_center = self.get_center()\n        self.add_updater(self._update_wave)\n\n    def stop_wave(self):\n        self.remove_updater(self._update_wave)\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[tool.poetry]\nname = \"manim-physics\"\nversion = \"0.4.0\"\ndescription = \"Support physics simulation\"\nauthors = [\"Matheart <waautomationwong@gmail.com>\"]\nrepository = \"https://github.com/Matheart/manim-physics\"\nreadme=\"README.md\"\n\n[tool.poetry.dependencies]\npython = \">=3.9,<3.13\"\nmanim = \"~0.18.0\"\npymunk = \"^6.6.0\"\nshapely = \"^2.0.3\"\n\n[tool.poetry.group.dev]\noptional = true\n\n[tool.poetry.group.dev.dependencies]\npytest = \"^7.4.3\"\nblack = \">=23.11,<25.0\"\npre-commit = \"^3.5.0\"\nfuro = \"^2023.09.10\"\nSphinx = \"^7.2.6\"\nsphinx-copybutton = \"^0.5.2\"\nsphinxcontrib-programoutput = \"^0.17\"\nmyst-parser = \"^2.0.0\"\nmatplotlib = \"^3.8.2\"\n\n[build-system]\nrequires = [\"setuptools\", \"poetry-core>=1.0.0\"]\nbuild-backend = \"poetry.core.masonry.api\"\n\n[tool.poetry.plugins.\"manim.plugins\"]\n\"manim_physics\" = \"manim_physics\"\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/conftest.py",
    "content": "from __future__ import annotations\r\n\r\nimport sys\r\nfrom pathlib import Path\r\n\r\nimport pytest\r\n\r\nfrom manim import config, tempconfig\r\n\r\n\r\ndef pytest_addoption(parser):\r\n    parser.addoption(\r\n        \"--skip_slow\",\r\n        action=\"store_true\",\r\n        default=False,\r\n        help=\"Will skip all the slow marked tests. Slow tests are arbitrarily marked as such.\",\r\n    )\r\n    parser.addoption(\r\n        \"--show_diff\",\r\n        action=\"store_true\",\r\n        default=False,\r\n        help=\"Will show a visual comparison if a graphical unit test fails.\",\r\n    )\r\n    parser.addoption(\r\n        \"--set_test\",\r\n        action=\"store_true\",\r\n        default=False,\r\n        help=\"Will create the control data for EACH running tests. \",\r\n    )\r\n\r\n\r\ndef pytest_configure(config):\r\n    config.addinivalue_line(\"markers\", \"skip_end_to_end: mark test as end_to_end test\")\r\n\r\n\r\ndef pytest_collection_modifyitems(config, items):\r\n    if not config.getoption(\"--skip_slow\"):\r\n        return\r\n    else:\r\n        slow_skip = pytest.mark.skip(\r\n            reason=\"Slow test skipped due to --disable_slow flag.\",\r\n        )\r\n        for item in items:\r\n            if \"slow\" in item.keywords:\r\n                item.add_marker(slow_skip)\r\n\r\n\r\n@pytest.fixture(scope=\"session\")\r\ndef python_version():\r\n    # use the same python executable as it is running currently\r\n    # rather than randomly calling using python or python3, which\r\n    # may create problems.\r\n    return sys.executable\r\n\r\n\r\n@pytest.fixture\r\ndef reset_cfg_file():\r\n    cfgfilepath = Path(__file__).parent / \"test_cli\" / \"manim.cfg\"\r\n    original = cfgfilepath.read_text()\r\n    yield\r\n    cfgfilepath.write_text(original)\r\n\r\n\r\n@pytest.fixture\r\ndef using_opengl_renderer():\r\n    \"\"\"Standard fixture for running with opengl that makes tests use a standard_config.cfg with a temp dir.\"\"\"\r\n    with tempconfig({\"renderer\": \"opengl\"}):\r\n        yield\r\n    # as a special case needed to manually revert back to cairo\r\n    # due to side effects of setting the renderer\r\n    config.renderer = \"cairo\"\r\n"
  },
  {
    "path": "tests/test_electromagnetism.py",
    "content": "__module_test__ = \"electromagnetism\"\n\nfrom manim import *\nfrom manim.utils.testing.frames_comparison import frames_comparison\n\nfrom manim_physics.electromagnetism.electrostatics import *\nfrom manim_physics.electromagnetism.magnetostatics import *\n\n\n@frames_comparison\ndef test_electric_field(scene):\n    charge1 = Charge(-1, LEFT + DOWN)\n    charge2 = Charge(2, RIGHT + DOWN)\n    charge3 = Charge(-1, UP)\n    field = ElectricField(charge1, charge2, charge3)\n    scene.add(charge1, charge2, charge3)\n    scene.add(field)\n\n\n@frames_comparison\ndef test_magnetic_field(scene):\n    wire = Wire(Circle(2).rotate(PI / 2, UP))\n    field = MagneticField(wire)\n    scene.add(field, wire)\n\n\n@frames_comparison(base_scene=ThreeDScene)\ndef test_magnetic_field_multiple_wires(scene):\n    wire1 = Wire(Circle(2).rotate(PI / 2, RIGHT).shift(UP * 2))\n    wire2 = Wire(Circle(2).rotate(PI / 2, RIGHT).shift(UP * -2))\n    mag_field = MagneticField(wire1, wire2)\n    scene.set_camera_orientation(PI / 3, PI / 4)\n    scene.add(wire1, wire2, mag_field)\n"
  },
  {
    "path": "tests/test_lensing.py",
    "content": "__module_test__ = \"optics\"\nfrom manim import *\nfrom manim.utils.testing.frames_comparison import frames_comparison\n\nfrom manim_physics import *\n\n\n@frames_comparison\ndef test_rays_lens(scene):\n    lens_style = {\"fill_opacity\": 0.5, \"color\": BLUE}\n    a = Lens(-100, 1, **lens_style).shift(LEFT)\n    a2 = Lens(100, 1, **lens_style).shift(RIGHT)\n    b = [\n        Ray(LEFT * 5 + UP * i, RIGHT, 8, [a, a2], color=RED)\n        for i in np.linspace(-2, 2, 10)\n    ]\n    scene.add(a, a2, *b)\n"
  },
  {
    "path": "tests/test_pendulum.py",
    "content": "__module_test__ = \"pendulum\"\n\nfrom manim import *\nfrom manim.utils.testing.frames_comparison import frames_comparison\n\nfrom manim_physics.rigid_mechanics.pendulum import *\n\n\n@frames_comparison(base_scene=SpaceScene)\ndef test_pendulum(scene: SpaceScene):\n    pends = VGroup(*[Pendulum(i) for i in np.linspace(1, 5, 7)])\n    scene.add(pends)\n    for p in pends:\n        scene.make_rigid_body(*p.bobs)\n        p.start_swinging()\n        scene.wait()\n\n\n@frames_comparison(base_scene=SpaceScene)\ndef test_multipendulum(scene):\n    p = MultiPendulum(RIGHT, LEFT)\n    scene.add(p)\n    scene.make_rigid_body(*p.bobs)\n    p.start_swinging()\n    scene.add(TracedPath(p.bobs[-1].get_center, stroke_color=BLUE))\n    scene.wait()\n"
  },
  {
    "path": "tests/test_rigid_mechanics.py",
    "content": "__module_test__ = \"rigid_mechanics\"\n\nfrom manim import *\nfrom manim.utils.testing.frames_comparison import frames_comparison\n\nfrom manim_physics.rigid_mechanics.rigid_mechanics import *\n\n\n@frames_comparison(base_scene=SpaceScene)\ndef test_rigid_mechanics(scene):\n    circle = Circle().shift(UP)\n    circle.set_fill(RED, 1)\n    circle.shift(DOWN + RIGHT)\n\n    rect = Square().shift(UP)\n    rect.rotate(PI / 4)\n    rect.set_fill(YELLOW_A, 1)\n    rect.shift(UP * 2)\n    rect.scale(0.5)\n\n    ground = Line([-4, -3.5, 0], [4, -3.5, 0])\n    wall1 = Line([-4, -3.5, 0], [-4, 3.5, 0])\n    wall2 = Line([4, -3.5, 0], [4, 3.5, 0])\n    walls = VGroup(ground, wall1, wall2)\n    scene.add(walls)\n\n    scene.play(\n        DrawBorderThenFill(circle),\n        DrawBorderThenFill(rect),\n    )\n    scene.make_rigid_body(rect, circle)\n    scene.make_static_body(walls)\n    scene.wait()\n"
  },
  {
    "path": "tests/test_wave.py",
    "content": "__module_test__ = \"waves\"\n\nfrom manim import *\nfrom manim.utils.testing.frames_comparison import frames_comparison\n\nfrom manim_physics.wave import *\n\n\n@frames_comparison()\ndef test_linearwave(scene):\n    wave = LinearWave()\n    wave.set(time=2)\n    scene.add(wave)\n\n\n@frames_comparison()\ndef test_radialwave(scene):\n    wave = RadialWave(\n        LEFT * 2 + DOWN * 5,  # Two source of waves\n        RIGHT * 2 + DOWN * 5,\n        checkerboard_colors=[BLUE_D],\n        stroke_width=0,\n    )\n    wave.set(time=2)\n    scene.add(wave)\n\n\n@frames_comparison\ndef test_standingwave(scene):\n    wave1 = StandingWave(1)\n    wave2 = StandingWave(2)\n    wave3 = StandingWave(3)\n    wave4 = StandingWave(4)\n    waves = VGroup(wave1, wave2, wave3, wave4)\n    waves.arrange(DOWN).move_to(ORIGIN)\n    scene.add(waves)\n    for wave in waves:\n        wave.start_wave()\n    scene.wait()\n\n"
  }
]