├── .gitignore ├── MANIFEST.in ├── Makefile ├── README.md ├── docker ├── Dockerfile └── docker-compose.yml ├── documentation ├── .ipynb_checkpoints │ ├── Introduction-checkpoint.ipynb │ └── Quadrotor-checkpoint.ipynb ├── Introduction.ipynb ├── Publish.md ├── Quadrotor.ipynb └── Viewport.ipynb ├── iopenscad ├── .ipynb_checkpoints │ └── Untitled-checkpoint.ipynb ├── .vscode │ └── settings.json ├── __init__.py ├── __main__.py ├── favicon.png ├── install.py ├── kernel.py ├── openscad │ ├── __init__.py │ ├── kernel.json │ ├── logo-32x32.png │ └── logo-64x64.png ├── parser.py └── scanner.py ├── setup.py └── tests ├── testParser.py └── testScanner.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | *.py[cod] 3 | __pycache__/ 4 | .ipynb_checkpoints 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | bin/ 10 | build/ 11 | develop-eggs/ 12 | dist/ 13 | eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # Installer logs 24 | pip-log.txt 25 | pip-delete-this-directory.txt 26 | 27 | # Unit test / coverage reports 28 | .tox/ 29 | .coverage 30 | .cache 31 | nosetests.xml 32 | coverage.xml 33 | 34 | # Translations 35 | *.mo 36 | 37 | # Mr Developer 38 | .mr.developer.cfg 39 | .project 40 | .pydevproject 41 | 42 | # Rope 43 | .ropeproject 44 | 45 | # Django stuff: 46 | *.log 47 | *.pot 48 | 49 | # Sphinx documentation 50 | docs/_build/ 51 | .ipynb_checkpoints 52 | 53 | # Other temp directories 54 | bak/ 55 | .vscode/ 56 | .ipynb_checkpoints 57 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include iopenscad/openscad/*.png 2 | include iopenscad/openscad/*.json 3 | include *.txt 4 | include *.rst 5 | include *.md 6 | include Makefile 7 | recursive-include docs *.rst 8 | recursive-include docs *.png 9 | recursive-include docs *.json 10 | recursive-include docs *.py 11 | prune .git 12 | prune docs/build 13 | prune dist 14 | prune build 15 | prune docs 16 | 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | python setup.py register 3 | python setup.py sdist --formats=zip upload 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenSCAD Kernel for Jupyter 2 | 3 | I was missing a Jupyter Kernel for [OpenSCAD](https://www.openscad.org/). So I took up the challenge and here is the result. 4 | 5 | Any regular text in a cell is added to the overall OpenSCAD code buffer. This allows you to build up an OpenSCAD model in multiple steps using Jupyter cells and document the design along the way. 6 | 7 | ## Preconditions 8 | - Jupyter or Jupyterlab should have been installed 9 | - Please make sure that OpenSCAD has been installed on your system (e.g. with apt install openscad) and that it can be called on the command line: 10 | 11 | ``` 12 | openscad -v 13 | ``` 14 | diplays the version. E.g. 2019.12.20 15 | 16 | ## Installation 17 | 18 | ``` 19 | pip install jupyter-openscad-kernel 20 | python -m iopenscad.install 21 | ``` 22 | 23 | ## Installation from source 24 | 25 | ``` 26 | git clone https://github.com/pschatzmann/jupyter-openscad-kernel.git 27 | cd jupyter-openscad-kernel 28 | pip install . 29 | python -m iopenscad.install 30 | 31 | ``` 32 | 33 | ## Optional Steps 34 | If you want to be able to display stl files you need to install a mime renderer for Jupyter. E.g 35 | ``` 36 | jupyter labextension install jupyterlab-viewer-3d 37 | ``` 38 | If you want to have syntax highlighting for OpenSCAD: 39 | ``` 40 | jupyter labextension install jupyterlab-openscad-syntax-highlighting 41 | ``` 42 | On linux, if you want to run openscad in a headless environment (e.g. Docker) you need to install xvfb 43 | ``` 44 | apt install -y xvfb 45 | ``` 46 | 47 | 48 | Now you can launch your kernel my calling 49 | 50 | ``` 51 | jupyter lab 52 | ``` 53 | or 54 | ``` 55 | jupyter workspace 56 | ``` 57 | ## Versions 58 | - 1.0 Initial Version 59 | - 1.0.1 Additional syntax checking; Publish to pypi 60 | - 1.0.13 Error corrections for parser and installer, support for %use 61 | - 1.0.14 Cleanup and corrections for spelling mistakes, Upgrade node and setuptools release in Dockerfile 62 | 63 | ## Further Information 64 | - A quick [Overview](https://www.pschatzmann.ch/home/2020/02/26/an-openscad-kernel-in-jupyter/) 65 | 66 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:groovy 2 | LABEL maintainer="phil.schatzmann@gmail.com" 3 | USER root 4 | RUN apt-get update && apt-get install -y curl software-properties-common 5 | RUN add-apt-repository ppa:openscad/releases 6 | RUN curl -sL https://deb.nodesource.com/setup_15.x | bash - 7 | RUN apt-get update && apt-get install -y git python3 python3-pip xvfb x11-utils nodejs 8 | RUN apt update && apt install -y openscad 9 | ENV PATH /opt/conda/envs/beakerx/bin:$PATH 10 | RUN pip3 install --upgrade setuptools jupyterlab 11 | WORKDIR /opt 12 | RUN git clone https://github.com/pschatzmann/openscad-kernel 13 | WORKDIR /opt/openscad-kernel 14 | RUN pip3 install . 15 | RUN python3 -m iopenscad.install 16 | RUN jupyter labextension install jupyterlab-openscad-syntax-highlighting 17 | RUN jupyter labextension install jupyterlab-viewer-3d 18 | RUN mkdir -p /home/openscad 19 | WORKDIR /home/openscad 20 | RUN cp /opt/openscad-kernel/documentation/* /home/openscad/ 21 | CMD jupyter lab --allow-root --ip=0.0.0.0 --port=8888 --no-browser 22 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | iopenscad: 4 | image: pschatzmann/iopenscad:latest 5 | container_name: iopenscad 6 | ports: 7 | - "8880:8888" 8 | restart: always 9 | build: . 10 | -------------------------------------------------------------------------------- /documentation/.ipynb_checkpoints/Introduction-checkpoint.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# An OpenSCAD Kernel in Jupyter\n", 8 | "\n", 9 | "I was missing a Jupyter Kernel for OpenSCAD. So I took up the challenge and here is the result. \n", 10 | "\n", 11 | "Any code in a regular a cell is added to the overall SCAD code. This allows you to build up a OpenSCAD model in multiple steps using Jupyter cells and document your design process along the way.\n", 12 | "\n", 13 | "## Magic Commands\n", 14 | "\n", 15 | "The kernel supports the following \"magic\" commands:\n", 16 | "- __%lsmagic__: Lists all magic commands\n", 17 | "- __%clear__: Clears the OpenSCAD Code in the current memory\n", 18 | "- __%command __: Defines the converter which translates the OpenSCAD code to the diplay format. The default value is openscad\n", 19 | "- __%display __: Adds some display code which is relevant for the cell only. One line only\n", 20 | "- __%%display __: Adds multple lines of OpenSCAD code which is relevant for the current cell only.\n", 21 | "- __%displayCode__: Displays the current OpenSCAD code\n", 22 | "- __%mime __: Defines the mime code which is used to render the content\n", 23 | "- __%include __: Adds the OpenSCAD code from a URL\n", 24 | "- __%saveAs __: Converts the the OpenSCAD into a output format and saves the result in a file.\n", 25 | " \n", 26 | "%lsmagic provides an overview of all supported magic commands" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": null, 32 | "metadata": {}, 33 | "outputs": [], 34 | "source": [ 35 | "%lsmagic " 36 | ] 37 | }, 38 | { 39 | "cell_type": "markdown", 40 | "metadata": {}, 41 | "source": [ 42 | "The %clear command is resetting the internal OpenSCAD code. We recommend that you start your workspace with it so that you start from an empty state if you call 'Run All Cells'." 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": null, 48 | "metadata": {}, 49 | "outputs": [], 50 | "source": [ 51 | "%clear" 52 | ] 53 | }, 54 | { 55 | "cell_type": "markdown", 56 | "metadata": {}, 57 | "source": [ 58 | "We use a command line command to convert the scad code to 2d or 3d output. By default this is openscad" 59 | ] 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": null, 64 | "metadata": {}, 65 | "outputs": [], 66 | "source": [ 67 | "%command " 68 | ] 69 | }, 70 | { 71 | "cell_type": "markdown", 72 | "metadata": {}, 73 | "source": [ 74 | "If you have openjcad installed you can use this instead" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": null, 80 | "metadata": {}, 81 | "outputs": [], 82 | "source": [ 83 | "%command openjscad" 84 | ] 85 | }, 86 | { 87 | "cell_type": "markdown", 88 | "metadata": {}, 89 | "source": [ 90 | "...but we want to keep openscad so we switch back to it" 91 | ] 92 | }, 93 | { 94 | "cell_type": "code", 95 | "execution_count": null, 96 | "metadata": {}, 97 | "outputs": [], 98 | "source": [ 99 | "%command xvfb-run openscad \n", 100 | "%command openscad " 101 | ] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "execution_count": null, 106 | "metadata": {}, 107 | "outputs": [], 108 | "source": [ 109 | "%clear" 110 | ] 111 | }, 112 | { 113 | "cell_type": "markdown", 114 | "metadata": {}, 115 | "source": [ 116 | "## Display\n", 117 | "You might want to display some SCAD code only temporarily e.g. to demonstrate some failed approaches. This can be be achieved with thel help of the %%display command at the beginning of the section. \n", 118 | "\n", 119 | "All code which comes after the %%display command is only relevant for the current cell and will be discarded by the other cells.\n" 120 | ] 121 | }, 122 | { 123 | "cell_type": "code", 124 | "execution_count": null, 125 | "metadata": {}, 126 | "outputs": [], 127 | "source": [ 128 | "%%display\n", 129 | "\n", 130 | "union() {\n", 131 | " cube(size = [2,1,1], center = true);\n", 132 | " cube(size = [1,1,2], center = true);\n", 133 | "} \n" 134 | ] 135 | }, 136 | { 137 | "cell_type": "markdown", 138 | "metadata": {}, 139 | "source": [ 140 | "When you just add some code - it is added to the memory which is available in all cells." 141 | ] 142 | }, 143 | { 144 | "cell_type": "code", 145 | "execution_count": null, 146 | "metadata": {}, 147 | "outputs": [], 148 | "source": [ 149 | "module test() {\n", 150 | " cube([1,2,3], center = true);\n", 151 | "}\n" 152 | ] 153 | }, 154 | { 155 | "cell_type": "markdown", 156 | "metadata": {}, 157 | "source": [ 158 | "If you want to render the content you can do this by calling %diplay \n" 159 | ] 160 | }, 161 | { 162 | "cell_type": "code", 163 | "execution_count": null, 164 | "metadata": {}, 165 | "outputs": [], 166 | "source": [ 167 | "module test1() {\n", 168 | " union() {\n", 169 | " cube([3,2,1], center = true);\n", 170 | " test();\n", 171 | " }\n", 172 | "}\n", 173 | "\n", 174 | "%display test1();" 175 | ] 176 | }, 177 | { 178 | "cell_type": "markdown", 179 | "metadata": {}, 180 | "source": [ 181 | "## Mime\n", 182 | "You can also define the output mime type which whill be used to render the content. The most important ones are\n", 183 | "- text/plain\n", 184 | "- image/png\n", 185 | "- model/stl\n", 186 | "\n", 187 | "The default setting is image/png" 188 | ] 189 | }, 190 | { 191 | "cell_type": "code", 192 | "execution_count": null, 193 | "metadata": {}, 194 | "outputs": [], 195 | "source": [ 196 | "%mime" 197 | ] 198 | }, 199 | { 200 | "cell_type": "markdown", 201 | "metadata": {}, 202 | "source": [ 203 | "If you want to display your Scad Code which has been defined in the current buffer (and not an image or 3D model) - you can set the mime to text/plain" 204 | ] 205 | }, 206 | { 207 | "cell_type": "code", 208 | "execution_count": null, 209 | "metadata": {}, 210 | "outputs": [], 211 | "source": [ 212 | "%mime text/plain\n" 213 | ] 214 | }, 215 | { 216 | "cell_type": "code", 217 | "execution_count": null, 218 | "metadata": {}, 219 | "outputs": [], 220 | "source": [ 221 | "%display" 222 | ] 223 | }, 224 | { 225 | "cell_type": "markdown", 226 | "metadata": {}, 227 | "source": [ 228 | "## Include\n", 229 | "You can also include some SCAD code with the help of an URL" 230 | ] 231 | }, 232 | { 233 | "cell_type": "code", 234 | "execution_count": null, 235 | "metadata": {}, 236 | "outputs": [], 237 | "source": [ 238 | "%mime image/png\n", 239 | "%include https://raw.githubusercontent.com/pschatzmann/openscad-models/master/Pig.scad\n", 240 | "\n", 241 | "%display" 242 | ] 243 | }, 244 | { 245 | "cell_type": "markdown", 246 | "metadata": {}, 247 | "source": [ 248 | "## SaveAs\n", 249 | "Finally you can save your result in a file. The file extension is used to determine the file type automaticlly. [The supported file types can be found in the OpenSCAD documentation](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/STL_Export)." 250 | ] 251 | }, 252 | { 253 | "cell_type": "code", 254 | "execution_count": null, 255 | "metadata": {}, 256 | "outputs": [], 257 | "source": [ 258 | "%saveAs pig.stl" 259 | ] 260 | }, 261 | { 262 | "cell_type": "code", 263 | "execution_count": null, 264 | "metadata": {}, 265 | "outputs": [], 266 | "source": [] 267 | } 268 | ], 269 | "metadata": { 270 | "kernelspec": { 271 | "display_name": "OpenSCAD", 272 | "language": "application-xopenscad", 273 | "name": "openscad" 274 | }, 275 | "language_info": { 276 | "codemirror_mode": { 277 | "name": "ipython", 278 | "version": 3 279 | }, 280 | "file_extension": ".py", 281 | "mimetype": "text/x-python", 282 | "name": "python", 283 | "nbconvert_exporter": "python", 284 | "pygments_lexer": "ipython3", 285 | "version": "3.7.6" 286 | }, 287 | "widgets": { 288 | "application/vnd.jupyter.widget-state+json": { 289 | "state": {}, 290 | "version_major": 2, 291 | "version_minor": 0 292 | } 293 | } 294 | }, 295 | "nbformat": 4, 296 | "nbformat_minor": 4 297 | } 298 | -------------------------------------------------------------------------------- /documentation/Introduction.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# An OpenSCAD Kernel in Jupyter\n", 8 | "\n", 9 | "I was missing a Jupyter Kernel for OpenSCAD. So I took up the challenge and here is the result. \n", 10 | "\n", 11 | "Any code in a regular a cell is added to the overall SCAD code. This allows you to build up a OpenSCAD model in multiple steps using Jupyter cells and document your design process along the way.\n", 12 | "\n", 13 | "## Magic Commands\n", 14 | "\n", 15 | "The kernel supports the following \"magic\" commands:\n", 16 | "- __%lsmagic__: Lists all magic commands\n", 17 | "- __%clear__: Clears the OpenSCAD Code in the current memory\n", 18 | "- __%command __: Defines the converter which translates the OpenSCAD code to the diplay format. The default value is openscad\n", 19 | "- __%display __: Adds some display code which is relevant for the cell only. One line only\n", 20 | "- __%%display __: Adds multple lines of OpenSCAD code which is relevant for the current cell only.\n", 21 | "- __%displayCode__: Displays the current OpenSCAD code\n", 22 | "- __%mime __: Defines the mime code which is used to render the content\n", 23 | "- __%include __: Adds the OpenSCAD code from a URL\n", 24 | "- __%saveAs __: Converts the the OpenSCAD into a output format and saves the result in a file.\n", 25 | " \n", 26 | "%lsmagic provides an overview of all supported magic commands" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": null, 32 | "metadata": {}, 33 | "outputs": [], 34 | "source": [ 35 | "%lsmagic " 36 | ] 37 | }, 38 | { 39 | "cell_type": "markdown", 40 | "metadata": {}, 41 | "source": [ 42 | "The %clear command is resetting the internal OpenSCAD code. We recommend that you start your workspace with it so that you start from an empty state if you call 'Run All Cells'." 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": null, 48 | "metadata": {}, 49 | "outputs": [], 50 | "source": [ 51 | "%clear" 52 | ] 53 | }, 54 | { 55 | "cell_type": "markdown", 56 | "metadata": {}, 57 | "source": [ 58 | "We use a command line command to convert the scad code to 2d or 3d output. By default this is openscad" 59 | ] 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": null, 64 | "metadata": {}, 65 | "outputs": [], 66 | "source": [ 67 | "%command " 68 | ] 69 | }, 70 | { 71 | "cell_type": "markdown", 72 | "metadata": {}, 73 | "source": [ 74 | "If you have openjcad installed you can use this instead\n", 75 | "\n", 76 | "```\n", 77 | "%command openjscad\n", 78 | "```\n", 79 | "\n", 80 | "If you run the code in a virtual environment you might need to use xvfb-run:\n", 81 | "\n", 82 | "```\n", 83 | "%command xvfb-run openscad \n", 84 | "```\n", 85 | "\n", 86 | "...but we usually can just use the proposed setting!\n", 87 | "\n" 88 | ] 89 | }, 90 | { 91 | "cell_type": "markdown", 92 | "metadata": {}, 93 | "source": [ 94 | "## Display\n", 95 | "You might want to display some SCAD code only temporarily e.g. to demonstrate some failed approaches. This can be be achieved with thel help of the %%display command at the beginning of the section. \n", 96 | "\n", 97 | "All code which comes after the %%display command is only relevant for the current cell and will be discarded by the other cells.\n" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": null, 103 | "metadata": {}, 104 | "outputs": [], 105 | "source": [ 106 | "%%display\n", 107 | "\n", 108 | "union() {\n", 109 | " cube(size = [2,1,1], center = true);\n", 110 | " cube(size = [1,1,2], center = true);\n", 111 | "} \n" 112 | ] 113 | }, 114 | { 115 | "cell_type": "markdown", 116 | "metadata": {}, 117 | "source": [ 118 | "When you just add some code - it is added to the memory which is available in all cells." 119 | ] 120 | }, 121 | { 122 | "cell_type": "code", 123 | "execution_count": null, 124 | "metadata": {}, 125 | "outputs": [], 126 | "source": [ 127 | "module test() {\n", 128 | " cube([1,2,3], center = true);\n", 129 | "}\n" 130 | ] 131 | }, 132 | { 133 | "cell_type": "markdown", 134 | "metadata": {}, 135 | "source": [ 136 | "If you want to render the content you can do this by calling %diplay \n" 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": null, 142 | "metadata": {}, 143 | "outputs": [], 144 | "source": [ 145 | "module test1() {\n", 146 | " union() {\n", 147 | " cube([3,2,1], center = true);\n", 148 | " test();\n", 149 | " }\n", 150 | "}\n", 151 | "\n", 152 | "%display test1();" 153 | ] 154 | }, 155 | { 156 | "cell_type": "markdown", 157 | "metadata": {}, 158 | "source": [ 159 | "## Mime\n", 160 | "You can also define the output mime type which whill be used to render the content. The most important ones are\n", 161 | "- text/plain\n", 162 | "- image/png\n", 163 | "- model/stl\n", 164 | "\n", 165 | "The default setting is image/png" 166 | ] 167 | }, 168 | { 169 | "cell_type": "code", 170 | "execution_count": null, 171 | "metadata": {}, 172 | "outputs": [], 173 | "source": [ 174 | "%mime" 175 | ] 176 | }, 177 | { 178 | "cell_type": "markdown", 179 | "metadata": {}, 180 | "source": [ 181 | "If you want to display your Scad Code which has been defined in the current buffer (and not an image or 3D model) - you can set the mime to text/plain" 182 | ] 183 | }, 184 | { 185 | "cell_type": "code", 186 | "execution_count": null, 187 | "metadata": {}, 188 | "outputs": [], 189 | "source": [ 190 | "%mime text/plain\n" 191 | ] 192 | }, 193 | { 194 | "cell_type": "code", 195 | "execution_count": null, 196 | "metadata": {}, 197 | "outputs": [], 198 | "source": [ 199 | "%display" 200 | ] 201 | }, 202 | { 203 | "cell_type": "markdown", 204 | "metadata": {}, 205 | "source": [ 206 | "## Include\n", 207 | "You can also include some SCAD code with the help of an URL" 208 | ] 209 | }, 210 | { 211 | "cell_type": "code", 212 | "execution_count": null, 213 | "metadata": {}, 214 | "outputs": [], 215 | "source": [ 216 | "%mime image/png\n", 217 | "%include https://raw.githubusercontent.com/pschatzmann/openscad-models/master/Pig.scad\n", 218 | "\n", 219 | "%display" 220 | ] 221 | }, 222 | { 223 | "cell_type": "markdown", 224 | "metadata": {}, 225 | "source": [ 226 | "## SaveAs\n", 227 | "Finally you can save your result in a file. The file extension is used to determine the file type automaticlly. [The supported file types can be found in the OpenSCAD documentation](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/STL_Export)." 228 | ] 229 | }, 230 | { 231 | "cell_type": "code", 232 | "execution_count": null, 233 | "metadata": {}, 234 | "outputs": [], 235 | "source": [ 236 | "%saveAs pig.stl" 237 | ] 238 | }, 239 | { 240 | "cell_type": "code", 241 | "execution_count": null, 242 | "metadata": {}, 243 | "outputs": [], 244 | "source": [] 245 | } 246 | ], 247 | "metadata": { 248 | "kernelspec": { 249 | "display_name": "OpenSCAD", 250 | "language": "application-xopenscad", 251 | "name": "openscad" 252 | }, 253 | "language_info": { 254 | "codemirror_mode": { 255 | "name": "ipython", 256 | "version": 3 257 | }, 258 | "file_extension": ".py", 259 | "mimetype": "text/x-python", 260 | "name": "python", 261 | "nbconvert_exporter": "python", 262 | "pygments_lexer": "ipython3", 263 | "version": "3.7.6" 264 | }, 265 | "widgets": { 266 | "application/vnd.jupyter.widget-state+json": { 267 | "state": {}, 268 | "version_major": 2, 269 | "version_minor": 0 270 | } 271 | } 272 | }, 273 | "nbformat": 4, 274 | "nbformat_minor": 4 275 | } -------------------------------------------------------------------------------- /documentation/Publish.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | - doc: contains documentation 3 | - docker: contains the build files for Docker 4 | - iopenscad: the python module for this project 5 | - openscad: Jupyter Kernel Specification 6 | 7 | # Publish Project to PyPi 8 | Here are the steps that are necessary to publish a new version 9 | Update the version information in the setup.py 10 | 11 | ## Update Github 12 | ``` 13 | git add . 14 | git tag V1.0.X 15 | git commit -m "message" 16 | git push origin master --tags 17 | ``` 18 | 19 | ## Build wheel 20 | ``` 21 | rm -r dist 22 | python3 setup.py sdist bdist_wheel 23 | ``` 24 | 25 | ## Publish 26 | ``` 27 | twine upload dist/* 28 | ``` 29 | 30 | ## Docker 31 | Build with 32 | ``` 33 | docker-compose build 34 | ``` 35 | Start with 36 | ``` 37 | docker-compose up 38 | ``` 39 | -------------------------------------------------------------------------------- /documentation/Viewport.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "a49d4807-c37a-444a-b082-3da33294e4d4", 6 | "metadata": {}, 7 | "source": [ 8 | "## Viewport" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": 1, 14 | "id": "516c40ef-838e-4cdc-b90f-80702e8132b5", 15 | "metadata": {}, 16 | "outputs": [ 17 | { 18 | "name": "stdout", 19 | "output_type": "stream", 20 | "text": [] 21 | }, 22 | { 23 | "name": "stdout", 24 | "output_type": "stream", 25 | "text": [ 26 | "Compiling design (CSG Products normalization)...\n", 27 | "Normalized CSG tree has 1 elements\n", 28 | "Geometries in cache: 1\n", 29 | "Geometry cache size in bytes: 728\n", 30 | "CGAL Polyhedrons in cache: 0\n", 31 | "CGAL cache size in bytes: 0\n", 32 | "Total rendering time: 0:00:00.125\n" 33 | ] 34 | }, 35 | { 36 | "data": { 37 | "image/png": "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" 38 | }, 39 | "metadata": { 40 | "image/png": { 41 | "height": 400, 42 | "width": 600 43 | } 44 | }, 45 | "output_type": "display_data", 46 | "source": "kernel" 47 | } 48 | ], 49 | "source": [ 50 | "cube([20,20,20]);\n", 51 | "%display\n" 52 | ] 53 | }, 54 | { 55 | "cell_type": "markdown", 56 | "id": "551a1166-0308-4189-a2f2-483ac9eb49b7", 57 | "metadata": {}, 58 | "source": [ 59 | "Usually the object is displayed in a \"comprehensive\" way. You can adjust the viewport however with the following variables:\n", 60 | "\n", 61 | "- __\\\\$vpr__: viewport rotation angles in degrees\n", 62 | "- __\\\\$vpt__: viewport translation\n", 63 | "- __\\\\$vpd__: viewport camera distance\n" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": 2, 69 | "id": "c676b98d-00b6-4534-b88c-7f33a7208457", 70 | "metadata": {}, 71 | "outputs": [ 72 | { 73 | "name": "stdout", 74 | "output_type": "stream", 75 | "text": [] 76 | }, 77 | { 78 | "name": "stdout", 79 | "output_type": "stream", 80 | "text": [ 81 | "WARNING: Viewall and autocenter disabled in favor of $vp*\n", 82 | "Compiling design (CSG Products normalization)...\n", 83 | "Normalized CSG tree has 1 elements\n", 84 | "Geometries in cache: 1\n", 85 | "Geometry cache size in bytes: 728\n", 86 | "CGAL Polyhedrons in cache: 0\n", 87 | "CGAL cache size in bytes: 0\n", 88 | "Total rendering time: 0:00:00.126\n" 89 | ] 90 | }, 91 | { 92 | "data": { 93 | "image/png": "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" 94 | }, 95 | "metadata": { 96 | "image/png": { 97 | "height": 400, 98 | "width": 600 99 | } 100 | }, 101 | "output_type": "display_data", 102 | "source": "kernel" 103 | } 104 | ], 105 | "source": [ 106 | "$vpt = [ 0,0,0 ];\n", 107 | "$vpr = [ 30,0,20 ];\n", 108 | "$vpd = 150;\n", 109 | "\n", 110 | "cube([20,20,20]);\n", 111 | "%display" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": null, 117 | "id": "2687fdf5-47d7-44b7-b3f9-8d5e9fa89e28", 118 | "metadata": {}, 119 | "outputs": [], 120 | "source": [] 121 | } 122 | ], 123 | "metadata": { 124 | "kernelspec": { 125 | "display_name": "OpenSCAD", 126 | "language": "application-xopenscad", 127 | "name": "openscad" 128 | }, 129 | "language_info": { 130 | "extension": ".scad", 131 | "mimetype": "application/x-openscad", 132 | "name": "OpenSCAD" 133 | } 134 | }, 135 | "nbformat": 4, 136 | "nbformat_minor": 5 137 | } 138 | -------------------------------------------------------------------------------- /iopenscad/.ipynb_checkpoints/Untitled-checkpoint.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [], 3 | "metadata": {}, 4 | "nbformat": 4, 5 | "nbformat_minor": 4 6 | } 7 | -------------------------------------------------------------------------------- /iopenscad/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.unittestArgs": [ 3 | "-v", 4 | "-s", 5 | ".", 6 | "-p", 7 | "test_*.py" 8 | ], 9 | "python.testing.pytestEnabled": false, 10 | "python.testing.nosetestsEnabled": false, 11 | "python.testing.unittestEnabled": true 12 | } -------------------------------------------------------------------------------- /iopenscad/__init__.py: -------------------------------------------------------------------------------- 1 | # __init__.py 2 | 3 | # Version of the realpython-reader package 4 | __version__ = "1.0.13" 5 | 6 | -------------------------------------------------------------------------------- /iopenscad/__main__.py: -------------------------------------------------------------------------------- 1 | from iopenscad.kernel import IOpenSCAD 2 | from ipykernel.kernelapp import IPKernelApp 3 | 4 | ## launch IOpenSCAD 5 | IPKernelApp.launch_instance(kernel_class=IOpenSCAD) 6 | -------------------------------------------------------------------------------- /iopenscad/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschatzmann/jupyter-openscad-kernel/ad614dc603b332f7b3b930ea5dc5a485355e32c4/iopenscad/favicon.png -------------------------------------------------------------------------------- /iopenscad/install.py: -------------------------------------------------------------------------------- 1 | 2 | ## 3 | # Installs the Jupyter kernel specification including the icons 4 | # 5 | import pathlib, os, distutils.core, setuptools 6 | import pkg_resources 7 | from pathlib import Path 8 | 9 | def main(): 10 | kernelPath = pathlib.Path(__file__).parent / "openscad" 11 | if not os.path.isdir(kernelPath): 12 | raise Exception('The directory does not exist {}'.format(str(kernelPath))) 13 | 14 | if not os.path.exists(kernelPath / "kernel.json"): 15 | raise Exception('The kernel does not exist {}'.format(str(kernelPath))) 16 | 17 | cmd = 'jupyter kernelspec install ' + str(kernelPath) 18 | if os.system(cmd) != 0: 19 | raise Exception('Could not install kernelspec {}'.format(cmd)) 20 | 21 | if __name__ == "__main__": 22 | main() 23 | 24 | -------------------------------------------------------------------------------- /iopenscad/kernel.py: -------------------------------------------------------------------------------- 1 | #### 2 | ## Jupyter Kernel for OpenSCAD 3 | ## 4 | import os 5 | import base64 6 | from iopenscad.parser import Parser 7 | from ipykernel.kernelbase import Kernel 8 | 9 | class IOpenSCAD(Kernel): 10 | banner = "OpenSCAD" 11 | implementation = 'ipopenscad' 12 | implementation_version = '1.0' 13 | language = 'openscad' # will be used for syntax highlighting 14 | language_version = '2019' 15 | language_info = {'name': 'OpenSCAD', 16 | 'mimetype': 'application/x-openscad', 17 | 'extension': '.scad'} 18 | complete = ["cube(size = [x,y,z], center = true);","sphere(r = 10);", 19 | "sphere(d = 20);","cylinder(h=15, r1=9.5, r2=19.5, center=true);", 20 | "square([10,10],center=true);","circle(r=10);","circle(d=20);", 21 | "translate([x,y,z])","rotate([x,y,z])","scale([x,y,z])","resize([x,y,z])","mirror([x,y,z])", 22 | "hull(){...};","minkowski(){...};", 23 | "union() {...};","difference(){...};","intersection(){...};", 24 | "include ","use ", 25 | "module name() { ... }","function name() = ... ;" 26 | ] 27 | parser = None 28 | isSetup = False 29 | 30 | ## 31 | # Executes the source code which is defined in a cell 32 | ## 33 | def do_execute(self, code, silent, 34 | store_history=True, 35 | user_expressions=None, 36 | allow_stdin=False): 37 | 38 | # Setup parser 39 | if not self.parser: 40 | self.parser = Parser() 41 | self.parser.setup() 42 | 43 | resultObj = None 44 | self.parser.parse(code) 45 | 46 | if not silent: 47 | # We send the standard output to the client. 48 | if self.parser.displayRendered: 49 | self.displayMessages(self.parser) 50 | self.parser.clearMessages() 51 | resultFile = self.parser.renderMime() 52 | if resultFile: 53 | resultObj = open(resultFile,'rb').read() 54 | if self.parser.mime=='text/plain': 55 | resultObj = resultObj.decode('utf-8') 56 | else: 57 | resultObj = base64.standard_b64encode(resultObj).decode('utf-8') 58 | 59 | if self.parser.getMessages().strip(): 60 | self.displayMessages(self.parser) 61 | if resultObj: 62 | self.displayImage(resultObj, self.parser.mime) 63 | else: 64 | if self.parser.getSourceCode().strip(): 65 | self.displayError(os.linesep+self.parser.getSourceCode()) 66 | else: 67 | self.displayMessages(self.parser) 68 | 69 | # We return the exection results. 70 | return {'status': 'ok', 71 | 'execution_count': self.execution_count, 72 | 'payload': [], 73 | 'user_expressions': {}, 74 | } 75 | 76 | ## 77 | # Determine completion result 78 | ## 79 | def do_complete(self, code, cursor_pos): 80 | char = code[cursor_pos] 81 | result = self.complete + self.parser.getModuleNames() + self.parser.lsCommands 82 | result.sort() 83 | 84 | # filter the result if necessary 85 | if char.strip(): 86 | subset = [] 87 | for cmd in result: 88 | if cmd.startswith(char): 89 | subset.append(cmd) 90 | result = subset 91 | 92 | content = { 93 | # The list of all matches to the completion request, such as 94 | # ['a.isalnum', 'a.isalpha'] for the above example. 95 | 'matches' : result, 96 | 97 | # The range of text that should be replaced by the above matches when a completion is accepted. 98 | # typically cursor_end is the same as cursor_pos in the request. 99 | 'cursor_start' : cursor_pos, 100 | 'cursor_end' : cursor_pos, 101 | 102 | # status should be 'ok' unless an exception was raised during the request, 103 | # in which case it should be 'error', along with the usual error message content 104 | # in other messages. 105 | 'status' : 'ok' 106 | } 107 | return content 108 | 109 | ## 110 | # Cleanup 111 | ## 112 | def do_shutdown(self, restart): 113 | self.parser.close() 114 | 115 | def displayInfo(self, info): 116 | stream_content = {'name': 'stdout', 'text': info} 117 | self.send_response(self.iopub_socket, 'stream', stream_content) 118 | 119 | def displayError(self, error): 120 | stream_content = {'name': 'stderr', 'text': error} 121 | self.send_response(self.iopub_socket, 'stream', stream_content) 122 | 123 | def displayMessages(self, parser): 124 | if self.parser.isError: 125 | self.displayError(parser.getMessages()) 126 | else: 127 | self.displayInfo(parser.getMessagesExt()) 128 | 129 | 130 | def displayImage(self, resultObj, mime): 131 | if resultObj: 132 | # We prepare the response with our rich 133 | # data (the plot). 134 | content = { 135 | 'source': 'kernel', 136 | 137 | # This dictionary may contain 138 | # different MIME representations of 139 | # the output. 140 | 'data': { 141 | mime: resultObj 142 | }, 143 | 144 | # We can specify the image size 145 | # in the metadata field. 146 | 'metadata' : { 147 | mime : { 148 | 'width': 600, 149 | 'height': 400 150 | } 151 | } 152 | } 153 | 154 | # We send the display_data message with 155 | # the contents. 156 | self.send_response(self.iopub_socket, 'display_data', content) 157 | 158 | -------------------------------------------------------------------------------- /iopenscad/openscad/__init__.py: -------------------------------------------------------------------------------- 1 | # __init__.py 2 | -------------------------------------------------------------------------------- /iopenscad/openscad/kernel.json: -------------------------------------------------------------------------------- 1 | { 2 | "argv": ["python", "-m", 3 | "iopenscad", "-f", 4 | "{connection_file}"], 5 | "display_name": "OpenSCAD", 6 | "name": "OpenSCAD", 7 | "language": "application-xopenscad" 8 | } 9 | -------------------------------------------------------------------------------- /iopenscad/openscad/logo-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschatzmann/jupyter-openscad-kernel/ad614dc603b332f7b3b930ea5dc5a485355e32c4/iopenscad/openscad/logo-32x32.png -------------------------------------------------------------------------------- /iopenscad/openscad/logo-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschatzmann/jupyter-openscad-kernel/ad614dc603b332f7b3b930ea5dc5a485355e32c4/iopenscad/openscad/logo-64x64.png -------------------------------------------------------------------------------- /iopenscad/parser.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Simple Parser and Renderer for SCAD commands. This implements the Kernel 3 | # support functionality which is needed 4 | # 5 | import urllib 6 | import os.path 7 | import subprocess 8 | import tempfile 9 | import os 10 | import re 11 | import logging 12 | import urllib 13 | import urllib.request 14 | from iopenscad.scanner import Scanner 15 | 16 | 17 | ## 18 | # Manages a library of include files. We support reference includes (which are 19 | # dynamically added to the source code ) and file includes (which download the 20 | # url into a file) 21 | ## 22 | class IncludeLibrary: 23 | dictionary = dict() 24 | 25 | ## Adds a library by name 26 | @classmethod 27 | def addRef(self, name, url): 28 | inc = IncludeRef(name, url) 29 | self.dictionary[name] = inc 30 | return inc 31 | 32 | def addFile(self, name, url): 33 | inc = IncludeFile(name, url) 34 | self.dictionary[name] = inc 35 | return inc 36 | 37 | @classmethod 38 | def get(self, name): 39 | return self.dictionary.get(name) 40 | 41 | 42 | ## 43 | # Provides the content that is included as String 44 | ## 45 | class IncludeRef: 46 | def __init__(self, name, url): 47 | self.name = name 48 | self.url = url 49 | self.content = None 50 | 51 | def getContent(self): 52 | if not self.content: 53 | f = urllib.request.urlopen(self.url) 54 | self.content = f.read().decode(encoding='UTF-8') 55 | f.close() 56 | return self.content 57 | 58 | def str(self): 59 | return self.name 60 | 61 | def resolve(self): 62 | return self.content 63 | 64 | def ref(self): 65 | return self.url 66 | 67 | 68 | ### 69 | # Downloads file from URL if it does not exist 70 | ## 71 | class IncludeFile: 72 | 73 | def __init__(self, name, url): 74 | self.url="" 75 | self.content="" 76 | self.absolutePath="" 77 | self.content="" 78 | self.name = name 79 | if not os.path.isfile(name): 80 | self.createPath(name) 81 | self.url = url 82 | f = urllib.request.urlopen(url) 83 | self.content = f.read().decode(encoding='UTF-8') 84 | text_file = open(name, "wt") 85 | text_file.write(self.content) 86 | text_file.close() 87 | 88 | if (name.startswith(os.sep)): 89 | self.absolutePath = name 90 | else: 91 | self.absolutePath = os.path.join(os. getcwd(), name) 92 | 93 | def createPath(self, name): 94 | right = name.rfind(os.sep) 95 | path = name[0:right] 96 | if (path and not os.path.isfile(path)): 97 | os.makedirs(path) 98 | 99 | def getContent(self): 100 | if not self.content: 101 | f = urllib.request.urlopen("file://"+self.absolutePath) 102 | self.content = f.read().decode(encoding='UTF-8') 103 | return self.content 104 | 105 | def str(self): 106 | return self.name 107 | 108 | def ref(self): 109 | return self.absolutePath 110 | 111 | def resolve(self): 112 | return "include("+self.absolutePath+")" 113 | 114 | 115 | ## 116 | # Converter which translates a scad text into a file of the indicated mime type. 117 | # There are currently 2 command line tools that can be used: openscad and 118 | # openjscad. 119 | ## 120 | class MimeConverter: 121 | def __init__(self): 122 | self.messages = "" 123 | self.tmpFiles = [] 124 | self.resultFile = None 125 | self.isError = False 126 | 127 | def clear(self): 128 | self.messages = "" 129 | self.isError = False 130 | 131 | def convert(self, scadCommand, scadCode, mime): 132 | self.clear() 133 | if scadCode.strip(): 134 | logging.info(scadCode) 135 | resultExt = self.mimeToExtension(mime) 136 | 137 | fd, self.resultFile = tempfile.mkstemp(suffix="."+resultExt, prefix=None, dir=None, text=True) 138 | self.tmpFiles.append(self.resultFile) 139 | 140 | if resultExt == 'txt': 141 | with open(fd, 'w') as f: 142 | f.write(scadCode) 143 | else: 144 | self.execute(scadCommand, scadCode) 145 | return self.resultFile 146 | else: 147 | logging.warning('Empty SCAD Code!') 148 | 149 | return None 150 | 151 | def saveAs(self, scadCommand, scadCode, fileName): 152 | self.resultFile = fileName 153 | self.execute(scadCommand, scadCode) 154 | 155 | 156 | def execute(self, openSCADConvertCommand, scadCode): 157 | # Open the file for writing. 158 | fd, inPath = tempfile.mkstemp(suffix=".scad", prefix=None, dir=None, text=True) 159 | with open(fd, 'w') as f: 160 | f.write(scadCode) 161 | 162 | command = openSCADConvertCommand+" "+inPath+" -o "+self.resultFile 163 | # openjscad example001.jscad -o test.stl 164 | p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 165 | for line in p.stdout.readlines(): 166 | self.messages+=line.decode("utf-8") 167 | 168 | retval = p.wait() 169 | self.isError = retval != 0 170 | os.remove(inPath) 171 | return retval 172 | 173 | def mimeToExtension(self, mime): 174 | list = mime.split("/") 175 | if len(list)!=2: 176 | raise Exception('Invalid mime type: {}'.format(mime)) 177 | result = list[1] 178 | if mime=="text/plain": 179 | result = "txt" 180 | if mime=="application/openscad": 181 | result = "txt" 182 | return result 183 | 184 | def getMessages(self): 185 | return self.messages 186 | 187 | def close(self): 188 | for file in self.tmpFiles: 189 | try: 190 | os.remove(file) 191 | except Exception as err: 192 | logging.error(err) 193 | 194 | self.tmpFiles = [] 195 | 196 | 197 | class Setup: 198 | def setup(self, scadCommand): 199 | # if the scadCommand is defined there is nothing to do 200 | if scadCommand: 201 | return scadCommand 202 | 203 | mc = MimeConverter() 204 | testSCAD = "cube([1,1,1]);" 205 | 206 | # check for openscad 207 | scadCommand = "openscad" 208 | if mc.convert(scadCommand,testSCAD,"image/png")!=None: 209 | return self.openSCADLinux(scadCommand) 210 | 211 | # check for openjscad 212 | scadCommand = "openjscad" 213 | if mc.convert(scadCommand, testSCAD,"image/png")!=None: 214 | return scadCommand 215 | 216 | # Default command if nothing is supported 217 | scadCommand = "openscad" 218 | return scadCommand 219 | 220 | def openSCADLinux(self, scadCommand): 221 | from sys import platform 222 | if platform == "linux" or platform == "linux2": 223 | try: 224 | os.environ['DISPLAY'] 225 | return scadCommand 226 | except KeyError: 227 | return "xvfb-run --auto-servernum --server-num=99 openscad" 228 | return scadCommand 229 | 230 | 231 | ## 232 | # Stores a single SCAD "statememnt". For our purpose we just consider a subset 233 | # which is relevant to prevent redundant code. 234 | ## 235 | class Statement: 236 | 237 | def __init__(self, statementTypePar, sourceCode): 238 | self.statementType = statementTypePar 239 | self.sourceCode = "".join(sourceCode) 240 | self.name = "-" 241 | if statementTypePar == "=" : 242 | self.name = self.sourceCode.split("=")[0].strip() 243 | elif statementTypePar == "include": 244 | self.name = self.extract(sourceCode,"<",">") 245 | elif statementTypePar == "use": 246 | self.name = self.extract(sourceCode,"<",">") 247 | elif statementTypePar == "module": 248 | self.name = self.extract(sourceCode,"module","(") 249 | elif statementTypePar == "function": 250 | self.name = self.extract(sourceCode,"function","(") 251 | elif statementTypePar == "comment": 252 | self.name = self.sourceCode.strip() 253 | elif statementTypePar == "-": 254 | self.name = self.sourceCode.strip() 255 | 256 | ## Logic to find the name with the help of a start and end tag 257 | def extract(self, wordList,fromStr,toStr): 258 | start = wordList.index(fromStr)+1 259 | end = wordList.index(toStr) 260 | return "".join(wordList[start:end]).strip() 261 | 262 | def str(self): 263 | return self.sourceCode 264 | 265 | 266 | ## 267 | # The kernal can submit the same code multiple times. The major goal of this parser 268 | # is to prevent duplicate definitions (mainly of modules or functions) 269 | ## 270 | 271 | class Parser: 272 | lsCommands = ["%clear", "%display", "%displayCode","%%display","%%displayCode", "%mime", "%command", "%lsmagic", "%include", "%use", "%saveAs"] 273 | 274 | def __init__(self): 275 | self.statements = [] 276 | self.tempStatement = Statement("-",[]) 277 | self.messages = "" 278 | self.mime = "image/png" 279 | self.converter = MimeConverter() 280 | self.scadCommand = "" 281 | self.isError = False 282 | self.displayRendered = False 283 | self.scanner = Scanner() 284 | 285 | 286 | def getStatements(self): 287 | return self.statements 288 | 289 | def getStatementsOfType(self, statementType): 290 | result = [] 291 | for s in self.statements: 292 | if s.statementType == statementType: 293 | result.append(s) 294 | return result 295 | 296 | def getSourceCode(self): 297 | ## persistend code 298 | result = "".join([elem.sourceCode for elem in self.getStatements()]) 299 | result += os.linesep 300 | ## temporary display code 301 | result += self.tempStatement.sourceCode 302 | result = result.replace(u'\xa0', u' ') 303 | return result 304 | 305 | def lineCount(self): 306 | return self.getSourceCode().count('\n') 307 | 308 | def addMessages(self, newMessage): 309 | if self.messages.strip(): 310 | self.messages = self.messages + os.linesep + newMessage 311 | else: 312 | self.messages = newMessage 313 | 314 | def getMessages(self): 315 | return self.messages 316 | 317 | def getMessagesExt(self): 318 | result = self.messages 319 | # if there is nothing to display we give at least some info 320 | if not result.strip() and not self.displayRendered: 321 | result = "Number of lines of OpenSCAD code: "+str(self.lineCount())+os.linesep 322 | return result 323 | 324 | def clearMessages(self): 325 | self.messages = "" 326 | self.isError = False 327 | 328 | def parse(self, scad): 329 | self.displayRendered = False 330 | self.clearMessages() 331 | self.tempStatement = Statement("-",[]) 332 | words = self.scanner.scann(scad) 333 | end = 1 334 | while len(words)>0 and end>0: 335 | if not words[0].strip(): 336 | ## collect white space 337 | end = self.scanner.findEndWhiteSpace(words) 338 | if "".join(words[0:end])!="": 339 | statement = Statement("whitespace",words[0:end]) 340 | self.insertStatement(statement) 341 | elif "/*" == "".join(words[0:3]): 342 | end = self.scanner.findEndString(words,"*/", 3) 343 | end = self.scanner.findEndWithNewLine(words,end) 344 | statement = Statement("comment", words[0:end]) 345 | self.insertStatement(statement) 346 | elif "//" == "".join(words[0:3]): 347 | end = self.scanner.findEnd1(words,os.linesep) 348 | statement = Statement("comment", words[0:end]) 349 | self.insertStatement(statement) 350 | elif "%include" == "".join(words[0:2]): 351 | end = self.processInclude(words) 352 | elif "%use" == "".join(words[0:2]): 353 | end = self.processUse(words) 354 | elif words[0] in ["include", "use"]: 355 | end = self.statements.index(";") 356 | end = self.scanner.findEndWithNewLine(words,end) 357 | statement = Statement(words[0], words[0:end]) 358 | self.insertStatement(statement) 359 | elif words[0] == "module": 360 | end = self.scanner.findEnd2(words,"{","}") 361 | end = self.scanner.findEndWithNewLine(words,end) 362 | statement = Statement("module",words[0:end]) 363 | self.insertStatement(statement) 364 | elif "%clear" == "".join(words[0:2]): 365 | end = 2 366 | self.close() 367 | self.addMessages( "SCAD code buffer has been cleared") 368 | elif "%%displayCode" == "".join(words[0:4]): 369 | self.displayRendered = True 370 | end = len(words) 371 | self.tempStatement = Statement(None,words[4:end]) 372 | self.addMessages( self.getSourceCode()) 373 | elif "%displayCode" == "".join(words[0:2]): 374 | end = self.scanner.findEnd1(words,os.linesep) 375 | tmpCode = "".join(words[2:end]) 376 | self.addMessages( self.getSourceCode()+tmpCode) 377 | elif "%display" == "".join(words[0:2]): 378 | self.displayRendered = True 379 | end = self.scanner.findEnd1(words,os.linesep) 380 | self.tempStatement = Statement(None,words[2:end]) 381 | elif "%%display" == "".join(words[0:4]): 382 | self.displayRendered = True 383 | end = len(words) 384 | self.tempStatement = Statement(None,words[4:end]) 385 | elif "%saveAs" == "".join(words[0:2]): 386 | end = self.processSaveAs(words) 387 | elif "%mime" == "".join(words[0:2]): 388 | end = self.scanner.findEnd1(words,os.linesep) 389 | mime = "".join(words[2:end]).strip() 390 | if mime: 391 | self.mime = mime 392 | self.addMessages( "The display mime type is '"+self.mime+"'") 393 | elif "%command" == "".join(words[0:2]): 394 | end = self.scanner.findEnd1(words,os.linesep) 395 | command = "".join(words[2:end]).strip() 396 | if command: 397 | self.setScadCommand(command) 398 | self.addMessages("The display command is '"+self.getScadCommand()+"'") 399 | elif "%lsmagic" == "".join(words[0:2]): 400 | end = 2 401 | commandsTxt = " ".join(self.lsCommands) 402 | self.addMessages("Available Commands: "+ commandsTxt ) 403 | else: 404 | end = self.processDefault(words) 405 | 406 | ## use unprocessed tail for next iteration 407 | words = words[end:] 408 | 409 | def renderMime(self): 410 | result = None 411 | try: 412 | code = self.getSourceCode().strip() 413 | 414 | if code: 415 | result = self.converter.convert(self.scadCommand, code, self.mime) 416 | self.addMessages(self.converter.getMessages()) 417 | self.isError = self.converter.isError 418 | 419 | except Exception as err: 420 | self.isError = True 421 | self.addMessages("Could render OpenSCAD code: "+str(err)) 422 | 423 | return result 424 | 425 | def saveAs(self, fileName): 426 | code = self.getSourceCode().strip() 427 | self.converter.saveAs(self.scadCommand, code, fileName) 428 | 429 | def insertStatement(self, newStatement): 430 | if newStatement.statementType in ["module","=","comment","-"]: 431 | for i in range(len(self.statements)): 432 | current = self.statements[i] 433 | if current.name == newStatement.name and current.statementType == newStatement.statementType: 434 | self.statements[i] = newStatement 435 | return 436 | self.statements.append(newStatement) 437 | 438 | ## Determines the currently defined module names 439 | def getModuleNames(self): 440 | result = [] 441 | for s in self.statements: 442 | if (s.statementType=='module'): 443 | result.append(s.name+"();") 444 | return result 445 | 446 | ## Determines the installed scad programs 447 | def setup(self): 448 | self.scadCommand = Setup().setup(self.scadCommand) 449 | return self.scadCommand 450 | 451 | def setScadCommand(self, cmd): 452 | self.scadCommand = cmd 453 | 454 | def getScadCommand(self): 455 | return self.scadCommand 456 | 457 | def getIncludeString(self, words, end): 458 | url = "".join(words[2:end]).strip() 459 | lib = IncludeLibrary.get(url) 460 | if not lib: 461 | lib = IncludeLibrary.addRef(url,url) 462 | includeString = lib.getContent().strip() 463 | return includeString 464 | 465 | def processInclude(self, words): 466 | end = self.scanner.findEnd1(words,os.linesep) 467 | try: 468 | scadCode = self.getIncludeString(words, end) 469 | useParser = Parser() 470 | useParser.parse(scadCode) 471 | count = 0 472 | for statement in useParser.statements: 473 | self.insertStatement(statement) 474 | count += 1 475 | self.addMessages("Included number of statements: "+str(count)) 476 | except Exception as err: 477 | self.isError = True 478 | self.addMessages("Could not include file: "+str(err)) 479 | return end 480 | 481 | def processUse(self, words): 482 | end = self.scanner.findEnd1(words,os.linesep) 483 | try: 484 | scadCode = self.getIncludeString(words, end) 485 | useParser = Parser() 486 | useParser.parse(scadCode) 487 | count = 0 488 | for statement in useParser.statements: 489 | if (statement.statementType in ["include","use","module","function","=","whitespace","comment"]): 490 | self.insertStatement(statement) 491 | count += 1 492 | self.addMessages("Included number of statements: "+str(count)) 493 | except Exception as err: 494 | self.isError = True 495 | self.addMessages("Could not include file: "+str(err)) 496 | return end 497 | 498 | def processSaveAs(self, words): 499 | end = self.scanner.findEnd1(words,os.linesep) 500 | try: 501 | fileName = "".join(words[2:end]).strip() 502 | self.saveAs(fileName) 503 | self.addMessages("File '" +fileName+ "' created") 504 | except Exception as err: 505 | self.addMessages("Could not save file: "+str(err)) 506 | return end 507 | 508 | def processDefault(self, words): 509 | if words[0:1]=="%": 510 | end = self.scanner.findEnd1(words,os.linesep) 511 | self.addMessages("Unsupported Command: "+"".join(words[0:end])) 512 | else: 513 | end = self.scanner.findEnd1(words,";") 514 | end = self.scanner.findEndWithNewLine(words,end) 515 | 516 | newStatementWords = words[0:end] 517 | cmd = "".join(newStatementWords) 518 | if (not cmd or cmd.strip().endswith(";")): 519 | statementType = "-" 520 | if "function" in newStatementWords: 521 | statementType = "function" 522 | elif "=" in newStatementWords: 523 | statementType = "=" 524 | elif not cmd and os.linesep in newStatementWords : 525 | statementType = "whitespace" 526 | statement = Statement(statementType, newStatementWords) 527 | self.insertStatement(statement) 528 | else: 529 | self.addMessages("Syntax error: this cell does not contain valid OpenSCAD code" ) 530 | self.isError = True 531 | return end 532 | 533 | def close(self): 534 | self.clearMessages() 535 | self.statements = [] 536 | self.converter.close() 537 | self.tempStatement = Statement("-",[]) 538 | 539 | -------------------------------------------------------------------------------- /iopenscad/scanner.py: -------------------------------------------------------------------------------- 1 | import re, os 2 | ### 3 | # Scanner which splits a string into individual tokens (words) 4 | # 5 | 6 | class Scanner: 7 | def scann(self, scad): 8 | words = re.split('(\W)', scad) 9 | return words 10 | 11 | # for a start tag we try to find the matching end tag: e.g for { } 12 | def findEnd2(self, words, start, end): 13 | index = 0 14 | wordPos = 0 15 | started = False 16 | while wordPos