├── .gitignore ├── .prettierrc ├── .vscode ├── launch.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── docs ├── debug.md ├── download_examples.md ├── examples.md ├── install.md ├── measure.md ├── quickstart.md ├── run.md ├── set_viewer_config.md ├── show.md ├── show_all.md ├── show_object.md └── snippets.md ├── examples ├── __convert_notebooks.py ├── box.py ├── colormaps.py ├── examples.py ├── hexapod.py ├── latest.py ├── logo.py ├── object-160x160mm.png ├── profile4040.step ├── set_viewer_config.py ├── show_object_usage.py ├── show_params.py ├── show_usage.py └── ttt.py ├── media ├── ocp-icon.afdesign ├── ocp-icon.png ├── ocp-icon.pxm ├── ocp-icon.svg ├── ocp-logo.afdesign ├── ocp-logo.png └── ocp-logo.svg ├── ocp-vscode-icons.afdesign ├── ocp_vscode ├── __init__.py ├── __main__.py ├── animation.py ├── backend.py ├── backend_logo.py ├── build123d.py ├── colors.py ├── comms.py ├── config.py ├── show.py ├── standalone.py ├── state.py ├── static │ └── js │ │ ├── comms.js │ │ └── logo.js └── templates │ └── viewer.html ├── package.json ├── pyproject.toml ├── pytests ├── test_serialisation.py └── test_show.py ├── resources ├── dark │ ├── install.svg │ ├── open.svg │ ├── output.svg │ ├── paste.svg │ ├── refresh.svg │ ├── settings.svg │ └── start.svg ├── light │ ├── install.svg │ ├── open.svg │ ├── output.svg │ ├── paste.svg │ ├── refresh.svg │ ├── settings.svg │ └── start.svg ├── ocp-icon.svg ├── ocp-logo.png └── viewer.html ├── screenshots ├── angle-tool.png ├── axes-and-grids.png ├── build123d_installed.png ├── build123d_snippet.gif ├── cadquery_installed.png ├── context_vars.png ├── debug.gif ├── edges-parent.png ├── faces-parent.png ├── glass-collapsed.png ├── glass.png ├── init.png ├── install-libraries.gif ├── install_ocp_vscode.png ├── jupyter-header.png ├── measure-tool.png ├── measure.gif ├── named-objects.png ├── ocp-on.png ├── ocp_icon.png ├── ocp_vscode-examples.gif ├── ocp_vscode_debug.png ├── ocp_vscode_installed.png ├── ocp_vscode_run.png ├── properties-tool.png ├── quickstart.gif ├── run-code.gif ├── topo-filter.png └── vertices-parent.png ├── src ├── controller.ts ├── demo.ts ├── display.ts ├── examples.ts ├── extension.ts ├── libraryManager.ts ├── logo.ts ├── output.ts ├── state.ts ├── statusManager.ts ├── system │ ├── shell.ts │ └── terminal.ts ├── test │ ├── runTest.ts │ └── suite │ │ ├── extension.test.ts │ │ └── index.ts ├── utils.ts ├── version.ts └── viewer.ts ├── test ├── .vscode │ └── launch.json ├── backend.py ├── sender.py ├── test_for_backend.py └── testextension.py ├── tsconfig.json ├── vsc-extension-quickstart.md └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | *.tgz 7 | vsc-extension-quickstart.md 8 | ipc 9 | ocp_vscode.egg-info 10 | build 11 | dist 12 | .DS_Store 13 | lego.py 14 | change.tar 15 | three-cad-viewer-*.tgz 16 | ocp-vscode-backend.log 17 | ocp_tessellate.log 18 | .ocp_vscode 19 | **/__pycache__/ 20 | main.py 21 | ocp_vscode/static/js/three-cad-viewer.esm.js 22 | ocp_vscode/static/css/three-cad-viewer.css 23 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "trailingComma": "none", 4 | "tabWidth": 4, 5 | "quoteProps": "as-needed" 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 13 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 14 | "preLaunchTask": "${defaultBuildTask}" 15 | // "env": { 16 | // "OCP_PORT": "3999" 17 | // } 18 | }, 19 | { 20 | "name": "Extension Tests", 21 | "type": "extensionHost", 22 | "request": "launch", 23 | "args": [ 24 | "--extensionDevelopmentPath=${workspaceFolder}", 25 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 26 | ], 27 | "outFiles": ["${workspaceFolder}/out/test/**/*.js"], 28 | "preLaunchTask": "${defaultBuildTask}" 29 | }, 30 | { 31 | "name": "Python: Current File", 32 | "type": "debugpy", 33 | "request": "launch", 34 | "program": "${file}", 35 | "console": "integratedTerminal", 36 | "args": ["--port", "3939"], 37 | "justMyCode": false 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | }, 18 | "label": "npm: watch", 19 | "detail": "tsc -watch -p ./" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | dist/ocp_vscode-*.tar.gz 2 | dist/ocp_vscode-*.whl 3 | test/ 4 | build/ 5 | src/ 6 | docs/ 7 | ocp_vscode/ 8 | ocp_vscode.egg-info/ 9 | .gitignore 10 | .ocp_vscode 11 | .pytest_cache 12 | .vscode 13 | .mypy_cache 14 | ocp_vscode/.mypy_cache 15 | main.py 16 | setup.cfg 17 | setup.py 18 | vsc-extension-quickstart.md 19 | test.sh 20 | 21 | out/logo_alt.js 22 | 23 | screenshots/*.gif 24 | screenshots/ocp_vscode_debug.png 25 | screenshots/ocp_vscode_installed.png 26 | screenshots/ocp_vscode_run.png 27 | screenshots/angle-tool.png 28 | screenshots/axes-and-grids.png 29 | screenshots/edges-parent.png 30 | screenshots/faces-parent.png 31 | screenshots/glass-collapsed.png 32 | screenshots/glass.png 33 | screenshots/install_ocp_vscode.png 34 | screenshots/jupyter-header.png 35 | screenshots/measure-tool.png 36 | screenshots/named-objects.png 37 | screenshots/ocp-on.png 38 | screenshots/ocp_vscode_installed.png 39 | screenshots/properties-tool.png 40 | screenshots/topo-filter.png 41 | screenshots/vertices-parent.png 42 | 43 | 44 | node_modules/three-cad-viewer/dist/three-cad-viewer.esm.min.js 45 | node_modules/three-cad-viewer/dist/three-cad-viewer.js 46 | node_modules/three-cad-viewer/dist/three-cad-viewer.min.js 47 | ocp-vscode-icons.afdesign 48 | Makefile 49 | 50 | media/ocp-logo.afdesign 51 | media/ocp-icon.pxm 52 | media/ocp-icon.pxm 53 | media/ocp-logo.afdesign 54 | media/ocp-icon.afdesign 55 | media/ocp-icon.png 56 | media/ocp-logo.png 57 | 58 | ocp_vscode/__pycache__ 59 | examples/* 60 | TODO 61 | node_modules/@aashutoshrathi 62 | node_modules/@eslint 63 | node_modules/@eslint-community 64 | node_modules/@humanwhocodes 65 | node_modules/@nodelib 66 | node_modules/@tootallnate 67 | node_modules/@types 68 | node_modules/@typescript-eslint 69 | node_modules/acorn 70 | node_modules/acorn-jsx 71 | node_modules/agent-base 72 | node_modules/ajv 73 | node_modules/ansi-colors 74 | node_modules/ansi-regex 75 | node_modules/ansi-styles 76 | node_modules/anymatch 77 | node_modules/argparse 78 | node_modules/array-union 79 | node_modules/azure-devops-node-api 80 | node_modules/balanced-match 81 | node_modules/base64-js 82 | node_modules/binary-extensions 83 | node_modules/bl 84 | node_modules/boolbase 85 | node_modules/brace-expansion 86 | node_modules/braces 87 | node_modules/browser-stdout 88 | node_modules/buffer 89 | node_modules/buffer-crc32 90 | node_modules/call-bind 91 | node_modules/callsites 92 | node_modules/camelcase 93 | node_modules/camel-case 94 | node_modules/chalk 95 | node_modules/cheerio 96 | node_modules/cheerio-select 97 | node_modules/chokidar 98 | node_modules/chownr 99 | node_modules/cliui 100 | node_modules/clean-css 101 | node_modules/color-convert 102 | node_modules/color-name 103 | node_modules/commander 104 | node_modules/concat-map 105 | node_modules/core-util-is 106 | node_modules/cross-spawn 107 | node_modules/css-select 108 | node_modules/css-what 109 | node_modules/debug 110 | node_modules/decamelize 111 | node_modules/decompress-response 112 | node_modules/deep-extend 113 | node_modules/deep-is 114 | node_modules/detect-libc 115 | node_modules/diff 116 | node_modules/dir-glob 117 | node_modules/doctrine 118 | node_modules/dom-serializer 119 | node_modules/domelementtype 120 | node_modules/domhandler 121 | node_modules/domutils 122 | node_modules/emoji-regex 123 | node_modules/end-of-stream 124 | node_modules/entities 125 | node_modules/escalade 126 | node_modules/escape-string-regexp 127 | node_modules/eslint 128 | node_modules/eslint-scope 129 | node_modules/eslint-visitor-keys 130 | node_modules/espree 131 | node_modules/esquery 132 | node_modules/esrecurse 133 | node_modules/estraverse 134 | node_modules/esutils 135 | node_modules/expand-template 136 | node_modules/fast-deep-equal 137 | node_modules/fast-glob 138 | node_modules/fast-json-stable-stringify 139 | node_modules/fast-levenshtein 140 | node_modules/fastq 141 | node_modules/fd-slicer 142 | node_modules/file-entry-cache 143 | node_modules/fill-range 144 | node_modules/find-up 145 | node_modules/flat 146 | node_modules/flat-cache 147 | node_modules/flatted 148 | node_modules/fs-constants 149 | node_modules/fs.realpath 150 | node_modules/fsevents 151 | node_modules/function-bind 152 | node_modules/get-caller-file 153 | node_modules/get-intrinsic 154 | node_modules/github-from-package 155 | node_modules/glob 156 | node_modules/glob-parent 157 | node_modules/globals 158 | node_modules/globby 159 | node_modules/grapheme-splitter 160 | node_modules/graphemer 161 | node_modules/has 162 | node_modules/has-flag 163 | node_modules/has-proto 164 | node_modules/has-symbols 165 | node_modules/he 166 | node_modules/hosted-git-info 167 | node_modules/html-minifier 168 | node_modules/htmlparser2 169 | node_modules/http-proxy-agent 170 | node_modules/https-proxy-agent 171 | node_modules/ieee754 172 | node_modules/ignore 173 | node_modules/immediate 174 | node_modules/import-fresh 175 | node_modules/imurmurhash 176 | node_modules/inflight 177 | node_modules/inherits 178 | node_modules/ini 179 | node_modules/is-binary-path 180 | node_modules/is-extglob 181 | node_modules/is-fullwidth-code-point 182 | node_modules/is-glob 183 | node_modules/is-number 184 | node_modules/is-path-inside 185 | node_modules/is-plain-obj 186 | node_modules/is-unicode-supported 187 | node_modules/isarray 188 | node_modules/isexe 189 | node_modules/js-yaml 190 | node_modules/json-schema-traverse 191 | node_modules/json-stable-stringify-without-jsonify 192 | node_modules/jsonc-parser 193 | node_modules/jszip 194 | node_modules/keytar 195 | node_modules/leven 196 | node_modules/levn 197 | node_modules/lie 198 | node_modules/linkify-it 199 | node_modules/locate-path 200 | node_modules/lodash.merge 201 | node_modules/log-symbols 202 | node_modules/lower-case 203 | node_modules/markdown-it 204 | node_modules/mdurl 205 | node_modules/merge2 206 | node_modules/micromatch 207 | node_modules/mime 208 | node_modules/mimic-response 209 | node_modules/minimatch 210 | node_modules/minimist 211 | node_modules/mkdirp-classic 212 | node_modules/mocha 213 | node_modules/ms 214 | node_modules/mute-stream 215 | node_modules/nanoid 216 | node_modules/napi-build-utils 217 | node_modules/natural-compare 218 | node_modules/natural-compare-lite 219 | node_modules/no-case 220 | node_modules/node-abi 221 | node_modules/node-addon-api 222 | node_modules/normalize-path 223 | node_modules/nth-check 224 | node_modules/object-inspect 225 | node_modules/once 226 | node_modules/optionator 227 | node_modules/p-limit 228 | node_modules/p-locate 229 | node_modules/pako 230 | node_modules/param-case 231 | node_modules/parent-module 232 | node_modules/parse-semver 233 | node_modules/parse5 234 | node_modules/parse5-htmlparser2-tree-adapter 235 | node_modules/path-exists 236 | node_modules/path-is-absolute 237 | node_modules/path-key 238 | node_modules/path-type 239 | node_modules/pend 240 | node_modules/picomatch 241 | node_modules/prebuild-install 242 | node_modules/prelude-ls 243 | node_modules/process-nextick-args 244 | node_modules/pump 245 | node_modules/punycode 246 | node_modules/qs 247 | node_modules/queue-microtask 248 | node_modules/randombytes 249 | node_modules/rc 250 | node_modules/read 251 | node_modules/readable-stream 252 | node_modules/readdirp 253 | node_modules/relateurl 254 | node_modules/require-directory 255 | node_modules/resolve-from 256 | node_modules/reusify 257 | node_modules/rimraf 258 | node_modules/run-parallel 259 | node_modules/safe-buffer 260 | node_modules/sax 261 | node_modules/serialize-javascript 262 | node_modules/setimmediate 263 | node_modules/shebang-command 264 | node_modules/shebang-regex 265 | node_modules/side-channel 266 | node_modules/simple-concat 267 | node_modules/simple-get 268 | node_modules/slash 269 | node_modules/source-map 270 | node_modules/string-width 271 | node_modules/string_decoder 272 | node_modules/strip-ansi 273 | node_modules/strip-json-comments 274 | node_modules/supports-color 275 | node_modules/tar-fs 276 | node_modules/tar-stream 277 | node_modules/text-table 278 | node_modules/three 279 | node_modules/tmp 280 | node_modules/to-regex-range 281 | node_modules/tslib 282 | node_modules/tsutils 283 | node_modules/tunnel 284 | node_modules/tunnel-agent 285 | node_modules/type-check 286 | node_modules/type-fest 287 | node_modules/typed-rest-client 288 | node_modules/typescript 289 | node_modules/uc.micro 290 | node_modules/uglify-js 291 | node_modules/underscore 292 | node_modules/upper-case 293 | node_modules/uri-js 294 | node_modules/url-join 295 | node_modules/util-deprecate 296 | node_modules/which 297 | node_modules/workerpool 298 | node_modules/wrap-ansi 299 | node_modules/wrappy 300 | node_modules/xml2js 301 | node_modules/xmlbuilder 302 | node_modules/y18n 303 | node_modules/yargs 304 | node_modules/yargs-parser 305 | node_modules/yargs-unparser 306 | node_modules/yauzl 307 | node_modules/yazl 308 | node_modules/yocto-queue 309 | 310 | three-cad-viewer-v*.tgz -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean_notebooks wheel install tests check_version dist check_dist upload_test upload bump release create-release docker docker_upload dist tests 2 | 3 | PYCACHE := $(shell find . -name '__pycache__') 4 | EGGS := $(wildcard *.egg-info) 5 | CURRENT_VERSION := $(shell awk '/current_version =/ {print substr($$3, 2, length($$3)-2)}' pyproject.toml) 6 | 7 | # https://github.com/jupyter/nbconvert/issues/637 8 | 9 | clean: 10 | @echo "=> Cleaning" 11 | @rm -fr build dist $(EGGS) $(PYCACHE) 12 | 13 | prepare: clean 14 | git add . 15 | git status 16 | git commit -m "cleanup before release" 17 | 18 | # Version commands 19 | bump: 20 | @echo Current version: $(CURRENT_VERSION) 21 | ifdef part 22 | bump-my-version bump $(part) --allow-dirty && grep current pyproject.toml 23 | else ifdef version 24 | bump-my-version bump --allow-dirty --new-version $(version) && grep current pyproject.toml 25 | else 26 | @echo "Provide part=major|minor|patch|release|build and optionally version=x.y.z..." 27 | exit 1 28 | endif 29 | 30 | dist: 31 | @echo Version: $(CURRENT_VERSION) 32 | @echo "Copying html, css and js file to standalone locations" 33 | @cp resources/viewer.html ocp_vscode/templates 34 | @cp node_modules/three-cad-viewer/dist/three-cad-viewer.esm.js ocp_vscode/static/js 35 | @cp node_modules/three-cad-viewer/dist/three-cad-viewer.css ocp_vscode/static/css 36 | 37 | @python -m build -n 38 | vsce package --yarn 39 | @ls -l dist/ 40 | 41 | 42 | vsix: 43 | @echo Version: $(CURRENT_VERSION) 44 | vsce package --yarn 45 | @ls -l *.vsix 46 | 47 | release: 48 | git add . 49 | git status 50 | git diff-index --quiet HEAD || git commit -m "Latest release: $(CURRENT_VERSION)" 51 | git tag -a v$(CURRENT_VERSION) -m "Latest release: $(CURRENT_VERSION)" 52 | 53 | create-release: 54 | @git push 55 | @git push --tags 56 | @github-release release -u bernhard-42 -r vscode-ocp-cad-viewer -t v$(CURRENT_VERSION) -n ocp-cad-viewer-$(CURRENT_VERSION) 57 | @sleep 2 58 | @github-release upload -u bernhard-42 -r vscode-ocp-cad-viewer -t v$(CURRENT_VERSION) -n ocp-cad-viewer-$(CURRENT_VERSION).vsix -f ocp-cad-viewer-$(CURRENT_VERSION).vsix 59 | @github-release upload -u bernhard-42 -r vscode-ocp-cad-viewer -t v$(CURRENT_VERSION) -n ocp_vscode-$(CURRENT_VERSION)-py3-none-any.whl -f dist/ocp_vscode-$(CURRENT_VERSION)-py3-none-any.whl 60 | @github-release upload -u bernhard-42 -r vscode-ocp-cad-viewer -t v$(CURRENT_VERSION) -n ocp_vscode-$(CURRENT_VERSION).tar.gz -f dist/ocp_vscode-$(CURRENT_VERSION).tar.gz 61 | 62 | # install: dist 63 | # @echo "=> Installing jupyter_cadquery" 64 | # @pip install --upgrade . 65 | 66 | # check_dist: 67 | # @twine check dist/* 68 | 69 | tests: 70 | NATIVE_TESSELLATOR=0 OCP_VSCODE_PYTEST=1 pytest -v -s pytests/ 71 | 72 | native_tests: 73 | NATIVE_TESSELLATOR=1 OCP_VSCODE_PYTEST=1 pytest -v -s pytests/ 74 | 75 | upload: 76 | @twine upload dist/* 77 | -------------------------------------------------------------------------------- /docs/debug.md: -------------------------------------------------------------------------------- 1 | # Debug code with visual debugging 2 | 3 | - Ensure `OCP: on` is shown in the status bar: 4 | 5 | ![OCP:on](../screenshots/ocp-on.png) 6 | 7 | Clicking on it will toggle `OCP:on` (visual debugging) and `OCP:off` (no visual debugging) 8 | 9 | - Set breakpoints and step over the code as usual 10 | 11 | - At any point, variables can be deselected in the tree and visual debugging will remember the setting for the following steps (e.g. to unselect some temp variables "out of scope") 12 | 13 | ![Debug code with visual debugging](../screenshots/debug.gif) -------------------------------------------------------------------------------- /docs/download_examples.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/docs/download_examples.md -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | ## Downloading examples 2 | 3 | - Go to the "Library Manager" and find "examples (download)" under an installed library. Note, not all libraries have examples!. Press the green download button 4 | - Check and modify (if needed) the target download path. Note, a file needs to be open so that the extension can get the working folder 5 | - The examples will be stored in a folder \_examples, e.g. cadquery_examples 6 | 7 | ![Use with run](../screenshots/ocp_vscode-examples.gif) -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | ## Install Libraries 2 | 3 | For each library available in the Library Manager, hover over the library and press the down-arrow to install it. VS Code will prompt you to ensure that the right environment is chosen. 4 | In case something goes wrong, the message will be visible in VS Code. 5 | -------------------------------------------------------------------------------- /docs/measure.md: -------------------------------------------------------------------------------- 1 | # Measure mode 2 | 3 | ![Measurement mode](../screenshots/measure.gif) 4 | 5 | Note: The parameter measure_tools is not needed any more 6 | 7 | ## Tools 8 | 9 | There are three tools: 10 | 11 | ![properties-tool](../screenshots/properties-tool.png) **Properties**: Get the properties of the object selected. 12 | 13 | ![measure-tool](../screenshots/measure-tool.png) **Measurement**: Get the distance of the two objects selected. 14 | 15 | ![angle-tool](../screenshots/angle-tool.png) **Angle**: Get the angle between the two objects selected. 16 | 17 | ## Topology Filter 18 | 19 | For easier selection, there is a **topology filter** 20 | 21 | ![topo-filter](../screenshots/topo-filter.png) 22 | 23 | There are keybinding shortcuts for the topology filters : 24 | 25 | - "v" : vertices 26 | - "e" : edges 27 | - "f" : faces 28 | - "s" : solid 29 | - "n" : none 30 | 31 | ## Deselect 32 | 33 | - In any of the tools pressing "escape" will delete all the selections 34 | - "backspace" or "mouse button right click" will delete the last selection only. 35 | -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quickstart experience 2 | 3 | The quickstart for _build123d_ under Windows can be seen in this animated gif: 4 | 5 | ![Quickstart experience on Windows](../screenshots/quickstart.gif) 6 | -------------------------------------------------------------------------------- /docs/run.md: -------------------------------------------------------------------------------- 1 | # Run code using Jupyter 2 | 3 | - Ensure your first line is `# %%. This will initiate the Jupyter environment 4 | 5 | ![Jupyter cell header](../screenshots/jupyter-header.png) 6 | 7 | - Use the "Run cell" button the Jupyter cell header or press `shift-enter` to exectute a cell and move to next cell 8 | or press `ctrl-enter` to exectute a cell and keep cursor at the position 9 | 10 | ![Run code using Jupyter](../screenshots/run-code.gif) 11 | -------------------------------------------------------------------------------- /docs/set_viewer_config.md: -------------------------------------------------------------------------------- 1 | ## set_viewer_config 2 | 3 | Change configuration of the OCP CAD Viewer from Python 4 | 5 | ### Command 6 | 7 | ```python 8 | set_viewer_config() 9 | ``` 10 | 11 | ### Arguments 12 | 13 | ```text 14 | 15 | Keywords 16 | 17 | - UI 18 | glass: (boolean) Use glass mode where tree is an overlay over the cad object 19 | tools: (boolean) Show tools 20 | tree_width: (int) Width of the object tree 21 | 22 | - Viewer 23 | axes: (boolean) Show axes 24 | axes0: (boolean) Show axes at (0,0,0) 25 | grid: (boolean) Show grid 26 | ortho: (boolean) Use orthographic projections 27 | transparent: (boolean) Show objects transparent 28 | black_edges: (boolean) Show edges in black color 29 | collapse: (enum) Collapse.LEAVES: collapse all single leaf nodes, 30 | Collapse.ROOT: expand root only, 31 | Collapse.ALL: collapse all nodes, 32 | Collapse.NONE: expand all nodes 33 | explode: (boolean) Turn on explode mode 34 | 35 | zoom: (int) Zoom factor of view 36 | position: (float 3-tuple) Camera position 37 | quaternion: (float 4-tuple) Camera orientation as quaternion 38 | target: (float 3-tuple) Camera look at target 39 | 40 | pan_speed: (float) Speed of mouse panning 41 | rotate_speed: (float) Speed of mouse rotate 42 | zoom_speed: (float) Speed of mouse zoom 43 | 44 | states: (dict) Set the visibility state of cad objects. Format: `{'path': [1, 1]}` 45 | `[1,1], [0,1]` (first element of the tuple) toggles faces 46 | `[1,1], [1,0]` (second element of the tuple) toggles edges 47 | tab: (boolean) Select a tab in the viewer ("tree", "clip", "material") 48 | 49 | clip_slider_0: (float) Set clipping slider 0 to a value 50 | clip_slider_1: (float) Set clipping slider 1 to a value 51 | clip_slider_2: (float) Set clipping slider 2 to a value 52 | clip_normal_0: (float 3-tuple) Set clipping normal 0 to a 3 dim tuple 53 | clip_normal_1: (float 3-tuple) Set clipping normal 1 to a 3 dim tuple 54 | clip_normal_2: (float 3-tuple) Set clipping normal 2 to a 3 dim tuple 55 | clip_intersection: (boolean) Turn on/off intersection clipping 56 | clip_planes: (boolean) Show/hide clipping helper planes 57 | clip_object_colors: (boolean) Toggle RGB and object color clipping caps 58 | 59 | - Renderer: 60 | default_edgecolor: Default color of the edges of a mesh 61 | default_opacity: Opacity value for transparent objects 62 | 63 | ambient_intensity: Intensity of ambient light 64 | direct_intensity: Intensity of direct light 65 | metalness: Metalness property of the default material 66 | roughness: Roughness property of the default material 67 | 68 | port: Port of OCP CAD Viewer 69 | ``` 70 | -------------------------------------------------------------------------------- /docs/show.md: -------------------------------------------------------------------------------- 1 | ## show 2 | 3 | ### Command 4 | 5 | The show command is used to show one or multiple CAD objects and comes with the following parameters: 6 | 7 | ```python 8 | show(*cad_objs, ) 9 | ``` 10 | 11 | ### Arguments 12 | 13 | ```text 14 | Parameters 15 | cad_objs: All cad objects that should be shown as positional parameters 16 | 17 | Keywords for show: 18 | names: List of names for the cad_objs. Needs to have the same length as cad_objs 19 | colors: List of colors for the cad_objs. Needs to have the same length as cad_objs 20 | alphas: List of alpha values for the cad_objs. Needs to have the same length as cad_objs 21 | port: The port the viewer listens to. Typically use 'set_port(port)' instead 22 | progress: Show progress of tessellation with None is no progress indicator. (default="-+*c") 23 | for object: "-": is reference, 24 | "+": gets tessellated with Python code, 25 | "*": gets tessellated with native code, 26 | "c": from cache 27 | 28 | Valid keywords to configure the viewer (**kwargs): 29 | - UI 30 | glass: Use glass mode where tree is an overlay over the cad object (default=False) 31 | tools: Show tools (default=True) 32 | tree_width: Width of the object tree (default=240) 33 | 34 | - Viewer 35 | axes: Show axes (default=False) 36 | axes0: Show axes at (0,0,0) (default=False) 37 | grid: Show grid (default=False) 38 | ortho: Use orthographic projections (default=True) 39 | transparent: Show objects transparent (default=False) 40 | default_opacity: Opacity value for transparent objects (default=0.5) 41 | black_edges: Show edges in black color (default=False) 42 | orbit_control: Mouse control use "orbit" control instead of "trackball" control (default=False) 43 | collapse: Collapse.LEAVES: collapse all single leaf nodes, 44 | Collapse.ROOT: expand root only, 45 | Collapse.ALL: collapse all nodes, 46 | Collapse.NONE: expand all nodes 47 | (default=Collapse.LEAVES) 48 | ticks: Hint for the number of ticks in both directions (default=10) 49 | center_grid: Center the grid at the origin or center of mass (default=False) 50 | up: Use z-axis ('Z') or y-axis ('Y') as up direction for the camera (default="Z") 51 | explode: Turn on explode mode (default=False) 52 | 53 | zoom: Zoom factor of view (default=1.0) 54 | position: Camera position 55 | quaternion: Camera orientation as quaternion 56 | target: Camera look at target 57 | reset_camera: Camera.RESET: Reset camera position, rotation, toom and target 58 | Camera.CENTER: Keep camera position, rotation, toom, but look at center 59 | Camera.KEEP: Keep camera position, rotation, toom, and target 60 | (default=Camera.RESET) 61 | 62 | clip_slider_0: Setting of clipping slider 0 (default=None) 63 | clip_slider_1: Setting of clipping slider 1 (default=None) 64 | clip_slider_2: Setting of clipping slider 2 (default=None) 65 | clip_normal_0: Setting of clipping normal 0 (default=[-1,0,0]) 66 | clip_normal_1: Setting of clipping normal 1 (default=[0,-1,0]) 67 | clip_normal_2: Setting of clipping normal 2 (default=[0,0,-1]) 68 | clip_intersection: Use clipping intersection mode (default=[False]) 69 | clip_planes: Show clipping plane helpers (default=False) 70 | clip_object_colors: Use object color for clipping caps (default=False) 71 | 72 | pan_speed: Speed of mouse panning (default=1) 73 | rotate_speed: Speed of mouse rotate (default=1) 74 | zoom_speed: Speed of mouse zoom (default=1) 75 | 76 | - Renderer 77 | deviation: Shapes: Deviation from linear deflection value (default=0.1) 78 | angular_tolerance: Shapes: Angular deflection in radians for tessellation (default=0.2) 79 | edge_accuracy: Edges: Precision of edge discretization (default: mesh quality / 100) 80 | 81 | default_color: Default mesh color (default=(232, 176, 36)) 82 | default_edgecolor: Default color of the edges of a mesh (default=#707070) 83 | default_facecolor: Default color of the edges of a mesh (default=#ee82ee) 84 | default_thickedgecolor: Default color of the edges of a mesh (default=#ba55d3) 85 | default_vertexcolor: Default color of the edges of a mesh (default=#ba55d3) 86 | ambient_intensity: Intensity of ambient light (default=1.00) 87 | direct_intensity: Intensity of direct light (default=1.10) 88 | metalness: Metalness property of the default material (default=0.30) 89 | roughness: Roughness property of the default material (default=0.65) 90 | 91 | render_edges: Render edges (default=True) 92 | render_normals: Render normals (default=False) 93 | render_mates: Render mates for MAssemblies (default=False) 94 | render_joints: Render build123d joints (default=False) 95 | show_parent: Render parent of faces, edges or vertices as wireframe (default=False) 96 | show_sketch_local: In build123d show local sketch in addition to relocate sketch (default=True) 97 | helper_scale: Scale of rendered helpers (locations, axis, mates for MAssemblies) (default=1) 98 | 99 | - Debug 100 | debug: Show debug statements to the VS Code browser console (default=False) 101 | timeit: Show timing information from level 0-3 (default=False) 102 | ``` 103 | 104 | ### Typically useful parameters 105 | 106 | - Provide maximum space for the CAD object with _glass_ mode 107 | 108 | `show(b, c, glass=True)` 109 | 110 | ![](../screenshots/glass.png) 111 | 112 | - Hide the tree in glass mode by collapsing the it 113 | 114 | `show(b, glass=True, collapse=Collapse.ALL)` 115 | 116 | ![](../screenshots/glass-collapsed.png) 117 | 118 | Other valid parameters are "1" (collapse leafs) and "E" (explode tree) 119 | 120 | - Names, colors and alpha values 121 | 122 | `show(b, c, colors=["red", "green"], names=["red box", "green cylinder"], alphas=[1.0, 0.2])` 123 | 124 | ![](../screenshots/named-objects.png) 125 | 126 | - Axes and grids 127 | 128 | `show(b, c, axes=True, axes0=False, grid=(True, True, False), ticks=40)` 129 | 130 | ![](../screenshots/axes-and-grids.png) 131 | 132 | - Keeping the camera position between `show` commands 133 | 134 | `show(b, c, reset_camera=False)` 135 | 136 | - Show parent object for edges, faces and vertices (build123d syntax) 137 | 138 | `show(b.vertices(), show_parent=True)` 139 | 140 | ![](../screenshots/vertices-parent.png) 141 | 142 | `show(b.edges().filter_by(Axis.Y), show_parent=True)` 143 | 144 | ![](../screenshots/edges-parent.png) 145 | 146 | `show(b.faces().filter_by(Axis.Y), show_parent=True)` 147 | 148 | ![](../screenshots/faces-parent.png) 149 | -------------------------------------------------------------------------------- /docs/show_all.md: -------------------------------------------------------------------------------- 1 | ## show_all 2 | 3 | Incrementally show CAD objects in Visual Studio Code. The command support the CQ-Editor parameters `obj`, `name` and `options` plus additional viewer specific args. 4 | 5 | ### Command 6 | 7 | ```python 8 | show_all(name=None, options=None, port=None, ) 9 | ``` 10 | 11 | ### Arguments 12 | 13 | ```text 14 | Show all variables in the current scope 15 | 16 | Parameters: 17 | variables: Only show objects with names in this list of variable names, 18 | i.e. do not use all from locals() 19 | exclude: List of variable names to exclude from "show_all" 20 | classes: Only show objects which are instances of the classes in this list 21 | _visual_debug: private variable, do not use! 22 | 23 | Keywords for show_all: 24 | Valid keywords for "show_all" are the same as for "show" 25 | ``` 26 | 27 | For more detail, see [show](show.md) 28 | 29 | ### Example 30 | 31 | ```python 32 | from build123d import * 33 | from ocp_vscode import * 34 | 35 | set_defaults(axes=True, transparent=False, collapse=1, grid=(True, True, True)) 36 | 37 | box = Box(1, 2, 1) 38 | chamfer(box.edges(), 0.4) 39 | sphere = Sphere(0.8) 40 | 41 | show_all( 42 | collapse="1", 43 | ortho=False 44 | ) 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/show_object.md: -------------------------------------------------------------------------------- 1 | ## show_object 2 | 3 | Incrementally show CAD objects in Visual Studio Code. The command support the CQ-Editor parameters `obj`, `name` and `options` plus additional viewer specific args. 4 | 5 | ### Command 6 | 7 | ```python 8 | show_object(obj, name=None, options=None, port=None, ) 9 | ``` 10 | 11 | ### Arguments 12 | 13 | ```text 14 | Parameters: 15 | obj: The CAD object to be shown 16 | 17 | Keywords for show_object: 18 | name: The name of the CAD object 19 | options: A dict of color and alpha value: {"alpha":0.5, "color": (64, 164, 223)} 20 | 0 <= alpha <= 1.0 and color is a 3-tuple of values between 0 and 255 21 | parent: Add another object, usually the parent of e.g. edges or vertices with alpha=0.25 22 | clear: In interactice mode, clear the stack of objects to be shown 23 | (typically used for the first object) 24 | port: The port the viewer listens to. Typically use 'set_port(port)' instead 25 | progress: Show progress of tessellation with None is no progress indicator. (default="-+*c") 26 | for object: "-": is reference, 27 | "+": gets tessellated with Python code, 28 | "*": gets tessellated with native code, 29 | "c": from cache 30 | 31 | Valid keywords to configure the viewer (**kwargs): 32 | - UI 33 | glass: Use glass mode where tree is an overlay over the cad object (default=False) 34 | tools: Show tools (default=True) 35 | tree_width: Width of the object tree (default=240) 36 | 37 | - Viewer 38 | axes: Show axes (default=False) 39 | axes0: Show axes at (0,0,0) (default=False) 40 | grid: Show grid (default=False) 41 | ortho: Use orthographic projections (default=True) 42 | transparent: Show objects transparent (default=False) 43 | default_opacity: Opacity value for transparent objects (default=0.5) 44 | black_edges: Show edges in black color (default=False) 45 | orbit_control: Mouse control use "orbit" control instead of "trackball" control (default=False) 46 | collapse: Collapse.LEAVES: collapse all single leaf nodes, 47 | Collapse.ROOT: expand root only, 48 | Collapse.ALL: collapse all nodes, 49 | Collapse.NONE: expand all nodes 50 | (default=Collapse.LEAVES) 51 | ticks: Hint for the number of ticks in both directions (default=10) 52 | center_grid: Center the grid at the origin or center of mass (default=False) 53 | up: Use z-axis ('Z') or y-axis ('Y') as up direction for the camera (default="Z") 54 | 55 | zoom: Zoom factor of view (default=1.0) 56 | position: Camera position 57 | quaternion: Camera orientation as quaternion 58 | target: Camera look at target 59 | reset_camera: Camera.RESET: Reset camera position, rotation, toom and target 60 | Camera.CENTER: Keep camera position, rotation, toom, but look at center 61 | Camera.KEEP: Keep camera position, rotation, toom, and target 62 | (default=Camera.RESET) 63 | 64 | clip_slider_0: Setting of clipping slider 0 (default=None) 65 | clip_slider_1: Setting of clipping slider 1 (default=None) 66 | clip_slider_2: Setting of clipping slider 2 (default=None) 67 | clip_normal_0: Setting of clipping normal 0 (default=[-1,0,0]) 68 | clip_normal_1: Setting of clipping normal 1 (default=[0,-1,0]) 69 | clip_normal_2: Setting of clipping normal 2 (default=[0,0,-1]) 70 | clip_intersection: Use clipping intersection mode (default=[False]) 71 | clip_planes: Show clipping plane helpers (default=False) 72 | clip_object_colors: Use object color for clipping caps (default=False) 73 | 74 | pan_speed: Speed of mouse panning (default=1) 75 | rotate_speed: Speed of mouse rotate (default=1) 76 | zoom_speed: Speed of mouse zoom (default=1) 77 | 78 | - Renderer 79 | deviation: Shapes: Deviation from linear deflection value (default=0.1) 80 | angular_tolerance: Shapes: Angular deflection in radians for tessellation (default=0.2) 81 | edge_accuracy: Edges: Precision of edge discretization (default: mesh quality / 100) 82 | 83 | default_color: Default mesh color (default=(232, 176, 36)) 84 | default_edgecolor: Default color of the edges of a mesh (default=(128, 128, 128)) 85 | default_facecolor: Default color of the edges of a mesh (default=#ee82ee / Violet) 86 | default_thickedgecolor: Default color of the edges of a mesh (default=#ba55d3 / MediumOrchid) 87 | default_vertexcolor: Default color of the edges of a mesh (default=#ba55d3 / MediumOrchid) 88 | ambient_intensity: Intensity of ambient light (default=1.00) 89 | direct_intensity: Intensity of direct light (default=1.10) 90 | metalness: Metalness property of the default material (default=0.30) 91 | roughness: Roughness property of the default material (default=0.65) 92 | 93 | 94 | render_edges: Render edges (default=True) 95 | render_normals: Render normals (default=False) 96 | render_mates: Render mates for MAssemblies (default=False) 97 | render_joints: Render build123d joints (default=False) 98 | show_parent: Render parent of faces, edges or vertices as wireframe (default=False) 99 | show_sketch_local: In build123d show local sketch in addition to relocate sketch (default=True) 100 | helper_scale: Scale of rendered helpers (locations, axis, mates for MAssemblies) (default=1) 101 | 102 | - Debug 103 | debug: Show debug statements to the VS Code browser console (default=False) 104 | timeit: Show timing information from level 0-3 (default=False) 105 | ``` 106 | 107 | ### Typically useful parameters 108 | 109 | - Reset the object stack: 110 | 111 | `reset_show()` 112 | 113 | - Restart a new objects stack 114 | 115 | `show_object(obj, clear=True)` 116 | 117 | For more detail, see [show](show.md) 118 | 119 | ### Example 120 | 121 | ```python 122 | import cadquery as cq 123 | from ocp_vscode import show_object, reset_show, set_defaults 124 | 125 | reset_show() # use for reapeated shift-enter execution to clean object buffer 126 | 127 | set_defaults(axes=True, transparent=False, collapse=1, grid=(True, True, True)) 128 | 129 | box = cq.Workplane().box(1, 2, 1).edges().chamfer(0.4) 130 | show_object(box, name="box", options={"alpha": 0.5}) 131 | 132 | sphere = cq.Workplane().sphere(0.6) 133 | 134 | show_object( 135 | sphere, 136 | # show_object args 137 | "sphere", 138 | {"color": (10, 100, 110)}, 139 | # three-cad-viewer args 140 | collapse="1", 141 | ortho=False 142 | ) 143 | ``` 144 | -------------------------------------------------------------------------------- /docs/snippets.md: -------------------------------------------------------------------------------- 1 | ## (Experimental) Install build123d snippets 2 | 3 | _OCP CAD Viewer for VS Code_ allows to install a _code-snippets_ file for build123d: 4 | 5 | - Use `Ctrl-Shift-P / Cmd-Shift-P` and select _OCP CAD Viewer: Install CAD snippets into /.vscode/_ 6 | - After typing `bd_` a list of snippets appears that guide the user through creation of some basic build123d patterns 7 | 8 | ![Use snippets](../screenshots/build123d_snippet.gif) -------------------------------------------------------------------------------- /examples/__convert_notebooks.py: -------------------------------------------------------------------------------- 1 | # %% 2 | from pathlib import Path 3 | import orjson 4 | 5 | path = Path("../jupyter-cadquery/examples") 6 | notebooks = [ 7 | path / "6-show_object.ipynb", 8 | path / "7-show.ipynb", 9 | path / "8-show_all.ipynb", 10 | path / "9-set_viewer_config.ipynb", 11 | ] 12 | 13 | target = Path("examples") 14 | 15 | # %% 16 | 17 | 18 | def convert(notebook): 19 | print(notebook) 20 | with open(notebook, "r") as fd: 21 | json_doc = orjson.loads(fd.read()) 22 | 23 | with open(target / f"{notebook.stem}.py", "w") as fd: 24 | fd.write("\n\n# %%\n\n") 25 | fd.write("from ocp_vscode import *\n") 26 | for cell in json_doc["cells"]: 27 | for line in cell["source"]: 28 | if "cv." in line: 29 | fd.write("# ") 30 | 31 | if ( 32 | "jupyter_cadquery" not in line 33 | and "open_viewer" not in line 34 | and not "close_viewer" in line 35 | ): 36 | fd.write(line) 37 | 38 | fd.write("\n\n# %%\n\n") 39 | 40 | 41 | for notebook in notebooks: 42 | convert(notebook) 43 | 44 | # %% 45 | -------------------------------------------------------------------------------- /examples/box.py: -------------------------------------------------------------------------------- 1 | # %% 2 | import cadquery as cq 3 | from ocp_vscode import * 4 | 5 | set_defaults( 6 | axes=True, 7 | transparent=False, 8 | collapse=Collapse.LEAVES, 9 | reset_camera=Camera.KEEP, 10 | grid=(True, True, True), 11 | ) 12 | # %% 13 | box = cq.Workplane().box(1, 2, 1).edges().chamfer(0.4) 14 | 15 | reset_show() 16 | show_object(box, name="Box", options={"alpha": 0.5}) 17 | 18 | # %% 19 | sphere = cq.Workplane().sphere(0.6) 20 | 21 | show_object( 22 | sphere, 23 | # show_object args 24 | "sphere", 25 | {"color": (10, 100, 110)}, 26 | # three-cad-viewer args 27 | ortho=False, 28 | ) 29 | 30 | # %% 31 | show_object( 32 | box, 33 | # Clear stack of objects 34 | clear=True, 35 | center_grid=True, 36 | grid=(True, False, False), 37 | ) 38 | 39 | # %% 40 | 41 | set_defaults( 42 | axes=False, 43 | transparent=False, 44 | collapse=Collapse.ROOT, 45 | reset_camera=Camera.RESET, 46 | grid=(False, False, False), 47 | ) 48 | -------------------------------------------------------------------------------- /examples/colormaps.py: -------------------------------------------------------------------------------- 1 | # %% 2 | 3 | import copy 4 | from build123d import * 5 | from ocp_vscode import * 6 | from ocp_vscode.colors import ListedColorMap 7 | 8 | 9 | def reference(obj, loc): 10 | return copy.copy(obj).move(loc) 11 | 12 | 13 | sphere = Sphere(1) 14 | spheres = [reference(sphere, loc) for loc in GridLocations(1, 2, 1, 20)] 15 | 16 | 17 | # %% 18 | show(*spheres, colors=ColorMap.tab20(alpha=0.8)) 19 | # %% 20 | show(*spheres, colors=ColorMap.tab20(reverse=True)) 21 | # %% 22 | show(*spheres, colors=ColorMap.segmented(20, "mpl:Greens", alpha=0.8)) 23 | # %% 24 | show(*spheres, colors=ColorMap.segmented(10, "mpl:Greens", alpha=0.8)) 25 | # %% 26 | show(*spheres, colors=ColorMap.golden_ratio("mpl:Greens", alpha=0.8)) 27 | # %% 28 | show(*spheres, colors=ColorMap.golden_ratio("mpl:Greens", reverse=True)) 29 | # %% 30 | show(*spheres, colors=ColorMap.segmented(10, "mpl:summer", reverse=True)) 31 | # %% 32 | show(*spheres, colors=ColorMap.seeded(42, "mpl:summer")) 33 | # %% 34 | show(*spheres, colors=ColorMap.seeded(4242, "mpl:summer")) 35 | # %% 36 | show(*spheres, colors=ColorMap.segmented(20, "hsv", reverse=False)) 37 | # %% 38 | show(*spheres, colors=ColorMap.segmented(10, "hsv", reverse=False)) 39 | # %% 40 | show(*spheres, colors=ColorMap.golden_ratio("hsv", alpha=0.8)) 41 | # %% 42 | show(*spheres, colors=ColorMap.seeded(42, "hsv")) 43 | # %% 44 | show( 45 | *spheres, 46 | colors=ColorMap.seeded(59798267586177, "rgb", lower=10, upper=100, brightness=2.2), 47 | ) 48 | # %% 49 | 50 | set_colormap(ColorMap.golden_ratio("mpl:Blues", alpha=0.8)) 51 | show(*spheres) 52 | # %% 53 | show(*spheres[:10]) 54 | # %% 55 | set_colormap(ColorMap.tab20(alpha=0.8)) 56 | show(*spheres) 57 | # %% 58 | set_colormap(ColorMap.seeded(42, "hsv", alpha=0.8)) 59 | show(*spheres) 60 | # %% 61 | set_colormap(ColorMap.segmented(20, "hsv")) 62 | show(*spheres) 63 | # %% 64 | reset_show() 65 | 66 | show_object(spheres[0]) 67 | show_object(spheres[1]) 68 | show_object(spheres[2]) 69 | show_object(spheres[3], options={"color": "black", "alpha": 1.0}) 70 | show_object(spheres[4]) 71 | show_object(spheres[5]) 72 | show_object(spheres[6]) 73 | 74 | # %% 75 | 76 | set_colormap(ListedColorMap(["red", "green", "blue"])) 77 | show(*spheres, colors=[None, "yellow"]) 78 | # %% 79 | 80 | boxes = [loc * Box(1, 1, 1) for loc in [Pos(2 * i, 0, 0) for i in range(20)]] 81 | 82 | colors = ColorMap.listed(20, "mpl:turbo", reverse=False) 83 | show(*boxes, colors=colors) 84 | 85 | # %% 86 | 87 | colors = ColorMap.listed(colors=["red", "green", "blue"]) 88 | show(*boxes, colors=colors) 89 | 90 | # %% 91 | 92 | sphere = Sphere(1) 93 | show(*spheres, colors=ColorMap.segmented(20, "mpl:hot")) 94 | 95 | # %% 96 | -------------------------------------------------------------------------------- /examples/examples.py: -------------------------------------------------------------------------------- 1 | # %% 2 | from build123d import * 3 | from ocp_vscode import * 4 | 5 | import cadquery as cq 6 | 7 | from ocp_tessellate.utils import Color 8 | 9 | 10 | # %% Example "box1" 11 | 12 | b = cq.Workplane().box(1, 1, 1) 13 | show(b) 14 | 15 | 16 | # %% Example "Image Face" 17 | 18 | f = ImageFace("examples/object-160x160mm.png", 600 / 912, name="imageplane") 19 | show(f) 20 | 21 | 22 | # %% Example "orientation box" 23 | 24 | b = Box(50, 100, 150) 25 | f = b.faces().sort_by(Axis.Y)[0] 26 | b += extrude(Plane(f) * Rot(0, 0, 0) * Text("ZX / Front", 12), 2) 27 | f = b.faces().sort_by(Axis.Y)[-1] 28 | b += extrude(Plane(f) * Rot(0, 0, 0) * Text("XZ / Back", 12), 2) 29 | 30 | f = b.faces().sort_by(Axis.X)[0] 31 | b += extrude(Plane(f) * Rot(0, 0, -90) * Text("ZY / Left", 12), 2) 32 | f = b.faces().sort_by(Axis.X)[-1] 33 | b += extrude(Plane(f) * Rot(0, 0, -90) * Text("YZ / Right", 12), 2) 34 | 35 | f = b.faces().sort_by(Axis.Z)[0] 36 | b += extrude(Plane(f) * Rot(0, 0, 90) * Text("XY / Bottom", 12), 2) 37 | f = b.faces().sort_by(Axis.Z)[-1] 38 | b += extrude(Plane(f) * Rot(0, 0, 90) * Text("YX / Top", 12), 2) 39 | 40 | show(b) 41 | 42 | 43 | # %% Example "dir box" 44 | 45 | fs = 6 46 | ex = -0.9 47 | b = Box(10, 10, 10) 48 | b = fillet(b.edges().group_by()[-1], 1) 49 | b = chamfer(b.edges().group_by()[0], 1) 50 | 51 | f = b.faces().sort_by(Axis.Y)[0] 52 | b -= extrude(Plane(f) * Rot(0, 0, -90) * Text("-Y", fs), ex) 53 | f = b.faces().sort_by(Axis.Y)[-1] 54 | b -= extrude(Plane(f) * Rot(0, 0, -90) * Text("+Y", fs), ex) 55 | 56 | f = b.faces().sort_by(Axis.X)[0] 57 | b -= extrude(Plane(f) * Rot(0, 0, -90) * Text("-X", fs), ex) 58 | f = b.faces().sort_by(Axis.X)[-1] 59 | b -= extrude(Plane(f) * Rot(0, 0, -90) * Text("+X", fs), ex) 60 | 61 | f = b.faces().sort_by(Axis.Z)[0] 62 | b -= extrude(Plane(f) * Rot(0, 0, 90) * Text("-Z", fs), ex) 63 | f = b.faces().sort_by(Axis.Z)[-1] 64 | b -= extrude(Plane(f) * Rot(0, 0, 90) * Text("+Z", fs), ex) 65 | 66 | show(b) 67 | 68 | 69 | # %% Example Boxes 70 | 71 | box1 = cq.Workplane("XY").box(10, 20, 30).edges(">X or X or Z") 82 | .fillet(3) 83 | ) 84 | box3.name = "box3" 85 | 86 | box4 = box3.mirror("XY").translate((0, -5, 0)) 87 | box4.name = "box4" 88 | 89 | box1 = box1.cut(box2).cut(box3).cut(box4) 90 | 91 | a1 = ( 92 | cq.Assembly(name="ensemble") 93 | .add(box1, name="red box", color=Color("#d7191c", 0.5)) 94 | .add(box3, name="green box", color=Color("#abdda4")) 95 | .add(box4, name="blue box", color=Color((43, 131, 186))) 96 | ) 97 | 98 | show(a1) 99 | 100 | 101 | # %% Example assembly 102 | 103 | s1 = Solid.make_box(1, 1, 1).move(Location((3, 3, 3))) 104 | s1.label, s1.color = "box", "red" 105 | 106 | s2 = Solid.make_cone(2, 1, 2).move(Location((-3, 3, 3))) 107 | s2.label, s2.color = "cone", "green" 108 | 109 | s3 = Solid.make_cylinder(1, 2).move(Location((-3, -3, 3))) 110 | s3.label, s3.color = "cylinder", "blue" 111 | 112 | s4 = Solid.make_sphere(2).move(Location((3, 3, -3))) 113 | s4.label = "sphere" 114 | 115 | s5 = Solid.make_torus(3, 1).move(Location((-3, 3, -3))) 116 | s5.label, s5.color = "torus", "cyan" 117 | 118 | c2 = Compound(label="c2", children=[s2, s3]) 119 | c3 = Compound(label="c3", children=[s4, s5]) 120 | c1 = Compound(label="c1", children=[s1, c2, c3]) 121 | 122 | show(c1) 123 | 124 | 125 | # %% Example Profile 4040 126 | profile4040 = import_step("examples/profile4040.step") 127 | profile4040.color = "silver" 128 | profile4040.label = "profile 40x40" 129 | show(profile4040) 130 | 131 | 132 | # %% Example Torus Knot 133 | 134 | from math import sin, cos, pi 135 | 136 | 137 | def points(t0, t1, samples): 138 | sa = 30 139 | return [ 140 | Vector( 141 | sa * (sin(t / samples) + 2 * sin(2 * t / samples)), 142 | sa * (cos(t / samples) - 2 * cos(2 * t / samples)), 143 | sa * (-sin(3 * t / samples)), 144 | ) 145 | for t in range(int(t0), int(t1 * samples)) 146 | ] 147 | 148 | 149 | zz = points(0, 2 * pi, 200) 150 | 151 | with BuildPart() as p: 152 | with BuildLine() as l: 153 | m1 = Spline(zz, periodic=False) 154 | m2 = Line(m1 @ 1, m1 @ 0) # prevent broken STEP 155 | pln = Plane(m1 @ 0, z_dir=m1 % 0) 156 | with BuildSketch(pln) as s: 157 | Circle(18) 158 | sweep(is_frenet=True) 159 | p.color = "#852e00" 160 | show(p) 161 | 162 | 163 | # %% Example faces 164 | 165 | b = box1.faces("not(|Z or |X or |Y)") 166 | b.name = "faces" 167 | box1.name = "box1" 168 | show(box1, b, names=["box", "faces"]) 169 | 170 | 171 | # %% Example edges 172 | 173 | b = box1.edges("not(|Z or |X or |Y)") 174 | b.name = "edges" 175 | box1.name = "box1" 176 | show(box1, b, names=["box", "edges"]) 177 | 178 | 179 | # %% Example faces 180 | 181 | b = box1.vertices("not(|Z or |X or |Y)") 182 | b.name = "vertices" 183 | box1.name = "box1" 184 | show(box1, b, names=["box", "vertices"]) 185 | 186 | 187 | # %% Example drops 188 | 189 | b = Box(1, 2, 3) - Plane.YZ * Cylinder(0.5, 1) 190 | b = fillet(b.edges().filter_by(Axis.X), 0.3) 191 | b = chamfer(b.edges().filter_by(Axis.Y), 0.1) 192 | b.label = "drops" 193 | show(b) 194 | 195 | 196 | # %% Example Single faces 197 | b = Box(1, 2, 3) 198 | b = fillet(b.edges(), 0.2).faces() 199 | show(b) 200 | 201 | 202 | # %% Example Single edges 203 | b = Box(1, 2, 3) 204 | b = fillet(b.edges(), 0.2).edges() 205 | show(b) 206 | 207 | 208 | # %% Example Single vertices 209 | b = Box(1, 2, 3) 210 | b = fillet(b.edges(), 0.2).vertices() 211 | show(b) 212 | -------------------------------------------------------------------------------- /examples/latest.py: -------------------------------------------------------------------------------- 1 | # %% 2 | from build123d import * 3 | from ocp_vscode import * 4 | 5 | set_defaults(reset_camera=Camera.KEEP, clip_planes=True, clip_object_colors=True) 6 | 7 | # %% 8 | 9 | show(Sphere(0.5)) 10 | 11 | # %% 12 | 13 | p2 = Box(1, 1, 1) 14 | 15 | show( 16 | p2, 17 | debug=True, 18 | clip_intersection=False, 19 | clip_planes=True, 20 | clip_object_colors=False, 21 | clip_slider_0=0.1, 22 | clip_slider_1=0.15, 23 | clip_slider_2=0.2, 24 | clip_normal_1=(0.12, -0.36, -0.92), 25 | clip_normal_2=(-0.58, 0.58, -0.58), 26 | ) 27 | set_viewer_config(tab="clip") 28 | 29 | # %% 30 | 31 | show(Sphere(0.5)) 32 | set_viewer_config(tab="clip") 33 | 34 | # %% 35 | 36 | show(Sphere(0.5)) 37 | set_viewer_config(tab="clip", clip_object_colors=True) 38 | 39 | # %% 40 | show(Sphere(0.5), reset_camera=True) 41 | set_viewer_config(tab="clip") 42 | 43 | # %% 44 | 45 | show( 46 | Box(1, 2, 3, align=(Align.MIN, Align.MIN, Align.MIN)), 47 | center_grid=True, 48 | grid=(True, False, False), 49 | ) 50 | # %% 51 | set_defaults(show_parent=True, reset_camera=Camera.RESET) 52 | show(Box(1, 2, 3).faces().edges().vertices()) 53 | # %% 54 | show(Box(1, 2, 3).faces().edges()) 55 | # %% 56 | show(Box(1, 2, 3).faces()) 57 | # %% 58 | with BuildPart() as p: 59 | Box(0.1, 0.1, 2) 60 | 61 | a = { 62 | "a": Vector(1, 2, 3), 63 | "b": [ 64 | Pos(2, 2, 2) * Cylinder(1, 1), 65 | (1, 2, 3), 66 | p, 67 | p.part, 68 | {"c": Vector(5, 2, 3), "d": Pos(-3, 0, 0) * Box(1, 2, 3), "e": 123}, 69 | ], 70 | } 71 | x = 0 72 | b = [ 73 | Pos(2, 4, 2) * Sphere(1), 74 | "wert", 75 | p, 76 | p.part, 77 | {"x": Pos(-5, -5, 0) * Box(2, 1, 0.5), "y": 123}, 78 | ] 79 | show_all() 80 | # %% 81 | -------------------------------------------------------------------------------- /examples/logo.py: -------------------------------------------------------------------------------- 1 | # %% 2 | from build123d import * 3 | from ocp_vscode import * 4 | from ocp_vscode.show import _convert 5 | import json 6 | import orjson 7 | from ocp_vscode.comms import default 8 | 9 | # %% 10 | a, b, thickness, distance, fontsize, depth = 13.6, 8.0, 1.0, 0.3, 20, 2 11 | 12 | pts = ((-a, 0), (0, b), (a, 0)) 13 | 14 | o = Text("O", fontsize, "Futura") 15 | cp = Text("CP", fontsize, "Futura") 16 | 17 | l1 = ThreePointArc(*pts) 18 | l1 += ThreePointArc(*(l1 @ 1, (0, -b), l1 @ 0)) 19 | 20 | l2 = offset([l1], thickness) 21 | l3 = offset([l1], thickness + distance) 22 | l4 = offset([l1], -distance) 23 | 24 | eye_face = make_face(l2) - make_face(l1) 25 | eye = extrude(eye_face, -2) 26 | 27 | eye_mask_face = make_face(l3) - make_face(l4) 28 | eye_mask = extrude(eye_mask_face, -2) 29 | 30 | logo_o = extrude(o, -depth) 31 | logo_cp = Pos(22.5, 0, 0) * extrude(cp, -depth) 32 | logo = logo_o + (logo_cp - eye_mask) 33 | 34 | center_wire = Wire(logo_o.faces().sort_by(Axis.Z).first.edges()[16:]) 35 | center = extrude(make_face(center_wire), 2) 36 | 37 | eye = eye + center 38 | 39 | logo = Plane.XZ * Pos(0, -20, 0) * logo 40 | eye = Plane.XZ * Pos(0, -20, 0) * eye 41 | # %% 42 | show( 43 | logo, 44 | eye, 45 | colors=[(85, 160, 227), (51, 51, 51)], 46 | names=["OCP", "Eye"], 47 | grid=(True, False, False), 48 | ortho=False, 49 | axes0=True, 50 | zoom=0.8, 51 | position=[124.5, -114.2, 11.1], 52 | quaternion=[0.5754, 0.2463, 0.2856, 0.7255], 53 | target=[10.4019, 0.4274, -24.7534], 54 | ) 55 | # %% 56 | c = _convert( 57 | logo, 58 | eye, 59 | colors=[(85, 160, 227), "#333"], 60 | names=["OCP", "Eye"], 61 | grid=(True, False, False), 62 | ortho=False, 63 | axes0=True, 64 | zoom=0.8, 65 | position=[124.5, -114.2, 11.1], 66 | quaternion=[0.5754, 0.2463, 0.2856, 0.7255], 67 | target=[10.4019, 0.4274, -24.7534], 68 | ) 69 | 70 | # %% 71 | with open("logo.txt", "w") as fp: 72 | fp.write(json.dumps(c[0], separators=(",", ":"))) 73 | 74 | # %% 75 | with open("mapping.txt", "w") as fp: 76 | fp.write(orjson.dumps(c[1], default=default).decode("utf-8")) 77 | -------------------------------------------------------------------------------- /examples/object-160x160mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/examples/object-160x160mm.png -------------------------------------------------------------------------------- /examples/set_viewer_config.py: -------------------------------------------------------------------------------- 1 | # %% 2 | 3 | from ocp_vscode import * 4 | import cadquery as cq 5 | 6 | 7 | def state(*attr): 8 | s = combined_config() 9 | s2 = status() 10 | for key in ["position", "quaternion", "target", "zoom"]: 11 | s[key] = s2[key] 12 | 13 | for a in attr: 14 | print(f"{a} = {s.get(a)}") 15 | 16 | 17 | def check(conf): 18 | def _eq(x, y): 19 | if isinstance(x, float) or isinstance(y, float): 20 | return abs(x - y) < 1e-2 21 | else: 22 | return x == y 23 | 24 | def equal(a, b): 25 | if isinstance(a, (list, tuple)): 26 | return len(a) == len(b) and all([_eq(a[i], b[i]) for i in range(len(a))]) 27 | else: 28 | return _eq(a, b) 29 | 30 | s = combined_config() 31 | s2 = status() 32 | for key in ["position", "quaternion", "target", "zoom"]: 33 | s[key] = s2[key] 34 | 35 | error = False 36 | for k, v in conf.items(): 37 | if not equal(s[k], v): 38 | print(f"ERROR: {k} is not {v}, but {s[k]}") 39 | error = True 40 | if not error: 41 | print("OK") 42 | 43 | 44 | set_defaults(reset_camera=Camera.KEEP) 45 | 46 | # %% 47 | 48 | box1 = cq.Workplane("XY").box(10, 20, 30).edges(">X or X or Z") 59 | .fillet(3) 60 | ) 61 | box3.name = "box3" 62 | 63 | box4 = box3.mirror("XY").translate((0, -5, 0)) 64 | box4.name = "box4" 65 | 66 | box1 = box1.cut(box2).cut(box3).cut(box4) 67 | a1 = ( 68 | cq.Assembly(name="ensemble") 69 | .add(box1, name="red box", color="#d7191c80") # transparent alpha = 0x80/0xFF 70 | .add(box3, name="green box", color="#abdda4") 71 | .add(box4, name="blue box", color=(43, 131, 186, 0.3)) # transparent, alpha = 0.3 72 | ) 73 | 74 | show(a1, debug=True) 75 | 76 | 77 | # %% 78 | 79 | state("axes", "axes0", "grid", "center_grid") 80 | 81 | # %% 82 | 83 | c = dict(axes=True, axes0=True, grid=(True, False, False), center_grid=True) 84 | 85 | set_viewer_config(**c) 86 | 87 | # %% 88 | 89 | state(*c.keys()) 90 | check(c) 91 | 92 | # %% 93 | 94 | reset_defaults() 95 | 96 | # %% 97 | 98 | check(dict(axes=False, axes0=True, grid=(False, False, False), center_grid=False)) 99 | 100 | # %% 101 | 102 | state("axes", "axes0", "grid", "center_grid") 103 | 104 | # %% 105 | 106 | c = dict(grid=(True, True, False)) 107 | set_viewer_config(**c) 108 | 109 | # %% 110 | 111 | state(*c.keys()) 112 | check(c) 113 | 114 | # %% 115 | 116 | state("transparent", "black_edges") 117 | 118 | # %% 119 | c = dict(transparent=True, black_edges=True) 120 | set_viewer_config(**c) 121 | 122 | # %% 123 | 124 | state(*c.keys()) 125 | check(c) 126 | 127 | # %% 128 | 129 | state("default_opacity") 130 | 131 | # %% 132 | 133 | c = dict(default_opacity=0.1) 134 | set_viewer_config(**c) 135 | 136 | # %% 137 | 138 | state(*c.keys()) 139 | check(c) 140 | 141 | # %% 142 | 143 | c = dict(default_opacity=0.9) 144 | set_viewer_config(**c) 145 | 146 | # %% 147 | 148 | state(*c.keys()) 149 | check(c) 150 | 151 | # %% 152 | 153 | state("ortho") 154 | 155 | # %% 156 | c = dict(ortho=False) 157 | set_viewer_config(**c) 158 | 159 | # %% 160 | 161 | state(*c.keys()) 162 | check(c) 163 | 164 | 165 | # %% 166 | 167 | c = dict(ortho=True) 168 | set_viewer_config(**c) 169 | 170 | # %% 171 | 172 | state(*c.keys()) 173 | check(c) 174 | 175 | # %% 176 | 177 | state("explode") 178 | 179 | # %% 180 | c = dict(explode=True) 181 | set_viewer_config(**c) 182 | 183 | # %% 184 | 185 | state(*c.keys()) 186 | check(c) 187 | 188 | # %% 189 | 190 | state("explode") 191 | 192 | # %% 193 | c = dict(explode=False) 194 | set_viewer_config(**c) 195 | 196 | # %% 197 | 198 | state(*c.keys()) 199 | check(c) 200 | 201 | # %% 202 | 203 | state("position", "quaternion", "target", "zoom") 204 | 205 | # %% 206 | 207 | c = dict( 208 | zoom=0.25, 209 | position=(-31.94953273066467, -113.96334389070142, 64.89655331113694), 210 | quaternion=( 211 | 0.4132988655589423, 212 | -0.0639778909519982, 213 | -0.13895512880168057, 214 | 0.8976538026303535, 215 | ), 216 | target=(-0.3630343271324728, -14.38330228182363, -24.503437918045126), 217 | ) 218 | set_viewer_config(**c) 219 | 220 | # %% 221 | state(*c.keys()) 222 | check(c) 223 | 224 | # %% 225 | 226 | reset_defaults() 227 | 228 | # %% 229 | 230 | state("default_edgecolor", "default_opacity", "transparent") 231 | 232 | # %% 233 | 234 | c = dict(default_edgecolor="#008000", default_opacity=0.1, transparent=True) 235 | set_viewer_config(**c) 236 | 237 | 238 | # %% 239 | 240 | state(*c.keys()) 241 | check(c) 242 | 243 | # %% 244 | 245 | reset_defaults() 246 | 247 | # %% 248 | 249 | state("default_edgecolor", "default_opacity", "transparent") 250 | 251 | # %% 252 | 253 | check(dict(default_edgecolor="#707070", default_opacity=0.5, transparent=False)) 254 | 255 | # %% 256 | 257 | set_viewer_config(tab="material") 258 | 259 | # %% 260 | 261 | state("ambient_intensity", "direct_intensity", "metalness", "roughness") 262 | 263 | # %% 264 | 265 | c = dict(ambient_intensity=1.85, direct_intensity=1.67, metalness=0.9, roughness=0.6) 266 | set_viewer_config(**c) 267 | 268 | # %% 269 | 270 | state(*c.keys()) 271 | check(c) 272 | 273 | # %% 274 | 275 | state("zoom_speed", "pan_speed", "rotate_speed") 276 | 277 | # %% 278 | 279 | c = dict(zoom_speed=0.1, pan_speed=0.1, rotate_speed=0.1) 280 | set_viewer_config(**c) 281 | 282 | # %% 283 | 284 | state(*c.keys()) 285 | check(c) 286 | 287 | # %% 288 | 289 | state("glass", "tools") 290 | 291 | # %% 292 | 293 | c = dict(glass=False, tools=True) 294 | 295 | set_viewer_config(**c) 296 | 297 | # %% 298 | 299 | state(*c.keys()) 300 | check(c) 301 | 302 | # %% 303 | 304 | c = dict(glass=True, tools=True) 305 | set_viewer_config(**c) 306 | 307 | # %% 308 | 309 | state(*c.keys()) 310 | check(c) 311 | 312 | # %% 313 | 314 | c = dict(tools=False) 315 | set_viewer_config(**c) 316 | 317 | # %% 318 | 319 | state(*c.keys()) 320 | check(c) 321 | 322 | # %% 323 | 324 | c = dict(tools=True, glass=True) 325 | 326 | set_viewer_config(**c) 327 | 328 | # %% 329 | 330 | state(*c.keys()) 331 | check(c) 332 | 333 | # %% 334 | 335 | set_viewer_config(tab="tree") 336 | 337 | # %% 338 | 339 | state("collapse") 340 | 341 | # %% 342 | 343 | c = dict(collapse=Collapse.ALL) 344 | 345 | set_viewer_config(**c) 346 | 347 | # %% 348 | 349 | state(*c.keys()) 350 | check(c) 351 | 352 | # %% 353 | 354 | c = dict(collapse=Collapse.NONE) 355 | set_viewer_config(**c) 356 | 357 | # %% 358 | 359 | state(*c.keys()) 360 | check(c) 361 | 362 | # %% 363 | 364 | show(a1, debug=True) 365 | 366 | # %% 367 | 368 | set_viewer_config(tab="clip") 369 | 370 | # %% 371 | 372 | state( 373 | "clip_slider_0", 374 | "clip_slider_1", 375 | "clip_slider_2", 376 | "clip_intersection", 377 | "clip_planes", 378 | "clip_object_colors", 379 | ) 380 | 381 | # %% 382 | 383 | c = dict( 384 | clip_slider_0=2, 385 | clip_slider_1=-7, 386 | clip_slider_2=7, 387 | clip_intersection=True, 388 | clip_planes=True, 389 | clip_object_colors=True, 390 | ) 391 | set_viewer_config(**c) 392 | 393 | # %% 394 | 395 | state(*c.keys()) 396 | check(c) 397 | 398 | 399 | # %% 400 | 401 | show(a1, debug=True) 402 | 403 | # %% 404 | 405 | set_viewer_config(tab="clip") 406 | 407 | # %% 408 | 409 | state( 410 | "clip_slider_0", 411 | "clip_slider_1", 412 | "clip_slider_2", 413 | "clip_normal_0", 414 | "clip_normal_1", 415 | "clip_normal_2", 416 | "clip_intersection", 417 | "clip_planes", 418 | "clip_object_colors", 419 | ), ("clip_normal_0", "clip_normal_1", "clip_normal_2") 420 | 421 | # %% 422 | 423 | c = dict( 424 | clip_slider_0=9.7, 425 | clip_slider_1=11.3, 426 | clip_slider_2=5.3, 427 | clip_normal_0=(-0.58, 0.58, -0.58), 428 | clip_normal_1=(0.16, -0.48, -0.87), 429 | clip_normal_2=(-0.56, 0.47, 0.68), 430 | clip_intersection=False, 431 | clip_planes=True, 432 | clip_object_colors=False, 433 | ) 434 | 435 | set_viewer_config(**c) 436 | 437 | # %% 438 | 439 | state(*c.keys()) 440 | check(c) 441 | -------------------------------------------------------------------------------- /examples/show_object_usage.py: -------------------------------------------------------------------------------- 1 | # %% 2 | 3 | from ocp_vscode import * 4 | import cadquery as cq 5 | from build123d import * 6 | 7 | 8 | set_defaults(show_parent=False) 9 | 10 | # %% 11 | 12 | box = cq.Workplane().box(1, 2, 3).edges().chamfer(0.1) 13 | 14 | # %% 15 | 16 | show_object( 17 | box.faces(">X"), name="green", options={"color": "green", "alpha": 0.2}, clear=True 18 | ) 19 | show_object(box.faces("Z"), name="blue", options={"color": "blue"}) 21 | show_object(box.faces(">>Z[-2]"), name="default", options={"alpha": 0.5}, axes=True) 22 | 23 | # %% 24 | 25 | show_object(box.wires(">X"), name="green", options={"color": "green"}, clear=True) 26 | show_object(box.wires("Z"), name="blue", options={"color": "blue"}, grid=(True, False, False) 29 | ) 30 | 31 | # %% 32 | 33 | show_object(box.edges("Y"), name="red", options={"color": "red"}) 35 | show_object(box.edges("Y"), name="red", options={"color": "red"}) 41 | show_object(box.vertices("OCP -------------------------------------------------------------------------------- /media/ocp-logo.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/media/ocp-logo.afdesign -------------------------------------------------------------------------------- /media/ocp-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/media/ocp-logo.png -------------------------------------------------------------------------------- /media/ocp-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /ocp-vscode-icons.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/ocp-vscode-icons.afdesign -------------------------------------------------------------------------------- /ocp_vscode/__init__.py: -------------------------------------------------------------------------------- 1 | """ocp_vscode - OCC viewer for VSCode""" 2 | 3 | # 4 | # Copyright 2023 Bernhard Walter 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | __version__ = "2.7.1" 19 | 20 | import os 21 | 22 | from .show import * 23 | from .config import * 24 | from .comms import * 25 | 26 | from .colors import * 27 | from .animation import Animation 28 | from ocp_tessellate.cad_objects import ImageFace 29 | 30 | try: 31 | from ocp_tessellate.tessellator import ( 32 | enable_native_tessellator, 33 | disable_native_tessellator, 34 | is_native_tessellator_enabled, 35 | ) 36 | 37 | if os.environ.get("NATIVE_TESSELLATOR") == "0": 38 | disable_native_tessellator() 39 | else: 40 | enable_native_tessellator() 41 | 42 | print( 43 | "Found and enabled native tessellator.\n" 44 | "To disable, call `disable_native_tessellator()`\n" 45 | "To enable, call `enable_native_tessellator()`\n" 46 | ) 47 | except: 48 | pass 49 | -------------------------------------------------------------------------------- /ocp_vscode/__main__.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from pathlib import Path 3 | 4 | import click 5 | import yaml 6 | from werkzeug.serving import get_interface_ip 7 | 8 | from ocp_vscode.backend import ViewerBackend 9 | from ocp_vscode.standalone import CONFIG_FILE, DEFAULTS, Viewer 10 | from ocp_vscode.state import resolve_path 11 | 12 | 13 | def represent_list(dumper, data): 14 | """ 15 | Represents a list in YAML format with flow style. 16 | 17 | Args: 18 | dumper: The YAML dumper instance. 19 | data: The list to be represented. 20 | 21 | Returns: 22 | The YAML representation of the list in flow style. 23 | """ 24 | return dumper.represent_sequence("tag:yaml.org,2002:seq", data, flow_style=True) 25 | 26 | 27 | yaml.add_representer(list, represent_list) 28 | 29 | 30 | def track_param(ctx, param, value): 31 | """ 32 | Tracks and stores the value of a parameter in the context object. 33 | 34 | This function checks if the context object (`ctx`) has an attribute `params_set`. 35 | If not, it initializes `params_set` as an empty dictionary. It then checks if the 36 | provided `value` is different from the parameter's default value or if the parameter's 37 | name is one of ["port", "host", "debug"]. If any of these conditions are met, it stores 38 | the parameter's name and value in the `params_set` dictionary. 39 | 40 | Args: 41 | ctx (object): The context object that will store the parameter values. 42 | param (object): The parameter object which contains the default value and name. 43 | value (any): The value of the parameter to be tracked. 44 | 45 | Returns: 46 | any: The value of the parameter. 47 | """ 48 | if not hasattr(ctx, "params_set"): 49 | ctx.params_set = {} 50 | if value != param.default or param.name in ["port", "host", "debug"]: 51 | ctx.params_set[param.name] = value 52 | return value 53 | 54 | 55 | @click.command() 56 | @click.option( 57 | "--create_configfile", 58 | is_flag=True, 59 | help="Create the configlie .ocpvscode_standalone in the home directory", 60 | callback=track_param, 61 | ) 62 | @click.option( 63 | "--backend", 64 | is_flag=True, 65 | help="Run measurement backend", 66 | callback=track_param, 67 | ) 68 | @click.option( 69 | "--host", 70 | default="127.0.0.1", 71 | help="The host to start OCP CAD with", 72 | callback=track_param, 73 | ) 74 | @click.option( 75 | "--port", 76 | default=3939, 77 | help="The port to start OCP CAD with", 78 | callback=track_param, 79 | ) 80 | @click.option( 81 | "--debug", 82 | is_flag=True, 83 | help="Show debugging information", 84 | callback=track_param, 85 | ) 86 | @click.option( 87 | "--timeit", 88 | is_flag=True, 89 | help="Show timing information", 90 | callback=track_param, 91 | ) 92 | @click.option( 93 | "--tree_width", 94 | help="OCP CAD Viewer navigation tree width (default: 240)", 95 | callback=track_param, 96 | ) 97 | @click.option( 98 | "--no_glass", 99 | is_flag=True, 100 | help="Do not use glass mode with transparent navigation tree", 101 | callback=track_param, 102 | ) 103 | @click.option( 104 | "--theme", 105 | default="light", 106 | help="Use theme 'light' or 'dark' (default: 'light')", 107 | callback=track_param, 108 | ) 109 | @click.option( 110 | "--no_tools", 111 | is_flag=True, 112 | help="Do not show toolbar", 113 | callback=track_param, 114 | ) 115 | @click.option( 116 | "--tree_width", 117 | default=240, 118 | help="Width of the CAD navigation tree (default: 240)", 119 | callback=track_param, 120 | ) 121 | @click.option( 122 | "--control", 123 | default="trackball", 124 | help="Use control mode 'orbit' or 'trackball'", 125 | callback=track_param, 126 | ) 127 | @click.option( 128 | "--reset_camera", 129 | default="reset", 130 | help="Set camera behavior to 'reset', 'keep' or 'center'", 131 | callback=track_param, 132 | ) 133 | @click.option( 134 | "--up", 135 | default="Z", 136 | help="Provides up direction, 'Z', 'Y' or 'L' (legacy) (default: Z)", 137 | callback=track_param, 138 | ) 139 | @click.option( 140 | "--rotate_speed", 141 | default=1, 142 | help="Rotation speed (default: 1)", 143 | callback=track_param, 144 | ) 145 | @click.option( 146 | "--zoom_speed", 147 | default=1, 148 | help="Zoom speed (default: 1)", 149 | callback=track_param, 150 | ) 151 | @click.option( 152 | "--pan_speed", 153 | default=1, 154 | help="Pan speed (default: 1)", 155 | callback=track_param, 156 | ) 157 | @click.option( 158 | "--axes", 159 | is_flag=True, 160 | help="Show axes", 161 | callback=track_param, 162 | ) 163 | @click.option( 164 | "--axes0", 165 | is_flag=True, 166 | help="Show axes at the origin (0, 0, 0)", 167 | callback=track_param, 168 | ) 169 | @click.option( 170 | "--black_edges", 171 | is_flag=True, 172 | help="Show edges in black", 173 | callback=track_param, 174 | ) 175 | @click.option( 176 | "--grid_xy", 177 | is_flag=True, 178 | help="Show grid on XY plane", 179 | callback=track_param, 180 | ) 181 | @click.option( 182 | "--grid_yz", 183 | is_flag=True, 184 | help="Show grid on YZ plane", 185 | callback=track_param, 186 | ) 187 | @click.option( 188 | "--grid_xz", 189 | is_flag=True, 190 | help="Show grid on XZ plane", 191 | callback=track_param, 192 | ) 193 | @click.option( 194 | "--center_grid", 195 | is_flag=True, 196 | help="Show grid planes crossing at center of object or global origin(default: False)", 197 | callback=track_param, 198 | ) 199 | @click.option( 200 | "--collapse", 201 | default=1, 202 | help="leaves: collapse all leaf nodes, all: collapse all nodes, none: expand all nodes, root: expand root only (default: leaves)", 203 | callback=track_param, 204 | ) 205 | @click.option( 206 | "--perspective", 207 | is_flag=True, 208 | help="Use perspective camera", 209 | callback=track_param, 210 | ) 211 | @click.option( 212 | "--ticks", 213 | default=10, 214 | help="Default number of ticks (default: 10)", 215 | callback=track_param, 216 | ) 217 | @click.option( 218 | "--transparent", 219 | is_flag=True, 220 | help="Show objects transparent", 221 | callback=track_param, 222 | ) 223 | @click.option( 224 | "--default_opacity", 225 | default=0.5, 226 | help="Default opacity for transparent objects (default: 0.5)", 227 | callback=track_param, 228 | ) 229 | @click.option( 230 | "--explode", 231 | is_flag=True, 232 | help="Turn explode mode on", 233 | callback=track_param, 234 | ) 235 | @click.option( 236 | "--angular_tolerance", 237 | default=0.2, 238 | help="Angular tolerance for tessellation algorithm (default: 0.2)", 239 | callback=track_param, 240 | ) 241 | @click.option( 242 | "--deviation", 243 | default=0.1, 244 | help="Deviation of for tessellation algorithm (default: 0.1)", 245 | callback=track_param, 246 | ) 247 | @click.option( 248 | "--default_color", 249 | default="#e8b024", 250 | help="Default shape color, CSS3 color names are allowed (default: #e8b024)", 251 | callback=track_param, 252 | ) 253 | @click.option( 254 | "--default_edgecolor", 255 | default="#707070", 256 | help="Default color of the edges of shapes, CSS3 color names are allowed (default: #707070)", 257 | callback=track_param, 258 | ) 259 | @click.option( 260 | "--default_thickedgecolor", 261 | default="MediumOrchid", 262 | help="Default color of lines, CSS3 color names are allowed (default: MediumOrchid)", 263 | callback=track_param, 264 | ) 265 | @click.option( 266 | "--default_facecolor", 267 | default="Violet", 268 | help="Default color of faces, CSS3 color names are allowed (default: Violet)", 269 | callback=track_param, 270 | ) 271 | @click.option( 272 | "--default_vertexcolor", 273 | default="MediumOrchid", 274 | help="Default color of vertices, CSS3 color names are allowed (default: MediumOrchid)", 275 | callback=track_param, 276 | ) 277 | @click.option( 278 | "--ambient_intensity", 279 | default=1, 280 | help="Intensity of ambient light (default: 1.00)", 281 | callback=track_param, 282 | ) 283 | @click.option( 284 | "--direct_intensity", 285 | default=1.1, 286 | help="Intensity of direct light (default: 1.10)", 287 | callback=track_param, 288 | ) 289 | @click.option( 290 | "--metalness", 291 | default=0.3, 292 | help="Metalness property of material (default: 0.30)", 293 | callback=track_param, 294 | ) 295 | @click.option( 296 | "--roughness", 297 | default=0.65, 298 | help="Roughness property of material (default: 0.65)", 299 | callback=track_param, 300 | ) 301 | @click.pass_context 302 | def main(ctx, **kwargs): 303 | """ 304 | Main function to either create a configuration file or start the viewer. 305 | Args: 306 | ctx: Context object containing parameters. 307 | **kwargs: Arbitrary keyword arguments for click. 308 | 309 | Returns: 310 | None 311 | """ 312 | 313 | if kwargs.get("create_configfile"): 314 | 315 | config_file = Path(resolve_path(CONFIG_FILE)) 316 | with open(config_file, "w", encoding="utf-8") as f: 317 | f.write(yaml.dump(DEFAULTS)) 318 | print(f"Created config file {config_file}") 319 | 320 | elif kwargs.get("backend"): 321 | 322 | port = kwargs["port"] 323 | 324 | backend = ViewerBackend(port) 325 | try: 326 | backend.start() 327 | except Exception as ex: # pylint: disable=broad-except 328 | print(ex) 329 | 330 | else: 331 | viewer = Viewer(ctx.params_set) 332 | 333 | port = kwargs["port"] 334 | host = kwargs["host"] 335 | 336 | if host == "0.0.0.0": 337 | print("\nThe viewer is running on all addresses:") 338 | print(f" - http://127.0.0.1:{port}/viewer") 339 | try: 340 | host = get_interface_ip(socket.AF_INET) 341 | print(f" - http://{host}:{port}/viewer\n") 342 | except: # pylint: disable=bare-except 343 | pass 344 | else: 345 | print(f"\nThe viewer is running on http://{host}:{port}/viewer\n") 346 | 347 | viewer.start() 348 | 349 | 350 | if __name__ == "__main__": 351 | main() 352 | -------------------------------------------------------------------------------- /ocp_vscode/animation.py: -------------------------------------------------------------------------------- 1 | """Animation class for the viewer""" 2 | 3 | # 4 | # Copyright 2023 Bernhard Walter 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import json 20 | 21 | from ocp_tessellate.utils import numpy_to_json 22 | from .comms import send_data 23 | 24 | 25 | def collect_paths(assembly, path=""): 26 | """Collect all paths in the assembly tree""" 27 | result = [] 28 | new_path = f"{path}/{assembly.label}" 29 | result.append(new_path) 30 | for child in assembly.children: 31 | result.extend(collect_paths(child, new_path)) 32 | return result 33 | 34 | 35 | class Animation: 36 | """Class to create animations for the viewer""" 37 | 38 | def __init__(self, assembly): 39 | self.tracks = [] 40 | self.is_cadquery = hasattr(assembly, "mates") and not hasattr( 41 | assembly, "fq_name" 42 | ) 43 | self.is_build123d = hasattr(assembly, "joints") 44 | if self.is_cadquery: 45 | self.paths = list(assembly.objects.keys()) 46 | else: 47 | self.paths = collect_paths(assembly) 48 | 49 | def add_track(self, path, action, times, values): 50 | # pylint: disable=line-too-long 51 | """ 52 | Adding a three.js animation track. 53 | 54 | Parameters 55 | ---------- 56 | path : string 57 | The path (or id) of the cad object for which this track is meant. 58 | Usually of the form `/top-level/level2/...` 59 | action : {"t", "tx", "ty", "tz", "q", "rx", "ry", "rz"} 60 | The action type: 61 | 62 | - "tx", "ty", "tz" for translations along the x, y or z-axis 63 | - "t" to add a position vector (3-dim array) to the current position of the CAD object 64 | - "rx", "ry", "rz" for rotations around x, y or z-axis 65 | - "q" to apply a quaternion to the location of the CAD object 66 | times : list of float or int 67 | An array of floats describing the points in time where CAD object (with id `path`) should be at the location 68 | defined by `action` and `values` 69 | values : list of float or int 70 | An array of same length as `times` defining the locations where the CAD objects should be according to the 71 | `action` provided. Formats: 72 | 73 | - "tx", "ty", "tz": float distance to move 74 | - "t": 3-dim tuples or lists defining the positions to move to 75 | - "rx", "ry", "rz": float angle in degrees 76 | - "q" quaternions of the form (x,y,z,w) the represent the rotation to be applied 77 | 78 | Examples 79 | -------- 80 | ``` 81 | AnimationTrack( 82 | '/bottom/left_middle/lower', # path 83 | 'rz', # action 84 | [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0], # times (seconds) 85 | [-15.0, -15.0, -15.0, 9.7, 20.0, 9.7, -15.0, -15.0, -15.0] # angles 86 | ) 87 | 88 | AnimationTrack( 89 | 'base/link_4_6', # path 90 | 't', # action 91 | [0.0, 1.0, 2.0, 3.0, 4.0], # times (seconds) 92 | [[0.0, 0.0, 0.0], [0.0, 1.9509, 3.9049], 93 | [0.0 , -3.2974, -16.7545], [0.0 , 0.05894 , -32.0217], 94 | [0.0 , -3.2212, -13.3424]] # 3-dim positions 95 | ) 96 | ``` 97 | 98 | See also 99 | -------- 100 | 101 | - [three.js NumberKeyframeTrack](https://threejs.org/docs/index.html?q=track#api/en/animation/tracks/NumberKeyframeTrack) 102 | - [three.js QuaternionKeyframeTrack](https://threejs.org/docs/index.html?q=track#api/en/animation/tracks/QuaternionKeyframeTrack) 103 | 104 | """ 105 | 106 | # if path[0] != "/": 107 | # path = f"/{path}" 108 | 109 | if len(times) != len(values): 110 | raise ValueError("Parameters 'times' and 'values' need to have same length") 111 | 112 | if self.is_cadquery: 113 | root, _, cq_path = path.strip("/").partition("/") 114 | 115 | if root not in self.paths or cq_path not in self.paths + [""]: 116 | raise ValueError(f"Path '{path}' does not exist in assembly") 117 | 118 | elif self.is_build123d: 119 | ... 120 | # if path not in self.paths: 121 | # raise ValueError(f"Path '{path}' does not exist in assembly") 122 | 123 | self.tracks.append((path, action, times, values)) 124 | 125 | def animate(self, speed): 126 | """Animate the tracks""" 127 | data = {"data": self.tracks, "type": "animation", "config": {"speed": speed}} 128 | send_data(json.loads(numpy_to_json(data))) 129 | -------------------------------------------------------------------------------- /ocp_vscode/comms.py: -------------------------------------------------------------------------------- 1 | """Communication with the viewer""" 2 | 3 | import base64 4 | import enum 5 | import json 6 | import os 7 | import socket 8 | import warnings 9 | 10 | from pathlib import Path 11 | 12 | from websockets.sync.client import connect 13 | 14 | import orjson 15 | from ocp_tessellate.utils import Timer 16 | from ocp_tessellate.ocp_utils import ( 17 | is_topods_shape, 18 | is_toploc_location, 19 | serialize, 20 | loc_to_tq, 21 | ) 22 | from .state import get_state, update_state, get_config_file 23 | 24 | 25 | # pylint: disable=unused-import 26 | try: 27 | from IPython import get_ipython 28 | import jupyter_console 29 | 30 | JCONSOLE = True 31 | except: # pylint: disable=bare-except 32 | JCONSOLE = False 33 | 34 | CMD_URL = "ws://127.0.0.1" 35 | CMD_PORT = 3939 36 | 37 | INIT_DONE = False 38 | 39 | 40 | warnings.simplefilter("once", UserWarning) 41 | 42 | 43 | def warn_once(message): 44 | def warning_on_one_line(message, category, filename, lineno, file=None, line=None): 45 | return "%s: %s\n" % (category.__name__, message) 46 | 47 | warnings.formatwarning = warning_on_one_line 48 | warnings.warn(message, UserWarning) 49 | 50 | 51 | # 52 | # Send data to the viewer 53 | # 54 | 55 | 56 | class MessageType(enum.IntEnum): 57 | """Message types""" 58 | 59 | DATA = 1 60 | COMMAND = 2 61 | UPDATES = 3 62 | LISTEN = 4 63 | BACKEND = 5 64 | BACKEND_RESPONSE = 6 65 | CONFIG = 7 66 | 67 | 68 | __all__ = [ 69 | "send_data", 70 | "send_command", 71 | "send_response", 72 | "set_port", 73 | "get_port", 74 | "listener", 75 | "is_pytest", 76 | ] 77 | 78 | 79 | def is_pytest(): 80 | return os.environ.get("OCP_VSCODE_PYTEST") == "1" 81 | 82 | 83 | def port_check(port): 84 | """Check whether the port is listening""" 85 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 86 | s.settimeout(1) 87 | result = s.connect_ex((get_host(), port)) == 0 88 | if result: 89 | s.close() 90 | return result 91 | 92 | 93 | def default(obj): 94 | """Default JSON serializer.""" 95 | if is_topods_shape(obj): 96 | return base64.b64encode(serialize(obj)).decode("utf-8") 97 | elif is_toploc_location(obj): 98 | return loc_to_tq(obj) 99 | elif isinstance(obj, enum.Enum): 100 | return obj.value 101 | else: 102 | raise TypeError(f"Object of type {type(obj)} is not JSON serializable") 103 | 104 | 105 | def get_port(): 106 | """Get the port""" 107 | if is_pytest(): 108 | return 3939 109 | 110 | if not INIT_DONE: 111 | find_and_set_port() 112 | set_connection_file() 113 | return CMD_PORT 114 | 115 | 116 | def get_host(): 117 | """Get the host""" 118 | return CMD_URL[5:] 119 | 120 | 121 | def set_port(port, host="127.0.0.1"): 122 | """Set the port""" 123 | global CMD_PORT, CMD_URL, INIT_DONE # pylint: disable=global-statement 124 | CMD_PORT = port 125 | CMD_URL = f"ws://{host}" 126 | INIT_DONE = True 127 | 128 | 129 | def _send(data, message_type, port=None, timeit=False): 130 | """Send data to the viewer""" 131 | global WS 132 | 133 | if port is None: 134 | if not INIT_DONE: 135 | find_and_set_port() 136 | set_connection_file() 137 | port = CMD_PORT 138 | try: 139 | with Timer(timeit, "", "json dumps", 1): 140 | j = orjson.dumps(data, default=default) # pylint: disable=no-member 141 | if message_type == MessageType.COMMAND: 142 | j = b"C:" + j 143 | elif message_type == MessageType.DATA: 144 | j = b"D:" + j 145 | elif message_type == MessageType.LISTEN: 146 | j = b"L:" + j 147 | elif message_type == MessageType.BACKEND: 148 | j = b"B:" + j 149 | elif message_type == MessageType.BACKEND_RESPONSE: 150 | j = b"R:" + j 151 | elif message_type == MessageType.CONFIG: 152 | j = b"S:" + j 153 | 154 | with Timer(timeit, "", f"websocket connect ({message_type.name})", 1): 155 | try: 156 | with connect(f"{CMD_URL}:{port}", close_timeout=0.05) as ws: 157 | ws.send(j) 158 | 159 | with Timer( 160 | timeit, "", f"websocket send {len(j)/1024/1024:.3f} MB", 1 161 | ): 162 | result = None 163 | if message_type == MessageType.COMMAND and not ( 164 | isinstance(data, dict) and data["type"] == "screenshot" 165 | ): 166 | try: 167 | result = json.loads(ws.recv()) 168 | except Exception as ex: # pylint: disable=broad-except 169 | print(ex) 170 | 171 | except Exception as ex: 172 | warn_once("The viewer doesn't seem to run: " + str(ex)) 173 | # set some dummy values to avoid errors 174 | return { 175 | "collapse": "none", 176 | "_splash": False, 177 | "default_facecolor": (1, 234, 56), 178 | "default_thickedgecolor": (123, 45, 6), 179 | "default_vertexcolor": (123, 45, 6), 180 | } 181 | 182 | return result 183 | 184 | except Exception as ex: # pylint: disable=broad-except 185 | print( 186 | f"Cannot connect to viewer on port {port}, is it running and the right port provided?" 187 | ) 188 | print(ex) 189 | return None 190 | 191 | 192 | def send_data(data, port=None, timeit=False): 193 | """Send data to the viewer""" 194 | return _send(data, MessageType.DATA, port, timeit) 195 | 196 | 197 | def send_config(config, port=None, title=None, timeit=False): 198 | """Send config to the viewer""" 199 | return _send(config, MessageType.CONFIG, port, timeit) 200 | 201 | 202 | def send_command(data, port=None, title=None, timeit=False): 203 | """Send command to the viewer""" 204 | result = _send(data, MessageType.COMMAND, port, timeit) 205 | if result.get("command") == "status": 206 | return result["text"] 207 | else: 208 | return result 209 | 210 | 211 | def send_backend(data, port=None, timeit=False): 212 | """Send data to the viewer""" 213 | return _send(data, MessageType.BACKEND, port, timeit) 214 | 215 | 216 | def send_response(data, port=None, timeit=False): 217 | """Send data to the viewer""" 218 | return _send(data, MessageType.BACKEND_RESPONSE, port, timeit) 219 | 220 | 221 | # 222 | # Receive data from the viewer 223 | # 224 | 225 | 226 | # async listerner for the websocket class 227 | # this will be called when the viewer sends data 228 | # the data is then passed to the callback function 229 | # 230 | def listener(callback): 231 | """Listen for data from the viewer""" 232 | 233 | def _listen(): 234 | last_config = {} 235 | with connect(f"{CMD_URL}:{CMD_PORT}", max_size=2**28) as websocket: 236 | websocket.send(b"L:register") 237 | while True: 238 | try: 239 | message = websocket.recv() 240 | if message is None: 241 | continue 242 | 243 | message = json.loads(message) 244 | if "model" in message.keys(): 245 | callback(message["model"], MessageType.DATA) 246 | 247 | if message.get("command") == "status": 248 | changes = message["text"] 249 | new_changes = {} 250 | for k, v in changes.items(): 251 | if k in last_config and last_config[k] == v: 252 | continue 253 | new_changes[k] = v 254 | last_config = changes 255 | callback(new_changes, MessageType.UPDATES) 256 | 257 | except Exception as ex: # pylint: disable=broad-except 258 | print(ex) 259 | break 260 | 261 | return _listen 262 | 263 | 264 | def find_and_set_port(): 265 | """Set the port and connection file""" 266 | 267 | def find_port(): 268 | port = None 269 | current_path = Path.cwd() 270 | states = get_state().items() 271 | for p, state in states: 272 | if not port_check(int(p)): 273 | print(f"Found stale configuration for port {p}, deleting it.") 274 | update_state(int(p), None, None) 275 | continue 276 | 277 | roots = state.get("roots", []) 278 | for root in roots: 279 | if current_path.is_relative_to(Path(root)): 280 | port = int(p) 281 | break 282 | if port is None: 283 | ports = [port for port, _ in states if port_check(int(port))] 284 | if len(ports) == 1: 285 | port = ports[0] 286 | elif len(ports) > 1: 287 | raise RuntimeError( 288 | f"\nMultiple ports found ({', '.join(ports)}) and the file is outside of any\n" 289 | "workspace folder of this VS Code instance:\n" 290 | "The right viewer cannot be auto detected, use set_port(port) in your code." 291 | ) 292 | else: 293 | print(f"Could not find port in config file {get_config_file()}") 294 | 295 | return port 296 | 297 | port = int(os.environ.get("OCP_PORT", "0")) 298 | 299 | if port > 0: 300 | print(f"Using predefined port {port} taken from environment variable OCP_PORT") 301 | else: 302 | port = find_port() 303 | if port is not None: 304 | print(f"Using port {port} taken from config file") 305 | elif port_check(3939): 306 | port = 3939 307 | print(f"Default port {port} is open, using it") 308 | 309 | set_port(port) 310 | 311 | 312 | def set_connection_file(): 313 | """Set the connection file for Jupyter in the state file .ocpvscode""" 314 | if JCONSOLE and hasattr(get_ipython(), "kernel"): 315 | kernel = get_ipython().kernel 316 | cf = kernel.config["IPKernelApp"]["connection_file"] 317 | with open(cf, "r", encoding="utf-8") as f: 318 | connection_info = json.load(f) 319 | 320 | if port_check(connection_info["iopub_port"]): 321 | print("Jupyter kernel running") 322 | update_state(CMD_PORT, "connection_file", cf) 323 | print(f"Jupyter connection file path written to {get_config_file()}") 324 | else: 325 | print("Jupyter kernel not responding") 326 | # elif not JCONSOLE: 327 | # print("Jupyter console not installed") 328 | # pass 329 | # else: 330 | # print("Jupyter kernel not running") 331 | -------------------------------------------------------------------------------- /ocp_vscode/standalone.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import orjson 3 | import shutil 4 | import socket 5 | import time 6 | import yaml 7 | from pathlib import Path 8 | from flask import Flask, render_template, request 9 | from flask_sock import Sock 10 | from simple_websocket import ConnectionClosed 11 | from ocp_vscode.comms import MessageType 12 | from ocp_vscode.backend import ViewerBackend 13 | from ocp_vscode.backend_logo import logo 14 | from ocp_vscode.state import resolve_path 15 | 16 | CONFIG_FILE = "~/.ocpvscode_standalone" 17 | 18 | DEFAULTS = { 19 | "debug": False, 20 | "no_glass": False, 21 | "no_tools": False, 22 | "tree_width": 240, 23 | "theme": "light", 24 | "control": "trackball", 25 | "modifier_keys": { 26 | "shift": "shiftKey", 27 | "ctrl": "ctrlKey", 28 | "meta": "metaKey", 29 | }, 30 | "new_tree_behavior": True, 31 | "pan_speed": 0.5, 32 | "rotate_speed": 1.0, 33 | "zoom_speed": 0.5, 34 | "axes": False, 35 | "axes0": False, 36 | "grid_xy": False, 37 | "grid_yz": False, 38 | "grid_xz": False, 39 | "perspective": False, 40 | "transparent": False, 41 | "black_edges": False, 42 | "collapse": "R", 43 | "reset_camera": "RESET", 44 | "up": "Z", 45 | "ticks": 10, 46 | "center_grid": False, 47 | "default_opacity": 0.5, 48 | "explode": False, 49 | "default_edgecolor": "#808080", 50 | "default_color": "#e8b024", 51 | "default_thickedgecolor": "MediumOrchid", 52 | "default_facecolor": "Violet", 53 | "default_vertexcolor": "MediumOrchid", 54 | "angular_tolerance": 0.2, 55 | "deviation": 0.1, 56 | "ambient_intensity": 1.0, 57 | "direct_intensity": 1.1, 58 | "metalness": 0.3, 59 | "roughness": 0.65, 60 | } 61 | 62 | SCRIPTS = """ 63 | 64 | 65 | 66 | """ 67 | 68 | JS = "./static/js/three-cad-viewer.esm.js" 69 | CSS = "./static/css/three-cad-viewer.css" 70 | 71 | STATIC = """ 72 | import { Comms } from "./static/js/comms.js"; 73 | import { logo } from "./static/js/logo.js"; 74 | """ 75 | 76 | 77 | def COMMS(host, port): 78 | return f""" 79 | const comms = new Comms("{host}", {port}); 80 | const vscode = {{postMessage: (msg) => {{ 81 | comms.sendStatus(msg); 82 | }} 83 | }}; 84 | const standaloneViewer = () => {{ 85 | 86 | const ocpLogo = JSON.parse(logo); 87 | decode(ocpLogo); 88 | 89 | viewer = showViewer(ocpLogo.data.shapes, ocpLogo.config); 90 | window.viewer = viewer; 91 | }} 92 | window.showViewer = standaloneViewer; 93 | """ 94 | 95 | 96 | INIT = """onload="showViewer()" """ 97 | 98 | 99 | def save_png_data_url(data_url, output_path): 100 | base64_data = data_url.split(",")[1] 101 | image_data = base64.b64decode(base64_data) 102 | suffix = "-temp" + hex(int(time.time() * 1e6))[2:] 103 | try: 104 | # first write to a temp name to avoid polling is successful before finished ... 105 | with open(output_path + suffix, "wb") as f: 106 | f.write(image_data) 107 | # ... and then rename to the actual filename 108 | shutil.move(output_path + suffix, output_path) 109 | 110 | print(f"Wrote png file to {output_path}") 111 | except Exception as ex: 112 | print("Cannot save png file:", str(ex)) 113 | 114 | 115 | class Viewer: 116 | def __init__(self, params): 117 | self.status = {} 118 | self.config = {} 119 | self.debug = params.get("debug", False) 120 | self.params = params 121 | 122 | self.configure(self.params) 123 | 124 | self.app = Flask(__name__) 125 | self.sock = Sock(self.app) 126 | 127 | self.backend = ViewerBackend(self.port) 128 | 129 | self.python_client = None 130 | self.javascript_client = None 131 | self.splash = True 132 | 133 | self.sock.route("/")(self.handle_message) 134 | self.app.add_url_rule("/viewer", "viewer", self.index) 135 | 136 | def debug_print(self, *msg): 137 | if self.debug: 138 | print("Debug:", *msg) 139 | 140 | def configure(self, params): 141 | # Start with defaults 142 | local_config = DEFAULTS.copy() 143 | 144 | # Then apply everything from the config file if it exists 145 | config_file = Path(resolve_path(CONFIG_FILE)) 146 | if config_file.exists(): 147 | with open(config_file, "r") as f: 148 | defaults = yaml.safe_load(f) 149 | for k, v in defaults.items(): 150 | local_config[k] = v 151 | 152 | local_config = dict(sorted(local_config.items())) 153 | 154 | # Get all params != their default value and apply it 155 | grid = [ 156 | local_config["grid_xy"], 157 | local_config["grid_yz"], 158 | local_config["grid_xz"], 159 | ] 160 | for k, v in params.items(): 161 | if k == "port": 162 | self.port = v 163 | elif k == "host": 164 | self.host = v 165 | elif k not in ["create_configfile"]: 166 | if v != local_config.get(k): 167 | if k == "grid_xy": 168 | grid[0] = True 169 | elif k == "grid_yz": 170 | grid[1] = True 171 | elif k == "grid_xz": 172 | grid[2] = True 173 | else: 174 | local_config[k] = v 175 | local_config["grid"] = grid 176 | local_config["reset_camera"] = local_config["reset_camera"].upper() 177 | 178 | local_config = dict(sorted(local_config.items())) 179 | 180 | for k, v in local_config.items(): 181 | if k in ["grid_xy", "grid_yz", "grid_xz"]: 182 | continue 183 | if k == "collapse": 184 | self.config["collapse"] = str(v) 185 | elif k == "no_glass": 186 | self.config["glass"] = not v 187 | elif k == "no_tools": 188 | self.config["tools"] = not v 189 | elif k == "perspective": 190 | self.config["ortho"] = not v 191 | 192 | else: 193 | self.config[k] = v 194 | 195 | self.debug_print("\nConfig:", self.config) 196 | 197 | def start(self): 198 | self.app.run(debug=self.debug, port=self.port, host=self.host) 199 | self.sock.init_app(self.app) 200 | self.backend.load_model(logo) 201 | 202 | def index(self): 203 | # The browser will connect with an ip/hostname that is reachable from remote. 204 | # Use this ip/hostname for the websocket connection 205 | address, port = request.host.split(":") 206 | return render_template( 207 | "viewer.html", 208 | standalone_scripts=SCRIPTS, 209 | standalone_imports=STATIC, 210 | standalone_comms=COMMS(address, port), 211 | standalone_init=INIT, 212 | styleSrc=CSS, 213 | scriptSrc=JS, 214 | treeWidth=self.config["tree_width"], 215 | **self.config, 216 | ) 217 | 218 | def not_registered(self): 219 | print( 220 | "\nNo browser registered. Please open the viewer in a browser or refresh the viewer page\n" 221 | ) 222 | 223 | def handle_message(self, ws): 224 | 225 | while True: 226 | data = ws.receive() 227 | if isinstance(data, bytes): 228 | data = data.decode("utf-8") 229 | 230 | message_type = data[0] 231 | data = data[2:] 232 | 233 | if message_type == "C": 234 | self.python_client = ws 235 | cmd = orjson.loads(data) 236 | if cmd == "status": 237 | self.debug_print("Received status command") 238 | self.python_client.send(orjson.dumps({"text": self.status})) 239 | 240 | elif cmd == "config": 241 | self.debug_print("Received config command") 242 | self.configure(self.params) 243 | self.config["_splash"] = self.splash 244 | self.python_client.send(orjson.dumps(self.config)) 245 | 246 | elif cmd.get("type") == "screenshot": 247 | self.debug_print("Received screenshot command") 248 | if self.javascript_client is None: 249 | self.not_registered() 250 | continue 251 | self.javascript_client.send(data) 252 | 253 | elif message_type == "D": 254 | self.python_client = ws 255 | self.debug_print("Received a new model") 256 | if self.javascript_client is None: 257 | self.not_registered() 258 | continue 259 | self.javascript_client.send(data) 260 | if self.splash: 261 | self.splash = False 262 | 263 | elif message_type == "U": 264 | self.javascript_client = ws 265 | message = orjson.loads(data) 266 | if message["command"] == "screenshot": 267 | filename = message["text"]["filename"] 268 | data_url = message["text"]["data"] 269 | self.debug_print("Received screenshot data for file", filename) 270 | save_png_data_url(data_url, filename) 271 | else: 272 | changes = message["text"] 273 | self.debug_print("Received incremental UI changes", changes) 274 | for key, value in changes.items(): 275 | self.status[key] = value 276 | self.backend.handle_event(changes, MessageType.UPDATES) 277 | 278 | elif message_type == "S": 279 | self.python_client = ws 280 | self.debug_print("Received a config") 281 | if self.javascript_client is None: 282 | self.not_registered() 283 | continue 284 | self.javascript_client.send(data) 285 | self.debug_print("Posted config to view") 286 | 287 | elif message_type == "L": 288 | self.javascript_client = ws 289 | print("\nBrowser as viewer client registered\n") 290 | 291 | elif message_type == "B": 292 | model = orjson.loads(data)["model"] 293 | self.backend.handle_event(model, MessageType.DATA) 294 | self.debug_print("Model data sent to the backend") 295 | 296 | elif message_type == "R": 297 | self.python_client = ws 298 | if self.javascript_client is None: 299 | self.not_registered() 300 | continue 301 | self.javascript_client.send(data) 302 | self.debug_print("Backend response received.", data) 303 | -------------------------------------------------------------------------------- /ocp_vscode/state.py: -------------------------------------------------------------------------------- 1 | """ 2 | state uses "poor man's locking": nodejs does not support fcntl (POSIX) or 3 | LockFileEx (Windows) locking. This module implements a simple locking 4 | mechanism for nodejs and Python leveraging "mkdir" to create locks. 5 | 6 | Ideas are taken from the node js library "proper-lockfile" 7 | (https://github.com/moxystudio/node-proper-lockfile) 8 | """ 9 | 10 | import json 11 | import os 12 | import time 13 | 14 | STALE_DURATION_MS = 3000 # 3 seconds 15 | RETRIES = 7 16 | INTERVAL_MS = 500 # INTERVAL_MS * RETRIES > STALE_DURATION 17 | CONFIG_FILE = "~/.ocpvscode" 18 | 19 | 20 | def get_config_file(): 21 | """Get the config file""" 22 | return resolve_path(CONFIG_FILE) 23 | 24 | 25 | def get_lock_file(file): 26 | """Get the lock file for the given file""" 27 | return f"{file}.lock" 28 | 29 | 30 | def resolve_path(file): 31 | """Resolve the canonical path of the file""" 32 | return os.path.realpath(os.path.expanduser(file)) 33 | 34 | 35 | def is_lock_stale(mtime, stale_seconds): 36 | """Check if the lock is stale""" 37 | return (time.time() - mtime) > stale_seconds 38 | 39 | 40 | def remove_lock(lockfile): 41 | """Remove the lockfile""" 42 | try: 43 | os.rmdir(lockfile) 44 | except FileNotFoundError: 45 | print(f"Lock file {lockfile} not found") 46 | except Exception as ex: 47 | raise RuntimeError(f"Unable to remove lock file {lockfile}") from ex 48 | 49 | 50 | def acquire_lock( 51 | lockfile, 52 | retries=RETRIES, 53 | interval_ms=INTERVAL_MS, 54 | stale_duration_ms=STALE_DURATION_MS, 55 | retry=0, 56 | ): 57 | """Acquire the lock for the given file""" 58 | 59 | # Use mkdir to create the lockfile 60 | try: 61 | os.mkdir(lockfile) 62 | # lock successfully acquired 63 | # print(f"Lock file {lockfile} aquired") 64 | except FileExistsError: 65 | print(f"Lock file {lockfile} already exists") 66 | if is_lock_stale(os.stat(lockfile).st_mtime, stale_duration_ms / 1000): 67 | print(f"Lock file {lockfile} is stale") 68 | try: 69 | # assume the lockfile is stale and simply remove it 70 | remove_lock(lockfile) 71 | except FileNotFoundError: 72 | pass # lock seems to be just removed, which is ok 73 | except Exception as ex: 74 | raise RuntimeError( 75 | f"Unable to remove stale lock file {lockfile}." 76 | ) from ex 77 | 78 | # try to acquire the lock again 79 | acquire_lock(lockfile, retries, interval_ms, stale_duration_ms, retry + 1) 80 | else: 81 | if retry < retries: 82 | time.sleep(interval_ms / 1000) 83 | print("Retrying to acquire lock") 84 | acquire_lock( 85 | lockfile, retries, interval_ms, stale_duration_ms, retry + 1 86 | ) 87 | else: 88 | raise RuntimeError( # pylint: disable=raise-missing-from 89 | f"Unable to acquire lock for file {lockfile} after {retries} retries" 90 | ) 91 | 92 | except Exception as ex: 93 | raise RuntimeError(f"Cannot create lock file {lockfile}.") from ex 94 | 95 | 96 | def lock(file, retries=RETRIES, interval_ms=INTERVAL_MS): 97 | """Lock the given file""" 98 | lockfile = get_lock_file(file) 99 | acquire_lock(lockfile, retries, interval_ms) 100 | 101 | 102 | def unlock(file): 103 | """Unlock the given file""" 104 | lockfile = get_lock_file(file) 105 | remove_lock(lockfile) 106 | 107 | 108 | def update_state(port, key=None, value=None): 109 | """Update the config file with the given key/value pair""" 110 | 111 | config_file = resolve_path(CONFIG_FILE) 112 | lock(config_file) 113 | try: 114 | # pylint: disable=consider-using-with 115 | fd = open(config_file, "r+", encoding="utf-8") 116 | config = fd.read() 117 | if config == "": 118 | config = {} 119 | else: 120 | config = json.loads(config) 121 | except FileNotFoundError: 122 | # pylint: disable=consider-using-with 123 | fd = open(config_file, "w+", encoding="utf-8") 124 | config = {} 125 | except Exception as ex: 126 | unlock(config_file) 127 | raise RuntimeError(f"Unable to open config file {config_file}.") from ex 128 | fd.close() 129 | 130 | port = str(port) 131 | if config.get(port) is None: 132 | config[port] = {} 133 | 134 | if key is None: 135 | del config[port] 136 | elif value is None: 137 | del config[port][key] 138 | elif isinstance(value, str): 139 | config[port][key] = value.rstrip(os.path.sep) 140 | else: 141 | config[port][key] = [v.rstrip(os.path.sep) for v in value] 142 | 143 | try: 144 | with open(config_file, "w", encoding="utf-8") as fd: 145 | fd.write(json.dumps(config, indent=2)) 146 | except Exception as ex: 147 | raise RuntimeError(f"Unable to write config file {config}.") from ex 148 | finally: 149 | unlock(config_file) 150 | 151 | 152 | def get_state(): 153 | """Get the config file""" 154 | config_file = resolve_path(CONFIG_FILE) 155 | lock(config_file) 156 | try: 157 | with open(config_file, "r", encoding="utf-8") as fd: 158 | config = fd.read() 159 | if config == "": 160 | config = {} 161 | else: 162 | config = json.loads(config) 163 | unlock(config_file) 164 | except FileNotFoundError as ex: 165 | with open(config_file, "w") as fd: 166 | fd.write("{}\n") 167 | config = {} 168 | 169 | return config 170 | -------------------------------------------------------------------------------- /ocp_vscode/static/js/comms.js: -------------------------------------------------------------------------------- 1 | function handleMessage(message) { 2 | console.log("Handling message"); 3 | window.postMessage(message, window.location.origin); 4 | } 5 | 6 | class Comms { 7 | constructor(host, port) { 8 | this.socket = new WebSocket(`ws://${host}:${port}`); 9 | this.ready = false; 10 | 11 | this.socket.onopen = (event) => { 12 | console.log("WebSocket connection established"); 13 | this.ready = true; 14 | this.register(); 15 | }; 16 | 17 | this.socket.onmessage = (event) => { 18 | console.log( 19 | "Message received from server:", 20 | event.data.substring(0, 200) + "..." 21 | ); 22 | handleMessage(event.data); 23 | }; 24 | 25 | this.socket.onerror = (error) => { 26 | console.error("WebSocket error:", error); 27 | }; 28 | 29 | this.socket.onclose = (event) => { 30 | console.log("WebSocket connection closed"); 31 | }; 32 | } 33 | 34 | register() { 35 | const msg = "L:{}"; 36 | this.socket.send(msg); 37 | } 38 | 39 | sendStatus(status) { 40 | if (this.ready) { 41 | const msg = `U:${JSON.stringify(status)}`; 42 | this.socket.send(msg); 43 | } 44 | } 45 | } 46 | 47 | export { Comms }; 48 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=45", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "ocp_vscode" 7 | version = "2.7.1" 8 | authors = [{ name = "Bernhard Walter", email = "b_walter@arcor.de" }] 9 | description = "OCP CAD Viewer for VSCode" 10 | readme = "README.md" 11 | requires-python = ">=3.9" 12 | keywords = [ 13 | "3d models", 14 | "3d printing", 15 | "3d viewing", 16 | "3d", 17 | "brep", 18 | "cad", 19 | "cadquery", 20 | "opencscade", 21 | "python", 22 | ] 23 | license = { text = "Apache-2.0" } 24 | classifiers = [ 25 | "License :: OSI Approved :: Apache Software License", 26 | "Operating System :: OS Independent", 27 | "Programming Language :: Python :: 3", 28 | ] 29 | 30 | dependencies = [ 31 | "ocp-tessellate>=3.0.11,<3.1.0", 32 | "requests", 33 | "ipykernel", 34 | "orjson", 35 | "websockets>=13.0,<14.0", 36 | "pyaml", 37 | "flask>=3.0,<4.0", 38 | "flask_sock>=0.7,<1.0", 39 | "click>=8.1,<9.0", 40 | ] 41 | 42 | [tool.setuptools] 43 | packages = ["ocp_vscode"] 44 | package-dir = { "" = "." } 45 | 46 | [tool.setuptools.package-data] 47 | ocp_vscode = ["templates/*.html", "static/css/*.css", "static/js/*.js"] 48 | 49 | [project.optional-dependencies] 50 | dev = ["questionary~=1.10.0", "bump-my-version", "black", "twine"] 51 | 52 | [project.urls] 53 | "Homepage" = "https://github.com/bernhard-42/vscode-ocp-cad-viewer" 54 | "Bug Tracker" = "https://github.com/bernhard-42/vscode-ocp-cad-viewer/issues" 55 | 56 | # black settings 57 | 58 | [tool.black] 59 | target-version = ["py39", "py310", "py311", "py312"] 60 | line-length = 88 61 | 62 | # bump-my-version settings 63 | 64 | [tool.bumpversion] 65 | current_version = "2.7.1" 66 | commit = false 67 | tag = false 68 | parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(?P\\d*)(?P\\d*)" 69 | serialize = ["{major}.{minor}.{patch}"] 70 | allow_dirty = true 71 | message = "Bump version: {current_version} → {new_version}" 72 | search = "{current_version}" 73 | replace = "{new_version}" 74 | regex = false 75 | ignore_missing_version = false 76 | ignore_missing_files = false 77 | sign_tags = false 78 | commit_args = "" 79 | setup_hooks = [] 80 | pre_commit_hooks = [] 81 | post_commit_hooks = [] 82 | 83 | [[tool.bumpversion.files]] 84 | filename = "pyproject.toml" 85 | search = 'version = "{current_version}"' 86 | replace = 'version = "{new_version}"' 87 | 88 | [[tool.bumpversion.files]] 89 | filename = "ocp_vscode/__init__.py" 90 | search = '__version__ = "{current_version}"' 91 | replace = '__version__ = "{new_version}"' 92 | 93 | [[tool.bumpversion.files]] 94 | filename = "src/version.ts" 95 | search = 'version = "{current_version}"' 96 | replace = 'version = "{new_version}"' 97 | 98 | [[tool.bumpversion.files]] 99 | filename = "package.json" 100 | search = '"version": "{current_version}"' 101 | replace = '"version": "{new_version}"' 102 | 103 | [[tool.bumpversion.files]] 104 | filename = "README.md" 105 | search = 'OCP CAD Viewer {current_version}' 106 | replace = 'OCP CAD Viewer {new_version}' 107 | -------------------------------------------------------------------------------- /resources/dark/install.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /resources/dark/open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /resources/dark/output.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /resources/dark/paste.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /resources/dark/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /resources/dark/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /resources/dark/start.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /resources/light/install.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /resources/light/open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /resources/light/output.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /resources/light/paste.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /resources/light/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /resources/light/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /resources/light/start.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /resources/ocp-icon.svg: -------------------------------------------------------------------------------- 1 | OCP -------------------------------------------------------------------------------- /resources/ocp-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/resources/ocp-logo.png -------------------------------------------------------------------------------- /screenshots/angle-tool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/angle-tool.png -------------------------------------------------------------------------------- /screenshots/axes-and-grids.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/axes-and-grids.png -------------------------------------------------------------------------------- /screenshots/build123d_installed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/build123d_installed.png -------------------------------------------------------------------------------- /screenshots/build123d_snippet.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/build123d_snippet.gif -------------------------------------------------------------------------------- /screenshots/cadquery_installed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/cadquery_installed.png -------------------------------------------------------------------------------- /screenshots/context_vars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/context_vars.png -------------------------------------------------------------------------------- /screenshots/debug.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/debug.gif -------------------------------------------------------------------------------- /screenshots/edges-parent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/edges-parent.png -------------------------------------------------------------------------------- /screenshots/faces-parent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/faces-parent.png -------------------------------------------------------------------------------- /screenshots/glass-collapsed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/glass-collapsed.png -------------------------------------------------------------------------------- /screenshots/glass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/glass.png -------------------------------------------------------------------------------- /screenshots/init.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/init.png -------------------------------------------------------------------------------- /screenshots/install-libraries.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/install-libraries.gif -------------------------------------------------------------------------------- /screenshots/install_ocp_vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/install_ocp_vscode.png -------------------------------------------------------------------------------- /screenshots/jupyter-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/jupyter-header.png -------------------------------------------------------------------------------- /screenshots/measure-tool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/measure-tool.png -------------------------------------------------------------------------------- /screenshots/measure.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/measure.gif -------------------------------------------------------------------------------- /screenshots/named-objects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/named-objects.png -------------------------------------------------------------------------------- /screenshots/ocp-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/ocp-on.png -------------------------------------------------------------------------------- /screenshots/ocp_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/ocp_icon.png -------------------------------------------------------------------------------- /screenshots/ocp_vscode-examples.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/ocp_vscode-examples.gif -------------------------------------------------------------------------------- /screenshots/ocp_vscode_debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/ocp_vscode_debug.png -------------------------------------------------------------------------------- /screenshots/ocp_vscode_installed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/ocp_vscode_installed.png -------------------------------------------------------------------------------- /screenshots/ocp_vscode_run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/ocp_vscode_run.png -------------------------------------------------------------------------------- /screenshots/properties-tool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/properties-tool.png -------------------------------------------------------------------------------- /screenshots/quickstart.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/quickstart.gif -------------------------------------------------------------------------------- /screenshots/run-code.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/run-code.gif -------------------------------------------------------------------------------- /screenshots/topo-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/topo-filter.png -------------------------------------------------------------------------------- /screenshots/vertices-parent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernhard-42/vscode-ocp-cad-viewer/f24cacf9e213d05c9ff33c26c34814cb7a96fc34/screenshots/vertices-parent.png -------------------------------------------------------------------------------- /src/demo.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | import * as fs from 'fs'; 4 | import { getCurrentFolder } from "./utils"; 5 | 6 | const build123d_demo = `# %% 7 | 8 | # The markers "# %%" separate code blocks for execution (cells) 9 | # Press shift-enter to exectute a cell and move to next cell 10 | # Press ctrl-enter to exectute a cell and keep cursor at the position 11 | # For more details, see https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter 12 | 13 | # %% 14 | 15 | from build123d import * 16 | from ocp_vscode import * 17 | 18 | # %% 19 | # Builder mode 20 | 21 | with BuildPart() as bp: 22 | Box(1,1,1) 23 | fillet(bp.edges(), radius=0.1) 24 | 25 | show(bp) 26 | 27 | # %% 28 | # Algebra mode 29 | 30 | b2 = Box(1,2,3) 31 | b2 = fillet(b2.edges(), 0.1) 32 | 33 | show(b2, axes=True, axes0=True, grid=(True, True, True), transparent=True) 34 | 35 | # %% 36 | 37 | ` 38 | 39 | const cadquery_demo = `# %% 40 | 41 | # The markers "# %%" separate code blocks for execution (cells) 42 | # Press shift-enter to exectute a cell and move to next cell 43 | # Press ctrl-enter to exectute a cell and keep cursor at the position 44 | # For more details, see https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter 45 | 46 | # %% 47 | 48 | import cadquery as cq 49 | from ocp_vscode import * 50 | 51 | # %% 52 | 53 | b = cq.Workplane().box(1,2,3).fillet(0.1) 54 | 55 | show(b) 56 | ` 57 | 58 | export function createDemoFile(lib: string) { 59 | return new Promise((resolve, reject) => { 60 | const current = getCurrentFolder()[0]; 61 | const demoFilePath = path.join(current, "ocp_vscode_demo.py"); 62 | if (lib === "build123d") { 63 | fs.writeFileSync(demoFilePath, build123d_demo); 64 | } else { 65 | fs.writeFileSync(demoFilePath, cadquery_demo); 66 | } 67 | try { 68 | vscode.workspace.openTextDocument(demoFilePath).then(doc => { 69 | vscode.window.showTextDocument(doc); 70 | resolve(true); 71 | }); 72 | } catch { 73 | reject(false); 74 | } 75 | }); 76 | } -------------------------------------------------------------------------------- /src/display.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Bernhard Walter 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import * as vscode from "vscode"; 18 | import * as fs from "fs"; 19 | 20 | export function template( 21 | styleSrc: vscode.Uri, 22 | scriptSrc: vscode.Uri, 23 | htmlSrc: vscode.Uri 24 | ) { 25 | let options = vscode.workspace.getConfiguration("OcpCadViewer.view"); 26 | 27 | const theme = options.get("dark") ? "dark" : "light"; 28 | const treeWidth = options.get("tree_width"); 29 | const control = options.get("orbit_control") ? "orbit" : "trackball"; 30 | const up = options.get("up"); 31 | const glass = options.get("glass"); 32 | const tools = options.get("tools"); 33 | 34 | let html = fs.readFileSync(htmlSrc.fsPath, "utf8"); // resources/webview.html 35 | 36 | html = html.replace("{{ standalone_scripts|safe }}", ""); 37 | html = html.replace("{{ standalone_imports|safe }}", ""); 38 | html = html.replace( 39 | "{{ standalone_comms|safe }}", 40 | "const vscode = acquireVsCodeApi();" 41 | ); 42 | html = html.replace("{{ standalone_init|safe }}", ""); 43 | html = html.replace("{{ styleSrc }}", styleSrc.toString()); 44 | html = html.replace("{{ scriptSrc }}", scriptSrc.toString()); 45 | html = html.replace("{{ theme }}", theme); 46 | html = html.replace("{{ treeWidth }}", `${treeWidth}`); 47 | html = html.replace("{{ control }}", control); 48 | html = html.replace("{{ up }}", `${up}`); 49 | html = html.replace(/\{\{ glass\|tojson \}\}/g, `${glass}`); 50 | html = html.replace(/\{\{ tools\|tojson \}\}/g, `${tools}`); 51 | 52 | return html; 53 | } 54 | -------------------------------------------------------------------------------- /src/examples.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as fs from "fs"; 3 | import * as os from "os"; 4 | import * as path from "path"; 5 | import { IncomingMessage } from "http"; 6 | import * as AdmZip from 'adm-zip'; 7 | import { https } from 'follow-redirects'; 8 | 9 | export async function download(library: string, destination: string) { 10 | const timeout = 10000; 11 | const exampleDownloads = 12 | vscode.workspace.getConfiguration("OcpCadViewer.advanced")["exampleDownloads"]; 13 | 14 | const archiveUrl = exampleDownloads[library]["zip"]; 15 | const examplePath = exampleDownloads[library]["example_path"]; 16 | const filename = path.basename(archiveUrl); 17 | const targetPath = path.join(destination, `${library}_examples`); 18 | 19 | let request = https.get(archiveUrl, (response: IncomingMessage) => { 20 | 21 | if (response.statusCode === 200) { 22 | const tempFolder = path.join(os.tmpdir(), "cadquery-viewer"); 23 | fs.mkdtemp(tempFolder, (err, folder) => { 24 | if (err) { 25 | vscode.window.showErrorMessage( 26 | `Cannot create temp folder ${folder}` 27 | ); 28 | return; 29 | } 30 | const downloadPath = path.join(folder, filename); 31 | var stream = fs.createWriteStream(downloadPath); 32 | response.pipe(stream); 33 | 34 | stream.on("finish", () => { 35 | stream.close(); 36 | vscode.window.showInformationMessage( 37 | `File "${archiveUrl}" downloaded successfully.` 38 | ); 39 | 40 | const zip = new AdmZip(downloadPath); 41 | try { 42 | zip.extractAllTo(folder, true); 43 | } catch (error) { 44 | vscode.window.showErrorMessage( 45 | `Unzipping "${downloadPath}" failed.` 46 | ); 47 | return; 48 | } 49 | fs.rename(path.join(folder, examplePath), targetPath, (err) => { 50 | if (err) { 51 | vscode.window.showErrorMessage( 52 | `Moving examples to "${targetPath}" failed.` 53 | ); 54 | } else { 55 | vscode.window.showInformationMessage( 56 | `Examples successfully downloaded to "${targetPath}".` 57 | ); 58 | } 59 | }); 60 | }); 61 | }); 62 | } else { 63 | vscode.window.showErrorMessage(`Cannot download ${archiveUrl}`); 64 | } 65 | }); 66 | 67 | request.setTimeout(timeout, function () { 68 | request.destroy(); 69 | }); 70 | 71 | request.on("error", function (e: any) { 72 | vscode.window.showErrorMessage( 73 | `Cannot download ${archiveUrl}` 74 | ); 75 | }); 76 | } 77 | -------------------------------------------------------------------------------- /src/output.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Bernhard Walter 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import * as vscode from "vscode"; 18 | 19 | const log = vscode.window.createOutputChannel("OCP CAD Viewer Log"); 20 | var is_open = false; 21 | 22 | function getPrefix(logLevel?: string) { 23 | let timestamp = ""; 24 | let level = ""; 25 | if (logLevel) { 26 | const d = new Date(); 27 | timestamp = `${d.toLocaleTimeString()}.${d 28 | .getMilliseconds() 29 | .toString() 30 | .padStart(3, "0")}} `; 31 | level = `${logLevel} `; 32 | } 33 | 34 | return `[${timestamp}${level}] `; 35 | } 36 | 37 | export function show() { 38 | if (is_open) { 39 | log.hide(); 40 | } else { 41 | log.show(true); 42 | } 43 | } 44 | 45 | export function info(msg: string) { 46 | const prefix = getPrefix("INFO "); 47 | log.appendLine(prefix + msg); 48 | } 49 | 50 | export function error(msg: string) { 51 | const prefix = getPrefix("ERROR"); 52 | log.appendLine(prefix + msg); 53 | } 54 | 55 | export function debug(msg: string) { 56 | const prefix = getPrefix("DEBUG"); 57 | log.appendLine(prefix + msg); 58 | } 59 | 60 | export function set_open(open: boolean) { 61 | is_open = open; 62 | } 63 | -------------------------------------------------------------------------------- /src/state.ts: -------------------------------------------------------------------------------- 1 | /* 2 | state uses "poor man's locking": nodejs does not support fcntl (POSIX) or 3 | LockFileEx (Windows) locking. This module implements a simple locking 4 | mechanism for nodejs and Python leveraging "mkdir" to create locks. 5 | 6 | Ideas are taken from the node js library "proper-lockfile" 7 | (https://github.com/moxystudio/node-proper-lockfile) 8 | */ 9 | 10 | import { promises as fs } from "fs"; 11 | import * as os from "os"; 12 | import * as path from "path"; 13 | import * as output from "./output"; 14 | 15 | const STALE_DURATION_MS = 3000; // 3 seconds 16 | const RETRIES = 7; 17 | const INTERVAL_MS = 500; // INTERVAL_MS * RETRIES > STALE_DURATION 18 | const CONFIG_FILE = "~/.ocpvscode" 19 | 20 | 21 | export async function getConfigFile(): Promise { 22 | return await resolvePath(CONFIG_FILE); 23 | } 24 | 25 | function rtrim(file: string, c: string): string { 26 | while (file.endsWith(c)) { 27 | file = file.substring(0, file.length - 1); 28 | } 29 | return file; 30 | } 31 | 32 | function sleep(msec: number) { 33 | return new Promise((resolve, _reject) => { 34 | setTimeout(() => { 35 | resolve(0); 36 | }, msec); 37 | }); 38 | } 39 | 40 | function getLockFile(file: string): string { 41 | return `${file}.lock`; 42 | } 43 | 44 | async function resolvePath(file: string): Promise { 45 | if (file.startsWith("~")) { 46 | const homedir = os.homedir(); 47 | file = path.join(homedir, file.substring(1)); 48 | } 49 | try { 50 | return await fs.realpath(file); 51 | } catch (error) { 52 | return file; 53 | } 54 | } 55 | 56 | function isLockStale(mtime: number, staleMilliseconds: number): boolean { 57 | return Date.now() - mtime > staleMilliseconds; 58 | } 59 | 60 | async function removeLock(lockfile: string) { 61 | try { 62 | await fs.rmdir(lockfile); 63 | output.debug(`Lock file ${lockfile} removed`); 64 | } catch (error: any) { 65 | if (error.code === "ENOENT") { 66 | output.debug(`Lock file ${lockfile} not found`); 67 | } else { 68 | throw new Error(`Unable to remove lock file ${lockfile}`); 69 | } 70 | } 71 | } 72 | 73 | async function acquireLock( 74 | lockfile: string, 75 | retries: number = RETRIES, 76 | intervalMs: number = INTERVAL_MS, 77 | staleDurationMs: number = STALE_DURATION_MS, 78 | retry: number = 0 79 | ) { 80 | try { 81 | await fs.mkdir(lockfile); 82 | output.debug(`Lock file ${lockfile} acquired`); 83 | } catch (error: any) { 84 | if (error.code === "EEXIST") { 85 | output.debug(`Lock file ${lockfile} already exists`); 86 | const stat = await fs.stat(lockfile); 87 | if (isLockStale(stat.mtimeMs, staleDurationMs)) { 88 | output.debug(`Lock file ${lockfile} is stale`); 89 | try { 90 | // assume the lockfile is stale and simply remove it 91 | await removeLock(lockfile); 92 | } catch (error) { 93 | // lock seems to be just removed, which is ok 94 | } 95 | // try to acquire the lock again 96 | await acquireLock( 97 | lockfile, 98 | retries, 99 | intervalMs, 100 | staleDurationMs, 101 | retry + 1 102 | ); 103 | } else { 104 | if (retry < retries) { 105 | await sleep(intervalMs); 106 | output.debug("Retrying to acquire lock"); 107 | await acquireLock( 108 | lockfile, 109 | retries, 110 | intervalMs, 111 | staleDurationMs, 112 | retry + 1 113 | ); 114 | } else { 115 | throw new Error( 116 | `Unable to acquire lock for file ${lockfile} after ${retries} retries` 117 | ); 118 | } 119 | } 120 | } else { 121 | output.debug(error); 122 | } 123 | } 124 | } 125 | 126 | async function lock(file: string, retries: number = RETRIES, intervalMs: number = INTERVAL_MS) { 127 | const lockfile = getLockFile(file); 128 | await acquireLock(lockfile, retries, intervalMs); 129 | } 130 | 131 | async function unlock(file: string) { 132 | const lockfile = getLockFile(file); 133 | await removeLock(lockfile); 134 | } 135 | 136 | export async function updateState( 137 | port: number, key: string | null, value: string | Array | null, initialize: boolean = false 138 | ) { 139 | let data; 140 | let fh: fs.FileHandle; 141 | 142 | const config_file = await resolvePath("~/.ocpvscode"); 143 | await lock(config_file); 144 | 145 | try { 146 | fh = await fs.open(config_file, "r+"); 147 | data = await fh.readFile({ encoding: "utf8" }); 148 | if (data.length > 0) { 149 | data = JSON.parse(data); 150 | } else { 151 | data = {}; 152 | } 153 | } catch (error) { 154 | fh = await fs.open(config_file, "w+"); 155 | data = {}; 156 | } finally { 157 | await unlock(config_file); 158 | } 159 | 160 | if (data[port] == null || initialize) { 161 | data[port] = {}; 162 | } 163 | if (key == null) { 164 | delete data[port]; 165 | } else if (value == null) { 166 | delete data[port][key]; 167 | } else if (typeof value === "string") { 168 | data[port][key] = [rtrim(value, path.sep)]; 169 | } else { 170 | data[port][key] = value.map((v) => rtrim(v, path.sep)); 171 | } 172 | let buffer: string = JSON.stringify(data, null, 2); 173 | try { 174 | const { bytesWritten } = await fh.write(buffer, 0, "utf-8"); 175 | await fh.truncate(bytesWritten); 176 | await fh.close(); 177 | } catch (error) { 178 | output.error(`${error}`); 179 | } finally { 180 | await unlock(config_file); 181 | } 182 | } 183 | 184 | interface State { 185 | roots: string[]; 186 | connection_file: string; 187 | } 188 | 189 | interface States { 190 | [key: string]: State; 191 | } 192 | 193 | class ResultState { 194 | port: number | null; 195 | state: State | null; 196 | 197 | constructor(port: number | null, state: State | null) { 198 | this.port = port; 199 | this.state = state; 200 | } 201 | } 202 | 203 | export async function getState(path: string): Promise { 204 | const config_file = await resolvePath("~/.ocpvscode"); 205 | 206 | let data: States; 207 | 208 | await lock(config_file); 209 | try { 210 | const config: string = await fs.readFile(config_file, "utf-8"); 211 | unlock(config_file); 212 | if (config === "") { 213 | data = {}; 214 | } else { 215 | data = JSON.parse(config); 216 | } 217 | } catch (error) { 218 | throw new Error(`Unable to open config file ${config_file}.`); 219 | } 220 | 221 | // exact match 222 | let port: string | null = null; 223 | for (const [p, v] of Object.entries(data)) { 224 | if (v.roots.includes(path)) { 225 | port = p; 226 | } 227 | } 228 | 229 | // else search nearest path 230 | for (const [p, v] of Object.entries(data)) { 231 | for (const root of v.roots) { 232 | if (path.startsWith(root)) { 233 | port = p; 234 | } 235 | } 236 | } 237 | 238 | // heuristic: if there is only one port, use it 239 | let ports = Object.keys(data); 240 | if (ports.length === 1) { 241 | port = ports[0] 242 | } 243 | 244 | if (port == null) { 245 | return new ResultState(null, null); 246 | } 247 | 248 | return new ResultState(parseInt(port, 10), data[port]) 249 | } 250 | -------------------------------------------------------------------------------- /src/statusManager.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Bernhard Walter 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import * as vscode from "vscode"; 18 | import { version as ocp_vscode_version } from "./version"; 19 | import * as output from "./output"; 20 | import { jupyterExtensionInstalled } from "./utils"; 21 | 22 | const URL = 23 | "https://github.com/bernhard-42/vscode-ocp-cad-viewer/releases/download"; 24 | 25 | export class StatusManagerProvider implements vscode.TreeDataProvider { 26 | installed: boolean = false; 27 | libraries: string[] = []; 28 | running: boolean = false; 29 | port: string = ""; 30 | version: string = ""; 31 | hasJupyterExtension: boolean = false; 32 | 33 | constructor() { 34 | this.version = ocp_vscode_version; 35 | this.hasJupyterExtension = jupyterExtensionInstalled(); 36 | } 37 | 38 | private _onDidChangeTreeData: vscode.EventEmitter< 39 | Status | undefined | null | void 40 | > = new vscode.EventEmitter(); 41 | 42 | readonly onDidChangeTreeData: vscode.Event< 43 | Status | undefined | null | void 44 | > = this._onDidChangeTreeData.event; 45 | 46 | async refresh(port: string = "") { 47 | if ((port !== "") && (port !== "")) { 48 | this.port = port; 49 | this.running = true; 50 | } else if (port === "") { 51 | this.running = false; 52 | this.port = ""; 53 | } 54 | this._onDidChangeTreeData.fire(); 55 | } 56 | 57 | getPort() { 58 | return this.port; 59 | } 60 | 61 | setLibraries(libraries: string[]) { 62 | this.libraries = []; 63 | libraries.forEach((library) => { 64 | if (library !== "ocp_tessellate") { 65 | // map ipykernel as library ro jupyter extension 66 | this.libraries.push(library == "ipykernel" ? "jupyter" : library) 67 | } 68 | }) 69 | } 70 | 71 | getTreeItem(element: Status): vscode.TreeItem { 72 | return element; 73 | } 74 | 75 | getChildren(element?: Status): Thenable { 76 | if (element) { 77 | let status: Status[] = []; 78 | if (element.label === "ocp_vscode") { 79 | status.push( 80 | new Status( 81 | "version", 82 | { "version": ocp_vscode_version }, 83 | vscode.TreeItemCollapsibleState.None 84 | ) 85 | ); 86 | if (this.running) { 87 | status.push( 88 | new Status( 89 | "port", 90 | { "port": this.port }, 91 | vscode.TreeItemCollapsibleState.None 92 | ) 93 | ); 94 | } 95 | } else if (element.label === "jupyter") { 96 | status.push( 97 | new Status( 98 | "extension", 99 | { 100 | "extension": (this.hasJupyterExtension) ? "installed" : "not installed", 101 | "jupyter": this.hasJupyterExtension 102 | }, 103 | vscode.TreeItemCollapsibleState.None 104 | ) 105 | ); 106 | } 107 | return Promise.resolve(status); 108 | } else { 109 | let status: Status[] = []; 110 | if (this.installed) { 111 | let state = vscode.TreeItemCollapsibleState.Expanded; 112 | status.push( 113 | new Status( 114 | "ocp_vscode", 115 | { "running": this.running ? "RUNNING" : "STOPPED" }, 116 | state 117 | ) 118 | ); 119 | this.libraries.sort().forEach((lib) => { 120 | if (lib !== "ocp_vscode") { 121 | status.push( 122 | new Status( 123 | lib, 124 | { "jupyter": this.hasJupyterExtension }, 125 | (lib === "jupyter") 126 | ? vscode.TreeItemCollapsibleState.Expanded 127 | : vscode.TreeItemCollapsibleState.None 128 | ) 129 | ); 130 | } 131 | }); 132 | } 133 | return Promise.resolve(status); 134 | } 135 | } 136 | 137 | async openViewer() { 138 | await vscode.commands.executeCommand("ocpCadViewer.ocpCadViewer"); 139 | } 140 | } 141 | 142 | export class Status extends vscode.TreeItem { 143 | constructor( 144 | public readonly label: string, 145 | private options: Record, 146 | public readonly collapsibleState: vscode.TreeItemCollapsibleState 147 | ) { 148 | super(label, collapsibleState); 149 | if (label === "ocp_vscode") { 150 | this.contextValue = "status"; 151 | 152 | } else if (label === "ipykernel") { 153 | label = "jupyter"; 154 | this.contextValue = options.jupyter ? "open" : "missing"; 155 | 156 | } else if (label === "jupyter_console") { 157 | label = "ipython"; 158 | this.contextValue = options.jupyter ? "console" : "missing"; 159 | 160 | 161 | } else { 162 | this.contextValue = "library"; 163 | } 164 | 165 | if (options.running !== undefined) { 166 | this.description = options.running; 167 | this.tooltip = 168 | options.running === "RUNNING" 169 | ? "OCP CAD Viewer is running" 170 | : "OCP CAD Viewer is stopped"; 171 | 172 | } else if (options.port !== undefined) { 173 | this.contextValue = "port"; 174 | this.description = options.port; 175 | this.tooltip = `OCP CAD Viewer is listening on port ${options.port}`; 176 | 177 | } else if (options.extension !== undefined) { 178 | this.contextValue = options.jupyter ? "jupyterExtInstalled" : "jupyterExtMissing"; 179 | this.description = options.extension; 180 | this.tooltip = `Jupyter extension is ${options.extension}`; 181 | 182 | } else if (options.version !== undefined) { 183 | this.contextValue = "version"; 184 | this.description = options.version; 185 | this.tooltip = `ocp_vscode extension ${options.version}`; 186 | } 187 | } 188 | } 189 | 190 | export function createStatusManager() { 191 | const statusManager = new StatusManagerProvider(); 192 | vscode.window.registerTreeDataProvider("ocpCadStatus", statusManager); 193 | vscode.window.createTreeView("ocpCadStatus", { 194 | treeDataProvider: statusManager 195 | }); 196 | 197 | output.info("Successfully registered CadqueryViewer Status Manager"); 198 | 199 | return statusManager; 200 | } 201 | -------------------------------------------------------------------------------- /src/system/shell.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import * as output from '../output'; 3 | import { getCurrentFolder } from '../utils'; 4 | 5 | function parsePipLibs(jsonData: string) { 6 | var data = JSON.parse(jsonData); 7 | let libraries = new Map(); 8 | Object.keys(data).forEach(key => { 9 | libraries.set(data[key]["name"].toLowerCase(), data[key]["version"]); 10 | }); 11 | return libraries; 12 | } 13 | 14 | export function pipList(python: string): Map { 15 | let workspaceFolder = getCurrentFolder()[0]; 16 | try { 17 | let result = execSync(`"${python}" -m pip list --format json`, { cwd: workspaceFolder }).toString(); 18 | return parsePipLibs(result); 19 | } catch (error: any) { 20 | output.error(error.stderr.toString()); 21 | return new Map(); 22 | }; 23 | } 24 | 25 | export function execute(cmd: string) { 26 | let currentFolder = getCurrentFolder()[0]; 27 | if (currentFolder === "") { 28 | currentFolder = "."; 29 | } 30 | try { 31 | let result = execSync(cmd, { cwd: currentFolder }).toString(); 32 | return result; 33 | } catch (error: any) { 34 | output.error(error.stderr.toString()); 35 | throw Error(error.message); 36 | }; 37 | } 38 | 39 | export function pythonVersion(python: string): string { 40 | try { 41 | return execSync(`"${python}" --version`).toString(); 42 | } catch (error: any) { 43 | output.error(error.stderr.toString()); 44 | return ""; 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /src/system/terminal.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { spawn } from "child_process"; 3 | import * as output from "../output"; 4 | import { getCurrentFolder } from "../utils"; 5 | 6 | export class TerminalExecute { 7 | writeEmitter = new vscode.EventEmitter(); 8 | terminal: vscode.Terminal | undefined; 9 | workspaceFolder: string | undefined; 10 | errorMsg: string = ""; 11 | stdout: string = ""; 12 | terminalName = "OCP CAD Viewer Terminal"; 13 | child: any = undefined; 14 | 15 | constructor(msg: string) { 16 | let pty = { 17 | onDidWrite: this.writeEmitter.event, 18 | open: () => this.writeEmitter.fire(msg + "\r\n\r\n"), 19 | close: () => { 20 | /* noop*/ 21 | }, 22 | handleInput: async (data: string) => { 23 | let charCode = data.charCodeAt(0); 24 | 25 | if (data === "\r") { 26 | this.writeEmitter.fire("\r\n\r\n"); 27 | } else if (charCode < 32) { 28 | this.writeEmitter.fire( 29 | `^${String.fromCharCode(charCode + 64)}` 30 | ); 31 | if (charCode === 3) { 32 | await this.killProcess(); 33 | this.writeEmitter.fire("\r\n"); 34 | } 35 | } else { 36 | data = data.replace("\r", "\r\n"); 37 | this.writeEmitter.fire(`${data}`); 38 | } 39 | } 40 | }; 41 | this.terminal = vscode.window.createTerminal({ 42 | name: this.terminalName, 43 | pty 44 | }); 45 | this.workspaceFolder = getCurrentFolder()[0]; 46 | 47 | vscode.window.onDidCloseTerminal(async t => { 48 | 49 | if (t.name === this.terminalName) { 50 | await this.killProcess(); 51 | this.terminal = undefined; 52 | this.child = undefined; 53 | } 54 | }); 55 | } 56 | 57 | async killProcess() { 58 | if (this.child && !this.child.killed) { 59 | this.child.kill(); 60 | vscode.window.showInformationMessage("Process killed"); 61 | output.info("Process killed"); 62 | this.child = undefined; 63 | } 64 | } 65 | 66 | async execute(commands: string[]): Promise { 67 | this.terminal?.show(); 68 | this.stdout = ""; 69 | return new Promise((resolve, reject) => { 70 | let command = commands.join("; "); 71 | this.child = spawn(command, { 72 | stdio: "pipe", 73 | shell: true, 74 | cwd: this.workspaceFolder 75 | }); 76 | output.info(`Running ${command}`); 77 | this.child.stderr.setEncoding("utf8"); 78 | this.child.stdout?.on("data", (data: string) => { 79 | this.stdout += data.toString(); 80 | this.print(data.toString()); 81 | }); 82 | this.child.stderr?.on("data", (data: string) => { 83 | this.errorMsg = data.toString(); 84 | this.print(this.errorMsg); 85 | }); 86 | this.child.on("exit", (code: number, signal: any) => { 87 | if (code === 0) { 88 | this.print(`Successfully executed '${command}`); 89 | vscode.window.showInformationMessage( 90 | `Successfully executed '${command}'` 91 | ); 92 | output.info(`Successfully executed '${command}'`); 93 | resolve(this.stdout); 94 | } else { 95 | vscode.window.showErrorMessage( 96 | `Failed to execute '${command}(${code})'` 97 | ); 98 | output.error(`Failed to execute '${command}(${code})'`); 99 | output.error(this.errorMsg); 100 | 101 | reject(new Error(code?.toString())); 102 | } 103 | }); 104 | }); 105 | } 106 | 107 | print(msg: string) { 108 | for (let line of msg.split(/\r?\n/)) { 109 | this.writeEmitter.fire(line + "\r\n"); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from '@vscode/test-electron'; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error('Failed to run tests'); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from 'vscode'; 6 | // import * as myExtension from '../../extension'; 7 | 8 | suite('Extension Test Suite', () => { 9 | vscode.window.showInformationMessage('Start all tests.'); 10 | 11 | test('Sample test', () => { 12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | color: true 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, '..'); 13 | 14 | return new Promise((c, e) => { 15 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return e(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run(failures => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)); 28 | } else { 29 | c(); 30 | } 31 | }); 32 | } catch (err) { 33 | console.error(err); 34 | e(err); 35 | } 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Bernhard Walter 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import * as vscode from "vscode"; 18 | import * as fs from "fs"; 19 | import * as net from "net"; 20 | import * as path from "path"; 21 | import { PythonExtension } from '@vscode/python-extension'; 22 | import * as output from "./output"; 23 | 24 | export function getEditor() { 25 | const editor = vscode.window.activeTextEditor; 26 | // if (editor === undefined) { 27 | // vscode.window.showWarningMessage("No editor window open or in focus"); 28 | // } 29 | return editor; 30 | } 31 | 32 | export function getCurrentFileUri(): vscode.Uri | undefined { 33 | const editor = getEditor(); 34 | if (editor) { 35 | return editor.document.uri; 36 | } 37 | return undefined; 38 | } 39 | export function getCurrentFilename(): vscode.Uri | undefined { 40 | const filename = getCurrentFileUri(); 41 | return filename; 42 | } 43 | 44 | export function getCurrentFolder(filename: vscode.Uri | undefined = undefined): [string, boolean] { 45 | let root: string | undefined = undefined; 46 | let isWorkspace = false; 47 | if (filename === undefined) { 48 | filename = getCurrentFilename(); 49 | } 50 | 51 | if (vscode.workspace?.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { 52 | for (let i = 0; i < vscode.workspace.workspaceFolders.length; i++) { 53 | if (filename?.fsPath.startsWith(vscode.workspace.workspaceFolders[i].uri.fsPath)) { 54 | root = vscode.workspace.workspaceFolders[i].uri.fsPath; 55 | isWorkspace = true; 56 | break; 57 | } 58 | } 59 | } 60 | if (root === undefined) { 61 | if (filename?.fsPath.endsWith(".py")) { 62 | root = vscode.workspace.getWorkspaceFolder(filename)?.uri.fsPath; 63 | if (root === undefined) { 64 | root = path.dirname(filename.fsPath); 65 | } 66 | } 67 | } 68 | if (!root) { 69 | vscode.window.showErrorMessage("No workspace folder found. Open a folder and click to focus an editor window."); 70 | return ["", false]; 71 | } 72 | return [root, isWorkspace]; 73 | } 74 | 75 | export async function inquiry(placeholder: string, options: string[]) { 76 | const answer = await vscode.window.showQuickPick(options, { 77 | placeHolder: placeholder 78 | }); 79 | return answer || ""; 80 | } 81 | 82 | export function jupyterExtensionInstalled() { 83 | return vscode.extensions.getExtension("ms-toolsai.jupyter") !== undefined; 84 | } 85 | 86 | class PythonPath { 87 | public static async getPythonPath( 88 | document?: vscode.TextDocument 89 | ): Promise { 90 | const pythonApi: PythonExtension = await PythonExtension.api(); 91 | const environmentPath = pythonApi.environments.getActiveEnvironmentPath(); 92 | const environment = await pythonApi.environments.resolveEnvironment(environmentPath); 93 | if (environment != null) { 94 | output.debug(`PythonPath: '${environment.path}', environment: ${environment.environment?.type}, ${environment.environment?.name}`); 95 | return environment.path; 96 | } else { 97 | output.debug(`PythonPath: 'python', environment: DEFAULT`); 98 | vscode.window.showErrorMessage("No Python environment seems to be selected, falling back to default - might not work!"); 99 | return "python"; 100 | } 101 | } 102 | 103 | public static getConfiguration( 104 | section?: string, 105 | document?: vscode.TextDocument 106 | ): vscode.WorkspaceConfiguration { 107 | if (document) { 108 | return vscode.workspace.getConfiguration(section, document.uri); 109 | } else { 110 | return vscode.workspace.getConfiguration(section); 111 | } 112 | } 113 | } 114 | 115 | export function getPythonPath() { 116 | let editor = getEditor(); 117 | return PythonPath.getPythonPath(editor?.document); 118 | } 119 | 120 | export function getPackageManager() { 121 | let cwd = getCurrentFolder()[0]; 122 | return fs.existsSync(path.join(cwd, "poetry.lock")) ? "poetry" : "pip"; 123 | } 124 | 125 | export async function isPortInUse(port: number): Promise { 126 | return new Promise((resolve) => { 127 | const tester = net.createServer() 128 | .once('error', (err: NodeJS.ErrnoException) => { 129 | if (err.code === 'EADDRINUSE') { 130 | resolve(true); 131 | } else { 132 | resolve(false); 133 | } 134 | }) 135 | .once('listening', () => { 136 | tester.close(); 137 | resolve(false); 138 | }) 139 | .listen(port); 140 | }); 141 | } -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Bernhard Walter 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | export const version = "2.7.1"; 18 | -------------------------------------------------------------------------------- /src/viewer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Bernhard Walter 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import * as fs from "fs"; 18 | import * as vscode from "vscode"; 19 | import { OCPCADController } from "./controller"; 20 | import * as output from "./output"; 21 | 22 | export class OCPCADViewer { 23 | /** 24 | * Track the currently panel. Only allow a single panel to exist at a time. 25 | */ 26 | 27 | public static currentPanel: OCPCADViewer | undefined; 28 | public static controller: OCPCADController | undefined; 29 | 30 | public static readonly viewType = "OCPCADViewer"; 31 | 32 | private readonly _panel: vscode.WebviewPanel; 33 | private _disposables: vscode.Disposable[] = []; 34 | 35 | public static async createOrShow( 36 | extensionUri: vscode.Uri, 37 | _controller: OCPCADController 38 | ) { 39 | this.controller = _controller; 40 | 41 | if (OCPCADViewer.currentPanel) { 42 | // If we already have a panel, show it. 43 | 44 | output.debug("Revealing existing webview panel"); 45 | 46 | OCPCADViewer.currentPanel._panel.reveal(vscode.ViewColumn.Two); 47 | } else { 48 | // Otherwise, create a new panel. 49 | 50 | output.debug("Creating new webview panel"); 51 | 52 | // get all current tabs 53 | const tabs: vscode.Tab[] = vscode.window.tabGroups.all.map(tg => tg.tabs).flat(); 54 | 55 | const panel = vscode.window.createWebviewPanel( 56 | OCPCADViewer.viewType, 57 | "OCP CAD Viewer", 58 | vscode.ViewColumn.Two, 59 | { 60 | enableScripts: true, 61 | retainContextWhenHidden: true 62 | } 63 | ); 64 | OCPCADViewer.currentPanel = new OCPCADViewer( 65 | panel, 66 | extensionUri 67 | ); 68 | 69 | // delete old tabs called "OCP CAD Viewer" 70 | for (var tab of tabs) { 71 | if (tab.label === "OCP CAD Viewer") { 72 | await vscode.window.tabGroups.close(tab); 73 | } 74 | } 75 | } 76 | } 77 | 78 | public static revive(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) { 79 | output.debug("Reviving webview panel"); 80 | 81 | vscode.commands.executeCommand('ocpCadViewer.ocpCadViewer'); 82 | } 83 | 84 | private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) { 85 | this._panel = panel; 86 | 87 | this._panel.onDidDispose(() => this.dispose(), null, this._disposables); 88 | this._panel.webview.html = ""; 89 | 90 | // Handle messages from the webview 91 | this._panel.webview.onDidReceiveMessage( 92 | (message) => { 93 | // output.debug(`Received message ${message} from Webview panel`); 94 | switch (message.command) { 95 | case "alert": 96 | vscode.window.showErrorMessage(message.text); 97 | return; 98 | case "screenshot": 99 | var data; 100 | if (typeof message.text.data === 'string' || message.text.data instanceof String) { 101 | data = Buffer.from(message.text.data.replace('data:image/png;base64,', ''), "base64"); 102 | } else { 103 | data = message.text.data; 104 | } 105 | var filename = message.text.filename; 106 | try { 107 | // first write to a temp name to avoid polling is successful before finished ... 108 | let suffix = "-temp" + Date.now().toString(16) 109 | fs.writeFileSync(filename + suffix, data); 110 | // ... and then rename to the actual filename 111 | fs.renameSync(filename + suffix, filename) 112 | vscode.window.showInformationMessage(`Screenshot saved as\n${filename}`); 113 | } catch (error) { 114 | vscode.window.showErrorMessage(`Error saving screenshot as\n${filename}`); 115 | } 116 | return; 117 | } 118 | }, 119 | null, 120 | this._disposables 121 | ); 122 | } 123 | 124 | public dispose() { 125 | output.debug("OCP CAD Viewer dispose"); 126 | OCPCADViewer.currentPanel = undefined; 127 | 128 | this._panel.dispose(); 129 | 130 | while (this._disposables.length) { 131 | const x = this._disposables.pop(); 132 | if (x) { 133 | x.dispose(); 134 | } 135 | } 136 | OCPCADViewer.controller?.dispose(); 137 | } 138 | 139 | public update(div: string) { 140 | if (div !== "") { 141 | output.debug("Updateing webview"); 142 | const webview = this._panel.webview; 143 | this._panel.title = "OCP CAD Viewer"; 144 | webview.html = div; 145 | } 146 | } 147 | 148 | public getView() { 149 | return this._panel.webview; 150 | } 151 | } 152 | /* */ -------------------------------------------------------------------------------- /test/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Utilisez IntelliSense pour en savoir plus sur les attributs possibles. 3 | // Pointez pour afficher la description des attributs existants. 4 | // Pour plus d'informations, visitez : https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python : fichier actif", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${file}", 12 | "console": "integratedTerminal", 13 | "justMyCode": false 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /test/backend.py: -------------------------------------------------------------------------------- 1 | from ocp_tessellate.ocp_utils import deserialize 2 | from asyncio import StreamWriter, StreamReader 3 | import asyncio 4 | 5 | BUFFER_SIZE_HEADER = 4 # Length of the header that contains the length of the message 6 | 7 | 8 | class AsyncServer: 9 | def __init__(self, host, port): 10 | self.host = host 11 | self.port = port 12 | 13 | async def handle_client(self, reader: StreamReader, writer: StreamWriter): 14 | header = await reader.read(BUFFER_SIZE_HEADER) 15 | buffer = await reader.read(int.from_bytes(header)) 16 | 17 | addr = writer.get_extra_info("peername") 18 | print(f"Received data from {addr!r}") 19 | 20 | self.handle_data(buffer) 21 | 22 | print("Closing the connection") 23 | writer.close() 24 | 25 | async def serve(self): 26 | print(f"Serving on {self.host}:{self.port}") 27 | server = await asyncio.start_server(self.handle_client, self.host, self.port) 28 | 29 | async with server: 30 | await server.serve_forever() 31 | 32 | def handle_data(self, data): 33 | obj = deserialize(data) 34 | print(obj) 35 | 36 | 37 | if __name__ == "__main__": 38 | server = AsyncServer("localhost", 9999) 39 | asyncio.run(server.serve()) 40 | -------------------------------------------------------------------------------- /test/sender.py: -------------------------------------------------------------------------------- 1 | from build123d import * 2 | import pickle 3 | from backend import BUFFER_SIZE_HEADER 4 | import asyncio 5 | from ocp_tessellate.ocp_utils import serialize 6 | 7 | box = Box(10, 10, 10).solid() 8 | 9 | 10 | class AsyncSender: 11 | def __init__(self, host, port): 12 | self.host = host 13 | self.port = port 14 | 15 | async def send(self, obj): 16 | try: 17 | data = serialize(obj) 18 | reader, writer = await asyncio.open_connection(self.host, self.port) 19 | 20 | header = len(data).to_bytes(BUFFER_SIZE_HEADER) 21 | writer.write(header + data) 22 | await writer.drain() 23 | 24 | writer.close() 25 | await writer.wait_closed() 26 | 27 | except Exception as e: 28 | print(f"Error sending/receiving message: {e}") 29 | 30 | 31 | if __name__ == "__main__": 32 | sender = AsyncSender("localhost", 9999) 33 | asyncio.run(sender.send(box)) 34 | -------------------------------------------------------------------------------- /test/test_for_backend.py: -------------------------------------------------------------------------------- 1 | from build123d import * 2 | from ocp_vscode import * 3 | 4 | # set_port(3939) 5 | # set_defaults(reset_camera=Camera.KEEP, ortho=True) 6 | 7 | with BuildPart() as p: 8 | Box(10, 10, 10) 9 | with Locations((5, 0, 0)): 10 | Box(10, 10, 5) 11 | c = Circle(10) 12 | cc = extrude(c, 10) 13 | for e in cc.faces()[0].edges(): 14 | print(e.geom_type()) 15 | v = Vertex(0, 0, 0) 16 | # print(c.geom_type()) 17 | pass 18 | # line = line.edges()[0] 19 | # show(p, f1, f2) 20 | -------------------------------------------------------------------------------- /test/testextension.py: -------------------------------------------------------------------------------- 1 | from build123d import * 2 | from ocp_vscode import * 3 | 4 | set_port(3939) 5 | set_defaults(reset_camera=Camera.KEEP, ortho=True) 6 | densa = 7800 / 1e6 # carbon steel density g/mm^3 7 | densb = 2700 / 1e6 # aluminum alloy 8 | densc = 1020 / 1e6 # ABS 9 | LB = 453.592 # g/lb 10 | ms = Mode.SUBTRACT 11 | LMH, LMV = LengthMode.HORIZONTAL, LengthMode.VERTICAL 12 | # %% 13 | 14 | with BuildPart() as p: 15 | with BuildSketch(Plane.XZ) as s: 16 | with BuildLine(Plane.XZ) as l: 17 | m1 = Line((3.5 / 2, 0), (5.625 / 2, 0)) 18 | m2 = Line(m1 @ 1, m1 @ 1 + (0, 3)) 19 | m3 = Line(m2 @ 1, m2 @ 1 + (-(5.625 - 3.75) / 2, 0)) 20 | m4 = Line(m3 @ 1, m3 @ 1 + (0, -0.375)) 21 | m5 = Line(m4 @ 1, m4 @ 1 + (-0.75 / 2, 0)) 22 | m6 = Line(m5 @ 1, m5 @ 1 + (0, -3 + 1.75 + 0.375)) 23 | m7 = Line(m6 @ 1, m6 @ 1 + (0.5 / 2, 0)) 24 | m8 = Line(m7 @ 1, m1 @ 0) 25 | make_face() 26 | revolve(axis=Axis.Z, revolution_arc=90) 27 | 28 | with BuildSketch(Plane.XZ.rotated((0, 0, -45))) as s2: 29 | Rectangle(1, 1.5, align=(Align.CENTER, Align.MIN)) 30 | with Locations((0, 1)): 31 | Rectangle(2, 0.25, align=(Align.CENTER, Align.MIN)) 32 | extrude(amount=-6, mode=ms) 33 | 34 | with Locations(Plane.XY.offset(3)): 35 | with PolarLocations(4.625 / 2, 1, 45): 36 | CounterBoreHole(0.375 / 2, 0.625 / 2, 0.25) 37 | mirror(about=Plane.XZ) 38 | mirror(about=Plane.YZ) 39 | 40 | 41 | r = RegularPolygon(100, 5) 42 | pp = extrude(r, 1000) 43 | pp -= Hole(2, 50) 44 | 45 | 46 | classes = (BuildPart, BuildSketch, BuildLine) # for OCP-vscode 47 | set_colormap(ColorMap.seeded(colormap="rgb", alpha=1, seed_value="vscod")) 48 | show2(pp, name="item") 49 | 50 | # b = Box(1, 2, 3) - Plane.YZ * Cylinder(0.5, 1) 51 | # b = fillet(b.edges().filter_by(Axis.X), 0.3) 52 | # b = chamfer(b.edges().filter_by(Axis.Y), 0.1) 53 | # c = Pos(3, 0, 0) * Box(1, 1, 1) 54 | # s = Pos(0, 2, 1) * Circle(0.5) 55 | 56 | # show2(b, c, s) 57 | print() 58 | print(f"part mass = {p.part.scale(IN).volume*densb/LB}") 59 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "outDir": "out", 6 | "lib": [ 7 | "ES2020" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "strict": true /* enable all strict type-checking options */ 12 | /* Additional Checks */ 13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your extension and command. 7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Get up and running straight away 13 | 14 | * Press `F5` to open a new window with your extension loaded. 15 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 16 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension. 17 | * Find output from your extension in the debug console. 18 | 19 | ## Make changes 20 | 21 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. 22 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 23 | 24 | ## Explore the API 25 | 26 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 27 | 28 | ## Run tests 29 | 30 | * Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`. 31 | * Press `F5` to run the tests in a new window with your extension loaded. 32 | * See the output of the test result in the debug console. 33 | * Make changes to `src/test/suite/extension.test.ts` or create new test files inside the `test/suite` folder. 34 | * The provided test runner will only consider files matching the name pattern `**.test.ts`. 35 | * You can create folders inside the `test` folder to structure your tests any way you want. 36 | 37 | ## Go further 38 | 39 | * [Follow UX guidelines](https://code.visualstudio.com/api/ux-guidelines/overview) to create extensions that seamlessly integrate with VS Code's native interface and patterns. 40 | * Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). 41 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace. 42 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 43 | --------------------------------------------------------------------------------