├── .github └── workflows │ └── python-publish.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE.txt ├── README.md ├── media └── examples │ ├── cutoff_corners.png │ ├── dashes.gif │ ├── dimension.png │ ├── hatches.gif │ ├── pointer_triangel.gif │ ├── round_corners.png │ └── test_dimension.gif ├── poetry.lock ├── pyproject.toml ├── src └── manim_cad_drawing_utils │ ├── __init__.py │ ├── dimensions.py │ ├── hatch.py │ ├── path_mapper.py │ ├── round_corners.py │ └── utils.py └── tests ├── __init__.py ├── test_dashes.py ├── test_dimensions.py ├── test_hatch.py ├── test_path_mapper.py ├── test_path_offsets.py └── test_round_corners.py /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Set up Python 3.8 14 | uses: actions/setup-python@v2 15 | with: 16 | python-version: 3.8 17 | 18 | - name: Install dependencies 19 | run: python -m pip install --upgrade poetry 20 | 21 | # TODO: Set PYPI_API_TOKEN to api token from pip in secrets 22 | - name: Configure pypi credentials 23 | env: 24 | PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }} 25 | run: poetry config http-basic.pypi __token__ "$PYPI_API_TOKEN" 26 | 27 | - name: Publish release to pypi 28 | run: poetry publish --build 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | #IDE stuff 141 | .idea 142 | 143 | # media outputs 144 | media/images/ 145 | media/videos/ 146 | media/texts/ -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | 4 | If you haven't read our code of conduct, please do so at the 5 | [manim repository](https://github.com/ManimCommunity/manim/blob/master/CODE_OF_CONDUCT.md#code-of-conduct). -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2020 4 | 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the “Software”), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Manim CAD drawing utils 2 | 3 | This is a collecion of various functions and utilities that help creating manimations that look like CAD drawings. 4 | Also some other stuff that just looks cool. 5 | 6 | Features: 7 | - Round corners 8 | - Chamfer corners 9 | - Dimensions 10 | - Dashed line, dashed mobject 11 | - Path offset mapping 12 | 13 | 14 | ## Installation 15 | `manim-CAD_Drawing_Utils` is a package on pypi, and can be directly installed using pip: 16 | ``` 17 | pip install manim-CAD_Drawing_Utils 18 | ``` 19 | Note: `CAD_Drawing_Utils` uses, and depends on SciPy and Manim. 20 | 21 | ## Usage 22 | Make sure include these two imports at the top of the .py file 23 | ```py 24 | from manim import * 25 | from manim_cad_drawing_utils import * 26 | ``` 27 | 28 | # Examples 29 | 30 | ## pointer 31 | 32 | ```py 33 | class test_dimension_pointer(Scene): 34 | def construct(self): 35 | mob1 = Round_Corners(Triangle().scale(2),0.3) 36 | p = ValueTracker(0) 37 | dim1 = Pointer_To_Mob(mob1,p.get_value(),r'triangel', pointer_offset=0.2) 38 | dim1.add_updater(lambda mob: mob.update_mob(mob1,p.get_value())) 39 | dim1.update() 40 | PM = Path_mapper(mob1) 41 | self.play(Create(mob1),rate_func=PM.equalize_rate_func(smooth)) 42 | self.play(Create(dim1)) 43 | self.play(p.animate.set_value(1),run_time=10) 44 | self.play(Uncreate(mob1,rate_func=PM.equalize_rate_func(smooth))) 45 | self.play(Uncreate(dim1)) 46 | self.wait() 47 | 48 | 49 | ``` 50 | ![pointer](/media/examples/pointer_triangel.gif) 51 | 52 | 53 | ## dimension 54 | 55 | ```py 56 | class test_dimension(Scene): 57 | def construct(self): 58 | mob1 = Round_Corners(Triangle().scale(2),0.3) 59 | dim1 = Angle_Dimension_Mob(mob1, 60 | 0.2, 61 | 0.6, 62 | offset=-4, 63 | ext_line_offset=1, 64 | color=RED) 65 | dim2 = Linear_Dimension(mob1.get_critical_point(RIGHT), 66 | mob1.get_critical_point(LEFT), 67 | direction=UP, 68 | offset=2.5, 69 | outside_arrow=True, 70 | ext_line_offset=-1, 71 | color=RED) 72 | self.play(Create(mob1)) 73 | self.play(Create(dim1), run_time=3) 74 | self.play(Create(dim2), run_time=3) 75 | self.wait(3) 76 | self.play(Uncreate(mob1), Uncreate(dim2)) 77 | 78 | ``` 79 | ![dimension](/media/examples/test_dimension.gif) 80 | 81 | ## hatching 82 | 83 | ```py 84 | class test_hatch(Scene): 85 | def construct(self): 86 | mob1 = Star().scale(2) 87 | # 1 hatch object creates parallel lines 88 | # 2 of them create rectangles 89 | hatch1 = Hatch_lines(mob1, angle=PI / 6, stroke_width=2) 90 | hatch1.add_updater(lambda mob: mob.become(Hatch_lines(mob1, angle=PI / 6, stroke_width=2))) 91 | hatch2 = Hatch_lines(mob1, angle=PI / 6 + PI / 2, offset=0.5, stroke_width=2) 92 | hatch2.add_updater(lambda mob: mob.become(Hatch_lines(mob1, angle=PI / 6 + PI / 2, offset=0.5, stroke_width=2))) 93 | 94 | self.add(hatch1,hatch2,mob1) 95 | self.play(Transform(mob1,Triangle()),run_time=2) 96 | self.wait() 97 | self.play(Transform(mob1, Circle()), run_time=2) 98 | self.wait() 99 | self.play(Transform(mob1, Star().scale(2)), run_time=2) 100 | self.wait() 101 | ``` 102 | ![hatching](/media/examples/hatches.gif) 103 | 104 | 105 | ## Dashed lines 106 | ```py 107 | class test_dash(Scene): 108 | def construct(self): 109 | mob1 = Round_Corners(Square().scale(3),radius=0.8).shift(DOWN*0) 110 | vt = ValueTracker(0) 111 | dash1 = Dashed_line_mobject(mob1,num_dashes=36,dashed_ratio=0.5,dash_offset=0) 112 | def dash_updater(mob): 113 | offset = vt.get_value()%1 114 | dshgrp = mob.generate_dash_mobjects( 115 | **mob.generate_dash_pattern_dash_distributed(36, dash_ratio=0.5, offset=offset) 116 | ) 117 | mob['dashes'].become(dshgrp) 118 | dash1.add_updater(dash_updater) 119 | 120 | self.add(dash1) 121 | self.play(vt.animate.set_value(2),run_time=6) 122 | self.wait(0.5) 123 | ``` 124 | ![hatching](/media/examples/dashes.gif) 125 | 126 | ## rounded corners 127 | 128 | ```py 129 | class Test_round(Scene): 130 | def construct(self): 131 | mob1 = RegularPolygon(n=4,radius=1.5,color=PINK).rotate(PI/4) 132 | mob2 = Triangle(radius=1.5,color=TEAL) 133 | crbase = Rectangle(height=0.5,width=3) 134 | mob3 = Union(crbase.copy().rotate(PI/4),crbase.copy().rotate(-PI/4),color=BLUE) 135 | mob4 = Circle(radius=1.3) 136 | mob2.shift(2.5*UP) 137 | mob3.shift(2.5*DOWN) 138 | mob1.shift(2.5*LEFT) 139 | mob4.shift(2.5*RIGHT) 140 | 141 | mob1 = Round_Corners(mob1, 0.25) 142 | mob2 = Round_Corners(mob2, 0.25) 143 | mob3 = Round_Corners(mob3, 0.25) 144 | self.add(mob1,mob2,mob3,mob4) 145 | ``` 146 | ![rounded_corners](/media/examples/round_corners.png) 147 | 148 | ## cut off corners 149 | 150 | ```py 151 | class Test_chamfer(Scene): 152 | def construct(self): 153 | mob1 = RegularPolygon(n=4,radius=1.5,color=PINK).rotate(PI/4) 154 | mob2 = Triangle(radius=1.5,color=TEAL) 155 | crbase = Rectangle(height=0.5,width=3) 156 | mob3 = Union(crbase.copy().rotate(PI/4),crbase.copy().rotate(-PI/4),color=BLUE) 157 | mob4 = Circle(radius=1.3) 158 | mob2.shift(2.5*UP) 159 | mob3.shift(2.5*DOWN) 160 | mob1.shift(2.5*LEFT) 161 | mob4.shift(2.5*RIGHT) 162 | 163 | mob1 = Chamfer_Corners(mob1, 0.25) 164 | mob2 = Chamfer_Corners(mob2,0.25) 165 | mob3 = Chamfer_Corners(mob3, 0.25) 166 | self.add(mob1,mob2,mob3,mob4) 167 | 168 | ``` 169 | ![cutoff_corners](/media/examples/cutoff_corners.png) -------------------------------------------------------------------------------- /media/examples/cutoff_corners.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GarryBGoode/Manim_CAD_Drawing_utils/d9ad2b4cf2006a7f4bc7e4c16d917d7ec93b7988/media/examples/cutoff_corners.png -------------------------------------------------------------------------------- /media/examples/dashes.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GarryBGoode/Manim_CAD_Drawing_utils/d9ad2b4cf2006a7f4bc7e4c16d917d7ec93b7988/media/examples/dashes.gif -------------------------------------------------------------------------------- /media/examples/dimension.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GarryBGoode/Manim_CAD_Drawing_utils/d9ad2b4cf2006a7f4bc7e4c16d917d7ec93b7988/media/examples/dimension.png -------------------------------------------------------------------------------- /media/examples/hatches.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GarryBGoode/Manim_CAD_Drawing_utils/d9ad2b4cf2006a7f4bc7e4c16d917d7ec93b7988/media/examples/hatches.gif -------------------------------------------------------------------------------- /media/examples/pointer_triangel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GarryBGoode/Manim_CAD_Drawing_utils/d9ad2b4cf2006a7f4bc7e4c16d917d7ec93b7988/media/examples/pointer_triangel.gif -------------------------------------------------------------------------------- /media/examples/round_corners.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GarryBGoode/Manim_CAD_Drawing_utils/d9ad2b4cf2006a7f4bc7e4c16d917d7ec93b7988/media/examples/round_corners.png -------------------------------------------------------------------------------- /media/examples/test_dimension.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GarryBGoode/Manim_CAD_Drawing_utils/d9ad2b4cf2006a7f4bc7e4c16d917d7ec93b7988/media/examples/test_dimension.gif -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "certifi" 3 | version = "2022.12.7" 4 | description = "Python package for providing Mozilla's CA Bundle." 5 | category = "main" 6 | optional = false 7 | python-versions = ">=3.6" 8 | 9 | [[package]] 10 | name = "charset-normalizer" 11 | version = "2.1.1" 12 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 13 | category = "main" 14 | optional = false 15 | python-versions = ">=3.6.0" 16 | 17 | [package.extras] 18 | unicode_backport = ["unicodedata2"] 19 | 20 | [[package]] 21 | name = "click" 22 | version = "8.1.3" 23 | description = "Composable command line interface toolkit" 24 | category = "main" 25 | optional = false 26 | python-versions = ">=3.7" 27 | 28 | [package.dependencies] 29 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 30 | 31 | [[package]] 32 | name = "click-default-group" 33 | version = "1.2.2" 34 | description = "Extends click.Group to invoke a command without explicit subcommand name" 35 | category = "main" 36 | optional = false 37 | python-versions = "*" 38 | 39 | [package.dependencies] 40 | click = "*" 41 | 42 | [[package]] 43 | name = "cloup" 44 | version = "0.13.1" 45 | description = "Adds features to Click: option groups, constraints, subcommand sections and help themes." 46 | category = "main" 47 | optional = false 48 | python-versions = ">=3.6" 49 | 50 | [package.dependencies] 51 | click = ">=7.1,<9.0" 52 | typing-extensions = {version = "*", markers = "python_version <= \"3.8\""} 53 | 54 | [[package]] 55 | name = "colorama" 56 | version = "0.4.6" 57 | description = "Cross-platform colored terminal text." 58 | category = "main" 59 | optional = false 60 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 61 | 62 | [[package]] 63 | name = "colour" 64 | version = "0.1.5" 65 | description = "converts and manipulates various color representation (HSL, RVB, web, X11, ...)" 66 | category = "main" 67 | optional = false 68 | python-versions = "*" 69 | 70 | [package.extras] 71 | test = ["nose"] 72 | 73 | [[package]] 74 | name = "commonmark" 75 | version = "0.9.1" 76 | description = "Python parser for the CommonMark Markdown spec" 77 | category = "main" 78 | optional = false 79 | python-versions = "*" 80 | 81 | [package.extras] 82 | test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] 83 | 84 | [[package]] 85 | name = "cython" 86 | version = "0.29.32" 87 | description = "The Cython compiler for writing C extensions for the Python language." 88 | category = "main" 89 | optional = false 90 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 91 | 92 | [[package]] 93 | name = "decorator" 94 | version = "5.1.1" 95 | description = "Decorators for Humans" 96 | category = "main" 97 | optional = false 98 | python-versions = ">=3.5" 99 | 100 | [[package]] 101 | name = "glcontext" 102 | version = "2.3.7" 103 | description = "Portable OpenGL Context" 104 | category = "main" 105 | optional = false 106 | python-versions = "*" 107 | 108 | [[package]] 109 | name = "idna" 110 | version = "3.4" 111 | description = "Internationalized Domain Names in Applications (IDNA)" 112 | category = "main" 113 | optional = false 114 | python-versions = ">=3.5" 115 | 116 | [[package]] 117 | name = "isosurfaces" 118 | version = "0.1.0" 119 | description = "Construct isolines/isosurfaces over a 2D/3D scalar field defined by a function (not a uniform grid)" 120 | category = "main" 121 | optional = false 122 | python-versions = "*" 123 | 124 | [package.dependencies] 125 | numpy = "*" 126 | 127 | [[package]] 128 | name = "manim" 129 | version = "0.17.2" 130 | description = "Animation engine for explanatory math videos." 131 | category = "main" 132 | optional = false 133 | python-versions = ">=3.8,<3.12" 134 | 135 | [package.dependencies] 136 | click = ">=7.2,<=9.0" 137 | click-default-group = ">=1.2.2,<2.0.0" 138 | cloup = ">=0.13.0,<0.14.0" 139 | colour = ">=0.1.5,<0.2.0" 140 | decorator = ">=5.0.7,<6.0.0" 141 | isosurfaces = "0.1.0" 142 | manimpango = ">=0.4.0.post0,<0.5.0" 143 | mapbox-earcut = ">=1.0.0,<2.0.0" 144 | moderngl = ">=5.6.3,<6.0.0" 145 | moderngl-window = ">=2.3.0,<3.0.0" 146 | networkx = ">=2.5,<3.0" 147 | numpy = ">=1.19,<2.0" 148 | Pillow = ">=9.1,<10.0" 149 | pycairo = ">=1.21,<2.0" 150 | pydub = ">=0.25.1,<0.26.0" 151 | Pygments = ">=2.10.0,<3.0.0" 152 | requests = ">=2.26.0,<3.0.0" 153 | rich = ">=6.0,<12.0.0 || >12.0.0" 154 | scipy = ">=1.7.3,<2.0.0" 155 | screeninfo = ">=0.8,<0.9" 156 | skia-pathops = ">=0.7.0,<0.8.0" 157 | srt = ">=3.5.0,<4.0.0" 158 | svgelements = ">=1.8.0,<2.0.0" 159 | tqdm = ">=4.62.3,<5.0.0" 160 | watchdog = ">=2.1.6,<3.0.0" 161 | 162 | [package.extras] 163 | jupyterlab = ["jupyterlab (>=3.0,<4.0)", "notebook (>=6.4,<7.0)"] 164 | gui = ["dearpygui (>=1.3.1,<2.0.0)"] 165 | 166 | [[package]] 167 | name = "manimpango" 168 | version = "0.4.3" 169 | description = "Bindings for Pango for using with Manim." 170 | category = "main" 171 | optional = false 172 | python-versions = ">=3.7" 173 | 174 | [[package]] 175 | name = "mapbox-earcut" 176 | version = "1.0.1" 177 | description = "Python bindings for the mapbox earcut C++ polygon triangulation library." 178 | category = "main" 179 | optional = false 180 | python-versions = "*" 181 | 182 | [package.dependencies] 183 | numpy = "*" 184 | 185 | [package.extras] 186 | test = ["pytest"] 187 | 188 | [[package]] 189 | name = "moderngl" 190 | version = "5.7.4" 191 | description = "ModernGL: High performance rendering for Python 3" 192 | category = "main" 193 | optional = false 194 | python-versions = "*" 195 | 196 | [package.dependencies] 197 | glcontext = ">=2.3.6,<3" 198 | 199 | [[package]] 200 | name = "moderngl-window" 201 | version = "2.4.2" 202 | description = "A cross platform helper library for ModernGL making window creation and resource loading simple" 203 | category = "main" 204 | optional = false 205 | python-versions = ">=3.6" 206 | 207 | [package.dependencies] 208 | moderngl = "<6" 209 | numpy = ">=1.16,<2" 210 | Pillow = ">=9,<10" 211 | pyglet = ">=2.0dev23" 212 | pyrr = ">=0.10.3,<1" 213 | 214 | [package.extras] 215 | pysdl2 = ["pysdl2"] 216 | pyside2 = ["PySide2 (<6)"] 217 | glfw = ["glfw"] 218 | pygame = ["pygame (>=2.1.2)"] 219 | pyqt5 = ["pyqt5"] 220 | pywavefront = ["pywavefront (>=1.3.3,<2)"] 221 | tk = ["pyopengltk (>=0.0.3)"] 222 | trimesh = ["trimesh (>=3.2.6,<4)", "scipy (>=1.3.2)"] 223 | 224 | [[package]] 225 | name = "multipledispatch" 226 | version = "0.6.0" 227 | description = "Multiple dispatch" 228 | category = "main" 229 | optional = false 230 | python-versions = "*" 231 | 232 | [package.dependencies] 233 | six = "*" 234 | 235 | [[package]] 236 | name = "networkx" 237 | version = "2.8.8" 238 | description = "Python package for creating and manipulating graphs and networks" 239 | category = "main" 240 | optional = false 241 | python-versions = ">=3.8" 242 | 243 | [package.extras] 244 | default = ["numpy (>=1.19)", "scipy (>=1.8)", "matplotlib (>=3.4)", "pandas (>=1.3)"] 245 | developer = ["pre-commit (>=2.20)", "mypy (>=0.982)"] 246 | doc = ["sphinx (>=5.2)", "pydata-sphinx-theme (>=0.11)", "sphinx-gallery (>=0.11)", "numpydoc (>=1.5)", "pillow (>=9.2)", "nb2plots (>=0.6)", "texext (>=0.6.6)"] 247 | extra = ["lxml (>=4.6)", "pygraphviz (>=1.9)", "pydot (>=1.4.2)", "sympy (>=1.10)"] 248 | test = ["pytest (>=7.2)", "pytest-cov (>=4.0)", "codecov (>=2.1)"] 249 | 250 | [[package]] 251 | name = "numpy" 252 | version = "1.24.1" 253 | description = "Fundamental package for array computing in Python" 254 | category = "main" 255 | optional = false 256 | python-versions = ">=3.8" 257 | 258 | [[package]] 259 | name = "pillow" 260 | version = "9.4.0" 261 | description = "Python Imaging Library (Fork)" 262 | category = "main" 263 | optional = false 264 | python-versions = ">=3.7" 265 | 266 | [package.extras] 267 | docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] 268 | tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] 269 | 270 | [[package]] 271 | name = "pycairo" 272 | version = "1.23.0" 273 | description = "Python interface for cairo" 274 | category = "main" 275 | optional = false 276 | python-versions = ">=3.7" 277 | 278 | [[package]] 279 | name = "pydub" 280 | version = "0.25.1" 281 | description = "Manipulate audio with an simple and easy high level interface" 282 | category = "main" 283 | optional = false 284 | python-versions = "*" 285 | 286 | [[package]] 287 | name = "pyglet" 288 | version = "2.0.3" 289 | description = "Cross-platform windowing and multimedia library" 290 | category = "main" 291 | optional = false 292 | python-versions = "*" 293 | 294 | [[package]] 295 | name = "pygments" 296 | version = "2.14.0" 297 | description = "Pygments is a syntax highlighting package written in Python." 298 | category = "main" 299 | optional = false 300 | python-versions = ">=3.6" 301 | 302 | [package.extras] 303 | plugins = ["importlib-metadata"] 304 | 305 | [[package]] 306 | name = "pyobjc-core" 307 | version = "9.0.1" 308 | description = "Python<->ObjC Interoperability Module" 309 | category = "main" 310 | optional = false 311 | python-versions = ">=3.7" 312 | 313 | [[package]] 314 | name = "pyobjc-framework-cocoa" 315 | version = "9.0.1" 316 | description = "Wrappers for the Cocoa frameworks on macOS" 317 | category = "main" 318 | optional = false 319 | python-versions = ">=3.7" 320 | 321 | [package.dependencies] 322 | pyobjc-core = ">=9.0.1" 323 | 324 | [[package]] 325 | name = "pyrr" 326 | version = "0.10.3" 327 | description = "3D mathematical functions using NumPy" 328 | category = "main" 329 | optional = false 330 | python-versions = "*" 331 | 332 | [package.dependencies] 333 | multipledispatch = "*" 334 | numpy = "*" 335 | 336 | [[package]] 337 | name = "requests" 338 | version = "2.28.1" 339 | description = "Python HTTP for Humans." 340 | category = "main" 341 | optional = false 342 | python-versions = ">=3.7, <4" 343 | 344 | [package.dependencies] 345 | certifi = ">=2017.4.17" 346 | charset-normalizer = ">=2,<3" 347 | idna = ">=2.5,<4" 348 | urllib3 = ">=1.21.1,<1.27" 349 | 350 | [package.extras] 351 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 352 | use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] 353 | 354 | [[package]] 355 | name = "rich" 356 | version = "13.0.0" 357 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 358 | category = "main" 359 | optional = false 360 | python-versions = ">=3.7.0" 361 | 362 | [package.dependencies] 363 | commonmark = ">=0.9.0,<0.10.0" 364 | pygments = ">=2.6.0,<3.0.0" 365 | typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} 366 | 367 | [package.extras] 368 | jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] 369 | 370 | [[package]] 371 | name = "scipy" 372 | version = "1.10.0" 373 | description = "Fundamental algorithms for scientific computing in Python" 374 | category = "main" 375 | optional = false 376 | python-versions = "<3.12,>=3.8" 377 | 378 | [package.dependencies] 379 | numpy = ">=1.19.5,<1.27.0" 380 | 381 | [package.extras] 382 | test = ["pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "asv", "mpmath", "gmpy2", "threadpoolctl", "scikit-umfpack", "pooch"] 383 | doc = ["sphinx (!=4.1.0)", "pydata-sphinx-theme (==0.9.0)", "sphinx-design (>=0.2.0)", "matplotlib (>2)", "numpydoc"] 384 | dev = ["mypy", "typing-extensions", "pycodestyle", "flake8", "rich-click", "click", "doit (>=0.36.0)", "pydevtool"] 385 | 386 | [[package]] 387 | name = "screeninfo" 388 | version = "0.8.1" 389 | description = "Fetch location and size of physical screens." 390 | category = "main" 391 | optional = false 392 | python-versions = ">=3.6.2,<4.0.0" 393 | 394 | [package.dependencies] 395 | Cython = {version = "*", markers = "sys_platform == \"darwin\""} 396 | pyobjc-framework-Cocoa = {version = "*", markers = "sys_platform == \"darwin\""} 397 | 398 | [[package]] 399 | name = "six" 400 | version = "1.16.0" 401 | description = "Python 2 and 3 compatibility utilities" 402 | category = "main" 403 | optional = false 404 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 405 | 406 | [[package]] 407 | name = "skia-pathops" 408 | version = "0.7.4" 409 | description = "Python access to operations on paths using the Skia library" 410 | category = "main" 411 | optional = false 412 | python-versions = ">=3.7" 413 | 414 | [package.extras] 415 | testing = ["pytest", "coverage", "pytest-xdist", "pytest-randomly"] 416 | 417 | [[package]] 418 | name = "srt" 419 | version = "3.5.2" 420 | description = "A tiny library for parsing, modifying, and composing SRT files." 421 | category = "main" 422 | optional = false 423 | python-versions = ">=2.7" 424 | 425 | [[package]] 426 | name = "svgelements" 427 | version = "1.9.0" 428 | description = "Svg Elements Parsing" 429 | category = "main" 430 | optional = false 431 | python-versions = "*" 432 | 433 | [[package]] 434 | name = "tqdm" 435 | version = "4.64.1" 436 | description = "Fast, Extensible Progress Meter" 437 | category = "main" 438 | optional = false 439 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 440 | 441 | [package.dependencies] 442 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 443 | 444 | [package.extras] 445 | dev = ["py-make (>=0.1.0)", "twine", "wheel"] 446 | notebook = ["ipywidgets (>=6)"] 447 | slack = ["slack-sdk"] 448 | telegram = ["requests"] 449 | 450 | [[package]] 451 | name = "typing-extensions" 452 | version = "4.4.0" 453 | description = "Backported and Experimental Type Hints for Python 3.7+" 454 | category = "main" 455 | optional = false 456 | python-versions = ">=3.7" 457 | 458 | [[package]] 459 | name = "urllib3" 460 | version = "1.26.13" 461 | description = "HTTP library with thread-safe connection pooling, file post, and more." 462 | category = "main" 463 | optional = false 464 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 465 | 466 | [package.extras] 467 | brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] 468 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"] 469 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 470 | 471 | [[package]] 472 | name = "watchdog" 473 | version = "2.2.1" 474 | description = "Filesystem events monitoring" 475 | category = "main" 476 | optional = false 477 | python-versions = ">=3.6" 478 | 479 | [package.extras] 480 | watchmedo = ["PyYAML (>=3.10)"] 481 | 482 | [metadata] 483 | lock-version = "1.1" 484 | python-versions = ">=3.8,<=3.11" 485 | content-hash = "3df5c6645f4ab105e24c49753035acd9a209f61772eef3befe08a8403754d5a1" 486 | 487 | [metadata.files] 488 | certifi = [] 489 | charset-normalizer = [] 490 | click = [ 491 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 492 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 493 | ] 494 | click-default-group = [ 495 | {file = "click-default-group-1.2.2.tar.gz", hash = "sha256:d9560e8e8dfa44b3562fbc9425042a0fd6d21956fcc2db0077f63f34253ab904"}, 496 | ] 497 | cloup = [ 498 | {file = "cloup-0.13.1-py2.py3-none-any.whl", hash = "sha256:04a29a483e122c04f401547dcbce451ce002ff3e392308122619d5b9009f321f"}, 499 | {file = "cloup-0.13.1.tar.gz", hash = "sha256:ea0acc67eed994b86e79b70d76bc2ea525b7f98f3cd8e63696896d549597ef4d"}, 500 | ] 501 | colorama = [] 502 | colour = [ 503 | {file = "colour-0.1.5-py2.py3-none-any.whl", hash = "sha256:33f6db9d564fadc16e59921a56999b79571160ce09916303d35346dddc17978c"}, 504 | {file = "colour-0.1.5.tar.gz", hash = "sha256:af20120fefd2afede8b001fbef2ea9da70ad7d49fafdb6489025dae8745c3aee"}, 505 | ] 506 | commonmark = [ 507 | {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, 508 | {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, 509 | ] 510 | cython = [] 511 | decorator = [ 512 | {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, 513 | {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, 514 | ] 515 | glcontext = [] 516 | idna = [] 517 | isosurfaces = [ 518 | {file = "isosurfaces-0.1.0-py3-none-any.whl", hash = "sha256:a3421f7e7115f72f8f1af538ac4723e5570b1aaa0ddfc6a86520d2d781f3e91f"}, 519 | {file = "isosurfaces-0.1.0.tar.gz", hash = "sha256:fa1b44e5e59d2f429add49289ab89e36f8dcda49b7badd99e0beea273be331f4"}, 520 | ] 521 | manim = [] 522 | manimpango = [] 523 | mapbox-earcut = [] 524 | moderngl = [] 525 | moderngl-window = [] 526 | multipledispatch = [ 527 | {file = "multipledispatch-0.6.0-py2-none-any.whl", hash = "sha256:407e6d8c5fa27075968ba07c4db3ef5f02bea4e871e959570eeb69ee39a6565b"}, 528 | {file = "multipledispatch-0.6.0-py3-none-any.whl", hash = "sha256:a55c512128fb3f7c2efd2533f2550accb93c35f1045242ef74645fc92a2c3cba"}, 529 | {file = "multipledispatch-0.6.0.tar.gz", hash = "sha256:a7ab1451fd0bf9b92cab3edbd7b205622fb767aeefb4fb536c2e3de9e0a38bea"}, 530 | ] 531 | networkx = [] 532 | numpy = [] 533 | pillow = [] 534 | pycairo = [] 535 | pydub = [ 536 | {file = "pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6"}, 537 | {file = "pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f"}, 538 | ] 539 | pyglet = [] 540 | pygments = [] 541 | pyobjc-core = [] 542 | pyobjc-framework-cocoa = [] 543 | pyrr = [ 544 | {file = "pyrr-0.10.3-py3-none-any.whl", hash = "sha256:d8af23fb9bb29262405845e1c98f7339fbba5e49323b98528bd01160a75c65ac"}, 545 | {file = "pyrr-0.10.3.tar.gz", hash = "sha256:3c0f7b20326e71f706a610d58f2190fff73af01eef60c19cb188b186f0ec7e1d"}, 546 | ] 547 | requests = [] 548 | rich = [] 549 | scipy = [] 550 | screeninfo = [] 551 | six = [ 552 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 553 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 554 | ] 555 | skia-pathops = [] 556 | srt = [ 557 | {file = "srt-3.5.2.tar.gz", hash = "sha256:7aa4ad5ce4126d3f53b3e7bc4edaa86653d0378bf1c0b1ab8c59f5ab41384450"}, 558 | ] 559 | svgelements = [] 560 | tqdm = [] 561 | typing-extensions = [] 562 | urllib3 = [] 563 | watchdog = [] 564 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "manim-CAD_Drawing_Utils" 3 | version = "0.0.4" 4 | description = "A collection of utility functions to for creating CAD-like visuals in Manim." 5 | license = "MIT" 6 | authors = ["GarryBGoode"] 7 | readme = "README.md" 8 | repository = "https://github.com/GarryBGoode/Manim_CAD_Drawing_utils" 9 | documentation="" 10 | classifiers= [ 11 | "Development Status :: 4 - Beta", 12 | "License :: OSI Approved :: MIT License", 13 | "Topic :: Scientific/Engineering", 14 | "Topic :: Multimedia :: Video", 15 | "Topic :: Multimedia :: Graphics", 16 | "Programming Language :: Python :: 3.6", 17 | "Programming Language :: Python :: 3.7", 18 | "Programming Language :: Python :: 3.8", 19 | "Programming Language :: Python :: 3.9", 20 | "Natural Language :: English", 21 | ] 22 | 23 | [tool.poetry.dependencies] 24 | python = ">=3.8,<=3.11" 25 | manim = ">=0.15.2,<=1.0" 26 | scipy = "*" 27 | 28 | [tool.poetry.plugins."manim.plugins"] 29 | "manim_cad_drawing_utils" = "manim_cad_drawing_utils" 30 | 31 | [build-system] 32 | requires = ["poetry-core>=1.0.0"] 33 | build-backend = "poetry.core.masonry.api" 34 | -------------------------------------------------------------------------------- /src/manim_cad_drawing_utils/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1.0' 2 | from .dimensions import * 3 | from .path_mapper import * 4 | from .round_corners import * 5 | from .utils import * 6 | from .hatch import * 7 | 8 | -------------------------------------------------------------------------------- /src/manim_cad_drawing_utils/dimensions.py: -------------------------------------------------------------------------------- 1 | from manim import * 2 | from .utils import angle_between_vectors_signed 3 | from .round_corners import * 4 | from .path_mapper import * 5 | 6 | class Pointer_Label_Free(VDict): 7 | def __init__(self, 8 | point, 9 | text:str, 10 | offset_vector=(RIGHT+DOWN), 11 | pointer_offset=0, 12 | **kwargs): 13 | text_buff = 0.1 14 | if not 'stroke_width' in kwargs: 15 | kwargs['stroke_width'] = DEFAULT_STROKE_WIDTH 16 | super().__init__(**kwargs) 17 | 18 | if isinstance(text,str): 19 | textmob = Text(text,**kwargs) 20 | elif isinstance(text,Mobject): 21 | textmob = text 22 | else: 23 | textmob = Text('A',**kwargs) 24 | self.add({'text': textmob}) 25 | self.twidth = (self['text'].get_critical_point(RIGHT)-self['text'].get_critical_point(LEFT))[0] 26 | self.twidth = self.twidth + text_buff * 2 27 | 28 | if 'stroke_width' in kwargs: 29 | stroke_width_loc = kwargs['stroke_width'] 30 | else: 31 | stroke_width_loc = DEFAULT_STROKE_WIDTH 32 | 33 | self.pointer_offset = pointer_offset + stroke_width_loc/100 34 | 35 | dim_line = VMobject(**kwargs).set_points_as_corners([point + normalize(offset_vector)*self.pointer_offset, 36 | point + offset_vector, 37 | point+offset_vector + 38 | self.twidth*np.sign(offset_vector[0])*RIGHT]) 39 | self.add({'line':dim_line}) 40 | self.add({'arrow':CAD_ArrowHead(self['line'],anchor_point=0,**kwargs)}) 41 | self['arrow'].arrowhead.rotate(PI,about_point=ORIGIN) 42 | self['arrow'].add_updater(self['arrow'].default_updater) 43 | self['arrow'].update() 44 | theight = (self['text'].get_critical_point(UP)-self['text'].get_critical_point(DOWN))[1] 45 | self['text'].move_to(self['line'].points[3,:]+UP*theight*0.75,aligned_edge=LEFT*np.sign(offset_vector[0])) 46 | 47 | 48 | 49 | 50 | def update_point(self, point, offset_vector=(RIGHT+DOWN)): 51 | self['line'].set_points_as_corners([point + normalize(offset_vector)*self.pointer_offset, 52 | point + offset_vector, 53 | point + offset_vector + 54 | self.twidth * np.sign(offset_vector[0]) * RIGHT]) 55 | 56 | theight = (self['text'].get_critical_point(UP)-self['text'].get_critical_point(DOWN))[1] 57 | self['text'].move_to(self['line'].points[3,:]+UP*theight*0.75,aligned_edge=LEFT*np.sign(offset_vector[0])) 58 | 59 | 60 | class Pointer_To_Mob(Pointer_Label_Free): 61 | def __init__(self, 62 | mob:Mobject, 63 | proportion, 64 | text:str, 65 | dist=1, 66 | pointer_offset=0, 67 | **kwargs): 68 | point = mob.point_from_proportion(proportion) 69 | 70 | # if the mob center and point happens to be the same, it causes problems 71 | # it can happen if all the mob is 1 point 72 | offset_ref = point - mob.get_center() 73 | if 'offset' in kwargs: 74 | offset=kwargs['offset'] 75 | kwargs.pop('offset') 76 | else: 77 | if np.linalg.norm(offset_ref)>1e-6: 78 | offset = normalize(point - mob.get_center())*dist 79 | else: 80 | # I had no better idea to handle this than to go upright 81 | offset = normalize(RIGHT+UP)*dist 82 | super().__init__(point,text, offset_vector=offset, pointer_offset=pointer_offset, **kwargs) 83 | 84 | def update_mob(self,mob, proportion, dist=1): 85 | point = mob.point_from_proportion(proportion) 86 | # if the mob center and point happens to be the same, it causes problems 87 | # it can happen if all the mob is 1 point 88 | offset_ref = point - mob.get_center() 89 | if np.linalg.norm(offset_ref) > 1e-6: 90 | offset = normalize(point - mob.get_center()) * dist 91 | else: 92 | # I had no better idea to handle this than to go upright 93 | offset = normalize(RIGHT + UP) * dist 94 | super().update_point(point,offset) 95 | 96 | 97 | class Linear_Dimension(VDict): 98 | def __init__(self, 99 | start, 100 | end, 101 | text=None, 102 | direction=ORIGIN, 103 | outside_arrow=False, 104 | offset=2, 105 | ext_line_offset=0, 106 | tip_len=DEFAULT_ARROW_TIP_LENGTH, 107 | **kwargs): 108 | super().__init__(**kwargs) 109 | self.start = start 110 | self.end = end 111 | diff_vect = end-start 112 | norm_vect = normalize(rotate_vector(diff_vect,PI/2)) 113 | 114 | 115 | if direction is ORIGIN: 116 | ofs_vect = norm_vect * offset 117 | ofs_dir = norm_vect 118 | else: 119 | ofs_dir = normalize(direction) 120 | ofs_vect = ofs_dir * offset 121 | if not 'stroke_width' in kwargs: 122 | kwargs['stroke_width'] = DEFAULT_STROKE_WIDTH 123 | 124 | startpoint = start + ofs_dir * np.dot((diff_vect), ofs_dir) / 2 + ofs_vect 125 | endpoint = end - ofs_dir * np.dot((diff_vect), ofs_dir) / 2 + ofs_vect 126 | 127 | self.arrow_offset = 0 128 | if not outside_arrow: 129 | if 'stroke_width' in kwargs: 130 | self.arrow_offset = kwargs['stroke_width'] * 1 / 100 131 | else: 132 | self.arrow_offset = DEFAULT_STROKE_WIDTH*1/100 133 | ext_dir = normalize(endpoint-startpoint) 134 | main_line = Line(start=startpoint + self.arrow_offset*ext_dir, 135 | end=endpoint - self.arrow_offset*ext_dir, 136 | **kwargs) 137 | arrow1 = CAD_ArrowHead(main_line,anchor_point=1, arrow_size=tip_len, reversed_arrow=False) 138 | arrow2 = CAD_ArrowHead(main_line, anchor_point=0, arrow_size=tip_len, reversed_arrow=True) 139 | else: 140 | extension = tip_len*3*(normalize(endpoint-startpoint)) 141 | main_line = Line(start=startpoint-extension, 142 | end=endpoint+extension, 143 | **kwargs) 144 | arrow1 = CAD_ArrowHead(main_line, anchor_point=1, arrow_size=tip_len, reversed_arrow=True) 145 | arrow1.arrowhead.shift(-np.linalg.norm(extension)*RIGHT) 146 | arrow1.default_updater(0) 147 | arrow2 = CAD_ArrowHead(main_line, anchor_point=0, arrow_size=tip_len, reversed_arrow=False) 148 | arrow2.arrowhead.shift(np.linalg.norm(extension) * RIGHT) 149 | arrow2.default_updater(0) 150 | arrow1.update() 151 | arrow2.update() 152 | 153 | 154 | 155 | self.add({'ext_line_1': Line(start=start + ofs_dir * ext_line_offset, 156 | end=startpoint + 0.25 * (normalize(startpoint-start)), 157 | **kwargs)}) 158 | self.add({'ext_line_2': Line(start=end + ofs_dir * ext_line_offset, 159 | end=endpoint + 0.25 * (normalize(endpoint-end)), 160 | **kwargs)}) 161 | self.add({'main_line': main_line}) 162 | self.add({'arrow1': arrow1}) 163 | self.add({'arrow2': arrow2}) 164 | 165 | if isinstance(text, str): 166 | textmob = Text(text, **kwargs) 167 | textmob.set_stroke(opacity=0) 168 | elif isinstance(text, Mobject): 169 | textmob = text 170 | else: 171 | dist = np.linalg.norm(startpoint-endpoint) 172 | textmob = Text(f"{dist:.2}",**kwargs) 173 | textmob.set_stroke(opacity=0) 174 | 175 | text_angle = (main_line.get_angle()+PI/2)%PI-PI/2 176 | if abs(text_angle+PI/2)<1e-8: 177 | text_angle=PI/2 178 | self.text_h = textmob.height 179 | text_w = textmob.width 180 | if not outside_arrow: 181 | text_space = np.linalg.norm(self.start - self.end)-tip_len*2 182 | else: 183 | text_space = np.linalg.norm(self.start - self.end) 184 | 185 | 186 | if text_w > (text_space*0.8): 187 | textmob.scale((text_space*0.8)/text_w) 188 | self.text_h = textmob.height 189 | 190 | textmob.rotate(text_angle) 191 | textmob.move_to(self['main_line'].get_center() + rotate_vector(UP,text_angle)*self.text_h) 192 | self.add({'text': textmob}) 193 | 194 | 195 | class Angle_Dimension_3point(VDict): 196 | def __init__(self, 197 | start, 198 | end, 199 | arc_center, 200 | offset=2, 201 | text=None, 202 | outside_arrow=False, 203 | ext_line_offset=0, 204 | tip_len=DEFAULT_ARROW_TIP_LENGTH, 205 | **kwargs): 206 | super().__init__(**kwargs) 207 | 208 | if not 'stroke_width' in kwargs: 209 | kwargs['stroke_width'] = DEFAULT_STROKE_WIDTH 210 | 211 | self.angle = angle_between_vectors_signed(start-arc_center,end-arc_center) 212 | radius = (np.linalg.norm(start-arc_center)+np.linalg.norm(end-arc_center))/2 + offset 213 | angle_0 = angle_of_vector(start-arc_center) 214 | angle_1 = angle_between_vectors_signed(start-arc_center,end-arc_center) 215 | 216 | base_arc = Arc(radius=radius, 217 | start_angle=angle_0, 218 | arc_center=arc_center, 219 | angle=angle_1, 220 | **kwargs) 221 | arc_p0 = base_arc.point_from_proportion(0) 222 | arc_p1 = base_arc.point_from_proportion(1) 223 | line1 = Line(start=start + normalize(arc_p0-start) * ext_line_offset, 224 | end=arc_p0 + normalize(arc_p0-start)*tip_len, 225 | **kwargs 226 | ) 227 | line2 = Line(start=end + normalize(arc_p1-end) * ext_line_offset, 228 | end=arc_p1 + normalize(arc_p1-end)*tip_len, 229 | **kwargs 230 | ) 231 | # self.add(line1,line2) 232 | self.add({'ext_line_1': line1}) 233 | self.add({'ext_line_2': line2}) 234 | if not outside_arrow: 235 | 236 | if 'stroke_width' in kwargs: 237 | self.arrow_offset = kwargs['stroke_width'] * 1 / 100 238 | else: 239 | self.arrow_offset = DEFAULT_STROKE_WIDTH*1/100 240 | 241 | arrow1 = CAD_ArrowHead(base_arc, anchor_point=1, arrow_size=tip_len, reversed_arrow=False) 242 | arrow2 = CAD_ArrowHead(base_arc, anchor_point=0, arrow_size=tip_len, reversed_arrow=True) 243 | 244 | arrow1.arrowhead.shift(LEFT*self.arrow_offset) 245 | arrow2.arrowhead.shift(RIGHT * self.arrow_offset) 246 | arrow1.generate_points() 247 | arrow2.generate_points() 248 | self.add({'base_arc': base_arc}) 249 | self.add({'arrow_1': arrow1}) 250 | self.add({'arrow_2': arrow2}) 251 | else: 252 | extension = tip_len*3 * np.sign(angle_1) 253 | angle_ext = extension/radius 254 | 255 | base_arc = Arc(radius=radius, 256 | start_angle=angle_0-angle_ext, 257 | angle=+self.angle + angle_ext * 2, 258 | arc_center=arc_center, 259 | **kwargs) 260 | arrow1 = CAD_ArrowHead(base_arc, anchor_point=1, arrow_size=tip_len, reversed_arrow=True) 261 | arrow1.arrowhead.shift(extension * RIGHT) 262 | arrow1.default_updater(0) 263 | arrow2 = CAD_ArrowHead(base_arc, anchor_point=0, arrow_size=tip_len, reversed_arrow=False) 264 | arrow2.arrowhead.shift(-extension * RIGHT) 265 | arrow2.default_updater(0) 266 | self.add({'base_arc': base_arc}) 267 | self.add({'arrow_1': arrow1}) 268 | self.add({'arrow_2': arrow2}) 269 | 270 | if isinstance(text,str): 271 | textmob = Text(text,**kwargs) 272 | textmob.set_stroke(opacity=0) 273 | elif isinstance(text,Mobject): 274 | textmob = text 275 | else: 276 | textmob = Text(f"{abs(angle_1/DEGREES):.0f}°", **kwargs) 277 | textmob.set_stroke(opacity=0) 278 | 279 | pos_text = base_arc.point_from_proportion(0.5) 280 | angle_text = (angle_of_vector(base_arc.point_from_proportion(0.5+1e-6) - 281 | (base_arc.point_from_proportion(0.5-1e-6))) + PI / 2) % PI - PI / 2 282 | if abs(angle_text+PI/2)<1e-8: 283 | angle_text=PI/2 284 | self.text_h = textmob.height 285 | textmob.rotate(angle_text) 286 | textmob.move_to(pos_text + rotate_vector(UP,angle_text)*self.text_h) 287 | self.add({'text': textmob}) 288 | 289 | class Angle_Dimension_Mob(Angle_Dimension_3point): 290 | def __init__(self, 291 | Mob: Mobject, 292 | start_proportion, 293 | end_proportion, 294 | **kwargs): 295 | start = Mob.point_from_proportion(start_proportion) 296 | end = Mob.point_from_proportion(end_proportion) 297 | start_diff_points = np.clip(np.array([start_proportion+1e-6,start_proportion-1e-6]),0,1) 298 | end_diff_points = np.clip(np.array([end_proportion + 1e-6, end_proportion - 1e-6]),0,1) 299 | v_start = normalize(Mob.point_from_proportion(start_diff_points[1])-Mob.point_from_proportion(start_diff_points[0])) 300 | v_end = normalize(Mob.point_from_proportion(end_diff_points[1])-Mob.point_from_proportion(end_diff_points[0])) 301 | v_mat = np.concatenate((np.reshape(v_start[0:2], (2,1)),np.reshape(-v_end[0:2], (2, 1))),axis=1) 302 | dP = end-start 303 | k = np.linalg.solve(v_mat,dP[0:2]) 304 | center = start+k[0]*v_start 305 | # center = np.reshape(center,(3,0)) 306 | super().__init__(start, 307 | end, 308 | center.flatten(), 309 | **kwargs) 310 | 311 | class CAD_ArrowHead(Curve_Warp): 312 | def __init__(self, 313 | target_curve: VMobject, 314 | anchor_point=1, 315 | arrow_size=DEFAULT_ARROW_TIP_LENGTH, 316 | reversed_arrow=False, 317 | **kwargs): 318 | 319 | # set default value 320 | self.__reversed_arrow = False 321 | self.arrowhead = VMobject() 322 | self.arrowhead.set_points_as_corners([(LEFT*2+UP), 323 | ORIGIN, 324 | LEFT*2+DOWN]) 325 | 326 | 327 | # set input value 328 | self.reversed_arrow = reversed_arrow 329 | self.set_arrow_size(arrow_size) 330 | self.arrowhead.match_style(target_curve) 331 | 332 | super().__init__(self.arrowhead,target_curve,anchor_point=anchor_point,**kwargs) 333 | 334 | 335 | def default_updater(self,mob): 336 | self.PM.generate_length_map() 337 | self.generate_points() 338 | 339 | def get_tip_pos(self): 340 | ''' By convention, the tip of the arrow should be the right-most point of the shape, unless it's reversed''' 341 | if self.__reversed_arrow: 342 | return self.arrowhead.get_critical_point(LEFT)[0] 343 | else: 344 | return self.arrowhead.get_critical_point(RIGHT)[0] 345 | 346 | def set_arrow_size(self,arrow_size): 347 | act_size = (self.arrowhead.get_critical_point(RIGHT)-self.arrowhead.get_critical_point(LEFT))[0] 348 | if act_size!=0: 349 | self.arrowhead.shift(-self.get_tip_pos()*RIGHT) 350 | self.arrowhead.points *= arrow_size/act_size 351 | self.arrowhead.shift(+self.get_tip_pos() * RIGHT) 352 | 353 | def flip_arrow(self): 354 | self.arrowhead.flip() 355 | 356 | @property 357 | def reversed_arrow(self): 358 | return self.__reversed_arrow 359 | 360 | @reversed_arrow.setter 361 | def reversed_arrow(self, rev): 362 | if rev != self.__reversed_arrow: 363 | self.flip_arrow() 364 | self.__reversed_arrow = rev 365 | -------------------------------------------------------------------------------- /src/manim_cad_drawing_utils/hatch.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from manim import * 3 | from scipy.optimize import root 4 | 5 | class Hatch_lines(VGroup): 6 | def __init__(self, target_mobject: Mobject, angle=PI/6, offset=0.3,**kwargs): 7 | super().__init__(**kwargs) 8 | self.target = target_mobject 9 | target_size_xy = [self.target.get_critical_point(RIGHT)-self.target.get_critical_point(LEFT), 10 | self.target.get_critical_point(UP)-self.target.get_critical_point(DOWN)] 11 | target_size_diag = np.linalg.norm(target_size_xy) 12 | num_lines = int(target_size_diag//offset) 13 | line_v = np.array([np.cos(angle),np.sin(angle),0]) 14 | offs_v = np.array([-np.sin(angle),np.cos(angle),0]) 15 | center = self.target.get_center() 16 | for k in range(num_lines*2): 17 | line_loc = Line(center-line_v*target_size_diag*1.5,center+line_v*target_size_diag*1.5) 18 | line_loc.shift((k-(num_lines-1))*offs_v*offset) 19 | # line_loc = Intersection(line_loc,self.target) 20 | intersect_idx = curve_intersection(self.target,line_loc) 21 | if any(intersect_idx[1]): 22 | 23 | intersect_indexes = sorted(intersect_idx[1]) 24 | for j in range(len(intersect_idx[1])//2): 25 | idx1 = int(intersect_indexes[0+2*j] // 1) 26 | alpha1 = intersect_indexes[0+2*j] % 1 27 | idx2 = int(intersect_indexes[1+2*j] // 1) 28 | alpha2 = intersect_indexes[1+2*j] % 1 29 | point1 = line_loc.get_nth_curve_function(idx1)(alpha1) 30 | point2 = line_loc.get_nth_curve_function(idx2)(alpha2) 31 | line_loc2 = Line(start=point1,end=point2,**kwargs) 32 | self.add(line_loc2) 33 | 34 | 35 | def curve_intersection(vmob1: VMobject, vmob2: VMobject): 36 | """Intersection points of 2 VMobjects. 37 | Finds all intersections of 2 vmobjects, as long as there is only 1 interesection per bezier subcurve. 38 | Self intersections are not found.""" 39 | 40 | intersect_indx_1 = np.array([]) 41 | intersect_indx_2 = np.array([]) 42 | for i in range(vmob1.get_num_curves()): 43 | for j in range(vmob2.get_num_curves()): 44 | curve_1 = vmob1.get_nth_curve_points(i) 45 | curve_2 = vmob2.get_nth_curve_points(j) 46 | x_range_1 = np.array([np.amax(curve_1[:, 0]), np.amin(curve_1[:, 0])]) 47 | x_range_2 = np.array([np.amax(curve_2[:, 0]), np.amin(curve_2[:, 0])]) 48 | y_range_1 = np.array([np.amax(curve_1[:, 1]), np.amin(curve_1[:, 1])]) 49 | y_range_2 = np.array([np.amax(curve_2[:, 1]), np.amin(curve_2[:, 1])]) 50 | 51 | distinct_x = x_range_2[1] > x_range_1[0] or x_range_1[1] > x_range_2[0] 52 | distinct_y = y_range_2[1] > y_range_1[0] or y_range_1[1] > y_range_2[0] 53 | 54 | overlap = not (distinct_x or distinct_y) 55 | 56 | if overlap: 57 | curve_fun_1 = vmob1.get_nth_curve_function(i) 58 | curve_fun_2 = vmob2.get_nth_curve_function(j) 59 | 60 | sol = root(lambda t: (curve_fun_1(t[0])[0:2]-curve_fun_2(t[1])[0:2]),np.array((0.5,0.5))) 61 | if sol.success: 62 | if 0 < sol.x[0] < 1 and 0 < sol.x[1] < 1: 63 | intersect_indx_1 = np.append(intersect_indx_1, sol.x[0] + i) 64 | intersect_indx_2 = np.append(intersect_indx_2, sol.x[1] + j) 65 | 66 | return intersect_indx_1, intersect_indx_2 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/manim_cad_drawing_utils/path_mapper.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from manim import * 3 | from .utils import angle_between_vectors_signed 4 | from .round_corners import * 5 | 6 | class Path_mapper(VMobject): 7 | def __init__(self,path_source:VMobject,num_of_path_points=100,**kwargs): 8 | super().__init__(**kwargs) 9 | self.num_of_path_points = num_of_path_points 10 | self.path= path_source 11 | self.generate_length_map() 12 | 13 | def generate_length_map(self): 14 | norms = np.array(0) 15 | for k in range(self.path.get_num_curves()): 16 | norms = np.append(norms, self.path.get_nth_curve_length_pieces(k,sample_points=11)) 17 | # add up length-pieces in array form 18 | self.pathdata_lengths = np.cumsum(norms) 19 | self.pathdata_alpha = np.linspace(0, 1, self.pathdata_lengths.size) 20 | 21 | def cubic_to_quads(self, cubic_points): 22 | # based on https://ttnghia.github.io/pdf/QuadraticApproximation.pdf 23 | q_points = np.empty((0,3)) 24 | gamma = 0.5 25 | q1 = cubic_points[0,:] + 3/2*gamma * (cubic_points[1,:]-cubic_points[0,:]) 26 | q3 = cubic_points[3,:] + 3/2*(1-gamma) * (cubic_points[2,:]-cubic_points[3,:]) 27 | q2 = (1-gamma)*q1 + gamma * q3 28 | q0 = cubic_points[0,:] 29 | q4 = cubic_points[3,:] 30 | q_points = np.append(q_points,(q0,q1,q2,q2,q3,q4),axis=0) 31 | return q_points 32 | 33 | def calc_len_bezier_quad(self,points): 34 | a = points[0, :] 35 | b = points[1, :] 36 | c = points[2, :] 37 | 38 | B = b-a 39 | F = c-b 40 | A = F-B 41 | nF = np.linalg.norm(F) 42 | nA = np.linalg.norm(A) 43 | nB = np.linalg.norm(B) 44 | if nA>1e-8: 45 | L = (nF * np.dot(A, F) - nB * np.dot(A, B)) / (nA ** 2) + (nA ** 2 * nB ** 2 - np.dot(A, B) ** 2) / \ 46 | (nA ** 3) * (np.log(nA * nF + np.dot(A, F)) - np.log(nA * nB + np.dot(A, B))) 47 | return L 48 | else: 49 | return np.linalg.norm(a-c) 50 | 51 | def calc_len_with_quads(self): 52 | L = 0 53 | for k in range(self.path.get_num_curves()): 54 | points = self.path.get_nth_curve_points(k) 55 | quads = self.cubic_to_quads(points) 56 | L0 = self.calc_len_bezier_quad(quads[:3, :]) 57 | L1 = self.calc_len_bezier_quad(quads[3:, :]) 58 | L += L0+L1 59 | return L 60 | 61 | def get_path_length(self): 62 | return self.pathdata_lengths[-1] 63 | 64 | def alpha_from_length(self,s): 65 | if hasattr(s, '__iter__'): 66 | return [np.interp(t, self.pathdata_lengths, self.pathdata_alpha) for t in s] 67 | else: 68 | return np.interp(s, self.pathdata_lengths, self.pathdata_alpha) 69 | 70 | def length_from_alpha(self,a): 71 | if hasattr(a, '__iter__'): 72 | return [np.interp(t, self.pathdata_alpha, self.pathdata_lengths) for t in a] 73 | else: 74 | return np.interp(a, self.pathdata_alpha, self.pathdata_lengths) 75 | 76 | def equalize_alpha(self, a): 77 | 'used for inverting the alpha behavior' 78 | return self.alpha_from_length(a*self.get_path_length()) 79 | 80 | def equalize_rate_func(self, rate_func): 81 | ''' 82 | Specifically made to be used with Create() animation. 83 | :param rate_func: rate function to be equalized 84 | :return: callable new rate function 85 | Example: 86 | class test_path_mapper_anim(Scene): 87 | def construct(self): 88 | mob1 = round_corners(Triangle(fill_color=TEAL,fill_opacity=0).scale(3),0.5) 89 | PM = Path_mapper(mob1) 90 | mob2 = mob1.copy() 91 | mob1.shift(LEFT * 2.5) 92 | mob2.shift(RIGHT * 2.5) 93 | 94 | self.play(Create(mob1,rate_func=PM.equalize_rate_func(smooth)),Create(mob2),run_time=5) 95 | self.wait() 96 | ''' 97 | def eq_func(t:float): 98 | return self.equalize_alpha(rate_func(t)) 99 | return eq_func 100 | 101 | def point_from_proportion(self, alpha: float) -> np.ndarray: 102 | ''' 103 | Override original implementation. 104 | Should be the same, except it uses pre calculated length table and should be faster a bit. 105 | ''' 106 | if hasattr(alpha, '__iter__'): 107 | values = self.alpha_from_length(alpha * self.get_path_length()) 108 | ret = np.empty((0,3)) 109 | for a in values: 110 | if a == 1: 111 | index = self.path.get_num_curves() - 1 112 | remainder = 1 113 | else: 114 | index = int(a * self.path.get_num_curves() // 1) 115 | remainder = (a * self.path.get_num_curves()) % 1 116 | p = self.path.get_nth_curve_function(index)(remainder) 117 | ret = np.concatenate([ret,np.reshape(p,(1,3))],axis=0) 118 | return ret 119 | else: 120 | a = self.alpha_from_length(alpha*self.get_path_length()) 121 | if a==1: 122 | index = self.path.get_num_curves()-1 123 | remainder = 1 124 | else: 125 | index = int(a * self.path.get_num_curves() // 1) 126 | remainder = (a * self.path.get_num_curves()) % 1 127 | return self.path.get_nth_curve_function(index)(remainder) 128 | 129 | def get_length_between_points(self,b,a): 130 | ''' 131 | Signed arc length between to points. 132 | :param b: second point 133 | :param a: first point 134 | :return: length (b-a) 135 | ''' 136 | return self.length_from_alpha(b)-self.length_from_alpha(a) 137 | 138 | def get_length_between_points_wrapped(self,b,a): 139 | ''' This function wraps around the length between two points similar to atan2 method. 140 | Useful for closed mobjects. 141 | Returns distance value between -L/2...L/2 ''' 142 | AB = self.get_length_between_points(b,a) 143 | L = self.get_path_length() 144 | return (AB%L-L/2)%L-L/2 145 | 146 | def get_length_between_points_tuple(self,b,a): 147 | ''' Function to get the 2 absolute lengths between 2 parameters on closed mobjects. 148 | Useful for closed mobjects. 149 | :returns tuple (shorter, longer)''' 150 | 151 | AB = abs(self.get_length_between_points(b,a)) 152 | L = self.get_path_length() 153 | if AB>L/2: 154 | return (L - AB), AB 155 | else: 156 | return AB, (L - AB) 157 | 158 | def get_bezier_index_from_length(self,s): 159 | a = self.alpha_from_length(s) 160 | nc = self.path.get_num_curves() 161 | indx = int(a * nc // 1) 162 | bz_a = a * nc % 1 163 | if indx==nc: 164 | indx = nc-1 165 | bz_a=1 166 | return (indx,bz_a) 167 | 168 | def get_tangent_unit_vector(self,s): 169 | # diff_bez_points = 1/3*(self.path.points[1:,:]-self.path.points[:-1,:]) 170 | indx, bz_a = self.get_bezier_index_from_length(s) 171 | points = self.path.get_nth_curve_points(indx) 172 | dpoints = (points[1:,:]-points[:-1,:])/3 173 | bzf = bezier(dpoints) 174 | point = bzf(bz_a) 175 | return normalize(point) 176 | 177 | def get_tangent_angle(self,s): 178 | tv = self.get_tangent_unit_vector(s) 179 | return angle_of_vector(tv) 180 | 181 | def get_normal_unit_vector(self,s): 182 | tv = self.get_tangent_unit_vector(s) 183 | return rotate_vector(tv,PI/2) 184 | 185 | def get_curvature_vector(self,s): 186 | indx, bz_a = self.get_bezier_index_from_length(s) 187 | points = self.path.get_nth_curve_points(indx) 188 | dpoints = (points[1:, :] - points[:-1, :]) * 3 189 | ddpoints = (dpoints[1:, :] - dpoints[:-1, :]) * 2 190 | deriv = bezier(dpoints)(bz_a) 191 | dderiv = bezier(ddpoints)(bz_a) 192 | curv = np.cross(deriv, dderiv) / (np.linalg.norm(deriv)**3) 193 | return curv 194 | 195 | def get_curvature(self,s): 196 | return np.linalg.norm(self.get_curvature_vector(s)) 197 | 198 | # DashedVMobject 199 | 200 | class Dashed_line_mobject(VDict): 201 | def __init__(self,target_mobject:VMobject, 202 | num_dashes=15, 203 | dashed_ratio=0.5, 204 | dash_offset=0.0, 205 | **kwargs): 206 | super().__init__(**kwargs) 207 | self.path = Path_mapper(target_mobject,num_of_path_points=10*target_mobject.get_num_curves()) 208 | # self.path.add_updater(lambda mob: mob.generate_length_map()) 209 | 210 | dshgrp = self.generate_dash_mobjects( 211 | **self.generate_dash_pattern_dash_distributed(num_dashes,dash_ratio = dashed_ratio,offset=dash_offset) 212 | ) 213 | self.add({'dashes':dshgrp}) 214 | self['dashes'].match_style(target_mobject) 215 | 216 | def generate_dash_pattern_metric(self,dash_len,space_len, num_dashes, offset=0): 217 | ''' generate dash pattern in metric curve-length space''' 218 | period = dash_len + space_len 219 | n = num_dashes 220 | full_len = self.path.get_path_length() 221 | dash_starts = [(i * period + offset) for i in range(n)] 222 | dash_ends = [(i * period + dash_len + offset) for i in range(n)] 223 | k=0 224 | while k full_len: 233 | dash_ends.pop(k) 234 | dash_starts.pop(k) 235 | k+=1 236 | return {'dash_starts':dash_starts,'dash_ends':dash_ends} 237 | 238 | def generate_dash_pattern_dash_distributed(self,num_dashes,dash_ratio = 0.5,offset=0.0): 239 | full_len = self.path.get_path_length() 240 | period = full_len / num_dashes 241 | dash_len = period * dash_ratio 242 | space_len = period * (1-dash_ratio) 243 | n = num_dashes+2 244 | 245 | return self.generate_dash_pattern_metric(dash_len, space_len, n, offset=(offset-1)*period) 246 | 247 | def generate_dash_mobjects(self,dash_starts=[0],dash_ends=[1]): 248 | ref_mob = self.path.path 249 | a_list = self.path.alpha_from_length(dash_starts) 250 | b_list = self.path.alpha_from_length(dash_ends) 251 | ret=[] 252 | for i in range(len(dash_starts)): 253 | mobcopy = VMobject().match_points(ref_mob) 254 | ret.append(mobcopy.pointwise_become_partial(mobcopy,a_list[i],b_list[i])) 255 | return VGroup(*ret) 256 | 257 | 258 | class DashDot_mobject(Dashed_line_mobject): 259 | def __init__(self, 260 | target_mobject: VMobject, 261 | num_dashes=15, 262 | dashed_ratio=0.35, 263 | dash_offset=0.0, 264 | dot_scale=1, 265 | **kwargs): 266 | super().__init__(target_mobject, 267 | num_dashes, 268 | dashed_ratio, 269 | dash_offset, 270 | **kwargs) 271 | dot = Circle(radius=target_mobject.get_stroke_width()/100*dot_scale, 272 | fill_opacity=1, 273 | stroke_opacity=0, 274 | num_components=6, 275 | fill_color=target_mobject.get_stroke_color()) 276 | dash_marks = self.generate_dash_pattern_dash_distributed(num_dashes,dashed_ratio,dash_offset) 277 | 278 | full_len = self.path.get_path_length() 279 | if dash_marks['dash_starts'][0]L: 413 | endpoint = self.PM.point_from_proportion(1) 414 | tanv = self.PM.get_tangent_unit_vector(L) 415 | nv = rotate_vector(tanv,PI/2) 416 | x_1 = x-L 417 | p = endpoint + x_1 * tanv + y * nv 418 | self.points = np.append(self.points, np.reshape(p,(1,3)), axis=0) 419 | else: 420 | startpoint = self.PM.point_from_proportion(0) 421 | tanv = self.PM.get_tangent_unit_vector(0) 422 | nv = rotate_vector(tanv, PI / 2) 423 | x_1 = x 424 | p = startpoint + x_1 * tanv + y * nv 425 | self.points = np.append(self.points, np.reshape(p, (1, 3)), axis=0) 426 | -------------------------------------------------------------------------------- /src/manim_cad_drawing_utils/round_corners.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from manim import * 3 | from scipy.optimize import fsolve 4 | from scipy.optimize import root 5 | from scipy.optimize import root_scalar 6 | from .utils import angle_between_vectors_signed 7 | 8 | __all__ = [ 9 | "Round_Corner_Param", 10 | "Round_Corners", 11 | "Chamfer_Corner_Param", 12 | "Chamfer_Corners" 13 | ] 14 | 15 | 16 | def Round_Corner_Param(radius,curve_points_1,curve_points_2): 17 | bez_func_1 = bezier(curve_points_1) 18 | diff_func_1 = bezier((curve_points_1[1:, :] - curve_points_1[:-1, :]) / 3) 19 | bez_func_2 = bezier(curve_points_2) 20 | diff_func_2= bezier((curve_points_2[1:, :] - curve_points_2[:-1, :]) / 3) 21 | 22 | def find_crossing(p1,p2,n1,n2): 23 | t = fsolve(lambda t: p1[:2]+n1[:2]*t[0]-(p2[:2]+n2[:2]*t[1]),[0,0]) 24 | return t, p1+n1*t[0] 25 | 26 | def rad_cost_func(t): 27 | angle_sign = np.sign( angle_between_vectors_signed(diff_func_1(t[0]),diff_func_2(t[1]))) 28 | p1 = bez_func_1((t[0])) 29 | n1 = normalize(rotate_vector(diff_func_1(t[0]),angle_sign* PI / 2)) 30 | p2 = bez_func_2((t[1])) 31 | n2 = normalize(rotate_vector(diff_func_2(t[1]), angle_sign* PI / 2)) 32 | d = (find_crossing(p1, p2, n1, n2))[0] 33 | # 2 objectives for optimization: 34 | # - the normal distances should be equal to each other 35 | # - the normal distances should be equal to the target radius 36 | # I'm hoping that in this form at least a tangent circle will be found (first goal), 37 | # even if there is no solution at the desired radius. I don't really know, fsolve() and roots() is magic. 38 | return ((d[0])-(d[1])),((d[1])+(d[0])-2*radius) 39 | 40 | k = root(rad_cost_func,np.asarray([0.5,0.5]),method='hybr')['x'] 41 | 42 | p1 = bez_func_1(k[0]) 43 | n1 = normalize(rotate_vector(diff_func_1(k[0]), PI / 2)) 44 | p2 = bez_func_2(k[1]) 45 | n2 = normalize(rotate_vector(diff_func_2(k[1]), PI / 2)) 46 | d, center = find_crossing(p1, p2, n1, n2) 47 | r = abs(d[0]) 48 | start_angle = np.arctan2((p1-center)[1],(p1-center)[0]) 49 | cval = np.dot(p1-center,p2-center) 50 | sval = (np.cross(p1-center,p2-center))[2] 51 | angle = np.arctan2(sval,cval) 52 | 53 | out_param = {'radius': r, 'arc_center': center, 'start_angle': start_angle, 'angle': angle} 54 | 55 | return out_param, k 56 | 57 | 58 | def Round_Corners(mob:VMobject,radius=0.2): 59 | i=0 60 | while i < mob.get_num_curves() and i<1e5: 61 | ind1 = i % mob.get_num_curves() 62 | ind2 = (i+1) % mob.get_num_curves() 63 | curve_1 = mob.get_nth_curve_points(ind1) 64 | curve_2 = mob.get_nth_curve_points(ind2) 65 | handle1 = curve_1[-1,:]-curve_1[-2,:] 66 | handle2 = curve_2[1, :] - curve_2[0, :] 67 | # angle_test = (np.cross(normalize(anchor1),normalize(anchor2)))[2] 68 | angle_test = angle_between_vectors_signed(handle1,handle2) 69 | if abs(angle_test)>1E-6: 70 | params, k = Round_Corner_Param(radius,curve_1,curve_2) 71 | cut_curve_points_1 = partial_bezier_points(curve_1, 0, k[0]) 72 | cut_curve_points_2 = partial_bezier_points(curve_2, k[1], 1) 73 | loc_arc = Arc(**params,num_components=5) 74 | # mob.points = np.delete(mob.points, slice((ind1 * 4), (ind1 + 1) * 4), axis=0) 75 | # mob.points = np.delete(mob.points, slice((ind2 * 4), (ind2 + 1) * 4), axis=0) 76 | mob.points[ind1 * 4:(ind1 + 1) * 4, :] = cut_curve_points_1 77 | mob.points[ind2 * 4:(ind2 + 1) * 4, :] = cut_curve_points_2 78 | mob.points = np.insert(mob.points,ind2*4,loc_arc.points,axis=0) 79 | i=i+loc_arc.get_num_curves()+1 80 | else: 81 | i=i+1 82 | 83 | if i==mob.get_num_curves()-1 and not mob.is_closed(): 84 | break 85 | 86 | return mob 87 | 88 | 89 | 90 | def Chamfer_Corner_Param(offset,curve_points_1,curve_points_2): 91 | # this is ugly, I know, don't judge 92 | if hasattr(offset,'iter'): 93 | ofs = [offset[0], offset[1]] 94 | else: 95 | ofs = [offset, offset] 96 | bez_func_1 = bezier(curve_points_1) 97 | bez_func_2 = bezier(curve_points_2) 98 | 99 | #copied from vectorized mobject length stuff 100 | def get_norms_and_refs(curve): 101 | sample_points = 10 102 | refs = np.linspace(0, 1, sample_points) 103 | points = np.array([curve(a) for a in np.linspace(0, 1, sample_points)]) 104 | diffs = points[1:] - points[:-1] 105 | norms = np.cumsum(np.apply_along_axis(np.linalg.norm, 1, diffs)) 106 | norms = np.insert(norms,0,0) 107 | return norms,refs 108 | 109 | norms1,refs1 = get_norms_and_refs(bez_func_1) 110 | norms2,refs2 = get_norms_and_refs(bez_func_2) 111 | a1 = (np.interp(norms1[-1]-ofs[0], norms1, refs1)) 112 | a2 = (np.interp(ofs[1], norms2, refs2)) 113 | p1 = bez_func_1(a1) 114 | p2 = bez_func_2(a2) 115 | param = {'start':p1,'end':p2} 116 | 117 | return param, [a1,a2] 118 | 119 | 120 | def Chamfer_Corners(mob:VMobject,offset=0.2): 121 | i=0 122 | while i < mob.get_num_curves() and i<1e5: 123 | ind1 = i % mob.get_num_curves() 124 | ind2 = (i+1) % mob.get_num_curves() 125 | curve_1 = mob.get_nth_curve_points(ind1) 126 | curve_2 = mob.get_nth_curve_points(ind2) 127 | handle1 = curve_1[-1,:]-curve_1[-2,:] 128 | handle2 = curve_2[1, :] - curve_2[0, :] 129 | # angle_test = (np.cross(normalize(anchor1),normalize(anchor2)))[2] 130 | angle_test = angle_between_vectors_signed(handle1,handle2) 131 | if abs(angle_test)>1E-6: 132 | params, k = Chamfer_Corner_Param(offset,curve_1,curve_2) 133 | cut_curve_points_1 = partial_bezier_points(curve_1, 0, k[0]) 134 | cut_curve_points_2 = partial_bezier_points(curve_2, k[1], 1) 135 | loc_line = Line(**params) 136 | # mob.points = np.delete(mob.points, slice((ind1 * 4), (ind1 + 1) * 4), axis=0) 137 | # mob.points = np.delete(mob.points, slice((ind2 * 4), (ind2 + 1) * 4), axis=0) 138 | mob.points[ind1 * 4:(ind1 + 1) * 4, :] = cut_curve_points_1 139 | mob.points[ind2 * 4:(ind2 + 1) * 4, :] = cut_curve_points_2 140 | mob.points = np.insert(mob.points,ind2*4,loc_line.points,axis=0) 141 | i=i+loc_line.get_num_curves()+1 142 | else: 143 | i=i+1 144 | 145 | if i==mob.get_num_curves()-1 and not mob.is_closed(): 146 | break 147 | 148 | return mob 149 | 150 | 151 | 152 | 153 | 154 | # with tempconfig({"quality": "medium_quality", "disable_caching": True}): 155 | # scene = Test_corners() 156 | # scene.render() -------------------------------------------------------------------------------- /src/manim_cad_drawing_utils/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from manim import * 3 | 4 | __all__ = ["angle_between_vectors_signed", 5 | "Bezier_Handlebars"] 6 | 7 | def angle_between_vectors_signed(v1,v2): 8 | ''' 9 | Get signed angle between vectors according to right hand rule. 10 | :param v1: first vector 11 | :param v2: second vector 12 | :return: angle of rotation that rotates v1 to be co-linear with v2. Range: -PI...+PI 13 | ''' 14 | cval = np.dot(v1, v2) 15 | sval = (np.cross(v1, v2))[2] 16 | return np.arctan2(sval, cval) 17 | 18 | class Bezier_Handlebars(VDict): 19 | ''' 20 | Creates circles (dots) and lines on the points of another mobject. 21 | Used for visualizing bezier control points and handles, helps debugging. 22 | ''' 23 | def __init__(self,target_mobject:VMobject, **kwargs): 24 | super().__init__(**kwargs) 25 | self.target = target_mobject 26 | self.generate_circles(0.03) 27 | self.generate_lines() 28 | 29 | 30 | def generate_circles(self,r): 31 | DotGroup = VGroup(*[ 32 | Circle(radius=r, 33 | arc_center=p, 34 | color=TEAL, 35 | fill_opacity=1, 36 | stroke_opacity=0, 37 | num_components=4) 38 | for p in self.target.points]) 39 | self['dots'] = DotGroup 40 | 41 | def move_circles(self): 42 | for zipthing in zip(self['dots'],self.target.points): 43 | zipthing[0].move_to(zipthing[1]) 44 | 45 | def generate_lines(self): 46 | pointlist = zip(self.target.points[0::4, :], self.target.points[1::4, :], self.target.points[2::4, :], 47 | self.target.points[3::4, :]) 48 | LineGroup = VGroup(*[VGroup(*[ 49 | Line(p[0],p[1],color=TEAL, stroke_width=2),Line(p[2],p[3],color=TEAL, stroke_width=2)] 50 | ) for p in pointlist]) 51 | self['lines'] = LineGroup 52 | 53 | def move_lines(self): 54 | pointlist = zip(self.target.points[0::4, :], self.target.points[1::4, :], self.target.points[2::4, :], 55 | self.target.points[3::4, :]) 56 | for zipthing in zip(self['lines'],pointlist): 57 | # Line.put_start_and_end_on() 58 | zipthing[0][0].put_start_and_end_on(zipthing[1][0],zipthing[1][1]) 59 | zipthing[0][1].put_start_and_end_on(zipthing[1][2], zipthing[1][3]) -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GarryBGoode/Manim_CAD_Drawing_utils/d9ad2b4cf2006a7f4bc7e4c16d917d7ec93b7988/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_dashes.py: -------------------------------------------------------------------------------- 1 | from manim import * 2 | from manim_cad_drawing_utils import * 3 | 4 | class test(Scene): 5 | def construct(self): 6 | mob1 = Round_Corners(Square().scale(3),radius=0.8).shift(DOWN*0) 7 | vt = ValueTracker(0) 8 | dash1 = Dashed_line_mobject(mob1,num_dashes=36,dashed_ratio=0.5,dash_offset=0) 9 | def dash_updater(mob): 10 | offset = vt.get_value()%1 11 | dshgrp = mob.generate_dash_mobjects( 12 | **mob.generate_dash_pattern_dash_distributed(36, dash_ratio=0.5, offset=offset) 13 | ) 14 | mob['dashes'].become(dshgrp) 15 | dash1.add_updater(dash_updater) 16 | 17 | self.add(dash1) 18 | self.play(vt.animate.set_value(2),run_time=6) 19 | self.wait(0.5) 20 | 21 | class test_ddot(Scene): 22 | def construct(self): 23 | # mob1 = ParametricFunction(lambda t: [t,np.sin(t),0], t_range=[-PI,PI],stroke_opacity=0.3) 24 | # mob1 = Circle(stroke_opacity=1) 25 | mob1 = Line(LEFT*6,RIGHT*6) 26 | dash2 = DashDot_mobject(mob1) 27 | self.add(dash2) 28 | 29 | # with tempconfig({"quality": "medium_quality", "disable_caching": True}): 30 | # scene = test_ddot() 31 | # scene.render() -------------------------------------------------------------------------------- /tests/test_dimensions.py: -------------------------------------------------------------------------------- 1 | from manim import * 2 | from manim_cad_drawing_utils import * 3 | 4 | 5 | class test_dimension_pointer(Scene): 6 | def construct(self): 7 | mob1 = Round_Corners(Triangle().scale(2),0.3) 8 | p = ValueTracker(0) 9 | dim1 = Pointer_To_Mob(mob1,p.get_value(),r'triangel', pointer_offset=0.2) 10 | dim1.add_updater(lambda mob: mob.update_mob(mob1,p.get_value())) 11 | dim1.update() 12 | PM = Path_mapper(mob1) 13 | self.play(Create(mob1),rate_func=PM.equalize_rate_func(smooth)) 14 | self.play(Create(dim1)) 15 | self.play(p.animate.set_value(1),run_time=10) 16 | self.play(Uncreate(mob1,rate_func=PM.equalize_rate_func(smooth))) 17 | self.play(Uncreate(dim1)) 18 | self.wait() 19 | 20 | 21 | class test_dimension_base(Scene): 22 | def construct(self): 23 | mob1 = Round_Corners(Triangle().scale(2),0.3) 24 | dim0 = Linear_Dimension(mob1.point_from_proportion(0.0), 25 | mob1.point_from_proportion(0.3), 26 | color=RED) 27 | dim1 = Linear_Dimension(mob1.get_critical_point(UP), 28 | mob1.get_critical_point(DOWN), 29 | direction=RIGHT, 30 | offset=3, 31 | color=RED) 32 | dim2 = Linear_Dimension(mob1.get_critical_point(RIGHT), 33 | mob1.get_critical_point(LEFT), 34 | direction=UP, 35 | offset=2.5, 36 | outside_arrow=True, 37 | ext_line_offset=-1, 38 | text='something something lorem ipsum dolores', 39 | color=RED) 40 | self.add(mob1,dim0,dim1,dim2) 41 | 42 | 43 | class test_dimension(Scene): 44 | def construct(self): 45 | mob1 = Round_Corners(Triangle().scale(2),0.3) 46 | dim1 = Angle_Dimension_Mob(mob1, 47 | 0.2, 48 | 0.6, 49 | offset=-4, 50 | ext_line_offset=1, 51 | color=RED) 52 | dim2 = Linear_Dimension(mob1.get_critical_point(RIGHT), 53 | mob1.get_critical_point(LEFT), 54 | direction=UP, 55 | offset=2.5, 56 | outside_arrow=True, 57 | ext_line_offset=-1, 58 | color=RED) 59 | self.play(Create(mob1)) 60 | self.play(Create(dim1), run_time=3) 61 | self.play(Create(dim2), run_time=3) 62 | self.wait(3) 63 | self.play(Uncreate(mob1), Uncreate(dim2)) 64 | 65 | 66 | class test_angle(Scene): 67 | def construct(self): 68 | mob1 = Triangle().scale(2) 69 | dim2 = Angle_Dimension_3point(mob1.get_nth_curve_points(1)[0], 70 | mob1.get_nth_curve_points(0)[0], 71 | mob1.get_nth_curve_points(2)[0], 72 | offset=-6, 73 | outside_arrow=False, 74 | ext_line_offset=3, 75 | color=RED) 76 | dim3 = Angle_Dimension_3point(mob1.get_nth_curve_points(1)[0], 77 | mob1.get_nth_curve_points(0)[0], 78 | mob1.get_nth_curve_points(2)[0], 79 | offset=1.5, 80 | outside_arrow=True, 81 | ext_line_offset=0.2, 82 | color=RED) 83 | self.add(mob1,dim2,dim3) 84 | 85 | 86 | class test_arrow(Scene): 87 | def construct(self): 88 | mob1 = Round_Corners(Triangle().scale(2), 0.3) 89 | # mob1 = Triangle().scale(2) 90 | arrow = CAD_ArrowHead(mob1) 91 | vt = ValueTracker(0) 92 | arrow.add_updater(lambda mob: mob.set(anchor_point=vt.get_value())) 93 | arrow.add_updater(arrow.default_updater) 94 | PM = Path_mapper(mob1) 95 | self.add(mob1,arrow) 96 | self.play(vt.animate.set_value(1),run_time=6, rate_func=PM.equalize_rate_func(rate_functions.linear)) 97 | 98 | 99 | if __name__=="__main__": 100 | with tempconfig({"quality": "medium_quality", "disable_caching": True}): 101 | scene = test_dimension() 102 | scene.render() 103 | -------------------------------------------------------------------------------- /tests/test_hatch.py: -------------------------------------------------------------------------------- 1 | from manim import * 2 | from manim_cad_drawing_utils import * 3 | 4 | 5 | class test_hatch(Scene): 6 | def construct(self): 7 | mob1 = Star().scale(2) 8 | # 1 hatch object creates parallel lines 9 | # 2 of them create rectangles 10 | hatch1 = Hatch_lines(mob1, angle=PI / 6, stroke_width=2) 11 | hatch1.add_updater(lambda mob: mob.become(Hatch_lines(mob1, angle=PI / 6, stroke_width=2))) 12 | hatch2 = Hatch_lines(mob1, angle=PI / 6 + PI / 2, offset=0.5, stroke_width=2) 13 | hatch2.add_updater(lambda mob: mob.become(Hatch_lines(mob1, angle=PI / 6 + PI / 2, offset=0.5, stroke_width=2))) 14 | 15 | self.add(hatch1,hatch2,mob1) 16 | self.play(Transform(mob1,Triangle()),run_time=2) 17 | self.wait() 18 | self.play(Transform(mob1, Circle()), run_time=2) 19 | self.wait() 20 | self.play(Transform(mob1, Star().scale(2)), run_time=2) 21 | self.wait() 22 | 23 | if __name__=="__main__": 24 | with tempconfig({"quality": "medium_quality", "disable_caching": True}): 25 | scene = test_hatch() 26 | scene.render() 27 | -------------------------------------------------------------------------------- /tests/test_path_mapper.py: -------------------------------------------------------------------------------- 1 | from manim import * 2 | from manim_cad_drawing_utils import * 3 | 4 | class test_path_mapper_anim(Scene): 5 | def construct(self): 6 | mob1 = Round_Corners(Triangle(fill_color=TEAL,fill_opacity=0).scale(3),0.5) 7 | PM = Path_mapper(mob1) 8 | mob2 = mob1.copy() 9 | mob1.shift(LEFT * 2.5) 10 | mob2.shift(RIGHT * 2.5) 11 | 12 | self.play(Create(mob1,rate_func=PM.equalize_rate_func(smooth)),Create(mob2),run_time=5) 13 | self.wait() 14 | 15 | 16 | class test_path_mapper_curve(Scene): 17 | def construct(self): 18 | mob1 = Round_Corners(Triangle().scale(3),0.5) 19 | PM = Path_mapper(mob1) 20 | vt = ValueTracker(0) 21 | 22 | mob2 = Circle(radius=0.1) 23 | def mob2_update(mob): 24 | a = vt.get_value() 25 | p = mob1.point_from_proportion(a) 26 | # s = PM.length_from_alpha(a) 27 | s = a*PM.get_path_length() 28 | ofs = PM.get_normal_unit_vector(s) * PM.get_curvature_vector(s)[2] 29 | mob.move_to(p+ofs) 30 | mob2.add_updater(mob2_update) 31 | 32 | curve_t = Text('0') 33 | def t_updater(mob): 34 | val = PM.get_curvature_vector(vt.get_value() * PM.get_path_length())[2] 35 | mob.become(Text(f'{val:.3}').next_to(mob1)) 36 | curve_t.add_updater(t_updater) 37 | 38 | curve_t.next_to(mob1) 39 | 40 | self.play(Create(mob1,rate_func=PM.equalize_rate_func(smooth))) 41 | self.play(Create(mob2)) 42 | self.add(curve_t) 43 | self.wait() 44 | self.play(vt.animate.set_value(1),run_time=20,rate_func=linear) 45 | self.wait() 46 | 47 | # 48 | # with tempconfig({"quality": "medium_quality", "disable_caching": True}): 49 | # scene = test_path_mapper_curve() 50 | # scene.render() -------------------------------------------------------------------------------- /tests/test_path_offsets.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from manim import * 3 | from scipy.optimize import root 4 | from manim_cad_drawing_utils import * 5 | 6 | 7 | 8 | 9 | class test_offset(Scene): 10 | def construct(self): 11 | vt = ValueTracker(0) 12 | ref_mob = Arc(radius=1,start_angle=0,angle=PI,color=BLUE,num_components=6).shift(DOWN) 13 | ref_mob.points[-1,:] = ref_mob.points[-1:,:] + 0.5 * LEFT 14 | ref_mob.points[0, :] = ref_mob.points[0, :] + 0.5 * RIGHT 15 | Ofs_grp = VGroup() 16 | 17 | def ofs_function(s,gain=1): 18 | return - gain * vt.get_value() 19 | 20 | for k in range(3): 21 | offset_mob = Path_Offset_Mobject(ref_mob,num_of_samples=20, ofs_func=ofs_function, ofs_func_kwargs={'gain':k},color=BLUE) 22 | Ofs_grp.add(offset_mob) 23 | for k in range(len(Ofs_grp)): 24 | Ofs_grp[k].add_updater(Ofs_grp[k].default_updater) 25 | Ofs_grp.update() 26 | self.play(Create(ref_mob)) 27 | self.add(Ofs_grp) 28 | self.wait() 29 | self.play(vt.animate.set_value(1),run_time=1) 30 | self.wait() 31 | self.play(vt.animate.set_value(0.5), run_time=1) 32 | self.wait() 33 | self.play(vt.animate.set_value(-0.2), run_time=1) 34 | self.wait() 35 | self.play(vt.animate.set_value(0), run_time=1) 36 | self.wait() 37 | self.remove(Ofs_grp) 38 | self.play(Uncreate(ref_mob)) 39 | self.wait() 40 | 41 | 42 | 43 | class test_bulge(Scene): 44 | def construct(self): 45 | mob1 = Round_Corners(Square().scale(3),radius=1).shift(DOWN*0) 46 | vt = ValueTracker(0) 47 | gt = ValueTracker(1) 48 | 49 | def ofs_func(t,gain=1): 50 | x = 30 * (t-vt.get_value()) 51 | x2 = 30 * (t - vt.get_value()-1) 52 | x3 = 30 * (t - vt.get_value()+1) 53 | return 0.02 + gain*0.4 * (np.exp(-(x)**2) + np.exp(-(x3)**2) + np.exp(-(x2)**2)) 54 | ofspath = Path_Offset_Mobject(mob1,ofs_func, ofs_func_kwargs={'gain':gt.get_value()},fill_color=RED,fill_opacity=0,stroke_opacity=0) 55 | ofspath2 = Path_Offset_Mobject(mob1, ofs_func, ofs_func_kwargs={'gain':-gt.get_value()},fill_color=RED, fill_opacity=0, stroke_opacity=0) 56 | bulge = VMobject(stroke_opacity=1,fill_opacity=1) 57 | 58 | def ofs_updater(mob): 59 | curve1 = mob.generate_offset_paths(gen_ref_curve=False, gen_ofs_point=True) 60 | mob.points = curve1 61 | 62 | ofspath.add_updater(ofs_updater) 63 | ofspath2.add_updater(ofs_updater) 64 | ofspath.add_updater(lambda mob: mob.set_ofs_func_kwargs({'gain': gt.get_value()})) 65 | ofspath2.add_updater(lambda mob: mob.set_ofs_func_kwargs({'gain': -gt.get_value()})) 66 | 67 | def bulge_updater(mob:VMobject): 68 | point_list = np.concatenate((ofspath.points,ofspath2.reverse_points().points),axis=0) 69 | mob.points=point_list 70 | bulge.add_updater(bulge_updater) 71 | 72 | 73 | debugdots = Bezier_Handlebars(ofspath) 74 | debugdots.add_updater(lambda mob: mob.move_circles()) 75 | debugdots.add_updater(lambda mob: mob.move_lines()) 76 | self.add(ofspath,ofspath2,bulge) 77 | # mob1.set_stroke(opacity=0) 78 | self.play(vt.animate.set_value(0.5),run_time=6) 79 | self.wait(0.5) 80 | self.play(vt.animate.set_value(0.65), run_time=6) 81 | self.play(gt.animate.set_value(0), rate_func=rate_functions.there_and_back) 82 | self.wait(0.5) 83 | self.play(vt.animate.set_value(1.2), run_time=6,rate_func=rate_functions.ease_in_out_back) 84 | self.wait(0.5) 85 | self.play(gt.animate.set_value(0),rate_func=rate_functions.there_and_back) 86 | # self.play(gt.animate.set_value(1)) 87 | self.play(vt.animate.set_value(0), run_time=6) 88 | self.wait(0.5) 89 | 90 | class test_sq_wave(Scene): 91 | def construct(self): 92 | # mob1 = Round_Corners(Square().scale(5), radius=1).shift(DOWN * 2) 93 | mob1 = Circle(1).scale(5).shift(DOWN * 2) 94 | 95 | def ofs_func(t): 96 | if t%0.05>0.025: 97 | return 0.4 98 | else: 99 | return 0 100 | 101 | ofspath = Path_Offset_Mobject(mob1, ofs_func, discontinuities=[z*0.025 for z in range(int(1/0.025))]) 102 | dbg = Bezier_Handlebars(ofspath) 103 | self.add(ofspath,dbg) 104 | 105 | 106 | class Test_warp(Scene): 107 | def construct(self): 108 | mob = Triangle(fill_opacity=1,fill_color=TEAL).scale(0.2).rotate(-PI/2).move_to(ORIGIN) 109 | ref = Arc(radius=2,angle=PI) 110 | arrowhead = Curve_Warp(mob,ref,anchor_point=0.5) 111 | arrowhead.add_updater(lambda mob: mob.generate_points()) 112 | 113 | 114 | self.add(NumberPlane(),ref,mob,arrowhead) 115 | self.play(mob.animate.stretch(5,0)) 116 | self.wait() 117 | self.play(mob.animate.stretch(2, 1)) 118 | self.wait() 119 | self.play(Rotate(mob, PI / 2), run_time=2) 120 | self.wait() 121 | self.play(Rotate(mob, PI / 2), run_time=2) 122 | self.wait() 123 | self.play(Rotate(mob, PI / 2), run_time=2) 124 | self.wait() 125 | self.play(Rotate(mob, PI / 2), run_time=2) 126 | self.wait() 127 | self.play(mob.animate.stretch(1/2, 1)) 128 | self.play(mob.animate.stretch(1/5,0)) 129 | 130 | 131 | 132 | if __name__=="__main__": 133 | with tempconfig({"quality": "medium_quality", "disable_caching": True}): 134 | scene = test_sq_wave() 135 | scene.render() 136 | -------------------------------------------------------------------------------- /tests/test_round_corners.py: -------------------------------------------------------------------------------- 1 | from manim import * 2 | from manim_cad_drawing_utils import * 3 | 4 | class Test_chamfer(Scene): 5 | def construct(self): 6 | mob1 = RegularPolygon(n=4,radius=1.5,color=PINK).rotate(PI/4) 7 | mob2 = Triangle(radius=1.5,color=TEAL) 8 | crbase = Rectangle(height=0.5,width=3) 9 | mob3 = Union(crbase.copy().rotate(PI/4),crbase.copy().rotate(-PI/4),color=BLUE) 10 | mob4 = Circle(radius=1.3) 11 | mob2.shift(2.5*UP) 12 | mob3.shift(2.5*DOWN) 13 | mob1.shift(2.5*LEFT) 14 | mob4.shift(2.5*RIGHT) 15 | 16 | mob1 = Chamfer_Corners(mob1, 0.25) 17 | mob2 = Chamfer_Corners(mob2,0.25) 18 | mob3 = Chamfer_Corners(mob3, 0.25) 19 | self.add(mob1,mob2,mob3,mob4) 20 | 21 | class Test_round(Scene): 22 | def construct(self): 23 | mob1 = RegularPolygon(n=4,radius=1.5,color=PINK).rotate(PI/4) 24 | mob2 = Triangle(radius=1.5,color=TEAL) 25 | crbase = Rectangle(height=0.5,width=3) 26 | mob3 = Union(crbase.copy().rotate(PI/4),crbase.copy().rotate(-PI/4),color=BLUE) 27 | mob4 = Circle(radius=1.3) 28 | mob2.shift(2.5*UP) 29 | mob3.shift(2.5*DOWN) 30 | mob1.shift(2.5*LEFT) 31 | mob4.shift(2.5*RIGHT) 32 | 33 | mob1 = Round_Corners(mob1, 0.25) 34 | mob2 = Round_Corners(mob2, 0.25) 35 | mob3 = Round_Corners(mob3, 0.25) 36 | self.add(mob1,mob2,mob3,mob4) 37 | 38 | 39 | class Patrick(Scene): 40 | def construct(self): 41 | mob1 = Star(outer_radius=4) 42 | # randomize handles 43 | mob1.points[1::4,:] = mob1.points[1::4,:]+rotate_vector(UP*(0.4*np.random.random()+0.2),np.random.random()*TAU) 44 | mob1.points[2::4, :] = mob1.points[2::4, :] + rotate_vector(UP * (0.4*np.random.random()+0.2), np.random.random() * TAU) 45 | 46 | pat = Round_Corners(mob1,radius=0.35) 47 | pat.set_fill(color=RED_C,opacity=1) 48 | 49 | 50 | self.add(pat) 51 | --------------------------------------------------------------------------------