├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── .gitignore ├── LICENSE.md ├── LICENSE.monaco ├── LICENSE.viewstl ├── Makefile ├── README.md ├── axes.scad ├── examples └── fonts.scad ├── fonts.conf ├── jest-puppeteer.config.js ├── jest.config.js ├── package.json ├── public ├── axes.glb ├── browserfs.min.js ├── complete.wav ├── favicon.ico ├── fonts │ └── InterVariable.woff2 ├── gh-fork-ribbon.min.css ├── index.html ├── libraries │ └── .gitkeep ├── logo192.png ├── logo512.png ├── manifest.json ├── model-viewer.min.js └── skybox-lights.jpg ├── src ├── components │ ├── App.tsx │ ├── CustomizerPanel.tsx │ ├── EditorPanel.tsx │ ├── ExportButton.tsx │ ├── FilePicker.tsx │ ├── Footer.tsx │ ├── HelpMenu.tsx │ ├── MultimaterialColorsDialog.tsx │ ├── PanelSwitcher.tsx │ ├── SettingsMenu.tsx │ ├── ViewerPanel.tsx │ └── contexts.ts ├── fs │ ├── BrowserFS.d.ts │ ├── filesystem.ts │ └── zip-archives.ts ├── index.css ├── index.tsx ├── io │ ├── common.ts │ ├── export_3mf.ts │ ├── export_glb.ts │ ├── image_hashes.ts │ └── import_off.ts ├── language │ ├── openscad-builtins.ts │ ├── openscad-completions.ts │ ├── openscad-editor-options.ts │ ├── openscad-language.ts │ ├── openscad-pseudoparser.ts │ └── openscad-register-language.ts ├── runner │ ├── actions.ts │ ├── openscad-runner.ts │ ├── openscad-worker.ts │ └── output-parser.ts ├── state │ ├── app-state-future.ts │ ├── app-state.ts │ ├── customizer-types.ts │ ├── deep-mutate.ts │ ├── default-scad.ts │ ├── formats.ts │ ├── fragment-state.ts │ ├── initial-state.ts │ └── model.ts └── utils.ts ├── tests └── e2e.test.js ├── tsconfig.json └── webpack.config.js /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | version: 2 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | commit-message: 9 | prefix: "chore(ci): " 10 | groups: 11 | github-actions: 12 | patterns: 13 | - "*" 14 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test Build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-24.04 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | node: [ 15 | {"name": "LTS", "version": "lts/-2"}, 16 | {"name": "latest", "version": "latest"} 17 | ] 18 | name: Node ${{ matrix.node.name }} 19 | env: 20 | CI: true 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Setup node ${{ matrix.node.name }} 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node.version }} 27 | - run: npm install 28 | - run: make public 29 | - run: npm run build 30 | - name: Archive production artifacts 31 | uses: actions/upload-artifact@v4 32 | with: 33 | name: dist-node-${{ matrix.node.name }} 34 | path: dist 35 | retention-days: 30 36 | - run: NODE_ENV=development npm run test:e2e 37 | - run: NODE_ENV=production npm run test:e2e 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | libs 3 | dist 4 | public/libraries 5 | public/openscad.js 6 | public/openscad.wasm 7 | src/wasm 8 | package-lock.json 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /LICENSE.monaco: -------------------------------------------------------------------------------- 1 | https://github.com/microsoft/monaco-editor 2 | 3 | The MIT License (MIT) 4 | 5 | Copyright (c) 2016 - present Microsoft Corporation 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /LICENSE.viewstl: -------------------------------------------------------------------------------- 1 | https://github.com/omrips/viewstl 2 | 3 | MIT License 4 | 5 | Copyright (c) 2020 omrips 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Pin WASM build to a version known to work 2 | WASM_BUILD_URL=https://files.openscad.org/playground/OpenSCAD-2025.03.25.wasm24456-WebAssembly-web.zip 3 | # WASM_SNAPSHOT_JS_URL=https://files.openscad.org/snapshots/.snapshot_wasm.js 4 | # WASM_BUILD_URL=$(shell curl ${WASM_SNAPSHOT_JS_URL} 2>/dev/null | grep https | sed -E "s/.*(https:[^']+)'.*/\1/" ) 5 | 6 | SINGLE_BRANCH_MAIN=--branch main --single-branch 7 | SINGLE_BRANCH=--branch master --single-branch 8 | SHALLOW=--depth 1 9 | 10 | SHELL:=/usr/bin/env bash 11 | WASM_BUILD=Release 12 | 13 | all: public 14 | 15 | .PHONY: public wasm 16 | public: \ 17 | src/wasm \ 18 | public/openscad.js \ 19 | public/openscad.wasm \ 20 | public/libraries/fonts.zip \ 21 | public/libraries/openscad.zip \ 22 | public/libraries/NopSCADlib.zip \ 23 | public/libraries/BOSL.zip \ 24 | public/libraries/BOSL2.zip \ 25 | public/libraries/boltsparts.zip \ 26 | public/libraries/OpenSCAD-Snippet.zip \ 27 | public/libraries/funcutils.zip \ 28 | public/libraries/FunctionalOpenSCAD.zip \ 29 | public/libraries/YAPP_Box.zip \ 30 | public/libraries/MCAD.zip \ 31 | public/libraries/smooth-prim.zip \ 32 | public/libraries/plot-function.zip \ 33 | public/libraries/openscad-tray.zip \ 34 | public/libraries/closepoints.zip \ 35 | public/libraries/Stemfie_OpenSCAD.zip \ 36 | public/libraries/pathbuilder.zip \ 37 | public/libraries/openscad_attachable_text3d.zip \ 38 | public/libraries/brailleSCAD.zip \ 39 | public/libraries/UB.scad.zip \ 40 | public/libraries/lasercut.zip 41 | 42 | clean: 43 | rm -fR libs build 44 | rm -fR public/openscad.{js,wasm} 45 | rm -fR public/libraries/*.zip 46 | rm -fR src/wasm 47 | 48 | dist/index.js: public 49 | npm run build 50 | 51 | dist/openscad-worker.js: src/openscad-worker.ts src/wasm/openscad.js 52 | npx rollup -c 53 | 54 | src/wasm: libs/openscad-wasm 55 | rm -f src/wasm 56 | ln -sf "$(shell pwd)/libs/openscad-wasm" src/wasm 57 | 58 | wasm: libs/openscad 59 | ( cd libs/openscad && ./scripts/wasm-base-docker-run.sh emcmake cmake -B build -DCMAKE_BUILD_TYPE=$(WASM_BUILD) -DEXPERIMENTAL=1 ) 60 | ( cd libs/openscad && ./scripts/wasm-base-docker-run.sh /bin/bash -c "cmake --build build -j || cmake --build build -j2 || cmake --build build" ) 61 | mkdir -p libs/openscad-wasm 62 | cp libs/openscad/build/openscad.* libs/openscad-wasm/ 63 | 64 | libs/openscad-wasm: 65 | mkdir -p libs/openscad-wasm 66 | wget ${WASM_BUILD_URL} -O libs/openscad-wasm.zip 67 | ( cd libs/openscad-wasm && unzip ../openscad-wasm.zip ) 68 | 69 | public/openscad.js: libs/openscad-wasm libs/openscad-wasm/openscad.js 70 | ln -sf libs/openscad-wasm/openscad.js public/openscad.js 71 | 72 | public/openscad.wasm: libs/openscad-wasm libs/openscad-wasm/openscad.wasm 73 | ln -sf libs/openscad-wasm/openscad.wasm public/openscad.wasm 74 | 75 | # Var w/ noto fonts 76 | NOTO_FONTS=\ 77 | libs/noto/NotoNaskhArabic-Bold.ttf \ 78 | libs/noto/NotoNaskhArabic-Regular.ttf \ 79 | libs/noto/NotoSans-Bold.ttf \ 80 | libs/noto/NotoSans-Italic.ttf \ 81 | libs/noto/NotoSans-Regular.ttf \ 82 | libs/noto/NotoSansArmenian-Bold.ttf \ 83 | libs/noto/NotoSansArmenian-Regular.ttf \ 84 | libs/noto/NotoSansBalinese-Regular.ttf \ 85 | libs/noto/NotoSansBengali-Bold.ttf \ 86 | libs/noto/NotoSansBengali-Regular.ttf \ 87 | libs/noto/NotoSansDevanagari-Bold.ttf \ 88 | libs/noto/NotoSansDevanagari-Regular.ttf \ 89 | libs/noto/NotoSansEthiopic-Bold.ttf \ 90 | libs/noto/NotoSansEthiopic-Regular.ttf \ 91 | libs/noto/NotoSansGeorgian-Bold.ttf \ 92 | libs/noto/NotoSansGeorgian-Regular.ttf \ 93 | libs/noto/NotoSansGujarati-Bold.ttf \ 94 | libs/noto/NotoSansGujarati-Regular.ttf \ 95 | libs/noto/NotoSansGurmukhi-Bold.ttf \ 96 | libs/noto/NotoSansGurmukhi-Regular.ttf \ 97 | libs/noto/NotoSansHebrew-Bold.ttf \ 98 | libs/noto/NotoSansHebrew-Regular.ttf \ 99 | libs/noto/NotoSansJavanese-Regular.ttf \ 100 | libs/noto/NotoSansKannada-Bold.ttf \ 101 | libs/noto/NotoSansKannada-Regular.ttf \ 102 | libs/noto/NotoSansKhmer-Bold.ttf \ 103 | libs/noto/NotoSansKhmer-Regular.ttf \ 104 | libs/noto/NotoSansLao-Bold.ttf \ 105 | libs/noto/NotoSansLao-Regular.ttf \ 106 | libs/noto/NotoSansMongolian-Regular.ttf \ 107 | libs/noto/NotoSansMyanmar-Bold.ttf \ 108 | libs/noto/NotoSansMyanmar-Regular.ttf \ 109 | libs/noto/NotoSansOriya-Bold.ttf \ 110 | libs/noto/NotoSansOriya-Regular.ttf \ 111 | libs/noto/NotoSansSinhala-Bold.ttf \ 112 | libs/noto/NotoSansSinhala-Regular.ttf \ 113 | libs/noto/NotoSansTamil-Bold.ttf \ 114 | libs/noto/NotoSansTamil-Regular.ttf \ 115 | libs/noto/NotoSansThai-Bold.ttf \ 116 | libs/noto/NotoSansThai-Regular.ttf \ 117 | libs/noto/NotoSansTibetan-Bold.ttf \ 118 | libs/noto/NotoSansTibetan-Regular.ttf \ 119 | libs/noto/NotoSansTifinagh-Regular.ttf \ 120 | 121 | # Way too big for now, also can't make them work yet: 122 | # libs/noto/NotoSansCJKtc-Bold.otf 123 | # libs/noto/NotoSansCJKtc-Regular.otf 124 | 125 | public/libraries/fonts.zip: $(NOTO_FONTS) libs/liberation 126 | mkdir -p public/libraries 127 | zip -r $@ -j fonts.conf libs/noto/*.ttf libs/liberation/{*.ttf,LICENSE,AUTHORS} 128 | 129 | libs/noto/%.ttf: 130 | mkdir -p libs/noto 131 | wget https://github.com/openmaptiles/fonts/raw/master/noto-sans/$(notdir $@) -O $@ 132 | 133 | libs/noto/%.otf: 134 | mkdir -p libs/noto 135 | wget https://github.com/openmaptiles/fonts/raw/master/noto-sans/$(notdir $@) -O $@ 136 | 137 | libs/liberation: 138 | git clone --recurse https://github.com/shantigilbert/liberation-fonts-ttf.git ${SHALLOW} ${SINGLE_BRANCH} $@ 139 | 140 | libs/openscad: 141 | git clone --recurse https://github.com/openscad/openscad.git ${SHALLOW} ${SINGLE_BRANCH} $@ 142 | 143 | public/libraries/openscad.zip: libs/openscad 144 | mkdir -p public/libraries 145 | ( cd libs/openscad ; zip -r - `find examples -name '*.scad' | grep -v tests` ) > public/libraries/openscad.zip 146 | 147 | libs/BOSL2: 148 | git clone --recurse https://github.com/BelfrySCAD/BOSL2.git ${SHALLOW} ${SINGLE_BRANCH} $@ 149 | 150 | public/libraries/BOSL2.zip: libs/BOSL2 151 | mkdir -p public/libraries 152 | ( cd libs/BOSL2 ; zip -r ../../public/libraries/BOSL2.zip *.scad LICENSE examples ) 153 | 154 | libs/BOSL: 155 | git clone --recurse https://github.com/revarbat/BOSL.git ${SHALLOW} ${SINGLE_BRANCH} $@ 156 | 157 | public/libraries/BOSL.zip: libs/BOSL 158 | mkdir -p public/libraries 159 | ( cd libs/BOSL ; zip -r ../../public/libraries/BOSL.zip *.scad LICENSE ) 160 | 161 | libs/NopSCADlib: 162 | git clone --recurse https://github.com/nophead/NopSCADlib.git ${SHALLOW} ${SINGLE_BRANCH} $@ 163 | 164 | public/libraries/NopSCADlib.zip: libs/NopSCADlib 165 | mkdir -p public/libraries 166 | ( cd libs/NopSCADlib ; zip -r ../../public/libraries/NopSCADlib.zip `find . -name '*.scad'` COPYING ) 167 | 168 | libs/funcutils: 169 | git clone --recurse https://github.com/thehans/funcutils.git ${SHALLOW} ${SINGLE_BRANCH} $@ 170 | 171 | public/libraries/funcutils.zip: libs/funcutils 172 | mkdir -p public/libraries 173 | ( cd libs/funcutils ; zip -r ../../public/libraries/funcutils.zip *.scad LICENSE ) 174 | 175 | libs/FunctionalOpenSCAD: 176 | git clone --recurse https://github.com/thehans/FunctionalOpenSCAD.git ${SHALLOW} ${SINGLE_BRANCH} $@ 177 | 178 | public/libraries/FunctionalOpenSCAD.zip: libs/FunctionalOpenSCAD 179 | mkdir -p public/libraries 180 | ( cd libs/FunctionalOpenSCAD ; zip -r ../../public/libraries/FunctionalOpenSCAD.zip *.scad LICENSE ) 181 | 182 | libs/YAPP_Box: 183 | git clone --recurse https://github.com/mrWheel/YAPP_Box.git ${SHALLOW} ${SINGLE_BRANCH_MAIN} $@ 184 | 185 | public/libraries/YAPP_Box.zip: libs/YAPP_Box 186 | mkdir -p public/libraries 187 | ( cd libs/YAPP_Box ; zip -r ../../public/libraries/YAPP_Box.zip `find . -name '*.scad'` LICENSE ) 188 | 189 | libs/MCAD: 190 | git clone --recurse https://github.com/openscad/MCAD.git ${SHALLOW} ${SINGLE_BRANCH} $@ 191 | 192 | public/libraries/MCAD.zip: libs/MCAD 193 | mkdir -p public/libraries 194 | ( cd libs/MCAD ; zip -r ../../public/libraries/MCAD.zip *.scad bitmap/*.scad LICENSE ) 195 | 196 | libs/boltsparts: 197 | git clone --recurse https://github.com/boltsparts/boltsparts.git ${SHALLOW} ${SINGLE_BRANCH_MAIN} $@ 198 | 199 | public/libraries/boltsparts.zip: libs/boltsparts 200 | mkdir -p public/libraries 201 | ( cd libs/boltsparts/openscad ; zip -r ../../../public/libraries/boltsparts.zip `find . -name '*.scad' | grep -v tests` ../LICENSE ) 202 | 203 | libs/OpenSCAD-Snippet: 204 | git clone --recurse https://github.com/AngeloNicoli/OpenSCAD-Snippet.git ${SHALLOW} ${SINGLE_BRANCH_MAIN} $@ 205 | 206 | public/libraries/OpenSCAD-Snippet.zip: libs/OpenSCAD-Snippet 207 | mkdir -p public/libraries 208 | ( cd libs/OpenSCAD-Snippet ; zip -r ../../public/libraries/OpenSCAD-Snippet.zip `find . -name '*.scad'` LICENSE ) 209 | 210 | libs/Stemfie_OpenSCAD: 211 | git clone --recurse https://github.com/Cantareus/Stemfie_OpenSCAD.git ${SHALLOW} ${SINGLE_BRANCH_MAIN} $@ 212 | 213 | public/libraries/Stemfie_OpenSCAD.zip: libs/Stemfie_OpenSCAD 214 | mkdir -p public/libraries 215 | ( cd libs/Stemfie_OpenSCAD ; zip -r ../../public/libraries/Stemfie_OpenSCAD.zip *.scad LICENSE ) 216 | 217 | libs/pathbuilder: 218 | git clone --recurse https://github.com/dinther/pathbuilder.git ${SHALLOW} ${SINGLE_BRANCH_MAIN} $@ 219 | 220 | public/libraries/pathbuilder.zip: libs/pathbuilder 221 | mkdir -p public/libraries 222 | ( cd libs/pathbuilder ; zip -r ../../public/libraries/pathbuilder.zip *.scad demo/*.scad LICENSE ) 223 | 224 | libs/openscad_attachable_text3d: 225 | git clone --recurse https://github.com/jon-gilbert/openscad_attachable_text3d.git ${SHALLOW} ${SINGLE_BRANCH_MAIN} $@ 226 | 227 | public/libraries/openscad_attachable_text3d.zip: libs/openscad_attachable_text3d 228 | mkdir -p public/libraries 229 | ( cd libs/openscad_attachable_text3d ; zip -r ../../public/libraries/openscad_attachable_text3d.zip *.scad LICENSE ) 230 | 231 | libs/brailleSCAD: 232 | git clone --recurse https://github.com/BelfrySCAD/brailleSCAD.git ${SHALLOW} ${SINGLE_BRANCH_MAIN} $@ 233 | 234 | public/libraries/brailleSCAD.zip: libs/brailleSCAD 235 | mkdir -p public/libraries 236 | ( cd libs/brailleSCAD ; zip -r ../../public/libraries/brailleSCAD.zip *.scad LICENSE ) 237 | 238 | # libs/threads: 239 | # git clone --recurse https://github.com/rcolyer/threads.git ${SHALLOW} ${SINGLE_BRANCH} $@ 240 | 241 | # public/libraries/threads.zip: libs/threads 242 | # mkdir -p public/libraries 243 | # ( cd libs/threads ; zip -r ../../public/libraries/threads.zip *.scad LICENSE.txt ) 244 | 245 | libs/smooth-prim: 246 | git clone --recurse https://github.com/rcolyer/smooth-prim.git ${SHALLOW} ${SINGLE_BRANCH} $@ 247 | 248 | public/libraries/smooth-prim.zip: libs/smooth-prim 249 | mkdir -p public/libraries 250 | ( cd libs/smooth-prim ; zip -r ../../public/libraries/smooth-prim.zip *.scad LICENSE.txt ) 251 | 252 | libs/plot-function: 253 | git clone --recurse https://github.com/rcolyer/plot-function.git ${SHALLOW} ${SINGLE_BRANCH} $@ 254 | 255 | public/libraries/plot-function.zip: libs/plot-function 256 | mkdir -p public/libraries 257 | ( cd libs/plot-function ; zip -r ../../public/libraries/plot-function.zip *.scad LICENSE.txt ) 258 | 259 | libs/closepoints: 260 | git clone --recurse https://github.com/rcolyer/closepoints.git ${SHALLOW} ${SINGLE_BRANCH} $@ 261 | 262 | public/libraries/closepoints.zip: libs/closepoints 263 | mkdir -p public/libraries 264 | ( cd libs/closepoints ; zip -r ../../public/libraries/closepoints.zip *.scad LICENSE.txt ) 265 | 266 | libs/UB.scad: 267 | git clone --recurse https://github.com/UBaer21/UB.scad.git ${SHALLOW} ${SINGLE_BRANCH_MAIN} $@ 268 | 269 | public/libraries/UB.scad.zip: libs/UB.scad 270 | mkdir -p public/libraries 271 | ( cd libs/UB.scad ; zip -r ../../public/libraries/UB.scad.zip libraries/*.scad LICENSE examples/UBexamples ) 272 | 273 | libs/openscad-tray: 274 | git clone --recurse https://github.com/sofian/openscad-tray.git ${SHALLOW} ${SINGLE_BRANCH_MAIN} $@ 275 | 276 | public/libraries/openscad-tray.zip: libs/openscad-tray 277 | mkdir -p public/libraries 278 | ( cd libs/openscad-tray ; zip -r ../../public/libraries/openscad-tray.zip *.scad LICENSE ) 279 | 280 | libs/lasercut: 281 | git clone --recurse https://github.com/bmsleight/lasercut.git ${SHALLOW} ${SINGLE_BRANCH} $@ 282 | public/libraries/lasercut.zip: libs/lasercut 283 | mkdir -p public/libraries 284 | ( cd libs/lasercut ; zip -r ../../public/libraries/lasercut.zip *.scad LICENSE ) 285 | 286 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenSCAD Playground 2 | 3 | [Open the Demo](https://ochafik.com/openscad2) 4 | 5 | 6 | image 7 | 8 | 9 | This is a limited port of [OpenSCAD](https://openscad.org) to WebAssembly, using at its core a headless WASM build of OpenSCAD ([done by @DSchroer](https://github.com/DSchroer/openscad-wasm)), wrapped in a UI made of pretty [PrimeReact](https://github.com/primefaces/primereact) components, a [React Monaco editor](https://github.com/react-monaco-editor/react-monaco-editor) (VS Codesque power!), and an interactive [model-viewer](https://modelviewer.dev/) renderer. 10 | 11 | It defaults to the [Manifold backend](https://github.com/openscad/openscad/pull/4533) so it's **super** fast. 12 | 13 | Enjoy! 14 | 15 | Licenses: see [LICENSES](./LICENSE). 16 | 17 | ## Features 18 | 19 | - Automatic preview on edit (F5), and full rendering on Ctrl+Enter (or F6). Using a trick to force $preview=true. 20 | - [Customizer](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Customizer) support 21 | - Syntax highlighting 22 | - Ships with many standard SCAD libraries (can browse through them in the UI) 23 | - Autocomplete of imports 24 | - Autocomplete of symbols / function calls (pseudo-parses file and its transitive imports) 25 | - Responsive layout. On small screens editor and viewer are stacked onto each other, while on larger screens they can be side-by-side 26 | - Installable as a PWA (then persists edits in localStorage instead of the hash fragment). On iOS just open the sharing panel and tap "Add to Home Screen". *Should not* require any internet connectivity once cached. 27 | 28 | ## Roadmap 29 | 30 | - [x] Add tests! 31 | - [x] Persist camera state 32 | - [x] Support 2D somehow? (e.g. add option in OpenSCAD to output 2D geometry as non-closed polysets, or to auto-extrude by some height) 33 | - [x] Proper Preview rendering: have OpenSCAD export the preview scene to a rich format (e.g. glTF, with some parts being translucent when prefixed w/ % modifier) and display it using https://modelviewer.dev/ maybe) 34 | - ~~Rebuild w/ (and sync) ochafik@'s filtered kernel (https://github.com/openscad/openscad/pull/4160) to fix(ish) 2D operations~~ 35 | - [x] Bundle more examples (ask users to contribute) 36 | - Animation rendering (And other formats than STL) 37 | - [x] Compress URL fragment 38 | - [x] Mobile (iOS) editing support: switch to https://www.npmjs.com/package/react-codemirror ? 39 | - [ ] Replace Makefile w/ something that reads the libs metadata 40 | - [ ] Merge modifiers rendering code to openscad 41 | - Model /home fs in shared state. have two clear paths: /libraries for builtins, and /home for user data. State pointing to /libraries paths needs not store the data except if there's overrides (flagged as modifications in the file picker) 42 | - Drag and drop of files (SCAD, STL, etc) and Zip archives. For assets, auto insert the corresponding import. 43 | - Fuller PWA support w/ link Sharing, File opening / association to *.scad files... 44 | - Look into accessibility 45 | - Setup [OPENSCADPATH](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Libraries#Setting_OPENSCADPATH) env var w/ Emscripten to ensure examples that include assets / import local files will run fine. 46 | - Detect which bundled libraries are included / used in the sources and only download these rather than wait for all of the zips. Means the file explorer would need to be more lazy or have some prebuilt hierarchy. 47 | - Preparse builtin libraries definitions at compile time, ship the JSON. 48 | 49 | ## Building 50 | 51 | Prerequisites: 52 | * wget 53 | * GNU make 54 | * npm 55 | * Docker able to run amd64 containers. If running on a different platform (including Silicon Mac), you can add support for amd64 images through QEMU with: 56 | 57 | ```bash 58 | docker run --privileged --rm tonistiigi/binfmt --install all 59 | ``` 60 | 61 | Local dev: 62 | 63 | ```bash 64 | make public 65 | npm install 66 | npm start 67 | # http://localhost:4000/ 68 | ``` 69 | 70 | Local prod (test both the different inlining and serving under a prefix): 71 | 72 | ```bash 73 | make public 74 | npm install 75 | npm run start:prod 76 | # http://localhost:3000/dist/ 77 | ``` 78 | 79 | Deployment (edit "homepage" in `package.json` to match your deployment root!): 80 | 81 | ```bash 82 | make public 83 | npm install 84 | NODE_ENV=production npm run build 85 | 86 | rm -fR ../ochafik.github.io/openscad2 && cp -R dist ../ochafik.github.io/openscad2 87 | # Now commit and push changes, wait for site update and enjoy! 88 | ``` 89 | 90 | ## Build your own WASM binary 91 | 92 | [Makefile](./Makefile) fetches a prebuilt OpenSCAD web WASM binary, but you can build your own in a couple of minutes: 93 | 94 | - **Optional**: use your own openscad fork / branch: 95 | 96 | ```bash 97 | rm -fR libs/openscad 98 | ln -s $PWD/../absolute/path/to/your/openscad libs/openscad 99 | 100 | # If you had a native build directory, delete it. 101 | rm -fR libs/openscad/build 102 | ``` 103 | 104 | - Build WASM binary (add `WASM_BUILD=Debug` argument if you'd like to debug any cryptic crashes): 105 | 106 | ```bash 107 | make wasm 108 | ``` 109 | 110 | - Then continue the build: 111 | 112 | ```bash 113 | make public 114 | npm start 115 | ``` 116 | 117 | ## Adding OpenSCAD libraries 118 | 119 | You'll need to update 3 files (search for BOSL2 for an example): 120 | 121 | - [Makefile](./Makefile): to pull the library's code (optionally alias some files for easier imports) and package it as a `.zip` archive 122 | 123 | - [src/fs/zip-archives.ts](./src/fs/zip-archives.ts): to use the `.zip` archive in the UI (both for file explorer and automatic imports mounting) 124 | 125 | - [LICENSE.md](./LICENSE.md): most libraries require proper disclosure of their usage and of their license. If a license is unique, paste it in full, otherwise, link to one of the standard ones already there. 126 | 127 | Send us a PR, then once it's merged request an update to the hosted https://ochafik.com/openscad2 demo. 128 | -------------------------------------------------------------------------------- /axes.scad: -------------------------------------------------------------------------------- 1 | module arrow(length=30, shaft_radius=1, head_radius=2, head_length=5) { 2 | cylinder(h=length-head_length, r=shaft_radius, center=false); 3 | 4 | translate([0, 0, length-head_length]) 5 | cylinder(h=head_length, r1=head_radius, r2=0, center=false); 6 | } 7 | 8 | color("red") 9 | rotate([0, 90, 0]) 10 | arrow(); 11 | 12 | color("green") 13 | rotate([-90, 0, 0]) 14 | arrow(); 15 | 16 | color("blue") 17 | rotate([0, 0, 90]) 18 | arrow(); 19 | 20 | module letter(text) 21 | linear_extrude(1) text(text, halign="center", valign="center"); 22 | 23 | letter_dist = 38; 24 | union() { 25 | color("red") 26 | translate([letter_dist, 0, 0]) 27 | rotate([45, 0, 45]) 28 | letter("X"); 29 | color("green") 30 | rotate([0, 0, 90]) 31 | translate([letter_dist, 0, 0]) 32 | rotate([45, 0, -45]) 33 | letter("Y"); 34 | color("blue") 35 | rotate([0, -90, 0]) 36 | translate([letter_dist, 0, 0]) 37 | rotate([90+45, 0, 0]) 38 | rotate([0, 0, -90]) 39 | letter("Z"); 40 | } 41 | 42 | color("grey") 43 | cube(10, center=true); 44 | 45 | color([0, 0, 0, $preview ? 0.05 : 0]) 46 | sphere(r=43); 47 | -------------------------------------------------------------------------------- /examples/fonts.scad: -------------------------------------------------------------------------------- 1 | include 2 | include 3 | 4 | script="Arabic"; // [Arabic,Armenian,Balinese,Bengali,Devanagari,English,Ethiopic,Georgian,Gujarati,Gurmukhi,Hebrew,Javanese,Kannada,Khmer,Lao,Mongolian,Myanmar,Oriya,Sinhala,Tamil,Thai,Tibetan,Tifinagh] 5 | 6 | style="Regular"; // [Regular,Bold,Italic] 7 | 8 | fonts=[ 9 | "Noto Naskh Arabic", 10 | // "Noto Naskh Arabic:style=Bold", 11 | "Noto Sans", 12 | // "Noto Sans:style=Bold", 13 | // "Noto Sans:style=Italic", 14 | "Noto SansArmenian", 15 | // "Noto SansArmenian:style=Bold", 16 | "Noto SansBalinese", 17 | "Noto SansBengali", 18 | // "Noto SansBengali:style=Bold", 19 | "Noto Sans CJK TC", 20 | "Noto SansDevanagari", 21 | // "Noto SansDevanagari:style=Bold", 22 | "Noto SansEthiopic", 23 | // "Noto SansEthiopic:style=Bold", 24 | "Noto SansGeorgian", 25 | // "Noto SansGeorgian:style=Bold", 26 | "Noto SansGujarati", 27 | // "Noto SansGujarati:style=Bold", 28 | "Noto SansGurmukhi", 29 | // "Noto SansGurmukhi:style=Bold", 30 | "Noto SansHebrew", 31 | // "Noto SansHebrew:style=Bold", 32 | "Noto SansJavanese", 33 | "Noto SansKannada", 34 | // "Noto SansKannada:style=Bold", 35 | "Noto SansKhmer", 36 | // "Noto SansKhmer:style=Bold", 37 | "Noto SansLao", 38 | // "Noto SansLao:style=Bold", 39 | "Noto SansMongolian", 40 | "Noto SansMyanmar", 41 | // "Noto SansMyanmar:style=Bold", 42 | "Noto SansOriya", 43 | // "Noto SansOriya:style=Bold", 44 | "Noto SansSinhala", 45 | // "Noto SansSinhala:style=Bold", 46 | "Noto SansTamil", 47 | // "Noto SansTamil:style=Bold", 48 | "Noto SansThai", 49 | // "Noto SansThai:style=Bold", 50 | "Noto SansTibetan", 51 | // "Noto SansTibetan:style=Bold", 52 | "Noto SansTifinagh", 53 | ]; 54 | 55 | function pick_font(language, i=0) = 56 | i < 0 || i > len(fonts) 57 | ? "Noto Sans" + str(i) 58 | : is_undef(str_find(fonts[i], language)) 59 | ? pick_font(language, i=i+1) 60 | : fonts[i]; 61 | 62 | font = pick_font(script); 63 | 64 | greeting = struct_val([ 65 | ["Arabic", "سلام"], 66 | ["Armenian", "Բարև"], 67 | ["Balinese", "ᬒᬁᬓᬭ"], 68 | ["Bengali", "নমস্কার"], 69 | // ["CJK TC": "你好"], 70 | ["Devanagari", "नमस्ते"], 71 | ["English", "Hello"], 72 | ["Ethiopic", "ሰላም"], 73 | ["Georgian", "გამარჯობა"], 74 | ["Gujarati", "નમસ્તે"], 75 | ["Gurmukhi", "ਸਤ ਸ੍ਰੀ ਅਕਾਲ"], 76 | ["Hebrew", "שלום"], 77 | ["Javanese", "ꦱꦸꦒꦼꦁꦫꦮꦸꦃ"], 78 | ["Kannada", "ನಮಸ್ಕಾರ"], 79 | ["Khmer", "សួស្តី"], 80 | ["Lao", "ສະບາຍດີ"], 81 | ["Mongolian", "ᠰᠠᠶᠢᠨ ᠪᠠᠶᠢᠨᠠ ᠣᠣ"], 82 | ["Myanmar", "မင်္ဂလာပါ"], 83 | ["Oriya", "ନମସ୍କାର"], 84 | ["Sinhala", "ආයුබෝවන්"], 85 | ["Tamil", "வணக்கம்"], 86 | ["Thai", "สวัสดี"], 87 | ["Tibetan", "བཀྲ་ཤིས་བདེ་ལེགས།"], 88 | ["Tifinagh", "ⴰⵣⵓⵍ"], 89 | ], script, "Hello"); 90 | 91 | direction = struct_val([ 92 | ["Arabic", "rtl"], 93 | ["Hebrew", "rtl"], 94 | ], script, "ltr"); 95 | 96 | echo(greeting=greeting, 97 | font=font, 98 | script=script, 99 | direction=direction, 100 | style=style); 101 | 102 | color("gray") 103 | translate([0, debug ? -60 : -20, 0]) 104 | linear_extrude(1) 105 | text( 106 | greeting, 107 | font=str(font, ":style=", style), 108 | direction=direction, 109 | script=script, 110 | halign="center", 111 | valign="center"); 112 | 113 | // You can find the original for the following example in the file explorer above, 114 | // under openscad / examples / Basic / CSG-modules.scad 115 | 116 | // CSG-modules.scad - Basic usage of modules, if, color, $fs/$fa 117 | 118 | // Change this to false to remove the helper geometry 119 | debug = true; 120 | 121 | // Global resolution 122 | $fs=$preview ? 1 : 0.1; // Don't generate smaller facets than 0.1 mm 123 | $fa=$preview ? 15 : 5; // Don't generate larger angles than 5 degrees 124 | 125 | rotate([-90, 0, 0]) { 126 | // Main geometry 127 | difference() { 128 | intersection() { 129 | body(); 130 | intersector(); 131 | } 132 | holes(); 133 | } 134 | 135 | // Helpers 136 | if (debug) helpers(); 137 | } 138 | 139 | // Core geometric primitives. 140 | // These can be modified to create variations of the final object 141 | 142 | module body() { 143 | color("Blue") sphere(10); 144 | } 145 | 146 | module intersector() { 147 | color("Red") cube(15, center=true); 148 | } 149 | 150 | module holeObject() { 151 | color("Lime") cylinder(h=20, r=5, center=true); 152 | } 153 | 154 | // Various modules for visualizing intermediate components 155 | 156 | module intersected() { 157 | intersection() { 158 | body(); 159 | intersector(); 160 | } 161 | } 162 | 163 | module holeA() rotate([0,90,0]) holeObject(); 164 | module holeB() rotate([90,0,0]) holeObject(); 165 | module holeC() holeObject(); 166 | 167 | module holes() { 168 | union() { 169 | holeA(); 170 | holeB(); 171 | holeC(); 172 | } 173 | } 174 | 175 | module helpers() { 176 | // Inner module since it's only needed inside helpers 177 | module line() color("Black") cylinder(r=1, h=10, center=true); 178 | 179 | scale(0.5) { 180 | translate([-30,0,-40]) { 181 | intersected(); 182 | translate([-15,0,-35]) body(); 183 | translate([15,0,-35]) intersector(); 184 | translate([-7.5,0,-17.5]) rotate([0,30,0]) line(); 185 | translate([7.5,0,-17.5]) rotate([0,-30,0]) line(); 186 | } 187 | translate([30,0,-40]) { 188 | holes(); 189 | translate([-10,0,-35]) holeA(); 190 | translate([10,0,-35]) holeB(); 191 | translate([30,0,-35]) holeC(); 192 | translate([5,0,-17.5]) rotate([0,-20,0]) line(); 193 | translate([-5,0,-17.5]) rotate([0,30,0]) line(); 194 | translate([15,0,-17.5]) rotate([0,-45,0]) line(); 195 | } 196 | translate([-20,0,-22.5]) rotate([0,45,0]) line(); 197 | translate([20,0,-22.5]) rotate([0,-45,0]) line(); 198 | } 199 | } 200 | 201 | echo(version=version()); 202 | // Written by Marius Kintel 203 | // 204 | // To the extent possible under law, the author(s) have dedicated all 205 | // copyright and related and neighboring rights to this software to the 206 | // public domain worldwide. This software is distributed without any 207 | // warranty. 208 | // 209 | // You should have received a copy of the CC0 Public Domain 210 | // Dedication along with this software. 211 | // If not, see . 212 | -------------------------------------------------------------------------------- /fonts.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /jest-puppeteer.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('jest-environment-puppeteer').JestPuppeteerConfig} */ 2 | const config = { 3 | launch: { 4 | headless: process.env.CI === "true", 5 | args: [ 6 | // https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md#what-if-i-dont-have-root-access-to-the-machine-and-cant-install-anything 7 | '--no-sandbox', 8 | ], 9 | }, 10 | server: { 11 | command: `npm run start:${process.env.NODE_ENV}`, 12 | port: process.env.NODE_ENV === 'production' ? 3000 : 4000, 13 | launchTimeout: 180000, 14 | }, 15 | }; 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('jest').Config} */ 2 | const config = { 3 | preset: "jest-puppeteer", 4 | testMatch: [ 5 | "**/tests/**/*.js", 6 | ], 7 | }; 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openscad-playground", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "homepage": "https://ochafik.com/openscad2/", 7 | "dependencies": { 8 | "@gltf-transform/core": "^4.1.1", 9 | "@gltf-transform/extensions": "^4.1.1", 10 | "@monaco-editor/loader": "^1.4.0", 11 | "@monaco-editor/react": "^4.6.0", 12 | "@testing-library/jest-dom": "^5.17.0", 13 | "@testing-library/react": "^13.4.0", 14 | "@testing-library/user-event": "^13.5.0", 15 | "blurhash": "^2.0.5", 16 | "chroma-js": "^3.1.2", 17 | "debug": "^4.4.0", 18 | "jszip": "^3.10.1", 19 | "monaco-editor": "^0.52.2", 20 | "primeflex": "^3.3.1", 21 | "primeicons": "^7.0.0", 22 | "primereact": "^10.8.5", 23 | "react": "^18.3.1", 24 | "react-dom": "^18.3.1", 25 | "thumbhash": "^0.1.1", 26 | "uuid": "^11.0.3", 27 | "uzip": "^0.20201231.0" 28 | }, 29 | "scripts": { 30 | "test:e2e": "jest", 31 | "start:development": "npx webpack serve --mode=development", 32 | "start:production": "NODE_ENV=production PUBLIC_URL=http://localhost:3000/dist/ npm run build && npx serve", 33 | "start": "npm run start:development", 34 | "build": "NODE_ENV=production webpack --mode=production" 35 | }, 36 | "eslintConfig": { 37 | "extends": [ 38 | "react-app", 39 | "react-app/jest" 40 | ] 41 | }, 42 | "browserslist": { 43 | "production": [ 44 | ">0.2%", 45 | "not dead", 46 | "not op_mini all" 47 | ], 48 | "development": [ 49 | "last 1 chrome version", 50 | "last 1 firefox version", 51 | "last 1 safari version" 52 | ] 53 | }, 54 | "engines": { 55 | "node": ">=18.12.0" 56 | }, 57 | "devDependencies": { 58 | "@types/chroma-js": "^2.4.5", 59 | "@types/debug": "^4.1.12", 60 | "@types/filesystem": "^0.0.36", 61 | "@types/jest": "^29.5.14", 62 | "@types/node": "^22.10.2", 63 | "@types/react": "^18.3.18", 64 | "@types/react-dom": "^18.3.5", 65 | "@types/uzip": "^0.20201231.2", 66 | "@types/web": "^0.0.140", 67 | "copy-webpack-plugin": "^12.0.2", 68 | "css-loader": "^7.1.2", 69 | "jest": "^29.7.0", 70 | "jest-puppeteer": "^11.0.0", 71 | "livereload": "^0.9.3", 72 | "puppeteer": "^23.11.1", 73 | "serve": "^14.2.4", 74 | "style-loader": "^4.0.0", 75 | "ts-loader": "^9.5.1", 76 | "tslib": "^2.8.1", 77 | "webpack": "^5.97.1", 78 | "webpack-cli": "^6.0.1", 79 | "webpack-dev-server": "^5.2.0", 80 | "workbox-webpack-plugin": "^7.3.0" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /public/axes.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscad/openscad-playground/3da8d92aeab41d4aff3c0f65f821749a0f5e7a9a/public/axes.glb -------------------------------------------------------------------------------- /public/complete.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscad/openscad-playground/3da8d92aeab41d4aff3c0f65f821749a0f5e7a9a/public/complete.wav -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscad/openscad-playground/3da8d92aeab41d4aff3c0f65f821749a0f5e7a9a/public/favicon.ico -------------------------------------------------------------------------------- /public/fonts/InterVariable.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscad/openscad-playground/3da8d92aeab41d4aff3c0f65f821749a0f5e7a9a/public/fonts/InterVariable.woff2 -------------------------------------------------------------------------------- /public/gh-fork-ribbon.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * "Fork me on GitHub" CSS ribbon v0.2.3 | MIT License 3 | * https://github.com/simonwhitaker/github-fork-ribbon-css 4 | */.github-fork-ribbon{width:12.1em;height:12.1em;position:absolute;overflow:hidden;top:0;right:0;z-index:9999;pointer-events:none;font-size:13px;text-decoration:none;text-indent:-999999px}.github-fork-ribbon.fixed{position:fixed}.github-fork-ribbon:active,.github-fork-ribbon:hover{background-color:rgba(0,0,0,0)}.github-fork-ribbon:after,.github-fork-ribbon:before{position:absolute;display:block;width:15.38em;height:1.54em;top:3.23em;right:-3.23em;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg)}.github-fork-ribbon:before{content:"";padding:.38em 0;background-color:#a00;background-image:-webkit-gradient(linear,left top,left bottom,from(rgba(0,0,0,0)),to(rgba(0,0,0,.15)));background-image:-webkit-linear-gradient(top,rgba(0,0,0,0),rgba(0,0,0,.15));background-image:-moz-linear-gradient(top,rgba(0,0,0,0),rgba(0,0,0,.15));background-image:-ms-linear-gradient(top,rgba(0,0,0,0),rgba(0,0,0,.15));background-image:-o-linear-gradient(top,rgba(0,0,0,0),rgba(0,0,0,.15));background-image:linear-gradient(to bottom,rgba(0,0,0,0),rgba(0,0,0,.15));-webkit-box-shadow:0 .15em .23em 0 rgba(0,0,0,.5);-moz-box-shadow:0 .15em .23em 0 rgba(0,0,0,.5);box-shadow:0 .15em .23em 0 rgba(0,0,0,.5);pointer-events:auto}.github-fork-ribbon:after{content:attr(data-ribbon);color:#fff;font:700 1em "Helvetica Neue",Helvetica,Arial,sans-serif;line-height:1.54em;text-decoration:none;text-shadow:0 -.08em rgba(0,0,0,.5);text-align:center;text-indent:0;padding:.15em 0;margin:.15em 0;border-width:.08em 0;border-style:dotted;border-color:#fff;border-color:rgba(255,255,255,.7)}.github-fork-ribbon.left-bottom,.github-fork-ribbon.left-top{right:auto;left:0}.github-fork-ribbon.left-bottom,.github-fork-ribbon.right-bottom{top:auto;bottom:0}.github-fork-ribbon.left-bottom:after,.github-fork-ribbon.left-bottom:before,.github-fork-ribbon.left-top:after,.github-fork-ribbon.left-top:before{right:auto;left:-3.23em}.github-fork-ribbon.left-bottom:after,.github-fork-ribbon.left-bottom:before,.github-fork-ribbon.right-bottom:after,.github-fork-ribbon.right-bottom:before{top:auto;bottom:3.23em}.github-fork-ribbon.left-top:after,.github-fork-ribbon.left-top:before,.github-fork-ribbon.right-bottom:after,.github-fork-ribbon.right-bottom:before{-webkit-transform:rotate(-45deg);-moz-transform:rotate(-45deg);-ms-transform:rotate(-45deg);-o-transform:rotate(-45deg);transform:rotate(-45deg)} 5 | /*# sourceMappingURL=gh-fork-ribbon.min.css.map */ -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | OpenSCAD Playground 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 | 122 | 123 | 124 | 128 | 132 | Fork me on GitHub 133 | 134 | 135 | 136 |
137 | 138 | 139 | -------------------------------------------------------------------------------- /public/libraries/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscad/openscad-playground/3da8d92aeab41d4aff3c0f65f821749a0f5e7a9a/public/libraries/.gitkeep -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscad/openscad-playground/3da8d92aeab41d4aff3c0f65f821749a0f5e7a9a/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscad/openscad-playground/3da8d92aeab41d4aff3c0f65f821749a0f5e7a9a/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "OpenSCAD", 3 | "name": "OpenSCAD Playground", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/skybox-lights.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscad/openscad-playground/3da8d92aeab41d4aff3c0f65f821749a0f5e7a9a/public/skybox-lights.jpg -------------------------------------------------------------------------------- /src/components/App.tsx: -------------------------------------------------------------------------------- 1 | // Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING. 2 | 3 | import React, { CSSProperties, useEffect, useState } from 'react'; 4 | import {MultiLayoutComponentId, State, StatePersister} from '../state/app-state' 5 | import { Model } from '../state/model'; 6 | import EditorPanel from './EditorPanel'; 7 | import ViewerPanel from './ViewerPanel'; 8 | import Footer from './Footer'; 9 | import { ModelContext, FSContext } from './contexts'; 10 | import PanelSwitcher from './PanelSwitcher'; 11 | import { ConfirmDialog } from 'primereact/confirmdialog'; 12 | import CustomizerPanel from './CustomizerPanel'; 13 | 14 | 15 | export function App({initialState, statePersister, fs}: {initialState: State, statePersister: StatePersister, fs: FS}) { 16 | const [state, setState] = useState(initialState); 17 | 18 | const model = new Model(fs, state, setState, statePersister); 19 | useEffect(() => model.init()); 20 | 21 | useEffect(() => { 22 | const handleKeyDown = (event: KeyboardEvent) => { 23 | if (event.key === 'F5') { 24 | event.preventDefault(); 25 | model.render({isPreview: true, now: true}) 26 | } else if (event.key === 'F6') { 27 | event.preventDefault(); 28 | model.render({isPreview: false, now: true}) 29 | } else if (event.key === 'F7') { 30 | event.preventDefault(); 31 | model.export(); 32 | } 33 | }; 34 | window.addEventListener('keydown', handleKeyDown); 35 | return () => { 36 | window.removeEventListener('keydown', handleKeyDown); 37 | }; 38 | }, []); 39 | 40 | const zIndexOfPanelsDependingOnFocus = { 41 | editor: { 42 | editor: 3, 43 | viewer: 1, 44 | customizer: 0, 45 | }, 46 | viewer: { 47 | editor: 2, 48 | viewer: 3, 49 | customizer: 1, 50 | }, 51 | customizer: { 52 | editor: 0, 53 | viewer: 1, 54 | customizer: 3, 55 | } 56 | } 57 | 58 | const layout = state.view.layout 59 | const mode = state.view.layout.mode; 60 | function getPanelStyle(id: MultiLayoutComponentId): CSSProperties { 61 | if (layout.mode === 'multi') { 62 | const itemCount = (layout.editor ? 1 : 0) + (layout.viewer ? 1 : 0) + (layout.customizer ? 1 : 0) 63 | return { 64 | flex: 1, 65 | maxWidth: Math.floor(100/itemCount) + '%', 66 | display: (state.view.layout as any)[id] ? 'flex' : 'none' 67 | } 68 | } else { 69 | return { 70 | flex: 1, 71 | zIndex: Number((zIndexOfPanelsDependingOnFocus as any)[id][layout.focus]), 72 | } 73 | } 74 | } 75 | 76 | return ( 77 | 78 | 79 |
82 | 83 | 84 | 85 |
90 | 91 | 96 | 97 | 102 |
103 | 104 |
105 | 106 |
107 |
108 |
109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /src/components/CustomizerPanel.tsx: -------------------------------------------------------------------------------- 1 | // Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING. 2 | 3 | import React, { CSSProperties, useContext } from 'react'; 4 | import { ModelContext } from './contexts.ts'; 5 | 6 | import { Dropdown } from 'primereact/dropdown'; 7 | import { Slider } from 'primereact/slider'; 8 | import { Checkbox } from 'primereact/checkbox'; 9 | import { InputNumber } from 'primereact/inputnumber'; 10 | import { InputText } from 'primereact/inputtext'; 11 | import { Fieldset } from 'primereact/fieldset'; 12 | import { Parameter } from '../state/customizer-types.ts'; 13 | import { Button } from 'primereact/button'; 14 | 15 | export default function CustomizerPanel({className, style}: {className?: string, style?: CSSProperties}) { 16 | 17 | const model = useContext(ModelContext); 18 | if (!model) throw new Error('No model'); 19 | 20 | const state = model.state; 21 | 22 | const handleChange = (name: string, value: any) => { 23 | model.setVar(name, value); 24 | }; 25 | 26 | const groupedParameters = (state.parameterSet?.parameters ?? []).reduce((acc, param) => { 27 | if (!acc[param.group]) { 28 | acc[param.group] = []; 29 | } 30 | acc[param.group].push(param); 31 | return acc; 32 | }, {} as { [key: string]: any[] }); 33 | 34 | const groups = Object.entries(groupedParameters); 35 | const collapsedTabSet = new Set(state.view.collapsedCustomizerTabs ?? []); 36 | const setTabOpen = (name: string, open: boolean) => { 37 | if (open) { 38 | collapsedTabSet.delete(name); 39 | } else { 40 | collapsedTabSet.add(name) 41 | } 42 | model.mutate(s => s.view.collapsedCustomizerTabs = Array.from(collapsedTabSet)); 43 | } 44 | 45 | return ( 46 |
56 | {groups.map(([group, params]) => ( 57 |
setTabOpen(group, false)} 64 | onExpand={() => setTabOpen(group, true)} 65 | collapsed={collapsedTabSet.has(group)} 66 | key={group} 67 | legend={group} 68 | toggleable={true}> 69 | {params.map((param) => ( 70 | 75 | ))} 76 |
77 | ))} 78 |
79 | ); 80 | }; 81 | 82 | function ParameterInput({param, value, className, style, handleChange}: {param: Parameter, value: any, className?: string, style?: CSSProperties, handleChange: (key: string, value: any) => void}) { 83 | return ( 84 |
91 |
100 |
106 | 107 |
{param.caption}
108 |
109 |
116 | {param.type === 'number' && 'options' in param && ( 117 | handleChange(param.name, e.value)} 122 | optionLabel="name" 123 | optionValue="value" 124 | /> 125 | )} 126 | {param.type === 'string' && param.options && ( 127 | handleChange(param.name, e.value)} 131 | optionLabel="name" 132 | optionValue="value" 133 | /> 134 | )} 135 | {param.type === 'boolean' && ( 136 | handleChange(param.name, e.checked)} 139 | /> 140 | )} 141 | {!Array.isArray(param.initial) && param.type === 'number' && !('options' in param) && ( 142 | handleChange(param.name, e.value)} 147 | /> 148 | )} 149 | {param.type === 'string' && !param.options && ( 150 | handleChange(param.name, e.target.value)} 154 | /> 155 | )} 156 | {Array.isArray(param.initial) && 'min' in param && ( 157 |
162 | {param.initial.map((_, index) => ( 163 | { 173 | const newArray = [...(value ?? param.initial)]; 174 | newArray[index] = e.value; 175 | handleChange(param.name, newArray); 176 | }} 177 | /> 178 | ))} 179 |
180 | )} 181 |
191 |
192 | {!Array.isArray(param.initial) && param.type === 'number' && param.min !== undefined && ( 193 | handleChange(param.name, e.value)} 204 | /> 205 | )} 206 |
207 | ); 208 | } -------------------------------------------------------------------------------- /src/components/EditorPanel.tsx: -------------------------------------------------------------------------------- 1 | // Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING. 2 | 3 | import React, { CSSProperties, useContext, useRef, useState } from 'react'; 4 | import Editor, { loader, Monaco } from '@monaco-editor/react'; 5 | import openscadEditorOptions from '../language/openscad-editor-options.ts'; 6 | import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; 7 | import { InputTextarea } from 'primereact/inputtextarea'; 8 | import { Button } from 'primereact/button'; 9 | import { MenuItem } from 'primereact/menuitem'; 10 | import { Menu } from 'primereact/menu'; 11 | import { buildUrlForStateParams } from '../state/fragment-state.ts'; 12 | import { getBlankProjectState, defaultSourcePath } from '../state/initial-state.ts'; 13 | import { ModelContext, FSContext } from './contexts.ts'; 14 | import FilePicker, { } from './FilePicker.tsx'; 15 | 16 | // const isMonacoSupported = false; 17 | const isMonacoSupported = (() => { 18 | const ua = window.navigator.userAgent; 19 | const iosWk = ua.match(/iPad|iPhone/i) && ua.match(/WebKit/i); 20 | return !iosWk; 21 | })(); 22 | 23 | let monacoInstance: Monaco | null = null; 24 | if (isMonacoSupported) { 25 | loader.init().then(mi => monacoInstance = mi); 26 | } 27 | 28 | export default function EditorPanel({className, style}: {className?: string, style?: CSSProperties}) { 29 | 30 | const model = useContext(ModelContext); 31 | if (!model) throw new Error('No model'); 32 | 33 | const menu = useRef(null); 34 | 35 | const state = model.state; 36 | 37 | const [editor, setEditor] = useState(null as monaco.editor.IStandaloneCodeEditor | null) 38 | 39 | if (editor) { 40 | const checkerRun = state.lastCheckerRun; 41 | const editorModel = editor.getModel(); 42 | if (editorModel) { 43 | if (checkerRun && monacoInstance) { 44 | monacoInstance.editor.setModelMarkers(editorModel, 'openscad', checkerRun.markers); 45 | } 46 | } 47 | } 48 | 49 | const onMount = (editor: monaco.editor.IStandaloneCodeEditor) => { 50 | editor.addAction({ 51 | id: "openscad-render", 52 | label: "Render OpenSCAD", 53 | run: () => model.render({isPreview: false, now: true}) 54 | }); 55 | editor.addAction({ 56 | id: "openscad-preview", 57 | label: "Preview OpenSCAD", 58 | run: () => model.render({isPreview: true, now: true}) 59 | }); 60 | editor.addAction({ 61 | id: "openscad-save-do-nothing", 62 | label: "Save (disabled)", 63 | keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS], 64 | run: () => {} 65 | }); 66 | editor.addAction({ 67 | id: "openscad-save-project", 68 | label: "Save OpenSCAD project", 69 | keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyS], 70 | run: () => model.saveProject() 71 | }); 72 | setEditor(editor) 73 | } 74 | 75 | return ( 76 |
84 |
87 | 88 | window.open(buildUrlForStateParams(getBlankProjectState()), '_blank'), 93 | target: '_blank', 94 | }, 95 | { 96 | // TODO: share text, title and rendering image 97 | // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share 98 | label: 'Share project', 99 | icon: 'pi pi-share-alt', 100 | disabled: true, 101 | }, 102 | { 103 | separator: true 104 | }, 105 | { 106 | // TODO: popup to ask for file name 107 | label: "New file", 108 | icon: 'pi pi-plus', 109 | disabled: true, 110 | }, 111 | { 112 | label: "Copy to new file", 113 | icon: 'pi pi-clone', 114 | disabled: true, 115 | }, 116 | { 117 | label: "Upload file(s)", 118 | icon: 'pi pi-upload', 119 | disabled: true, 120 | }, 121 | { 122 | label: 'Download sources', 123 | icon: 'pi pi-download', 124 | disabled: true, 125 | }, 126 | { 127 | separator: true 128 | }, 129 | { 130 | separator: true 131 | }, 132 | { 133 | label: 'Select All', 134 | icon: 'pi pi-info-circle', 135 | command: () => editor?.trigger(state.params.activePath, 'editor.action.selectAll', null), 136 | }, 137 | { 138 | separator: true 139 | }, 140 | { 141 | label: 'Find', 142 | icon: 'pi pi-search', 143 | command: () => editor?.trigger(state.params.activePath, 'actions.find', null), 144 | }, 145 | ] as MenuItem[]} popup ref={menu} /> 146 |
160 | 161 | 162 |
166 | {isMonacoSupported && ( 167 | model.source = s ?? ''} 173 | onMount={onMount} // TODO: This looks a bit silly, does it trigger a re-render?? 174 | options={{ 175 | ...openscadEditorOptions, 176 | fontSize: 16, 177 | lineNumbers: state.view.lineNumbers ? 'on' : 'off', 178 | }} 179 | /> 180 | )} 181 | {!isMonacoSupported && ( 182 | model.source = s.target.value ?? ''} 186 | /> 187 | )} 188 |
189 | 190 |
195 | {(state.currentRunLogs ?? []).map(([type, text], i) => ( 196 |
{text}
197 | ))} 198 |
199 | 200 |
201 | ) 202 | } 203 | -------------------------------------------------------------------------------- /src/components/ExportButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { ModelContext } from './contexts.ts'; 3 | 4 | import { SplitButton } from 'primereact/splitbutton'; 5 | import { MenuItem } from 'primereact/menuitem'; 6 | 7 | type ExtendedMenuItem = MenuItem & { buttonLabel?: string }; 8 | 9 | export default function ExportButton({className, style}: {className?: string, style?: React.CSSProperties}) { 10 | const model = useContext(ModelContext); 11 | if (!model) throw new Error('No model'); 12 | const state = model.state; 13 | 14 | const dropdownModel: ExtendedMenuItem[] = 15 | state.is2D ? [ 16 | { 17 | data: 'svg', 18 | buttonLabel: 'SVG', 19 | label: 'SVG (Simple Vector Graphics)', 20 | icon: 'pi pi-download', 21 | command: () => model!.setFormats('svg', undefined), 22 | }, 23 | { 24 | data: 'dxf', 25 | buttonLabel: 'DXF', 26 | label: 'DXF (Drawing Exchange Format)', 27 | icon: 'pi pi-download', 28 | command: () => model!.setFormats('dxf', undefined), 29 | }, 30 | ] : [ 31 | { 32 | data: 'glb', 33 | buttonLabel: 'Download GLB', 34 | label: 'GLB (binary glTF)', 35 | icon: 'pi pi-file', 36 | command: () => model!.setFormats(undefined, 'glb'), 37 | }, 38 | { 39 | data: 'stl', 40 | buttonLabel: 'Download STL', 41 | label: 'STL (binary)', 42 | icon: 'pi pi-file', 43 | command: () => model!.setFormats(undefined, 'stl'), 44 | }, 45 | { 46 | data: 'off', 47 | buttonLabel: 'Download OFF', 48 | label: 'OFF (Object File Format)', 49 | icon: 'pi pi-file', 50 | command: () => model!.setFormats(undefined, 'off'), 51 | }, 52 | { 53 | data: '3mf', 54 | buttonLabel: 'Download 3MF', 55 | label: '3MF (Multimaterial)', 56 | icon: 'pi pi-file', 57 | command: () => model!.setFormats(undefined, '3mf'), 58 | }, 59 | { 60 | separator: true 61 | }, 62 | { 63 | label: 'Edit materials' + ((state.params.extruderColors ?? []).length > 0 ? ` (${(state.params.extruderColors ?? []).length})` : ''), 64 | icon: 'pi pi-cog', 65 | command: () => model!.mutate(s => s.view.extruderPickerVisibility = 'editing'), 66 | } 67 | ]; 68 | 69 | const exportFormat = state.is2D ? state.params.exportFormat2D : state.params.exportFormat3D; 70 | const selectedItem = dropdownModel.filter(item => item.data === exportFormat)[0] || dropdownModel[0]!; 71 | 72 | return ( 73 |
74 | model!.export()} 81 | className="p-button-sm" 82 | /> 83 |
84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /src/components/FilePicker.tsx: -------------------------------------------------------------------------------- 1 | // Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING. 2 | 3 | import React, { CSSProperties, useContext } from 'react'; 4 | import { TreeSelect } from 'primereact/treeselect'; 5 | import { TreeNode } from 'primereact/treenode'; 6 | import { ModelContext, FSContext } from './contexts.ts'; 7 | import { getParentDir, join } from '../fs/filesystem.ts'; 8 | import { defaultSourcePath } from '../state/initial-state.ts'; 9 | import { zipArchives } from '../fs/zip-archives.ts'; 10 | 11 | const biasedCompare = (a: string, b: string) => 12 | a === 'openscad' ? -1 : b === 'openscad' ? 1 : a.localeCompare(b); 13 | 14 | function listFilesAsNodes(fs: FS, path: string, accept?: (path: string) => boolean): TreeNode[] { 15 | const files: [string, string][] = [] 16 | const dirs: [string, string][] = [] 17 | for (const name of fs.readdirSync(path)) { 18 | if (name.startsWith('.')) { 19 | continue; 20 | } 21 | const childPath = join(path, name); 22 | if (accept && !accept(childPath)) { 23 | continue; 24 | } 25 | const stat = fs.lstatSync(childPath); 26 | const isDirectory = stat.isDirectory(); 27 | if (!isDirectory && !name.endsWith('.scad')) { 28 | continue; 29 | } 30 | (isDirectory ? dirs : files).push([name, childPath]); 31 | } 32 | [files, dirs].forEach(arr => arr.sort(([a], [b]) => biasedCompare(a, b))); 33 | 34 | const nodes: TreeNode[] = [] 35 | for (const [arr, isDirectory] of [[files, false], [dirs, true]] as [[string, string][], boolean][]) { 36 | for (const [name, path] of arr) { 37 | let children: TreeNode[] = []; 38 | let label = name; 39 | if (path.lastIndexOf('/') === 0) { 40 | const config = zipArchives[name]; 41 | if (config && config.gitOrigin) { 42 | const repoUrl = config.gitOrigin.repoUrl; 43 | if (!children) children = []; 44 | 45 | children.push({ 46 | icon: 'pi pi-github', 47 | label: repoUrl.replaceAll("https://github.com/", ''), 48 | key: repoUrl, 49 | selectable: true, 50 | }); 51 | 52 | for (const [label, link] of Object.entries(config.docs ?? [])) { 53 | children.push({ 54 | icon: 'pi pi-book', 55 | label, 56 | key: link, 57 | selectable: true, 58 | }); 59 | } 60 | } 61 | } 62 | 63 | if (isDirectory) { 64 | children = [...children, ...listFilesAsNodes(fs, path, accept)]; 65 | if (children.length == 0) { 66 | continue; 67 | } 68 | } 69 | 70 | nodes.push({ 71 | icon: isDirectory ? 'pi pi-folder' : path === defaultSourcePath ? 'pi pi-home' : 'pi pi-file', 72 | label, 73 | data: path, 74 | key: path, 75 | children, 76 | selectable: !isDirectory 77 | }); 78 | } 79 | } 80 | return nodes; 81 | } 82 | 83 | export default function FilePicker({className, style}: {className?: string, style?: CSSProperties}) { 84 | const model = useContext(ModelContext); 85 | if (!model) throw new Error('No model'); 86 | const state = model.state; 87 | 88 | const fs = useContext(FSContext); 89 | 90 | const fsItems: TreeNode[] = []; 91 | for (const {path} of state.params.sources) { 92 | const parent = getParentDir(path); 93 | if (parent === '/') { 94 | fsItems.push({ 95 | icon: 'pi pi-home', 96 | label: path.split('/').pop(), 97 | data: path, 98 | key: path, 99 | selectable: true, 100 | }); 101 | } 102 | } 103 | if (fs) { 104 | fsItems.push(...listFilesAsNodes(fs, '/')); 105 | } 106 | 107 | return ( 108 | { 115 | const key = e.value; 116 | if (typeof key === 'string') { 117 | if (key.startsWith('https://')) { 118 | window.open(key, '_blank') 119 | } else { 120 | model.openFile(key); 121 | } 122 | } 123 | }} 124 | filter 125 | style={style} 126 | options={fsItems} /> 127 | ) 128 | } 129 | -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | // Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING. 2 | 3 | import React, { CSSProperties, useContext, useRef } from 'react'; 4 | import { ModelContext } from './contexts.ts'; 5 | import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; 6 | import { Button } from 'primereact/button'; 7 | import { ProgressBar } from 'primereact/progressbar'; 8 | import { Badge } from 'primereact/badge'; 9 | import { Toast } from 'primereact/toast'; 10 | import HelpMenu from './HelpMenu.tsx'; 11 | import ExportButton from './ExportButton.tsx'; 12 | import SettingsMenu from './SettingsMenu.tsx'; 13 | import MultimaterialColorsDialog from './MultimaterialColorsDialog.tsx'; 14 | 15 | 16 | export default function Footer({style}: {style?: CSSProperties}) { 17 | const model = useContext(ModelContext); 18 | if (!model) throw new Error('No model'); 19 | const state = model.state; 20 | 21 | const toast = useRef(null); 22 | 23 | const severityByMarkerSeverity = new Map([ 24 | [monaco.MarkerSeverity.Error, 'danger'], 25 | [monaco.MarkerSeverity.Warning, 'warning'], 26 | [monaco.MarkerSeverity.Info, 'info'], 27 | ]); 28 | const markers = state.lastCheckerRun?.markers ?? []; 29 | const getBadge = (s: monaco.MarkerSeverity) => { 30 | const count = markers.filter(m => m.severity == s).length; 31 | const sev = s == monaco.MarkerSeverity.Error ? 'danger' 32 | : s == monaco.MarkerSeverity.Warning ? 'warning' 33 | : s == monaco.MarkerSeverity.Info ? 'info' 34 | : 'success'; 35 | return <>{count > 0 && }; 36 | }; 37 | 38 | 39 | const maxMarkerSeverity = markers.length == 0 ? undefined : markers.map(m => m.severity).reduce((a, b) => Math.max(a, b)); 40 | 41 | return <> 42 | 49 | 50 |
55 | {state.output && !state.output.isPreview 56 | ? ( 57 | 58 | ) : state.previewing ? ( 59 | } 97 | 98 |
99 | 100 | 101 | 102 | 107 | 108 | 109 |
110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/components/HelpMenu.tsx: -------------------------------------------------------------------------------- 1 | // Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING. 2 | 3 | import { CSSProperties, useRef } from 'react'; 4 | import { Button } from 'primereact/button'; 5 | import { MenuItem } from 'primereact/menuitem'; 6 | import { Menu } from 'primereact/menu'; 7 | 8 | export default function HelpMenu({className, style}: {className?: string, style?: CSSProperties}) { 9 | const menuRef = useRef(null); 10 | return ( 11 | <> 12 | 51 | 52 |