├── .fiveserverrc ├── .gitattributes ├── .github └── workflows │ ├── add_copyright_headers.py │ ├── license.yml │ └── main.yml ├── .gitignore ├── .nojekyll ├── LICENSE ├── README.md ├── favicon.png ├── images ├── MakeBox.gif ├── MakeCylinder.gif ├── MakeExtrusion.gif ├── MakeFillet.gif ├── MakeHollow.gif └── MakeSphere.gif ├── index.html ├── jsconfig.json ├── package-lock.json ├── package.json ├── src ├── Backend │ ├── GeometryConverter.js │ ├── OpenCascadeMesher.js │ ├── main.js │ └── mainWorker.js ├── Frontend │ ├── Debug │ │ └── Debug.js │ ├── Input │ │ ├── Input.js │ │ ├── LeapFrameInterpolator.js │ │ ├── LeapJSInput.js │ │ ├── LeapPinchLocomotion.js │ │ ├── LeapTemporalWarping.js │ │ ├── MouseInput.js │ │ └── OpenXRInput.js │ ├── Tools │ │ ├── BoxTool.js │ │ ├── CleanEdgesTool.js │ │ ├── CopyTool.js │ │ ├── CylinderTool.js │ │ ├── DefaultTool.js │ │ ├── DifferenceTool.js │ │ ├── ExtrusionTool.js │ │ ├── FilletTool.js │ │ ├── General │ │ │ ├── Alerts.js │ │ │ ├── Cursor.js │ │ │ ├── Grid.js │ │ │ ├── LSTransformControls.js │ │ │ ├── Menu.js │ │ │ └── ToolUtils.js │ │ ├── OffsetTool.js │ │ ├── RedoTool.js │ │ ├── RemoveTool.js │ │ ├── RotateTool.js │ │ ├── ScaleTool.js │ │ ├── SphereTool.js │ │ ├── Tools.js │ │ ├── TranslateTool.js │ │ ├── UndoTool.js │ │ └── UnionTool.js │ ├── World │ │ ├── FileIO.js │ │ ├── History.js │ │ ├── TextMesh.js │ │ └── World.js │ └── main.js └── main.js └── textures ├── Box.png ├── CleanEdges.png ├── Copy.png ├── Cursor.png ├── Cylinder.png ├── Difference.png ├── Extrusion.png ├── Fillet.png ├── Offset.png ├── Redo.png ├── Remove.png ├── Rotate.png ├── Scale.png ├── Sphere.png ├── Translate.png ├── Undo.png └── Union.png /.fiveserverrc: -------------------------------------------------------------------------------- 1 | { 2 | "port": 8080, 3 | "useLocalIp": true, 4 | "remoteLogs": false, 5 | "https": true 6 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/add_copyright_headers.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import date 3 | import re 4 | 5 | header = '''/** 6 | * Copyright ''' + str(date.today().year) + ''' Ultraleap, Inc. 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | ''' 22 | 23 | for root, dirs, files in os.walk("./src"): 24 | for file in files: 25 | if file.lower().endswith(".js"): 26 | filepath = os.path.join(root, file) 27 | 28 | # Read File Text 29 | src_file = open(filepath, "r") 30 | text = src_file.read() 31 | src_file.close() 32 | 33 | # Add or Update the Copywrite Header 34 | if not text.startswith("/**"): 35 | print("Added Header to " + filepath) 36 | text = header + text 37 | else: 38 | # Brittle Updating Mechanism 39 | pattern = re.compile(r'(\/\*)(?:.|\n)*(\*\/\n\n)', re.MULTILINE) 40 | text = re.sub(pattern, header, text) 41 | 42 | # Write File Text 43 | src_file = open(filepath, "w") 44 | src_file.write(text) 45 | src_file.close() 46 | -------------------------------------------------------------------------------- /.github/workflows/license.yml: -------------------------------------------------------------------------------- 1 | name: LeapShape - Verify License Headers 2 | 3 | on: 4 | push: 5 | branches: 6 | #- main 7 | - feat-license 8 | 9 | jobs: 10 | test: 11 | name: Verify License Headers 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout Repository 15 | uses: actions/checkout@v2 16 | with: 17 | lfs: true 18 | 19 | - name: Update Copyright Headers 20 | run: python .github/workflows/add_copyright_headers.py 21 | 22 | - name: Commit Updated Headers 23 | run: | 24 | git config --local user.email "action@github.com" 25 | git config --local user.name "GitHub Actions" 26 | git add src 27 | git diff-index --quiet HEAD || git commit -m "Add License Header(s)" 28 | 29 | - name: Push Changes to branch 30 | uses: ad-m/github-push-action@master 31 | with: 32 | github_token: ${{ secrets.GITHUB_TOKEN }} 33 | branch: ${{ github.ref }} 34 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: LeapShape - Build Site 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | test: 10 | name: Rebuild Site 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repository 14 | uses: actions/checkout@v2 15 | with: 16 | lfs: true 17 | 18 | # Ideally this makes the next step run faster 19 | # But I haven't noticed a speed difference 20 | # May want to cache the ./node_modules directory directly... 21 | - name: Cache Node Modules 22 | uses: actions/cache@v2 23 | env: 24 | cache-name: cache-node-modules 25 | with: 26 | # npm cache files are stored in `~/.npm` on Linux/macOS 27 | path: ~/.npm 28 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 29 | restore-keys: | 30 | ${{ runner.os }}-build-${{ env.cache-name }}- 31 | ${{ runner.os }}-build- 32 | ${{ runner.os }}- 33 | 34 | - name: Install Node Modules 35 | run: npm install 36 | 37 | # Switching the HTML entrypoint over to the build 38 | # and compensating for the fact that 39 | # Safari and Firefox can't use modules in WebWorkers 40 | # but ESBuild can't NOT use modules in WebWorkers! 41 | - name: Pre-Process Site 42 | run: | 43 | sed -e 's/.\/src\//\/build\//g' index.html > index.html.tmp 44 | mv index.html.tmp index.html 45 | sed -e 's/\/\/import /import /g' src/Backend/mainWorker.js > src/Backend/mainWorker.js.tmp 46 | mv src/Backend/mainWorker.js.tmp src/Backend/mainWorker.js 47 | sed -e 's/importScripts/\/\/importScripts/g' src/Backend/mainWorker.js > src/Backend/mainWorker.js.tmp 48 | mv src/Backend/mainWorker.js.tmp src/Backend/mainWorker.js 49 | sed -e 's/\/\/import /import /g' src/Backend/OpenCascadeMesher.js > src/Backend/OpenCascadeMesher.js.tmp 50 | mv src/Backend/OpenCascadeMesher.js.tmp src/Backend/OpenCascadeMesher.js 51 | sed -e 's/importScripts/\/\/importScripts/g' src/Backend/OpenCascadeMesher.js > src/Backend/OpenCascadeMesher.js.tmp 52 | mv src/Backend/OpenCascadeMesher.js.tmp src/Backend/OpenCascadeMesher.js 53 | sed -e 's/\/\/export /export /g' src/Backend/OpenCascadeMesher.js > src/Backend/OpenCascadeMesher.js.tmp 54 | mv src/Backend/OpenCascadeMesher.js.tmp src/Backend/OpenCascadeMesher.js 55 | 56 | - name: Run esbuild 57 | run: npm run build 58 | 59 | - name: Commit the Updated Build Artifacts 60 | run: | 61 | git config --local user.email "action@github.com" 62 | git config --local user.name "GitHub Actions" 63 | git add build 64 | git add index.html 65 | git diff-index --quiet HEAD || git commit -m "Rebuild Site" 66 | 67 | # Force Push from main to gh-pages... 68 | # This means there is no history being accumulated 69 | - name: Push Changes to branch 70 | uses: ad-m/github-push-action@master 71 | with: 72 | github_token: ${{ secrets.GITHUB_TOKEN }} 73 | branch: gh-pages 74 | force: true 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # parcel-bundler cache (https://parceljs.org/) 61 | .cache 62 | 63 | # next.js build output 64 | .next 65 | 66 | # nuxt.js build output 67 | .nuxt 68 | 69 | # vuepress build output 70 | .vuepress/dist 71 | 72 | # Serverless directories 73 | .serverless 74 | 75 | # FuseBox cache 76 | .fusebox/ 77 | 78 | # Temporary files when seding esbuild incompatibilities 79 | .tmp 80 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leapmotion/LeapShape/65870a7463405557d0a5a8800bd31149012bcc6b/.nojekyll -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [LeapShape](https://leapmotion.github.io/LeapShape/) 2 |

3 | 4 | 5 | 6 | 7 | 8 | 9 |

10 | 11 | > **Note** 12 | > Hands only compatible with OpenXR and [Tracking v5.0.0 Releases](https://developer.leapmotion.com/releases#:~:text=TRACKING%20SOFTWARE%20WINDOWS%205.0.0) and older; Enable "Allow Web Apps". 13 | 14 | ## A Simple and Powerful 3D Modelling Tool 15 | 16 | Use simple gestures with your mouse or hands to model 3D shapes in your browser! 17 | 18 |

19 | 20 | 21 | 22 |

23 |

24 | 25 | 26 | 27 |

28 | 29 | ## Features 30 | - Simple Interface for working with Boxes, Spheres, Cylinders, and Extrusions 31 | - Powerful CSG Operations allow for infinite configurability 32 | - Modify Objects via Movement, Extrusion, Filleting/Chamfering, and Dilation/Hollowing 33 | - Snapping and Coordinate Previews for Precision Assembly 34 | - Model anywhere with first-class Desktop, Mobile, and VR Platform support 35 | - Export Models as .obj, .stl, .gltf, or .step 36 | - Clean and Modular ES6 codebase for extreme extensibility 37 | - **Free and Open Source under the Apache V2 License** 38 | 39 | #### Future Features* 40 | - Draw and Extrude Custom Profiles 41 | - Easily Install for Offline-use as a Progressive Web App 42 | 43 | ## Getting Started with Development 44 | Install [Node.js](https://nodejs.org/en/) first, then run `npm install` in the main folder. 45 | 46 | For development, [VSCode](https://code.visualstudio.com/) with [Five Server](https://marketplace.visualstudio.com/items?itemName=yandeu.five-server) is recommended. 47 | 48 | 49 | ## Credits 50 | 51 | LeapShape is based on several powerful libraries 52 | 53 | - [three.js](https://github.com/mrdoob/three.js/) (3D Rendering Engine) 54 | - [opencascade.js](https://github.com/donalffons/opencascade.js) (CAD Kernel) 55 | - [potpack](https://github.com/mapbox/potpack) (UV Atlas Packing) 56 | - [leapjs](https://github.com/leapmotion/leapjs) (Ultraleap Hand Tracking) 57 | - [esbuild](https://github.com/evanw/esbuild) (Build System) 58 | -------------------------------------------------------------------------------- /favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leapmotion/LeapShape/65870a7463405557d0a5a8800bd31149012bcc6b/favicon.png -------------------------------------------------------------------------------- /images/MakeBox.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leapmotion/LeapShape/65870a7463405557d0a5a8800bd31149012bcc6b/images/MakeBox.gif -------------------------------------------------------------------------------- /images/MakeCylinder.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leapmotion/LeapShape/65870a7463405557d0a5a8800bd31149012bcc6b/images/MakeCylinder.gif -------------------------------------------------------------------------------- /images/MakeExtrusion.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leapmotion/LeapShape/65870a7463405557d0a5a8800bd31149012bcc6b/images/MakeExtrusion.gif -------------------------------------------------------------------------------- /images/MakeFillet.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leapmotion/LeapShape/65870a7463405557d0a5a8800bd31149012bcc6b/images/MakeFillet.gif -------------------------------------------------------------------------------- /images/MakeHollow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leapmotion/LeapShape/65870a7463405557d0a5a8800bd31149012bcc6b/images/MakeHollow.gif -------------------------------------------------------------------------------- /images/MakeSphere.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leapmotion/LeapShape/65870a7463405557d0a5a8800bd31149012bcc6b/images/MakeSphere.gif -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leap Shape 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 62 | 63 | 64 | 65 |

66 |
67 | Leap Shape 0.1.1 68 | 69 |
70 | 71 |
72 | 73 |
74 | 75 |
76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "es6", 4 | "target": "es6" 5 | }, 6 | "include": ["src/**/*"], 7 | "typeAcquisition": { 8 | "include": [ 9 | "three" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leapshape", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "name": "leapshape", 8 | "license": "Apache-2.0", 9 | "dependencies": { 10 | "leapjs": "^1.1.1", 11 | "opencascade.js": "github:zalo/opencascade.js", 12 | "potpack": "^1.0.1", 13 | "three": "^0.147.0" 14 | }, 15 | "devDependencies": { 16 | "@types/three": "^0.146.0", 17 | "esbuild": "^0.12.6" 18 | } 19 | }, 20 | "node_modules/@types/three": { 21 | "version": "0.146.0", 22 | "resolved": "https://registry.npmjs.org/@types/three/-/three-0.146.0.tgz", 23 | "integrity": "sha512-75AgysUrIvTCB054eQa2pDVFurfeFW8CrMQjpzjt3yHBfuuknoSvvsESd/3EhQxPrz9si3+P0wiDUVsWUlljfA==", 24 | "dev": true, 25 | "dependencies": { 26 | "@types/webxr": "*" 27 | } 28 | }, 29 | "node_modules/@types/webxr": { 30 | "version": "0.5.0", 31 | "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.0.tgz", 32 | "integrity": "sha512-IUMDPSXnYIbEO2IereEFcgcqfDREOgmbGqtrMpVPpACTU6pltYLwHgVkrnYv0XhWEcjio9sYEfIEzgn3c7nDqA==", 33 | "dev": true 34 | }, 35 | "node_modules/esbuild": { 36 | "version": "0.12.29", 37 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.29.tgz", 38 | "integrity": "sha512-w/XuoBCSwepyiZtIRsKsetiLDUVGPVw1E/R3VTFSecIy8UR7Cq3SOtwKHJMFoVqqVG36aGkzh4e8BvpO1Fdc7g==", 39 | "dev": true, 40 | "hasInstallScript": true, 41 | "bin": { 42 | "esbuild": "bin/esbuild" 43 | } 44 | }, 45 | "node_modules/gl-matrix": { 46 | "version": "3.4.3", 47 | "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", 48 | "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==" 49 | }, 50 | "node_modules/leapjs": { 51 | "version": "1.1.1", 52 | "resolved": "https://registry.npmjs.org/leapjs/-/leapjs-1.1.1.tgz", 53 | "integrity": "sha512-AXyzH3HuIlqaJWAfziVJa0K0k45wiavQUH7utDbDrYzlqSkqqCR3ePt8kpaDEzujx8rA3iDX+E6x2CnNR1XRXQ==", 54 | "dependencies": { 55 | "gl-matrix": "^3.3.0", 56 | "ws": "^7.4.6" 57 | }, 58 | "engines": { 59 | "node": "~12.0.0" 60 | } 61 | }, 62 | "node_modules/opencascade.js": { 63 | "version": "0.1.17", 64 | "resolved": "git+ssh://git@github.com/zalo/opencascade.js.git#73e4c97b3fd9e3d3f131b7359f357b024ff85eff", 65 | "license": "LGPL-2.1-only" 66 | }, 67 | "node_modules/potpack": { 68 | "version": "1.0.2", 69 | "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", 70 | "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==" 71 | }, 72 | "node_modules/three": { 73 | "version": "0.147.0", 74 | "resolved": "https://registry.npmjs.org/three/-/three-0.147.0.tgz", 75 | "integrity": "sha512-LPTOslYQXFkmvceQjFTNnVVli2LaVF6C99Pv34fJypp8NbQLbTlu3KinZ0zURghS5zEehK+VQyvWuPZ/Sm8fzw==" 76 | }, 77 | "node_modules/ws": { 78 | "version": "7.5.9", 79 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", 80 | "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", 81 | "engines": { 82 | "node": ">=8.3.0" 83 | }, 84 | "peerDependencies": { 85 | "bufferutil": "^4.0.1", 86 | "utf-8-validate": "^5.0.2" 87 | }, 88 | "peerDependenciesMeta": { 89 | "bufferutil": { 90 | "optional": true 91 | }, 92 | "utf-8-validate": { 93 | "optional": true 94 | } 95 | } 96 | } 97 | }, 98 | "dependencies": { 99 | "@types/three": { 100 | "version": "0.146.0", 101 | "resolved": "https://registry.npmjs.org/@types/three/-/three-0.146.0.tgz", 102 | "integrity": "sha512-75AgysUrIvTCB054eQa2pDVFurfeFW8CrMQjpzjt3yHBfuuknoSvvsESd/3EhQxPrz9si3+P0wiDUVsWUlljfA==", 103 | "dev": true, 104 | "requires": { 105 | "@types/webxr": "*" 106 | } 107 | }, 108 | "@types/webxr": { 109 | "version": "0.5.0", 110 | "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.0.tgz", 111 | "integrity": "sha512-IUMDPSXnYIbEO2IereEFcgcqfDREOgmbGqtrMpVPpACTU6pltYLwHgVkrnYv0XhWEcjio9sYEfIEzgn3c7nDqA==", 112 | "dev": true 113 | }, 114 | "esbuild": { 115 | "version": "0.12.29", 116 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.29.tgz", 117 | "integrity": "sha512-w/XuoBCSwepyiZtIRsKsetiLDUVGPVw1E/R3VTFSecIy8UR7Cq3SOtwKHJMFoVqqVG36aGkzh4e8BvpO1Fdc7g==", 118 | "dev": true 119 | }, 120 | "gl-matrix": { 121 | "version": "3.4.3", 122 | "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", 123 | "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==" 124 | }, 125 | "leapjs": { 126 | "version": "1.1.1", 127 | "resolved": "https://registry.npmjs.org/leapjs/-/leapjs-1.1.1.tgz", 128 | "integrity": "sha512-AXyzH3HuIlqaJWAfziVJa0K0k45wiavQUH7utDbDrYzlqSkqqCR3ePt8kpaDEzujx8rA3iDX+E6x2CnNR1XRXQ==", 129 | "requires": { 130 | "gl-matrix": "^3.3.0", 131 | "ws": "^7.4.6" 132 | } 133 | }, 134 | "opencascade.js": { 135 | "version": "git+ssh://git@github.com/zalo/opencascade.js.git#73e4c97b3fd9e3d3f131b7359f357b024ff85eff", 136 | "from": "opencascade.js@github:zalo/opencascade.js" 137 | }, 138 | "potpack": { 139 | "version": "1.0.2", 140 | "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", 141 | "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==" 142 | }, 143 | "three": { 144 | "version": "0.147.0", 145 | "resolved": "https://registry.npmjs.org/three/-/three-0.147.0.tgz", 146 | "integrity": "sha512-LPTOslYQXFkmvceQjFTNnVVli2LaVF6C99Pv34fJypp8NbQLbTlu3KinZ0zURghS5zEehK+VQyvWuPZ/Sm8fzw==" 147 | }, 148 | "ws": { 149 | "version": "7.5.9", 150 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", 151 | "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", 152 | "requires": {} 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leapshape", 3 | "productName": "LeapShape", 4 | "description": "Browser BRep CAD in VR", 5 | "homepage": "https://leapmotion.github.io/LeapShape", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/leapmotion/LeapShape.git" 9 | }, 10 | "author": { 11 | "name": "Ultraleap" 12 | }, 13 | "scripts": { 14 | "build": "esbuild ./src/main.js ./src/Backend/mainWorker.js --bundle --minify --sourcemap --format=esm --target=es2020 --outdir=./build --external:fs --external:path --loader:.wasm=file --define:ESBUILD=true" 15 | }, 16 | "license": "Apache-2.0", 17 | "bundledDependencies": false, 18 | "dependencies": { 19 | "leapjs": "^1.1.1", 20 | "opencascade.js": "github:zalo/opencascade.js", 21 | "potpack": "^1.0.1", 22 | "three": "^0.147.0" 23 | }, 24 | "devDependencies": { 25 | "@types/three": "^0.146.0", 26 | "esbuild": "^0.12.6" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Backend/GeometryConverter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../node_modules/three/build/three.module.js'; 18 | 19 | /** This function converts the output of the OpenCascade 20 | * Mesh Data Callback to three.js BufferGeometry */ 21 | export default function ConvertGeometry(meshData) { 22 | if (!meshData) { console.error("Mesher returned false..."); return null; } 23 | // Accumulate data across faces into a single array 24 | let vertices = [], triangles = [], normals = [], colors = [], uvs = [], uv2s = [], vInd = 0, globalFaceIndex = 0; 25 | let faceMetaData = []; 26 | meshData[0].forEach((face) => { 27 | let faceMeta = {}; 28 | 29 | // Copy Vertices into three.js Vector3 List 30 | vertices.push(...face.vertex_coord); 31 | normals .push(...face.normal_coord); 32 | uvs .push(...face. uv_coord); 33 | uv2s .push(...face. oc_uv_coord); 34 | 35 | // Starting Triangle Index (inclusive) 36 | faceMeta.start = triangles.length / 3; 37 | 38 | // Sort Triangles into a three.js Face List 39 | for (let i = 0; i < face.tri_indexes.length; i += 3) { 40 | triangles.push( 41 | face.tri_indexes[i + 0] + vInd, 42 | face.tri_indexes[i + 1] + vInd, 43 | face.tri_indexes[i + 2] + vInd); 44 | } 45 | vInd += face.vertex_coord.length / 3; 46 | 47 | // Ending Triangle Index (exclusive) 48 | faceMeta.end = triangles.length / 3; 49 | faceMeta.index = globalFaceIndex++; 50 | faceMeta.is_planar = face.is_planar; 51 | faceMeta.average = face.average; 52 | faceMeta.normal = [face.normal_coord[0], face.normal_coord[1], face.normal_coord[2]]; 53 | faceMeta.uvBounds = [face.UMin, face.UMax, face.VMin, face.VMax]; 54 | faceMetaData.push(faceMeta); 55 | }); 56 | 57 | // Compile the connected vertices and faces into a geometry object 58 | let geometry = new THREE.BufferGeometry(); 59 | geometry.setIndex(triangles); 60 | geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) ); 61 | geometry.setAttribute( 'normal', new THREE.Float32BufferAttribute( normals, 3 ) ); 62 | geometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ) ); 63 | geometry.setAttribute( 'uv' , new THREE.Float32BufferAttribute( uvs, 2 ) ); 64 | geometry.setAttribute( 'uv2' , new THREE.Float32BufferAttribute( uv2s, 2 ) ); 65 | geometry.computeBoundingSphere(); 66 | geometry.computeBoundingBox(); 67 | 68 | // Add Edges to Object 69 | // This wild complexity is what allows all of the lines to be drawn in a single draw call 70 | // AND highlighted on a per-edge basis by the mouse hover. On the docket for refactoring. 71 | let lineVertices = []; let globalEdgeIndices = []; 72 | let curGlobalEdgeIndex = 0; let edgeVertices = 0; 73 | let globalEdgeMetadata = {}; globalEdgeMetadata[-1] = { start: -1, end: -1 }; 74 | meshData[1].forEach((edge) => { 75 | let edgeMetadata = {}; 76 | edgeMetadata.localEdgeIndex = edge.edge_index; 77 | edgeMetadata.start = globalEdgeIndices.length; 78 | for (let i = 0; i < edge.vertex_coord.length-3; i += 3) { 79 | lineVertices.push(new THREE.Vector3(edge.vertex_coord[i ], 80 | edge.vertex_coord[i + 1], 81 | edge.vertex_coord[i + 2])); 82 | 83 | lineVertices.push(new THREE.Vector3(edge.vertex_coord[i + 3], 84 | edge.vertex_coord[i + 1 + 3], 85 | edge.vertex_coord[i + 2 + 3])); 86 | globalEdgeIndices.push(curGlobalEdgeIndex); globalEdgeIndices.push(curGlobalEdgeIndex); 87 | edgeVertices++; 88 | } 89 | edgeMetadata.end = globalEdgeIndices.length-1; 90 | globalEdgeMetadata[curGlobalEdgeIndex] = edgeMetadata; 91 | curGlobalEdgeIndex++; 92 | }); 93 | 94 | let lineGeometry = new THREE.BufferGeometry().setFromPoints(lineVertices); 95 | let lineColors = []; for ( let i = 0; i < lineVertices.length; i++ ) { lineColors.push( 0, 0, 0 ); } 96 | lineGeometry.setAttribute( 'color', new THREE.Float32BufferAttribute( lineColors, 3 ) ); 97 | 98 | let line = new THREE.LineSegments(lineGeometry, window.world.lineMaterial); 99 | line.globalEdgeIndices = globalEdgeIndices; 100 | line.globalEdgeMetadata = globalEdgeMetadata; 101 | line.name = "Model Edges"; 102 | line.lineColors = lineColors; 103 | line.frustumCulled = false; 104 | line.layers.set(2); 105 | // End Adding Edges 106 | 107 | // A minor bit of dependency inversion, but for the greater good 108 | let mesh = new THREE.Mesh(geometry, window.world.shapeMaterial); 109 | mesh.material.color.setRGB(0.5, 0.5, 0.5); 110 | mesh.faceMetadata = faceMetaData; 111 | mesh.frustumCulled = false; 112 | mesh.castShadow = true; 113 | mesh.receiveShadow = true; 114 | mesh.add(line); 115 | return mesh; 116 | } 117 | -------------------------------------------------------------------------------- /src/Backend/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 ConvertGeometry from './GeometryConverter.js'; 18 | 19 | /** 20 | * This is the CAD Engine for LeapShape; 21 | * all CAD Operations are managed through here. 22 | */ 23 | class LeapShapeEngine { 24 | /** Initializes the CAD Worker Thread and Callback System */ 25 | constructor() { 26 | this.started = false; 27 | 28 | // Initialize the OpenCascade Worker Thread 29 | if (typeof ESBUILD !== 'undefined') { 30 | this.worker = new Worker(new URL( './Backend/mainWorker.js', import.meta.url )/*, { type: "module" }*/); 31 | } else { 32 | this.worker = new Worker('../src/Backend/mainWorker.js'/*, { type: "module" }*/); 33 | } 34 | 35 | // Ping Pong Messages Back and Forth based on their registration in messageHandlers 36 | this.messageHandlers = {}; this.executeHandlers = {}; 37 | this.worker.onmessage = (e) => { 38 | if(e.data.type in this.messageHandlers){ 39 | let response = this.messageHandlers[e.data.type](e.data.payload); 40 | if (response) { this.worker.postMessage({ "type": e.data.type, payload: response }) }; 41 | } 42 | } 43 | this.registerCallback("startupCallback", () => { console.log("Worker Started!"); this.started = true; }); 44 | 45 | // Handle Receiving Execution Results from the Engine 46 | this.executionQueue = []; 47 | this.registerCallback("execute", (payload) => { 48 | this.workerWorking = false; // Free the worker up to take more requests 49 | this.executeHandlers[payload.name]( 50 | payload.payload ? payload.payload.isMetadata ? payload.payload : ConvertGeometry(payload.payload) : null); 51 | 52 | // Dequeue 53 | if (this.executionQueue.length > 0) { this.execute(...this.executionQueue.pop());} 54 | }); 55 | 56 | this.workerWorking = false; 57 | } 58 | 59 | /** Registers a callback from the Worker Thread 60 | * @param {string} name Name of the callback 61 | * @param {function} callback The Callback to Execute */ 62 | registerCallback(name, callback) { this.messageHandlers[name] = callback; } 63 | 64 | /** Registers a callback from the Worker Thread 65 | * @param {string} name Unique Identifier for this Callback 66 | * @param {function} shapeOperation Function that creates the TopoDS_Shape to mesh 67 | * @param {number[]} operationArguments Arguments to the shape operation function 68 | * @param {function} meshDataCallback A callback containing the mesh data for this shape */ 69 | execute(name, shapeOperation, operationArguments, meshDataCallback) { 70 | // Queue Requests if the worker is busy 71 | if (this.workerWorking) { this.executionQueue.push(arguments); return; } 72 | 73 | this.workerWorking = true; 74 | this.executeHandlers[name] = meshDataCallback; 75 | this.worker.postMessage({ "type": "execute", payload: { 76 | name: name, 77 | shapeFunction: shapeOperation.toString(), 78 | shapeArguments: operationArguments 79 | }}); 80 | } 81 | } 82 | 83 | export { LeapShapeEngine }; 84 | -------------------------------------------------------------------------------- /src/Backend/mainWorker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 | // Leave these next 4 lines exactly as they are. The comments are toggled for ESBuild 18 | //import url from "../../node_modules/opencascade.js/dist/opencascade.wasm.wasm"; 19 | //import opencascade from '../../node_modules/opencascade.js/dist/opencascade.wasm.module.js'; 20 | //import { OpenCascadeMesher } from './OpenCascadeMesher.js'; 21 | importScripts('../../node_modules/opencascade.js/dist/opencascade.wasm.js', './OpenCascadeMesher.js'); 22 | 23 | /** This is the CAD Engine Worker Thread, where all the real work happens */ 24 | class LeapShapeEngineWorker { 25 | 26 | constructor() { 27 | this.shapes = {}; 28 | this.resolution = 0.0025; 29 | this.backendFunctions = {}; 30 | 31 | // Initialize the WebAssembly Module 32 | new opencascade({ 33 | locateFile(path) { 34 | if (path.endsWith('.wasm')) { 35 | return (typeof ESBUILD !== 'undefined') ? url : "../../node_modules/opencascade.js/dist/opencascade.wasm.wasm"; 36 | } 37 | return path; 38 | } 39 | }).then((openCascade) => { 40 | // Register the "OpenCascade" under the shorthand "this.oc" 41 | this.oc = openCascade; 42 | 43 | // Ping Pong Messages Back and Forth based on their registration in messageHandlers 44 | this.messageHandlers = {}; 45 | onmessage = (e) => { 46 | if (e.data.type in this.messageHandlers) { 47 | let response = this.messageHandlers[e.data.type](e.data.payload); 48 | if (response) { postMessage({ "type": e.data.type, payload: response }); }; 49 | } 50 | } 51 | 52 | // Send a message back to the main thread saying everything is a-ok... 53 | postMessage({ type: "startupCallback" }); 54 | this.messageHandlers["execute"] = this.execute.bind(this); 55 | 56 | // Capture Errors 57 | self.addEventListener('error', (event) => { this.postError(event); }); 58 | self.realConsoleError = console.error; 59 | console.error = this.fakeConsoleError.bind(this); 60 | 61 | // Set up a persistent Meshing System 62 | this.mesher = new OpenCascadeMesher(this.oc); 63 | }); 64 | } 65 | 66 | /** Executes a CAD operation from the Main Thread 67 | * @param {{name: string, shapeFunction: function, shapeArguments: number[], meshDataCallback: function}} payload */ 68 | execute(payload) { 69 | // Cache Backend Execution Functions to save on memory 70 | if (!(payload.shapeFunction in this.backendFunctions)) { 71 | this.safari = /(Safari|iPhone)/g.test(navigator.userAgent) && ! /(Chrome)/g.test(navigator.userAgent); 72 | this.backendFunctions[payload.shapeFunction] = 73 | new Function("return " + (this.safari ? "" : "function ") + payload.shapeFunction)().bind(this); 74 | } 75 | let op = this.backendFunctions[payload.shapeFunction]; 76 | 77 | let shape = null; 78 | try { 79 | shape = op(...payload.shapeArguments); 80 | if (shape && shape.isMetadata) { 81 | // Return the output raw if it's marked as data 82 | return { name: payload.name, payload: shape }; 83 | } else { 84 | // Otherwise Convert the Shape to a Mesh + Metadata 85 | if (!shape || shape.IsNull()) { console.error("Shape is null"); console.error(shape); } 86 | let meshData = this.mesher.shapeToMesh(shape, this.resolution, {}, {}); 87 | if (meshData) { this.shapes[payload.name] = shape; } 88 | return { name: payload.name, payload: meshData }; 89 | } 90 | } catch (e) { 91 | console.error("CAD Operation Failed!"); 92 | return { name: payload.name, payload: null }; 93 | } 94 | } 95 | 96 | /** Posts an error message back to the main thread 97 | * @param {ErrorEvent} event */ 98 | postError(event) { 99 | let path = event.filename.split("/"); 100 | postMessage({ 101 | "type": "error", payload: 102 | (path[path.length - 1] + ":" + event.lineno + " - " + event.message) 103 | }); 104 | } 105 | 106 | fakeConsoleError(...args) { 107 | if (args.length > 0) { 108 | postMessage({ "type": "error", payload: args[0] }); 109 | } 110 | self.realConsoleError.apply(console, arguments); 111 | } 112 | } 113 | 114 | // Initialize the worker as the top-level entrypoint in this scope 115 | var worker = new LeapShapeEngineWorker(); 116 | -------------------------------------------------------------------------------- /src/Frontend/Debug/Debug.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 { LeapShapeEngine } from '../../Backend/main.js'; 18 | 19 | /** This class provides Debug Utilities. */ 20 | class Debug { 21 | 22 | /** Reroute Console Errors to the Main Screen (for mobile) 23 | * @param {LeapShapeEngine} engine */ 24 | constructor(world, engine) { 25 | this.world = world; 26 | 27 | // Route Worker Errors Here 28 | engine.registerCallback("error", this.fakeError.bind(this)); 29 | 30 | // Intercept Main Window Errors as well 31 | window.realConsoleError = console.error; 32 | window.addEventListener('error', (event) => { 33 | let path = event.filename.split("/"); 34 | this.display((path[path.length - 1] + ":" + event.lineno + " - " + event.message)); 35 | }); 36 | console.error = this.fakeError.bind(this); 37 | 38 | // Record whether we're on Safari or Mobile (unused so far) 39 | this.safari = /(Safari)/g.test( navigator.userAgent ) && ! /(Chrome)/g.test( navigator.userAgent ); 40 | this.mobile = /(Android|iPad|iPhone|iPod|Oculus)/g.test(navigator.userAgent) || this.safari; 41 | } 42 | 43 | // Log Errors as
s over the main viewport 44 | fakeError(...args) { 45 | if (args.length > 0 && args[0]) { this.display(JSON.stringify(args[0])); } 46 | window.realConsoleError.apply(console, arguments); 47 | } 48 | 49 | display(text) { 50 | this.world.parent.tools.alerts.displayError(text); 51 | if (this.mobile) { 52 | let errorNode = window.document.createElement("div"); 53 | errorNode.innerHTML = text.fontcolor("red"); 54 | window.document.getElementById("info").appendChild(errorNode); 55 | } 56 | } 57 | 58 | } 59 | 60 | export { Debug }; 61 | -------------------------------------------------------------------------------- /src/Frontend/Input/Input.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../../node_modules/three/build/three.module.js'; 18 | import { MouseInput } from './MouseInput.js'; 19 | import { LeapJSInput } from './LeapJSInput.js'; 20 | import { OpenXRInput } from './OpenXRInput.js'; 21 | 22 | /** 23 | * This is the input abstraction object for LeapShape. 24 | * Mice, Touchscreens, Hands, and Controllers all 25 | * produce interaction rays. 26 | */ 27 | class InteractionRay { 28 | /** 29 | * Construct a new interaction ray from a three.js ray. 30 | * @param {THREE.Ray} ray The origin and direction of the interaction ray. 31 | */ 32 | constructor(ray) { 33 | this.ray = ray; 34 | this.active = false; 35 | this.justActivated = false; 36 | this.justDeactivated = false; 37 | this.activeMS = 0; 38 | this.hovering = false; 39 | this.lastHovering = false; 40 | } 41 | } 42 | 43 | /** This is the input abstraction for LeapShape. 44 | * Mouse, Touchscreen, Hands, and Controllers 45 | * are routed through here as InteractionRays. */ 46 | class Input { 47 | 48 | constructor(world) { 49 | // Add your new input abstraction here! 50 | this.inputs = { 51 | mouse : new MouseInput (world, this), 52 | hands : new LeapJSInput(world, this), 53 | openxr: new OpenXRInput(world, this) 54 | }; 55 | this.activeInput = this.inputs.mouse; 56 | this.ray = new InteractionRay(new THREE.Ray()); 57 | } 58 | 59 | /** 60 | * Update the various Input Abstractions and output the active InputRay. 61 | */ 62 | update() { 63 | for(let input in this.inputs){ 64 | this.inputs[input].update(); 65 | if (this.inputs[input].isActive()) { 66 | this.activeInput = this.inputs[input]; 67 | this.ray = this.inputs[input].ray; 68 | } 69 | } 70 | return this.ray; 71 | } 72 | 73 | } 74 | 75 | export { Input, InteractionRay }; 76 | -------------------------------------------------------------------------------- /src/Frontend/Input/LeapPinchLocomotion.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../../node_modules/three/build/three.module.js'; 18 | import "../../../node_modules/leapjs/leap-1.1.1.js"; 19 | import { World } from '../World/World.js'; 20 | 21 | /** This is the Leap Hand Tracking-based Camera Controller */ 22 | class LeapPinchLocomotion { 23 | /** Initialize Camera Control 24 | * @param {World} world 25 | * @param {THREE.Object3D} leftPinchSphere 26 | * @param {THREE.Object3D} rightPinchSphere */ 27 | constructor(world, leftPinchSphere, rightPinchSphere) { 28 | this.world = world; 29 | 30 | this.momentum = 0.125; 31 | this.horizontalRotation = true; 32 | this.enableScaling = true; 33 | 34 | this.curA = new THREE.Vector3(); // The dynamic world-space pinch points 35 | this.curB = new THREE.Vector3(); 36 | this.rootA = new THREE.Vector3(); // The stationary world-space anchors 37 | this.rootB = new THREE.Vector3(); 38 | this.residualMomentum = new THREE.Vector3(); 39 | this.isLeftPinching = false; 40 | this.isRightPinching = false; 41 | 42 | this.leftPinchSphere = leftPinchSphere; 43 | this.rightPinchSphere = rightPinchSphere; 44 | } 45 | 46 | /** Moves the camera and adds momentum */ 47 | update() { 48 | let left = this. leftPinchSphere; 49 | let right = this.rightPinchSphere; 50 | let leftPinching = left .visible; 51 | let rightPinching = right.visible; 52 | 53 | if (leftPinching && rightPinching) { // Set Points when Both Pinched 54 | left .getWorldPosition(this.curA); 55 | right.getWorldPosition(this.curB); 56 | 57 | if (!this.isLeftPinching || !this.isRightPinching) { 58 | this.rootA.copy(this.curA); 59 | this.rootB.copy(this.curB); 60 | } 61 | 62 | // Inform the user of their current world scale 63 | this.world.parent.tools.cursor.updateTarget(this.rootA.clone().add(this.rootB).multiplyScalar(0.5)); 64 | this.world.parent.tools.cursor.updateLabelNumbers(1.0 / this.world.camera.getWorldScale(new THREE.Vector3()).x); 65 | } else if (leftPinching) { // Set Points when Left Pinched 66 | this.oneHandedPinchMove(left, this.isLeftPinching, this.isRightPinching, 67 | this.rootA, this.curA, this.rootB, this.curB); 68 | } else if (rightPinching) { // Set Points when Right Pinched 69 | this.oneHandedPinchMove(right, this.isRightPinching, this.isLeftPinching, 70 | this.rootB, this.curB, this.rootA, this.curA); 71 | } else { // Apply Momentum to Dynamic Points when Unpinched 72 | this.curA.lerp(this.rootA, this.momentum); 73 | this.curB.lerp(this.rootB, this.momentum); 74 | } 75 | this.isLeftPinching = leftPinching; 76 | this.isRightPinching = rightPinching; 77 | 78 | // Transform the root so the (dynamic) cur points match the (stationary) root points 79 | 80 | let pivot = this.rootA.clone().add(this.rootB).multiplyScalar(0.5); 81 | let translation = pivot.clone().sub(this.curA.clone().add(this.curB).multiplyScalar(0.5)); 82 | 83 | let from = this. curB.clone().sub(this. curA).normalize(); 84 | let to = this.rootB.clone().sub(this.rootA).normalize(); 85 | if(this.horizontalRotation) { from.y = 0; to.y = 0; } 86 | let rotation = new THREE.Quaternion().setFromUnitVectors(from, to); 87 | 88 | let scale = (this.rootA.clone().sub(this.rootB)).length() / 89 | (this.curA .clone().sub(this. curB)).length(); 90 | 91 | // Apply movement to both the Camera Parent (and the Pinch Points to avoid accidental Verlet) 92 | this.applyMovement(this.world.camera.parent, pivot, translation, rotation, scale); 93 | this.applyMovement(left , pivot, translation, rotation, scale); 94 | this.applyMovement(right , pivot, translation, rotation, scale); 95 | 96 | this.world.camera.parent.updateWorldMatrix(true, true); 97 | } 98 | 99 | applyMovement(object, pivot, translation, rotation, scale) { 100 | // Apply Translation 101 | object.position.add(translation); 102 | 103 | if (this.rootA.x !== this.rootB.x) { 104 | // Apply Rotation 105 | this.Pivot(object.position, object.quaternion, pivot, rotation); 106 | 107 | // Apply Scale about Pivot 108 | if (!isNaN(scale) && this.enableScaling) { 109 | object.position.sub(pivot).multiplyScalar(scale).add(pivot); 110 | object.scale.multiplyScalar(scale); 111 | } 112 | } 113 | 114 | } 115 | 116 | /** Ambidextrous function for handling one-handed pinch movement with momentum. 117 | * @param {THREE.Object3D} thisPinch @param {boolean} thisIsPinching 118 | * @param {boolean} otherIsPinching @param {THREE.Vector3} thisRoot 119 | * @param {THREE.Vector3} thisCur @param {THREE.Vector3} otherRoot 120 | * @param {THREE.Vector3} otherCur */ 121 | oneHandedPinchMove(thisPinch, thisIsPinching, otherIsPinching, thisRoot, thisCur, otherRoot, otherCur) { 122 | thisPinch.getWorldPosition(thisCur); 123 | 124 | if (!thisIsPinching || otherIsPinching) { 125 | this.residualMomentum = otherCur.clone().sub(otherRoot); 126 | thisRoot.copy(thisCur); 127 | } else { 128 | otherCur.copy((otherRoot.clone().add(thisCur.clone().sub(thisRoot))).clone().add(this.residualMomentum)); 129 | } 130 | this.residualMomentum.multiplyScalar(1.0 - this.momentum); 131 | } 132 | 133 | /** Pivots the original position and quaternion about another point + quaternion 134 | * @param {THREE.Vector3} position @param {THREE.Quaternion} quaternion 135 | * @param {THREE.Vector3} pivotPoint @param {THREE.Quaternion} pivotQuaternion */ 136 | Pivot(position, quaternion, pivotPoint, pivotQuaternion) { 137 | position.sub(pivotPoint).applyQuaternion(pivotQuaternion).add(pivotPoint); 138 | quaternion.premultiply(pivotQuaternion); 139 | } 140 | 141 | } 142 | 143 | export { LeapPinchLocomotion }; 144 | -------------------------------------------------------------------------------- /src/Frontend/Input/LeapTemporalWarping.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../../node_modules/three/build/three.module.js'; 18 | import "../../../node_modules/leapjs/leap-1.1.1.js"; 19 | import { World } from '../World/World.js'; 20 | 21 | /** This resamples HMD Transforms to the moment the tracking frame was captured. 22 | * This eliminates swim when the user shakes their head but holds their hands still. */ 23 | class LeapTemporalWarping { 24 | /** This resamples HMD Transforms to the moment the tracking frame was captured. 25 | * This eliminates swim when the user shakes their head but holds their hands still. 26 | * @param {World} world */ 27 | constructor(world, interpolator) { 28 | this.world = world; 29 | this.interpolator = interpolator; 30 | 31 | this.nowToLeapOffsetUS = 0; 32 | this.historyLength = 20; 33 | this.history = new window.Leap.CircularBuffer(20); 34 | 35 | this.interpolatedFrame = { 36 | timestamp : 0, 37 | position : this.world.camera.position .clone(), 38 | quaternion: this.world.camera.quaternion.clone() 39 | }; 40 | 41 | // Temporary Swap Variables 42 | this.vec = new THREE.Vector3(); this.vec2 = new THREE.Vector3(); this.vec3 = new THREE.Vector3(); 43 | this.quat = new THREE.Quaternion(); this.quat2 = new THREE.Quaternion(); 44 | this.mat1 = new THREE.Matrix4(); this.mat2 = new THREE.Matrix4(); 45 | } 46 | 47 | /** Adds new element to the history and resamples the current time matrix. */ 48 | update() { 49 | this.currentTimestamp = (this.world.now * 1000) + this.interpolator.nowToLeapOffsetUS; 50 | 51 | // Add a new Head Transform to the history 52 | this.addFrameToHistory(this.currentTimestamp); 53 | // Sample a head transform from this time, 40ms ago 54 | return this.getInterpolatedFrame(this.interpolatedFrame, this.history, this.currentTimestamp - 40000); 55 | } 56 | 57 | /** Accumulate the current head transform into the history 58 | * @param {number} currentTimestamp */ 59 | addFrameToHistory(currentTimestamp) { 60 | if (this.history.get(this.historyLength - 1)) { 61 | let sample = this.history.get(this.historyLength - 1); 62 | sample.timestamp = currentTimestamp; 63 | sample.position .copy(this.world.camera.position ); 64 | sample.quaternion.copy(this.world.camera.quaternion); 65 | this.history.push(sample); 66 | } else { 67 | this.history.push({ 68 | timestamp : currentTimestamp, 69 | position : this.world.camera.position .clone(), 70 | quaternion: this.world.camera.quaternion.clone() 71 | }); 72 | } 73 | } 74 | 75 | /** Interpolates a frame to the given timestamp 76 | * @param {number} timestamp */ 77 | getInterpolatedFrame(frame, history, timestamp) { 78 | // Step through time until we have the two frames we'd like to interpolate between. 79 | let back = 0, doubleBack = 1; 80 | let aFrame = history.get(back+doubleBack) || this.interpolatedFrame; 81 | let bFrame = history.get(back); 82 | while (aFrame && aFrame.timestamp === bFrame.timestamp && doubleBack < 10) { 83 | doubleBack += 1; aFrame = history.get(back + doubleBack); 84 | } 85 | while (aFrame && bFrame && 86 | (!(bFrame.timestamp < timestamp || 87 | (aFrame.timestamp < timestamp && bFrame.timestamp > timestamp) || 88 | back == 198))) { // Only 200 entries in the history buffer 89 | back++; 90 | doubleBack = 1; 91 | aFrame = history.get(back+doubleBack); 92 | bFrame = history.get(back ); 93 | while (aFrame && aFrame.timestamp === bFrame.timestamp && doubleBack < 10) { 94 | doubleBack += 1; aFrame = history.get(back + doubleBack); 95 | } 96 | } 97 | 98 | if (aFrame && bFrame) { 99 | let aTimestamp = aFrame.timestamp, bTimestamp = bFrame.timestamp; 100 | let alpha = (timestamp - aTimestamp) / (bTimestamp - aTimestamp); 101 | 102 | // Debug visualize the temporal offset 103 | //this.world.parent.tools.cursor.updateTarget(this.vec.set(0,0,0)); 104 | //this.world.parent.tools.cursor.updateLabel(timestamp - aTimestamp);//this.nowToLeapOffsetUS); 105 | 106 | frame.timestamp = this.lerp(aTimestamp, bTimestamp, alpha); 107 | frame.position.lerpVectors(aFrame.position, bFrame.position, alpha); 108 | frame.quaternion.slerpQuaternions(aFrame.quaternion, bFrame.quaternion, alpha); 109 | } 110 | return frame; 111 | } 112 | 113 | /** Linearly Interpolate `a` to `b` by `alpha` 114 | * @param {number} a @param {number} b @param {number} alpha @returns {number} */ 115 | lerp(a, b, alpha) { return (a * (1 - alpha)) + (b * alpha); } 116 | 117 | } 118 | 119 | export { LeapTemporalWarping }; 120 | -------------------------------------------------------------------------------- /src/Frontend/Input/MouseInput.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../../node_modules/three/build/three.module.js'; 18 | import { World } from '../World/World.js'; 19 | import { InteractionRay, Input } from './Input.js'; 20 | 21 | /** This is the standard mouse (and touchscreen?) input. */ 22 | class MouseInput { 23 | /** Initialize Mouse Capture 24 | * @param {World} world 25 | * @param {Input} inputs */ 26 | constructor(world, inputs) { 27 | this.world = world; this.inputs = inputs; 28 | this.ray = new InteractionRay(new THREE.Ray()); 29 | this.lastTimestep = performance.now(); 30 | this.activeTime = 0; 31 | 32 | this.mouse = { x: 0, y: 0, buttons: -1 }; 33 | this.world.container.addEventListener( 'pointermove', this._onContainerMouse.bind(this) ); 34 | this.world.container.addEventListener( 'pointerdown', this._onContainerMouse.bind(this) ); 35 | this.world.container.addEventListener( 'pointerup' , this._onContainerMouse.bind(this) ); 36 | this.world.container.addEventListener( 'wheel' , this._onContainerMouse.bind(this) ); 37 | this.prevButton = 0; 38 | this.up = new THREE.Vector3(0, 1, 0); 39 | 40 | this.mobile = /(Android|iPad|iPhone|iPod|Oculus)/g.test(navigator.userAgent); 41 | } 42 | 43 | /** Triggered whenever the mouse moves over the application 44 | * @param {PointerEvent} event */ 45 | _onContainerMouse( event ) { 46 | event.preventDefault(); 47 | let rect = event.target.getBoundingClientRect(); 48 | this.mouse.x = ( ( event.clientX - rect.left ) / rect.width ) * 2 - 1; 49 | this.mouse.y = - ( ( event.clientY - rect.top ) / rect.height ) * 2 + 1; 50 | this.mouse.buttons = event.buttons; 51 | this.world.dirty = true; 52 | } 53 | 54 | update() { 55 | if (this.isActive()) { 56 | // Add Extra Fields for the active state 57 | this.ray.justActivated = false; this.ray.justDeactivated = false; 58 | this.ray.active = this.mouse.buttons === 1; 59 | if ( this.ray.active && this.prevButton === 0) { this.ray.justActivated = true; this.activeTime = 0; } 60 | if (!this.ray.active && this.prevButton === 1) { this.ray.justDeactivated = true; } 61 | this.ray.hovering = false; 62 | this.prevButton = this.mouse.buttons; 63 | if (this.ray.active) { this.activeTime += performance.now() - this.lastTimestep; } 64 | this.ray.activeMS = this.activeTime; 65 | this.lastTimestep = performance.now(); 66 | 67 | // Changes the cursor between the "Hovering" and "Passive" state 68 | this.world.container.style.cursor = this.ray.lastHovering ? "pointer" : "default"; 69 | 70 | // Set Ray Origin and Direction 71 | this.ray.ray.origin.setFromMatrixPosition(this.world.camera.matrixWorld); 72 | 73 | // Point ray into sky when not touching on mobile 74 | if (this.mobile && !this.ray.active && !this.ray.justDeactivated) { 75 | this.ray.ray.direction.copy(this.up); 76 | } else { 77 | this.ray.ray.direction.set(this.mouse.x, this.mouse.y, 0.5) 78 | .unproject(this.world.camera).sub(this.ray.ray.origin).normalize(); 79 | } 80 | } 81 | } 82 | 83 | /** Does this input want to take control? */ 84 | isActive() { return !(this.world.handsAreTracking || this.world.inVR); } 85 | 86 | } 87 | 88 | export { MouseInput }; 89 | -------------------------------------------------------------------------------- /src/Frontend/Input/OpenXRInput.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../../node_modules/three/build/three.module.js'; 18 | import { World } from '../World/World.js'; 19 | import { InteractionRay, Input } from './Input.js'; 20 | import { XRControllerModelFactory } from '../../../node_modules/three/examples/jsm/webxr/XRControllerModelFactory.js'; 21 | import { XRHandModelFactory } from '../../../node_modules/three/examples/jsm/webxr/XRHandModelFactory.js'; 22 | 23 | /** This manages all OpenXR-based input */ 24 | class OpenXRInput { 25 | /** Initialize OpenXR Controller and Hand Tracking 26 | * @param {World} world 27 | * @param {Input} inputs */ 28 | constructor(world, inputs) { 29 | this.world = world; this.inputs = inputs; 30 | 31 | this.vec = new THREE.Vector3(); this.vec2 = new THREE.Vector3(); this.vec3 = new THREE.Vector3(); 32 | this.quat = new THREE.Quaternion(); this.quat2 = new THREE.Quaternion(); 33 | this.mat1 = new THREE.Matrix4(); this.mat2 = new THREE.Matrix4(); 34 | this.upTilt = (new THREE.Quaternion).setFromEuler(new THREE.Euler( Math.PI / 3.5, 0, 0)); 35 | this.downTilt = (new THREE.Quaternion).setFromEuler(new THREE.Euler(-Math.PI / 3.5, 0, 0)); 36 | 37 | this.ray = new InteractionRay(new THREE.Ray()); 38 | this.lastTimestep = performance.now(); 39 | this.activeTime = 0; this.prevActive = false; 40 | this.mainHand = null; this.initialized = false; 41 | this.lastMainHand = null; 42 | this.cameraWorldPosition = new THREE.Vector3(); 43 | this.cameraWorldQuaternion = new THREE.Quaternion(); 44 | this.cameraWorldScale = new THREE.Quaternion(); 45 | this.identity = new THREE.Quaternion().identity(); 46 | 47 | //if (this.isActive()) { this.initialize(); } 48 | } 49 | 50 | initialize() { 51 | // Initialize Model Factories 52 | this.controllerModelFactory = new XRControllerModelFactory(); 53 | let modelPath = (typeof ESBUILD !== 'undefined') ? './models/' : "../../../models/"; 54 | this.handModelFactory = new XRHandModelFactory().setPath(modelPath); 55 | 56 | // Controllers 57 | this.controller1 = this.world.renderer.xr.getController(0); 58 | this.controller2 = this.world.renderer.xr.getController(1); 59 | this.controller1.inputState = { pinching: false }; this.controller1.visible = false; 60 | this.controller2.inputState = { pinching: false }; this.controller2.visible = false; 61 | this.controller1.traverse((element) => { if(element.layers){ element.layers.set(1); }}); 62 | this.controller2.traverse((element) => { if(element.layers){ element.layers.set(1); }}); 63 | this.world.cameraParent.add(this.controller1); this.world.cameraParent.add(this.controller2); 64 | this.controllerGrip1 = this.world.renderer.xr.getControllerGrip(0); 65 | this.controllerGrip2 = this.world.renderer.xr.getControllerGrip(1); 66 | this.controllerGrip1.add(this.controllerModelFactory.createControllerModel(this.controllerGrip1)); 67 | this.controllerGrip2.add(this.controllerModelFactory.createControllerModel(this.controllerGrip2)); 68 | this.controllerGrip1.traverse((element) => { if(element.layers){ element.layers.set(1); }}); 69 | this.controllerGrip2.traverse((element) => { if(element.layers){ element.layers.set(1); }}); 70 | this.world.cameraParent.add(this.controllerGrip1); this.world.cameraParent.add(this.controllerGrip2); 71 | 72 | // Controller Interaction 73 | this.controller1.addEventListener('selectstart', (e) => { this.controller1.inputState.pinching = true ; }); 74 | this.controller1.addEventListener('selectend' , (e) => { this.controller1.inputState.pinching = false; }); 75 | this.controller2.addEventListener('selectstart', (e) => { this.controller2.inputState.pinching = true ; }); 76 | this.controller2.addEventListener('selectend' , (e) => { this.controller2.inputState.pinching = false; }); 77 | 78 | // Hands 79 | this.hand1 = this.world.renderer.xr.getHand(0); 80 | this.hand2 = this.world.renderer.xr.getHand(1); 81 | this.hand1.inputState = { pinching: false }; 82 | this.hand2.inputState = { pinching: false }; 83 | this.handModel1 = this.handModelFactory.createHandModel(this.hand1, 'capsules'); 84 | this.handModel2 = this.handModelFactory.createHandModel(this.hand2, 'capsules'); 85 | this.hand1.add (this.handModel1); this.hand2.add (this.handModel2); 86 | this.world.cameraParent.add(this.hand1); this.world.cameraParent.add(this.hand2); 87 | this.hand1.layers.set(1); this.handModel1.layers.set(1); this.handModel1.frustumCulled = false; 88 | this.hand2.layers.set(1); this.handModel2.layers.set(1); this.handModel2.frustumCulled = false; 89 | this.hand1.traverse((element) => { if(element.layers){ element.layers.set(1); }}); 90 | this.hand2.traverse((element) => { if(element.layers){ element.layers.set(1); }}); 91 | 92 | // Controller Interaction 93 | this.hand1.addEventListener('pinchstart', (e) => { this.hand1.inputState.pinching = true ; }); 94 | this.hand1.addEventListener('pinchend' , (e) => { this.hand1.inputState.pinching = false; }); 95 | this.hand2.addEventListener('pinchstart', (e) => { this.hand2.inputState.pinching = true ; }); 96 | this.hand2.addEventListener('pinchend' , (e) => { this.hand2.inputState.pinching = false; }); 97 | 98 | // Pointer 99 | let lineGeometry = new THREE.BufferGeometry().setFromPoints( 100 | [new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -0.03)]); 101 | let line = new THREE.Line(lineGeometry); 102 | line.material.color.setRGB(0, 0, 0); 103 | line.name = 'line'; 104 | line.scale.z = 5; 105 | line.layers.set(1); 106 | line.frustumCulled = false; 107 | this.line1 = line.clone(); this.line1.quaternion.copy(this.downTilt); this.controller1.add(this.line1); 108 | this.line2 = line.clone(); this.line2.quaternion.copy(this. upTilt); this.controller2.add(this.line2); 109 | this.hoverColor = new THREE.Color(0, 1, 1); 110 | this.idleColor = new THREE.Color(0.5, 0.5, 0.5); 111 | this.initialized = true; 112 | } 113 | 114 | /** Updates visuals and regenerates the input ray */ 115 | update() { 116 | if (this.isActive()) { 117 | if (!this.initialized) { this.initialize(); } 118 | 119 | this.controller1.children[0].visible = false; 120 | this.controller2.children[0].visible = false; 121 | 122 | // Set Ray Origin and Input Direction 123 | if (this.mainHand && !this.mainHand .visible) { this.mainHand = null; this.secondaryHand = null; } 124 | if (!this.mainHand && this.controller2.visible) { this.mainHand = this.controller2; this.secondaryHand = this.controller1;} 125 | if (!this.mainHand && this.controller1.visible) { this.mainHand = this.controller1; this.secondaryHand = this.controller2;} 126 | if (this.mainHand) { 127 | this.mainHand.children[0].visible = true; 128 | this.mainHand.children[0].material.color.copy(this.ray.lastHovering ? this.hoverColor : this.idleColor); 129 | 130 | let isHand = (this.handModel1.children.length > 0 && this.handModel1.children[0].count > 0) || 131 | (this.handModel2.children.length > 0 && this.handModel2.children[0].count > 0); 132 | this.line1.quaternion.copy(isHand ? this.downTilt : this.identity); 133 | this.line2.quaternion.copy(isHand ? this. upTilt : this.identity); 134 | 135 | this.ray.ray.direction.copy(this.vec.set(0, 0, -1).applyQuaternion(this.mainHand.children[0].getWorldQuaternion(this.quat))); 136 | this.ray.ray.origin.copy(this.ray.ray.direction).multiplyScalar(isHand?0.0:0.05).add(this.mainHand.getWorldPosition(this.vec)); 137 | 138 | if (this.world.leftPinch && this.world.rightPinch){ 139 | this.world.leftPinch.position.copy(this.controller1.getWorldPosition(this.vec)); 140 | this.world.leftPinch. visible = this.controller1.inputState.pinching; 141 | this.world.rightPinch.position.copy(this.controller2.getWorldPosition(this.vec)); 142 | this.world.rightPinch.visible = this.controller2.inputState.pinching; 143 | } 144 | 145 | // Set the Menu Buttons to appear beside the users' secondary hand 146 | //this.world.camera.getWorldQuaternion(this.cameraWorldQuaternion); 147 | //this.world.camera.getWorldScale (this.cameraWorldScale); 148 | if (this.secondaryHand && this.world.parent.tools.menu) { 149 | let slots = this.world.parent.tools.menu.slots; 150 | if (slots) { 151 | this.secondaryHandTransform = (this.secondaryHand == this.controller1 ? 152 | this.hand1 : this.hand2).joints['middle-finger-phalanx-proximal']; 153 | 154 | if (!this.secondaryHandTransform) { this.secondaryHandTransform = this.secondaryHand; } 155 | 156 | // Calculate whether the secondary hand's palm is facing the camera 157 | this.vec .set(0, -1, 0).applyQuaternion(this.secondaryHandTransform.getWorldQuaternion(this.quat)); 158 | this.vec2.set(0, 0, -1).applyQuaternion(this.world.cameraWorldQuaternion); 159 | let facing = this.vec.dot(this.vec2); 160 | if (facing < 0.0) { 161 | // Array the Menu Items next to the user's secondary hand 162 | this.secondaryHandTransform.getWorldPosition(this.vec3); 163 | 164 | for (let s = 0; s < slots.length; s++) { 165 | let oldParent = slots[s].parent; 166 | this.world.scene.add(slots[s]); 167 | 168 | let chirality = (this.secondaryHand == this.controller1 ? -1 : 1); 169 | this.vec2.set((((s % 3) * 0.045) + 0.07) * chirality, 170 | 0.05 - (Math.floor(s / 3) * 0.055), 0.00).applyQuaternion(this.world.cameraWorldQuaternion); 171 | this.vec.set(-0.02 * chirality, 0, 0).applyQuaternion(this.quat).add(this.vec2) 172 | .multiplyScalar(this.world.cameraWorldScale.x).add(this.vec3); 173 | 174 | slots[s].position.copy(this.vec); 175 | oldParent.attach(slots[s]); 176 | } 177 | } 178 | } 179 | } 180 | 181 | } 182 | this.world.handsAreTracking = this.mainHand !== null; 183 | this.lastMainHand = this.mainHand; 184 | 185 | // Add Extra Fields for the active state 186 | this.ray.justActivated = false; this.ray.justDeactivated = false; 187 | this.ray.active = this.mainHand !== null && this.mainHand.inputState.pinching; 188 | if ( this.ray.active && !this.prevActive) { this.ray.justActivated = true; this.activeTime = 0; } 189 | if (!this.ray.active && this.prevActive) { this.ray.justDeactivated = true; } 190 | this.ray.hovering = false; 191 | this.prevActive = this.ray.active; 192 | if (this.ray.active) { this.activeTime += performance.now() - this.lastTimestep; } 193 | this.ray.activeMS = this.activeTime; 194 | this.lastTimestep = performance.now(); 195 | } 196 | } 197 | 198 | /** Does this input want to take control? */ 199 | isActive() { return this.world.inVR && !this.inputs.inputs.hands.handsAreTracking; } 200 | 201 | } 202 | 203 | export { OpenXRInput }; 204 | -------------------------------------------------------------------------------- /src/Frontend/Tools/CleanEdgesTool.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../../node_modules/three/build/three.module.js'; 18 | import oc from '../../../node_modules/opencascade.js/dist/opencascade.wasm.module.js'; 19 | import { Tools } from './Tools.js'; 20 | import { InteractionRay } from '../Input/Input.js'; 21 | import { snapToGrid } from './General/ToolUtils.js'; 22 | 23 | /** This class controls all of the CleanEdgesTool behavior */ 24 | class CleanEdgesTool { 25 | 26 | /** Create the CleanEdgesTool 27 | * @param {Tools} tools */ 28 | constructor(tools) { 29 | this.tools = tools; 30 | this.world = this.tools.world; 31 | this.engine = this.tools.engine; 32 | this.oc = oc; this.shapes = {}; 33 | 34 | this.state = -1; // -1 is Deactivated 35 | this.numEdgeCleanings = 0; 36 | 37 | // Create Metadata for the Menu System 38 | this.loader = new THREE.TextureLoader(); this.loader.setCrossOrigin (''); 39 | this.icon = this.loader.load ((typeof ESBUILD !== 'undefined') ? './textures/CleanEdges.png' : '../../../textures/CleanEdges.png' ); 40 | this.descriptor = { 41 | name: "Clean Edges Tool", 42 | icon: this.icon 43 | } 44 | } 45 | 46 | activate() { 47 | // Get Selected Objects 48 | this.selected = this.tools.tools[0].selected; 49 | this.tools.tools[0].clearSelection(); 50 | 51 | // Clean the Edges of Each Selected Object Individually 52 | for (let i = 0; i < this.selected.length; i++) { 53 | this.createCleanEdgesGeometry(this.selected[i]); 54 | this.numEdgeCleanings += 1; 55 | } 56 | 57 | this.deactivate(); 58 | } 59 | 60 | deactivate() { 61 | this.state = -1; 62 | this.tools.activeTool = null; 63 | } 64 | 65 | /** Update the CleanEdgesTool's State Machine 66 | * @param {InteractionRay} ray The Current Input Ray */ 67 | update(ray) { return; } 68 | 69 | /** @param {THREE.Mesh} shapeToCleanEdges */ 70 | createCleanEdgesGeometry(shapeToCleanEdges) { 71 | let shapeName = "CleanEdges #" + this.numEdgeCleanings; 72 | this.engine.execute(shapeName, this.createCleanEdges, [shapeToCleanEdges.shapeName], 73 | (mesh) => { 74 | if (mesh) { 75 | mesh.name = shapeName; 76 | mesh.shapeName = shapeName; 77 | this.tools.tools[0].toggleSelection(mesh); // Select the cleaned object 78 | 79 | // Creation of this Cleaned Edges Object 80 | this.world.history.addToUndo(mesh, shapeToCleanEdges, "Clean Edges"); 81 | } 82 | this.world.dirty = true; 83 | }); 84 | } 85 | 86 | /** Clean the Edges of this Shape in OpenCascade; to be executed on the Worker Thread */ 87 | createCleanEdges(shapeName) { 88 | if (shapeName in this.shapes) { 89 | let fusor = new this.oc.ShapeUpgrade_UnifySameDomain(this.shapes[shapeName], true, true); 90 | fusor.Build(); 91 | return fusor.Shape(); 92 | } 93 | } 94 | 95 | /** Whether or not to show this tool in the menu */ 96 | shouldShow() { return this.tools.tools[0].selected.length >= 1; } 97 | } 98 | 99 | export { CleanEdgesTool }; 100 | -------------------------------------------------------------------------------- /src/Frontend/Tools/CopyTool.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../../node_modules/three/build/three.module.js'; 18 | import oc from '../../../node_modules/opencascade.js/dist/opencascade.wasm.module.js'; 19 | import { Tools } from './Tools.js'; 20 | import { InteractionRay } from '../Input/Input.js'; 21 | import { snapToGrid } from './General/ToolUtils.js'; 22 | 23 | /** This class controls all of the CopyTool behavior */ 24 | class CopyTool { 25 | 26 | /** Create the CopyTool 27 | * @param {Tools} tools */ 28 | constructor(tools) { 29 | this.tools = tools; 30 | this.world = this.tools.world; 31 | this.engine = this.tools.engine; 32 | this.oc = oc; this.shapes = {}; 33 | 34 | this.state = -1; // -1 is Deactivated 35 | this.numCopies = 0; 36 | 37 | // Create Metadata for the Menu System 38 | this.loader = new THREE.TextureLoader(); this.loader.setCrossOrigin (''); 39 | this.icon = this.loader.load ((typeof ESBUILD !== 'undefined') ? './textures/Copy.png' : '../../../textures/Copy.png' ); 40 | this.descriptor = { 41 | name: "Copy Tool", 42 | icon: this.icon 43 | } 44 | } 45 | 46 | activate() { 47 | // Get Selected Objects 48 | this.selected = this.tools.tools[0].selected; 49 | this.tools.tools[0].clearSelection(); 50 | 51 | // Copy Each Selected Object Individually 52 | for (let i = 0; i < this.selected.length; i++) { 53 | this.createCopyGeometry(this.selected[i].shapeName); 54 | this.numCopies += 1; 55 | } 56 | 57 | this.deactivate(); 58 | } 59 | 60 | deactivate() { 61 | this.state = -1; 62 | this.tools.activeTool = null; 63 | } 64 | 65 | /** Update the CopyTool's State Machine 66 | * @param {InteractionRay} ray The Current Input Ray */ 67 | update(ray) { return; } 68 | 69 | /** @param {THREE.Mesh[]} copyMeshes */ 70 | createCopyGeometry(shapeNameToCopy) { 71 | let shapeName = "Copy #" + this.numCopies; 72 | this.engine.execute(shapeName, this.createCopy, [shapeNameToCopy], 73 | (mesh) => { 74 | if (mesh) { 75 | mesh.name = shapeName; 76 | mesh.shapeName = shapeName; 77 | this.tools.tools[0].toggleSelection(mesh); // Select the copied object 78 | 79 | // Creation of this Copy Object 80 | this.world.history.addToUndo(mesh, null, "Copy Object"); 81 | } 82 | this.world.dirty = true; 83 | }); 84 | } 85 | 86 | /** Create a Copy in OpenCascade; to be executed on the Worker Thread */ 87 | createCopy(copyObjectName) { 88 | if (copyObjectName in this.shapes) { return this.shapes[copyObjectName]; } 89 | } 90 | 91 | /** Whether or not to show this tool in the menu */ 92 | shouldShow() { return this.tools.tools[0].selected.length >= 1; } 93 | } 94 | 95 | export { CopyTool }; 96 | -------------------------------------------------------------------------------- /src/Frontend/Tools/DefaultTool.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../../node_modules/three/build/three.module.js'; 18 | import oc from '../../../node_modules/opencascade.js/dist/opencascade.wasm.module.js'; 19 | import { Tools } from './Tools.js'; 20 | import { InteractionRay } from '../Input/Input.js'; 21 | import { LSTransformControls } from './General/LSTransformControls.js'; 22 | 23 | /** This class controls all of the DefaultTool behavior */ 24 | class DefaultTool { 25 | 26 | /** Create the DefaultTool 27 | * @param {Tools} tools */ 28 | constructor(tools) { 29 | this.tools = tools; 30 | this.world = this.tools.world; 31 | this.engine = this.tools.engine; 32 | this.oc = oc; this.shapes = {}; 33 | 34 | this.state = -1; // -1 is Deactivated 35 | this.selected = []; 36 | 37 | this.hitObject = null; 38 | this.point = new THREE.Vector3(); 39 | this.tangentAxis = new THREE.Vector3(); 40 | this.rayPlane = new THREE.Mesh(new THREE.PlaneGeometry(1000, 1000), 41 | this.world.basicMaterial); 42 | this.vec = new THREE.Vector3(), this.quat1 = new THREE.Quaternion(), this.quat2 = new THREE.Quaternion(); 43 | this.xQuat = new THREE.Quaternion(), this.yQuat = new THREE.Quaternion(); 44 | this.startPos = new THREE.Vector3(); 45 | 46 | // Create Metadata for the Menu System 47 | this.loader = new THREE.TextureLoader(); this.loader.setCrossOrigin (''); 48 | this.icon = this.loader.load ((typeof ESBUILD !== 'undefined') ? './textures/Cursor.png' : '../../../textures/Cursor.png'); 49 | this.descriptor = { 50 | name: "Default Tool", 51 | icon: this.icon 52 | } 53 | 54 | // Initialize Transform Gizmo (which allows for the movement of objects around the scene) 55 | this.gizmo = new LSTransformControls(this.world.camera, this.world.container); 56 | this.gizmo.setTranslationSnap( this.tools.grid.gridPitch ); 57 | this.gizmo.setRotationSnap( THREE.MathUtils.degToRad( 15 ) ); 58 | this.gizmo.setScaleSnap( 0.25 ); 59 | this.gizmo.addEventListener('dragging-changed', (event) => { 60 | this.draggingGizmo = event.value; 61 | if (this.draggingGizmo) { 62 | // Record Current Matrix 63 | this.startPos.copy(this.gizmoTransform.position); 64 | } else { 65 | // Convert the Quaternion to Axis-Angle 66 | let q = this.gizmoTransform.quaternion; 67 | this.axis = new THREE.Vector3( 68 | q.x / Math.sqrt(1 - q.w * q.w), 69 | q.y / Math.sqrt(1 - q.w * q.w), 70 | q.z / Math.sqrt(1 - q.w * q.w)); 71 | this.angle = 2.0 * Math.acos(q.w) * 57.2958; 72 | 73 | // Compensate position for rotation and scale 74 | let rotDis = this.startPos.clone().applyQuaternion(q).sub(this.startPos); 75 | let scaDis = this.startPos.clone().multiplyScalar(this.gizmoTransform.scale.x).sub(this.startPos); 76 | 77 | // Get the Delta between Recorded and Current Transformations 78 | this.deltaPos = this.gizmoTransform.position.clone().sub(this.startPos).sub(rotDis).sub(scaDis); 79 | 80 | // Move the object via that matrix 81 | for (let i = 0; i < this.selected.length; i++) { 82 | this.moveShapeGeometry(this.selected[i], 83 | [this.selected[i].shapeName, 84 | this.deltaPos.x, this.deltaPos.y, this.deltaPos.z, 85 | this.axis.x, this.axis.y, this.axis.z, this.angle, 86 | this.gizmoTransform.scale.x]); 87 | } 88 | } 89 | }); 90 | this.gizmoTransform = new THREE.Group(); 91 | this.world.scene.add( this.gizmoTransform ); 92 | this.gizmo.attach( this.gizmoTransform ); 93 | this.draggingGizmo = false; 94 | this.gizmo.visible = false; 95 | this.gizmo.enabled = this.gizmo.visible; 96 | 97 | // Add Keyboard shortcuts for switching between modes 98 | window.addEventListener( 'keydown', ( event ) => { 99 | switch ( event.key ) { 100 | case "w": this.gizmo.setMode( "translate" ); break; 101 | case "e": this.gizmo.setMode( "rotate" ); break; 102 | case "r": this.gizmo.setMode( "scale" ); break; 103 | } 104 | } ); 105 | } 106 | 107 | /** Update the DefaultTool's State Machine 108 | * @param {InteractionRay} ray The Current Input Ray */ 109 | update(ray) { 110 | if (ray.hovering || this.state === -1) { 111 | return; // Tool is currently deactivated 112 | } else if (this.state === 0) { 113 | // Tool is currently in Selection Mode 114 | this.gizmo.update(ray); 115 | if (ray.active) { 116 | this.state = 1; 117 | } 118 | } else if (this.state === 1) { 119 | this.gizmo.update(ray); 120 | this.world.dirty = true; 121 | if (this.draggingGizmo) { 122 | this.gizmo.setTranslationSnap( this.tools.grid.gridPitch ); 123 | for (let i = 0; i < this.selected.length; i++) { 124 | let rotDis = this.startPos.clone().applyQuaternion(this.gizmoTransform.quaternion).sub(this.startPos); 125 | let scaDis = this.startPos.clone().multiplyScalar(this.gizmoTransform.scale.x).sub(this.startPos); 126 | this.selected[i].position.copy(this.gizmoTransform.position.clone().sub(this.startPos).sub(rotDis).sub(scaDis)); 127 | this.selected[i].quaternion.copy(this.gizmoTransform.quaternion); 128 | this.selected[i].scale.copy(this.gizmoTransform.scale); 129 | } 130 | } 131 | 132 | // Upon release, check if we tapped 133 | if (!ray.active) { 134 | if (!this.draggingGizmo && ray.activeMS < 200 135 | && !this.tools.engine.workerWorking) { // This last one prevents selecting parts in progress 136 | // Toggle an object's selection state 137 | if (this.raycastObject(ray)) { 138 | this.toggleSelection(this.hitObject); 139 | } 140 | } 141 | this.state = 0; 142 | } 143 | } 144 | 145 | this.gizmo.size = this.world.inVR ? 2 : 1; 146 | this.updateGizmoVisibility(); 147 | 148 | ray.hovering = this.draggingGizmo || ray.hovering; 149 | } 150 | 151 | /** Ask OpenCascade to Move the Shape on this Mesh 152 | * @param {THREE.Mesh} originalMesh */ 153 | moveShapeGeometry(originalMesh, moveShapeArgs) { 154 | let shapeName = "Transformed " + originalMesh.shapeName; 155 | this.engine.execute(shapeName, this.moveShape, moveShapeArgs, 156 | (mesh) => { 157 | originalMesh.position .set(0, 0, 0); 158 | originalMesh.scale .set(1, 1, 1); 159 | originalMesh.quaternion.set(0, 0, 0, 1); 160 | 161 | if (mesh) { 162 | mesh.shapeName = shapeName; 163 | mesh.name = originalMesh.name; 164 | this.world.history.addToUndo(mesh, originalMesh, "Movement"); 165 | this.clearSelection(originalMesh); 166 | this.toggleSelection(mesh); 167 | } 168 | this.world.dirty = true; 169 | }); 170 | } 171 | 172 | /** Create a moved shape in OpenCascade; to be executed on the Worker Thread */ 173 | moveShape(shapeToMove, x, y, z, xDir, yDir, zDir, degrees, scale) { 174 | // Use three transforms until SetValues comes in... 175 | let translation = new this.oc.gp_Trsf(), 176 | rotation = new this.oc.gp_Trsf(), 177 | scaling = new this.oc.gp_Trsf(); 178 | 179 | // Set Transformations 180 | translation.SetTranslation(new this.oc.gp_Vec(x, y, z)); 181 | 182 | if (degrees !== 0) { 183 | rotation.SetRotation( 184 | new this.oc.gp_Ax1(new this.oc.gp_Pnt(0, 0, 0), new this.oc.gp_Dir( 185 | new this.oc.gp_Vec(xDir, yDir, zDir))), degrees * 0.0174533); 186 | } 187 | if (scale !== 1) { scaling.SetScaleFactor(scale); } 188 | 189 | // Multiply together 190 | scaling.Multiply(rotation); translation.Multiply(scaling); 191 | 192 | return new this.oc.TopoDS_Shape(this.shapes[shapeToMove].Moved( 193 | new this.oc.TopLoc_Location(translation))); 194 | } 195 | 196 | updateGizmoVisibility() { 197 | // Both need to be set to make it inactive 198 | let gizmoActive = this.selected.length > 0; 199 | if (gizmoActive && !this.gizmo.visible) { 200 | this.world.scene.add(this.gizmo); 201 | } else if (!gizmoActive && this.gizmo.visible) { 202 | this.world.scene.remove(this.gizmo); 203 | } 204 | this.gizmo.visible = gizmoActive; 205 | this.gizmo.enabled = this.gizmo.visible; 206 | } 207 | 208 | positionTransformGizmo() { 209 | this.selectionBoundingBox = new THREE.Box3(); 210 | for (let i = 0; i < this.selected.length; i++){ 211 | if (i == 0) { 212 | this.selectionBoundingBox.setFromObject(this.selected[i]); 213 | } else { 214 | this.selectionBoundingBox.expandByObject(this.selected[i]); 215 | } 216 | } 217 | this.selectionBoundingBox.getCenter(this.gizmoTransform.position); 218 | this.tools.grid.snapToGrid(this.gizmoTransform.position, true); 219 | this.gizmoTransform.quaternion.set(0.0, 0.0, 0.0, 1.0); 220 | this.gizmoTransform.scale.set(1.0, 1.0, 1.0); 221 | } 222 | 223 | raycastObject(ray) { 224 | this.world.raycaster.set(ray.ray.origin, ray.ray.direction); 225 | let intersects = this.world.raycaster.intersectObject(this.world.history.shapeObjects, true);// 226 | if (intersects.length > 0) { 227 | this.hit = intersects[0]; 228 | 229 | // Record the hit object and plane... 230 | if (this.hit.object.shapeName) { 231 | this.hitObject = this.hit.object; 232 | this.point.copy(this.hit.point); 233 | this.worldNormal = this.hit.face.normal.clone() 234 | .transformDirection(this.hit.object.matrixWorld); 235 | } else { 236 | this.hitObject = null; 237 | } 238 | } else { 239 | this.hitObject = null; 240 | } 241 | return this.hitObject; 242 | } 243 | 244 | toggleSelection(obj) { 245 | if (obj && this.selected.includes(obj)) { 246 | this.clearSelection(obj); 247 | } else { 248 | obj.material = this.world.selectedMaterial; 249 | this.selected.push(obj); 250 | } 251 | this.positionTransformGizmo(); 252 | } 253 | 254 | clearSelection(obj) { 255 | if (obj && this.selected.includes(obj)) { 256 | // Clear this object from the selection 257 | obj.material = this.world.shapeMaterial; 258 | this.selected.splice(this.selected.indexOf(obj), 1); 259 | } else { 260 | // If no obj passed in, clear all 261 | for (let i = 0; i < this.selected.length; i++) { 262 | this.selected[i].material = this.world.shapeMaterial; 263 | } 264 | this.selected = []; 265 | 266 | // Both need to be set to make it inactive 267 | this.updateGizmoVisibility(); 268 | } 269 | this.positionTransformGizmo(); 270 | } 271 | 272 | activate() { 273 | if (this.tools.activeTool) { 274 | this.tools.activeTool.deactivate(); 275 | } 276 | this.state = 0; 277 | this.tools.activeTool = this; 278 | } 279 | 280 | deactivate() { 281 | this.state = -1; 282 | this.tools.activeTool = null; 283 | this.clearSelection(); 284 | //if (this.currentObject) { 285 | // this.currentObject.parent.remove(this.currentObject); 286 | //} 287 | } 288 | 289 | } 290 | 291 | export { DefaultTool }; 292 | -------------------------------------------------------------------------------- /src/Frontend/Tools/DifferenceTool.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../../node_modules/three/build/three.module.js'; 18 | import oc from '../../../node_modules/opencascade.js/dist/opencascade.wasm.module.js'; 19 | import { Tools } from './Tools.js'; 20 | import { InteractionRay } from '../Input/Input.js'; 21 | import { snapToGrid } from './General/ToolUtils.js'; 22 | 23 | /** This class controls all of the DifferenceTool behavior */ 24 | class DifferenceTool { 25 | 26 | /** Create the DifferenceTool 27 | * @param {Tools} tools */ 28 | constructor(tools) { 29 | this.tools = tools; 30 | this.world = this.tools.world; 31 | this.engine = this.tools.engine; 32 | this.oc = oc; this.shapes = {}; 33 | 34 | this.state = -1; // -1 is Deactivated 35 | this.numDifferences = 0; 36 | 37 | // Create Metadata for the Menu System 38 | this.loader = new THREE.TextureLoader(); this.loader.setCrossOrigin (''); 39 | this.icon = this.loader.load ((typeof ESBUILD !== 'undefined') ? './textures/Difference.png' : '../../../textures/Difference.png' ); 40 | this.descriptor = { 41 | name: "Difference Tool", 42 | icon: this.icon 43 | } 44 | } 45 | 46 | activate() { 47 | // Get Selected Objects 48 | this.selected = this.tools.tools[0].selected; 49 | if (this.selected.length > 1) { 50 | this.selectedShapes = []; 51 | for (let i = 0; i < this.selected.length; i++) { 52 | this.selectedShapes.push(this.selected[i].shapeName); 53 | } 54 | 55 | this.createDifferenceGeometry(this.selected, [this.selectedShapes]); 56 | this.numDifferences += 1; 57 | } 58 | 59 | this.deactivate(); 60 | } 61 | 62 | deactivate() { 63 | this.state = -1; 64 | this.tools.activeTool = null; 65 | } 66 | 67 | /** Update the DifferenceTool's State Machine 68 | * @param {InteractionRay} ray The Current Input Ray */ 69 | update(ray) { return; } 70 | 71 | /** @param {THREE.Mesh[]} differenceMeshes */ 72 | createDifferenceGeometry(differenceMeshes, createDifferenceArgs) { 73 | let shapeName = "Difference #" + this.numDifferences; 74 | this.engine.execute(shapeName, this.createDifference, createDifferenceArgs, 75 | (mesh) => { 76 | if (mesh) { 77 | mesh.name = shapeName; 78 | mesh.shapeName = shapeName; 79 | this.tools.tools[0].clearSelection(); 80 | 81 | // Creation of the Final Subtracted Difference Object 82 | this.world.history.addToUndo(mesh, null, "Subtraction"); 83 | 84 | // Individually Undoable Removal of Cutting Tools 85 | for (let s = 0; s < differenceMeshes.length; s++){ 86 | this.world.history.removeShape(differenceMeshes[s], "Original Shape"); 87 | } 88 | } 89 | this.world.dirty = true; 90 | }); 91 | } 92 | 93 | /** Create a Difference in OpenCascade; to be executed on the Worker Thread */ 94 | createDifference(differenceObjects) { 95 | if (differenceObjects.length >= 2) { 96 | let cut = false; 97 | let shape = this.shapes[differenceObjects[0]]; 98 | 99 | for (let s = 1; s < differenceObjects.length; s++){ 100 | let cuttingTool = this.shapes[differenceObjects[s]]; 101 | 102 | // Check to see if shape and fuseTool are touching 103 | let overlapChecker = new this.oc.BRepExtrema_DistShapeShape(shape, cuttingTool); 104 | overlapChecker.Perform(); 105 | 106 | if (overlapChecker.Value() <= 0 || overlapChecker.InnerSolution()) { 107 | let differenceOp = new this.oc.BRepAlgoAPI_Cut(shape, cuttingTool); 108 | differenceOp.SetFuzzyValue(0.00001); differenceOp.Build(); 109 | shape = differenceOp.Shape(); 110 | cut = true; 111 | } else { console.error("Skipping Shape; not touching..."); continue; } 112 | } 113 | 114 | return cut ? shape : null; 115 | } else { 116 | console.error("Cannot Diff; fewer than two objects in the selection..."); 117 | } 118 | } 119 | 120 | /** Whether or not to show this tool in the menu */ 121 | shouldShow() { return this.tools.tools[0].selected.length >= 2; } 122 | } 123 | 124 | export { DifferenceTool }; 125 | -------------------------------------------------------------------------------- /src/Frontend/Tools/ExtrusionTool.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../../node_modules/three/build/three.module.js'; 18 | import oc from '../../../node_modules/opencascade.js/dist/opencascade.wasm.module.js'; 19 | import { Tools } from './Tools.js'; 20 | import { InteractionRay } from '../Input/Input.js'; 21 | 22 | /** This class controls all of the ExtrusionTool behavior */ 23 | class ExtrusionTool { 24 | 25 | /** Create the ExtrusionTool 26 | * @param {Tools} tools */ 27 | constructor(tools) { 28 | this.tools = tools; 29 | this.world = this.tools.world; 30 | this.engine = this.tools.engine; 31 | this.oc = oc; this.shapes = {}; 32 | 33 | this.state = -1; // -1 is Deactivated 34 | this.numExtrusions = 0; 35 | this.distance = 0.001; 36 | this.height = 0.001; 37 | this.point = new THREE.Vector3(); 38 | this.worldCameraScale = new THREE.Vector3(); 39 | this.rayPlane = new THREE.Mesh(new THREE.PlaneGeometry(1000, 1000), 40 | this.world.basicMaterial); 41 | this.extrusionMesh = null; 42 | 43 | // Create Metadata for the Menu System 44 | this.loader = new THREE.TextureLoader(); this.loader.setCrossOrigin (''); 45 | this.icon = this.loader.load ((typeof ESBUILD !== 'undefined') ? './textures/Extrusion.png' : '../../../textures/Extrusion.png'); 46 | this.descriptor = { 47 | name: "Extrusion Tool", 48 | icon: this.icon 49 | } 50 | 51 | // All Extrusion handles are children of this 52 | this.handleParent = new THREE.Group(); 53 | this.world.scene.add(this.handleParent); 54 | 55 | // Create a pool of extrusion handles which can be assigned to faces in the scene 56 | this.handles = []; 57 | } 58 | 59 | /** Spawn Extrusion Handles on each flat face */ 60 | activate() { 61 | if (this.tools.activeTool) { this.tools.activeTool.deactivate(); } 62 | this.state = 0; 63 | this.tools.activeTool = this; 64 | 65 | // Place Extrusion Handles 66 | this.world.camera.getWorldScale(this.worldCameraScale); 67 | let curArrow = 0; 68 | for (let i = 0; i < this.world.history.shapeObjects.children.length; i++) { 69 | for (let j = 0; j < this.world.history.shapeObjects.children[i].faceMetadata.length; j++) { 70 | let faceData = this.world.history.shapeObjects.children[i].faceMetadata[j]; 71 | 72 | if (curArrow >= this.handles.length) { 73 | let dir = new THREE.Vector3( 1, 2, 0 ).normalize(); 74 | let origin = new THREE.Vector3( 0, 0, 0 ); 75 | this.handles.push(new THREE.ArrowHelper( dir, origin, 0.030, 0x00ffff )); 76 | } 77 | 78 | if (faceData.is_planar && curArrow < this.handles.length) { 79 | this.handles[curArrow].position.set(faceData.average[0], faceData.average[1], faceData.average[2]); 80 | let normal = new THREE.Vector3(faceData.normal[0], faceData.normal[1], faceData.normal[2]); 81 | this.handles[curArrow].setDirection(normal); 82 | this.handles[curArrow].setLength( 83 | 0.03 * this.worldCameraScale.x, 84 | 0.01 * this.worldCameraScale.x, 85 | 0.01 * this.worldCameraScale.x); 86 | this.handles[curArrow].faceIndex = faceData.index; 87 | this.handles[curArrow].parentObject = this.world.history.shapeObjects.children[i]; 88 | this.handles[curArrow].extrusionDirection = normal; 89 | 90 | this.handleParent.add(this.handles[curArrow++]); 91 | } 92 | } 93 | } 94 | } 95 | 96 | deactivate() { 97 | this.state = -1; 98 | this.tools.activeTool = null; 99 | if (this.currentExtrusion && this.currentExtrusion.parent) { 100 | this.currentExtrusion.parent.remove(this.currentExtrusion); 101 | } 102 | for (let i = 0; i < this.handles.length; i++) { 103 | this.handleParent.remove(this.handles[i]); 104 | } 105 | } 106 | 107 | /** Update the ExtrusionTool's State Machine 108 | * @param {InteractionRay} ray The Current Input Ray */ 109 | update(ray) { 110 | if (ray.hovering || this.state === -1) { 111 | return; // Tool is currently deactivated 112 | } else if(this.state === 0) { 113 | // Wait for the ray to be active and pointing at a drawable surface 114 | this.world.raycaster.set(ray.ray.origin, ray.ray.direction); 115 | let intersects = this.world.raycaster.intersectObject(this.handleParent, true); 116 | 117 | if (intersects.length > 0) { 118 | if (ray.justActivated && ray.active) { 119 | // TODO: Check if this face is in the metadata for Extrusion 120 | // if(intersects[0].object.shapeName){} 121 | 122 | this.hit = intersects[0].object.parent; 123 | this.point.copy(this.hit.position); 124 | this.state += 1; 125 | } 126 | ray.hovering = true; 127 | } 128 | } else if(this.state === 1) { 129 | // Resize the Height while dragging 130 | let upperSegment = this.hit.extrusionDirection.clone().multiplyScalar( 1000.0).add(this.point); 131 | let lowerSegment = this.hit.extrusionDirection.clone().multiplyScalar(-1000.0).add(this.point); 132 | let pointOnRay = new THREE.Vector3(), pointOnSegment = new THREE.Vector3(); 133 | let sqrDistToSeg = ray.ray.distanceSqToSegment(lowerSegment, upperSegment, pointOnRay, pointOnSegment); 134 | //this.height = pointOnSegment.sub(this.hit.position).dot(this.hit.extrusionDirection); 135 | this.snappedHeight = pointOnSegment.sub(this.point).dot(this.hit.extrusionDirection); 136 | this.snappedHeight = this.tools.grid.snapToGrid1D(this.snappedHeight); 137 | this.tools.cursor.updateLabelNumbers(this.snappedHeight); 138 | this.height = (!ray.active) ? this.snappedHeight : (this.height * 0.75) + (this.snappedHeight * 0.25); 139 | 140 | this.tools.cursor.updateTarget(this.hit.extrusionDirection.clone().multiplyScalar(this.height).add(this.point)); 141 | 142 | if (!this.currentExtrusion) { 143 | this.createPreviewExtrusionGeometry(this.point, 144 | [this.hit.extrusionDirection.x, this.hit.extrusionDirection.y, this.hit.extrusionDirection.z, 145 | 1, this.hit.faceIndex, this.hit.parentObject.shapeName, false]); 146 | } else if (this.currentExtrusion !== "Waiting...") { 147 | this.currentExtrusion.scale.y = this.height; 148 | this.currentExtrusion.children[0].material.emissive.setRGB( 149 | this.height > 0 ? 0.0 : 0.25, 150 | this.height > 0 ? 0.25 : 0.0 , 0.0); 151 | } 152 | ray.hovering = true; 153 | 154 | // When let go, deactivate and add to Undo! 155 | if (!ray.active) { 156 | this.createExtrusionGeometry(this.currentExtrusion, 157 | [this.hit.extrusionDirection.x, this.hit.extrusionDirection.y, this.hit.extrusionDirection.z, 158 | this.height, this.hit.faceIndex, this.hit.parentObject.shapeName, true]); 159 | 160 | this.numExtrusions += 1; 161 | this.currentExtrusion = null; 162 | this.deactivate(); 163 | } 164 | } 165 | } 166 | 167 | /** @param {THREE.Mesh} extrusionMesh */ 168 | createExtrusionGeometry(extrusionMesh, createExtrusionArgs) { 169 | let shapeName = "Extrusion #" + this.numExtrusions; 170 | this.engine.execute(shapeName, this.createExtrusion, createExtrusionArgs, 171 | (mesh) => { 172 | if (mesh) { 173 | mesh.name = extrusionMesh.name; 174 | mesh.shapeName = shapeName; 175 | if (this.hit.parentObject.shapeName) { 176 | this.world.history.addToUndo(mesh, this.hit.parentObject, "Extrusion"); 177 | this.hitObject = null; 178 | } else { 179 | this.world.history.addToUndo(mesh, null, "Extrusion"); 180 | } 181 | } 182 | 183 | extrusionMesh.parent.remove(extrusionMesh); 184 | this.world.dirty = true; 185 | }); 186 | } 187 | 188 | /** @param {THREE.Vector3} extrusionPivot */ 189 | createPreviewExtrusionGeometry(extrusionPivot, createExtrusionArgs) { 190 | let shapeName = "Extrusion #" + this.numExtrusions; 191 | this.currentExtrusion = "Waiting..."; 192 | this.engine.execute(shapeName, this.createExtrusion, createExtrusionArgs, 193 | (mesh) => { 194 | if (this.currentExtrusion && this.currentExtrusion.parent) { 195 | this.currentExtrusion.parent.remove(this.currentExtrusion); 196 | } 197 | 198 | if (mesh) { 199 | mesh.shapeName = shapeName; 200 | mesh.material = this.world.previewMaterial; 201 | 202 | this.currentExtrusion = new THREE.Group(); 203 | this.currentExtrusion.position.copy(extrusionPivot); 204 | this.currentExtrusion.quaternion.copy(new THREE.Quaternion() 205 | .setFromUnitVectors(new THREE.Vector3(0, 1, 0), 206 | new THREE.Vector3(createExtrusionArgs[0], createExtrusionArgs[1], createExtrusionArgs[2]))); 207 | //this.currentExtrusion.scale.y = 0.00001; 208 | this.currentExtrusion.attach(mesh); 209 | this.world.scene.add(this.currentExtrusion); 210 | } 211 | this.world.dirty = true; 212 | }); 213 | } 214 | 215 | /** Create a Extrusion in OpenCascade; to be executed on the Worker Thread */ 216 | createExtrusion(nx, ny, nz, height, faceIndex, hitObjectName, csg) { 217 | if (height != 0) { 218 | let hitObject = this.shapes[hitObjectName]; 219 | 220 | // Change the Extrusion Extension direction based on the height 221 | nx *= height; ny *= height; nz *= height; 222 | 223 | // Get a reference to the face to extrude 224 | let face = null; let face_index = 0; 225 | let anExplorer = new this.oc.TopExp_Explorer(hitObject, this.oc.TopAbs_FACE); 226 | for (anExplorer.Init(hitObject, this.oc.TopAbs_FACE); anExplorer.More(); anExplorer.Next()) { 227 | if (face_index === faceIndex) { 228 | face = this.oc.TopoDS.prototype.Face(anExplorer.Current()); 229 | break; 230 | } else { 231 | face_index += 1; 232 | } 233 | } 234 | 235 | if (face) { 236 | // Construct the Extrusion Shape 237 | let shape = new this.oc.BRepPrimAPI_MakePrism(face, 238 | new this.oc.gp_Vec(nx, ny, nz), true, true).Shape(); 239 | 240 | if (!csg) { return shape; } // Return the Raw Shape 241 | 242 | // Let's CSG this Extrusion onto/into the object it came from 243 | if (height > 0) { 244 | // The Height is Positive, let's Union 245 | let unionOp = new this.oc.BRepAlgoAPI_Fuse(hitObject, shape); 246 | unionOp.SetFuzzyValue(0.00001); 247 | unionOp.Build(); 248 | return unionOp.Shape(); 249 | //let unionOp = new this.oc.BRepBuilderAPI_Sewing(0.00001); 250 | //unionOp.Add(hitObject); 251 | //unionOp.Add(shape); 252 | //unionOp.Perform(); 253 | //return unionOp.SewedShape(); 254 | } else if (height < 0) { 255 | // The Height is Negative, let's Subtract 256 | let differenceOp = new this.oc.BRepAlgoAPI_Cut(hitObject, shape); 257 | differenceOp.SetFuzzyValue(0.00001); 258 | differenceOp.Build(); 259 | return differenceOp.Shape(); 260 | } 261 | } 262 | } 263 | } 264 | 265 | /** Whether or not to show this tool in the menu 266 | * Only Show when no objects are selected */ 267 | shouldShow() { return this.tools.tools[0].selected.length == 0; } 268 | } 269 | 270 | export { ExtrusionTool }; 271 | -------------------------------------------------------------------------------- /src/Frontend/Tools/General/Alerts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../../../node_modules/three/build/three.module.js'; 18 | import { TextMesh } from '../../World/TextMesh.js'; 19 | 20 | /** This is an in-scene helper for measurements and precision placement. */ 21 | class Alerts { 22 | 23 | /** Initialize the Alerts 24 | * @param {Tools} tools */ 25 | constructor(tools) { 26 | // Store a reference to the World 27 | this.tools = tools; 28 | this.world = tools.world; 29 | this.engine = this.world.parent.engine; 30 | this.cursor = this.tools.cursor; 31 | 32 | this.alerts = new THREE.Group(); 33 | this.alerts.name = "Alerts"; 34 | this.alerts.layers.set(1); // Ignore Raycasts 35 | 36 | this.targetPosition = new THREE.Vector3(); 37 | this.lastTimeTargetUpdated = performance.now(); 38 | this.position = this.alerts.position; 39 | this.hitObject = null; 40 | this.vec1 = new THREE.Vector3(); this.vec2 = new THREE.Vector3(); 41 | this.quat = new THREE.Quaternion(); 42 | this.fadeTime = 5000; 43 | 44 | // Create a Text Updating Label for the General Alert Data 45 | this.labels = []; 46 | for (let i = 0; i < 5; i++) { 47 | let label = new TextMesh(""); 48 | label.layers.set(1); // Ignore Raycasts 49 | label.frustumCulled = false; 50 | this.alerts.add (label); 51 | this.labels.push(label); 52 | } 53 | 54 | this.world.scene.add(this.alerts); 55 | } 56 | 57 | update() { 58 | if (performance.now() - this.lastTimeTargetUpdated < this.fadeTime) { 59 | let alpha = this.alerts.visible ? 0.25 : 1.0; 60 | 61 | this.alerts.visible = true; 62 | 63 | // Lerp the Alerts to the Target Position 64 | this.alerts.position.lerp(this.cursor.targetPosition, alpha); 65 | 66 | // Make the Alerts Contents Face the Camera 67 | this.alerts.quaternion.slerp(this.world.camera.getWorldQuaternion(this.quat), alpha); 68 | 69 | this.alerts.scale.copy(this.world.camera.getWorldScale(this.vec1)); 70 | 71 | // Lerp the Alerts to Stack on top of each other 72 | for (let i = 0; i < this.labels.length; i++){ 73 | let age = performance.now() - this.labels[i].lastUpdated; 74 | if (age < this.labels[i].displayTime) { 75 | this.labels[i].visible = true; 76 | this.labels[i].material.opacity = (this.labels[i].displayTime - age) / this.labels[i].displayTime; 77 | 78 | this.labels[i].position.y = 79 | (this.labels[i].position.y * (1.0 - 0.25)) + 80 | (this.labels[i].targetHeight * ( 0.25)); 81 | } else { 82 | this.labels[i].visible = false; 83 | } 84 | } 85 | 86 | } else { 87 | this.alerts.visible = false; 88 | } 89 | } 90 | 91 | displayInfo(text, r = 0, g = 0, b = 0, time = 2000) { 92 | // Move end label to the beginning 93 | this.labels.splice(0, 0, this.labels.splice(this.labels.length - 1, 1)[0]); 94 | // Render HTML Element's Text to the Mesh 95 | this.labels[0].update(text, r, g, b); 96 | this.labels[0].lastUpdated = performance.now(); 97 | this.labels[0].displayTime = time||2000; 98 | 99 | // Update the target height to stack the labels on top of eachother 100 | let curTargetHeight = this.labels[0].canonicalPosition.y * 2; 101 | for (let i = 0; i < this.labels.length; i++){ 102 | this.labels[i].targetHeight = curTargetHeight; 103 | curTargetHeight += this.labels[i].scale.y; 104 | } 105 | 106 | this.lastTimeTargetUpdated = performance.now(); 107 | } 108 | 109 | displayError(text) { 110 | this.displayInfo(text, 255, 0, 0, 5000); 111 | } 112 | } 113 | 114 | export { Alerts }; 115 | -------------------------------------------------------------------------------- /src/Frontend/Tools/General/Cursor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../../../node_modules/three/build/three.module.js'; 18 | import { TextMesh } from '../../World/TextMesh.js'; 19 | 20 | /** This is an in-scene helper for measurements and precision placement. */ 21 | class Cursor { 22 | 23 | /** Initialize the Cursor 24 | * @param {Tools} tools */ 25 | constructor(tools) { 26 | // Store a reference to the World 27 | this.tools = tools; 28 | this.world = tools.world; 29 | this.engine = this.world.parent.engine; 30 | 31 | this.sphereGeo = new THREE.SphereGeometry(0.003, 5, 5); 32 | this.cursor = new THREE.Mesh(this.sphereGeo, new THREE.MeshBasicMaterial( {depthTest: false})); 33 | this.cursor.material.color.set(0x00ffff); 34 | this.cursor.name = "Cursor"; 35 | this.cursor.receiveShadow = false; 36 | this.cursor.castShadow = false; 37 | this.cursor.layers.set(1); // Ignore Raycasts 38 | this.cursor.frustumCulled = false; 39 | this.targetPosition = new THREE.Vector3(); 40 | this.lastTimeTargetUpdated = performance.now(); 41 | this.position = this.cursor.position; 42 | this.hitObject = null; 43 | 44 | // Create a Text Updating Label for the Coordinate Data 45 | this.label = new TextMesh(''); 46 | this.label.frustumCulled = false; 47 | this.label.layers.set(1); // Ignore Raycasts 48 | this.cursor.add(this.label); 49 | 50 | this.vec1 = new THREE.Vector3(); this.vec2 = new THREE.Vector3(); 51 | this.quat = new THREE.Quaternion(); 52 | 53 | this.world.scene.add(this.cursor); 54 | } 55 | 56 | update() { 57 | if (performance.now() - this.lastTimeTargetUpdated < 100) { 58 | let alpha = this.cursor.visible ? 0.25 : 1.0; 59 | 60 | this.cursor.visible = true; 61 | 62 | // Lerp the Cursor to the Target Position 63 | this.cursor.position.lerp(this.targetPosition, alpha); 64 | 65 | // Make the Cursor Contents Face the Camera 66 | this.cursor.quaternion.slerp(this.world.camera.getWorldQuaternion(this.quat), alpha); 67 | 68 | this.cursor.scale.copy(this.world.camera.getWorldScale(this.vec1)); 69 | } else { 70 | this.cursor.visible = false; 71 | } 72 | } 73 | 74 | updateTarget(position, raycastHit) { 75 | this.targetPosition.copy(position); 76 | this.hit = raycastHit; 77 | this.lastTimeTargetUpdated = performance.now(); 78 | } 79 | 80 | updateLabel(text, r = 0, g = 0, b = 0) { 81 | if (this.label.text !== text) { 82 | this.label.update(text, r, g, b); 83 | } 84 | } 85 | 86 | updateLabelNumbers(...numbers) { 87 | // Compute New Label String 88 | let str = "("; 89 | numbers.forEach((num) => { str += Number(num.toFixed(2)) + ", "; }); 90 | str = str.substr(0, str.length - 2); 91 | str += ")"; 92 | 93 | this.updateLabel(str, 0, 0, 0); 94 | } 95 | } 96 | 97 | export { Cursor }; 98 | -------------------------------------------------------------------------------- /src/Frontend/Tools/General/Grid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../../../node_modules/three/build/three.module.js'; 18 | import oc from '../../../../node_modules/opencascade.js/dist/opencascade.wasm.module.js'; 19 | import { safeQuerySurface, snapToGrid } from './ToolUtils.js'; 20 | 21 | /** This is an in-scene helper for measurements and precision placement. */ 22 | class Grid { 23 | 24 | /** Initialize the Grid 25 | * @param {Tools} tools */ 26 | constructor(tools) { 27 | // Store a reference to the World 28 | this.tools = tools; this.world = tools.world; this.oc = oc; 29 | this.engine = this.world.parent.engine; 30 | 31 | this.gridPitch = 0.01; 32 | this.vec1 = new THREE.Vector3(); 33 | this.vec2 = new THREE.Vector3(); 34 | this.quat = new THREE.Quaternion(); 35 | this.quat1 = new THREE.Quaternion(); 36 | this.quat2 = new THREE.Quaternion(); 37 | this.normal = new THREE.Vector3(); 38 | this.color = new THREE.Color(); 39 | this.needsUpdate = true; 40 | this.updateCount = 0; 41 | 42 | // The coordinate space for the grid 43 | this.space = new THREE.Group(); 44 | this.space.layers.set(1); 45 | 46 | // The Visual Grid Mesh 47 | this.gridCells = 10; 48 | //this.gridMesh = new THREE.GridHelper( this.gridPitch * this.gridCells, this.gridCells, 0x000000, 0x000000 ); 49 | //this.gridMesh.material.opacity = 0.2; 50 | //this.gridMesh.material.transparent = true; 51 | //this.gridMesh.layers.set(1); 52 | //this.space.add(this.gridMesh); 53 | 54 | this.sphereGeometry = new THREE.SphereGeometry(0.001, 5, 5); 55 | this.gridSpheres = new THREE.InstancedMesh(this.sphereGeometry, new THREE.MeshBasicMaterial(), this.gridCells * (this.gridCells + 3) + 10); 56 | this.gridSpheres.castShadow = true; 57 | this.gridSpheres.layers.set(1); 58 | this.gridSpheres.frustumCulled = false; 59 | this.radius = 2; this.mat = new THREE.Matrix4(); this.gridCenter = new THREE.Vector3(); this.tempVec = new THREE.Vector3(); 60 | this.pos = new THREE.Vector3(); this.rot = new THREE.Quaternion().identity(); this.scale = new THREE.Vector3(); 61 | this.updateGridVisual(this.gridCenter); 62 | this.space.add(this.gridSpheres); 63 | 64 | this.world.scene.add(this.space); 65 | this.setVisible(false); 66 | } 67 | 68 | updateWithHit(raycastHit) { 69 | this.worldScale = this.world.camera.getWorldScale(this.vec1).x; 70 | this.gridPitch = 0.00125; 71 | while (this.gridPitch < (this.worldScale * 0.01)-Number.EPSILON) { this.gridPitch *= 2;} 72 | 73 | if (raycastHit.object.shapeName) { 74 | // Append the UV Bounds to the query since they're impossible to get in the query 75 | let index = raycastHit.faceIndex; 76 | for (let i = 0; i < raycastHit.object.faceMetadata.length; i++){ 77 | let curFace = raycastHit.object.faceMetadata[i]; 78 | if (curFace.start <= index && index < curFace.end) { 79 | raycastHit.uvBounds = curFace.uvBounds; break; 80 | } 81 | } 82 | 83 | safeQuerySurface(this.world.parent.engine, raycastHit, (queryResult) => { 84 | this.updateWithQuery(queryResult); 85 | }); 86 | } else { 87 | // Set Grid Position 88 | this.space.position.set( 89 | raycastHit.object.position.x, 90 | raycastHit.object.position.y, 91 | raycastHit.object.position.z); 92 | 93 | // Set Grid Rotation 94 | this.space.quaternion.identity(); 95 | this.normal.copy(raycastHit.face.normal.clone().transformDirection(raycastHit.object.matrixWorld)); 96 | 97 | // Set the dot center 98 | this.updateGridVisual(this.tempVec.copy(raycastHit.point)); 99 | 100 | this.needsUpdate = false; 101 | this.updateCount += 1; 102 | } 103 | } 104 | 105 | updateWithQuery(queryResult) { 106 | this.queryResult = queryResult; 107 | 108 | // Set Grid Position 109 | if (this.queryResult.hasOwnProperty("nX")) { 110 | if (this.queryResult.faceType == 0) { 111 | // If face is a plane, set the origin to the plane origin 112 | let origin = this.queryResult.grid[this.queryResult.grid.length - 1]; 113 | this.space.position.set(origin[0], origin[1], origin[2]); 114 | } else { 115 | // If face is curved, set the origin to the current point on the curve 116 | this.space.position.set(this.queryResult.x, this.queryResult.y, this.queryResult.z); 117 | } 118 | 119 | // Set Grid Rotation 120 | this.quat.identity(); 121 | 122 | //if (this.queryResult.tU) { 123 | // this.vec1.set(1, 0, 0).applyQuaternion(this.quat); 124 | // this.vec2.fromArray(this.queryResult.tU); 125 | // this.quat2.setFromUnitVectors(this.vec1, this.vec2); 126 | // this.quat.premultiply(this.quat2); 127 | //} 128 | //if (this.queryResult.tV) { 129 | // this.vec1.set(0, 0, 1).applyQuaternion(this.quat); 130 | // this.vec2.fromArray(this.queryResult.tV); 131 | // this.quat2.setFromUnitVectors(this.vec1, this.vec2); 132 | // this.quat.premultiply(this.quat2); 133 | //} 134 | 135 | this.vec1.set(0, 1, 0).applyQuaternion(this.quat); 136 | this.normal.set(this.queryResult.nX, this.queryResult.nY, this.queryResult.nZ); 137 | this.quat2.setFromUnitVectors(this.vec1, this.normal); 138 | this.quat.premultiply(this.quat2); 139 | 140 | this.vec1.set(0, 0, 1).applyQuaternion(this.quat); 141 | this.vec2.set(0, 1, 0).projectOnPlane(this.normal).normalize(); 142 | this.quat2.setFromUnitVectors(this.vec1, this.vec2); 143 | this.quat.premultiply(this.quat2); 144 | 145 | this.vec1.set(0, 1, 0).applyQuaternion(this.quat); 146 | this.quat2.setFromUnitVectors(this.vec1, this.normal); 147 | this.quat.premultiply(this.quat2); 148 | 149 | this.space.quaternion.copy(this.quat); 150 | 151 | 152 | // Set the dot center 153 | this.updateGridVisual(this.tempVec.set(this.queryResult.x, this.queryResult.y, this.queryResult.z), this.queryResult.grid); 154 | 155 | this.needsUpdate = false; 156 | this.updateCount += 1; 157 | } 158 | } 159 | 160 | /** Internal method to update the grid's position 161 | * @param {THREE.Vector3} worldCenter 162 | * @param {number[][]} grid */ 163 | updateGridVisual(worldCenter, grid) { 164 | this.space.updateWorldMatrix(true, true); 165 | this.gridSpheres.position.copy(this.space.worldToLocal(this.snapToGrid(worldCenter.clone(), false, false))); 166 | this.gridSpheres.updateWorldMatrix(true, true); 167 | let center = new THREE.Vector3().copy(worldCenter); 168 | this.gridSpheres.worldToLocal(center); 169 | 170 | let i = 0; 171 | for (let x = -this.gridCells / 2; x <= this.gridCells/2; x++){ 172 | for (let y = -this.gridCells / 2; y <= this.gridCells / 2; y++){ 173 | let newX = ((x ) * this.gridPitch) - center.x; 174 | let newY = ((y ) * this.gridPitch) - center.z; 175 | this.radius = Math.min(2, (8 * this.gridPitch * this.gridPitch) / 176 | ((newX * newX) + (newY * newY) + 0.0001)) * this.worldScale; 177 | 178 | this.pos.set(x * this.gridPitch, 0, y * this.gridPitch) 179 | this.mat.makeRotationFromQuaternion(this.rot) 180 | .scale(this.scale.set(this.radius, this.radius, this.radius)).setPosition(this.pos); 181 | this.gridSpheres.setMatrixAt(i, this.mat); 182 | this.gridSpheres.setColorAt (i, this.color.setRGB(0.7, 0.7, 0.7)); 183 | i++; 184 | } 185 | } 186 | 187 | if (grid) { 188 | for (let g = 0; g < grid.length; g++) { 189 | this.pos.set(grid[g][0], grid[g][1], grid[g][2]); 190 | this.gridSpheres.worldToLocal(this.pos); 191 | this.mat.makeRotationFromQuaternion(this.rot) 192 | .scale(this.scale.set( 193 | 2.5 * this.worldScale, 194 | 2.5 * this.worldScale, 195 | 2.5 * this.worldScale)).setPosition(this.pos); 196 | this.gridSpheres.setMatrixAt(i, this.mat); 197 | this.gridSpheres.setColorAt (i, this.color.setRGB(0.0, 1.0, 1.0)); 198 | i++; 199 | } 200 | } 201 | 202 | // Upload the adjusted instance data to the GPU 203 | this.gridSpheres.count = i; 204 | if (this.gridSpheres.instanceColor ) { this.gridSpheres.instanceColor .needsUpdate = true; } 205 | if (this.gridSpheres.instanceMatrix) { this.gridSpheres.instanceMatrix.needsUpdate = true; } 206 | } 207 | 208 | /** Snap this position to the grid 209 | * @param {THREE.Vector3} position 210 | * @param {boolean} volumetric 211 | * @param {boolean} useMagnetPoints */ 212 | snapToGrid(position, volumetric = false, useMagnetPoints = true) { 213 | this.vec1.copy(position); 214 | this.space.worldToLocal(this.vec1); 215 | if (!volumetric) { this.vec1.y = 0; } 216 | snapToGrid(this.vec1, this.gridPitch); 217 | this.space.localToWorld(this.vec1); 218 | 219 | // If we have magnet points... 220 | if (useMagnetPoints) { 221 | if (this.queryResult && this.queryResult.grid) { 222 | // Try snapping to magnet points 223 | let closestDistance = this.vec1.distanceTo(position), closestIndex = -1; 224 | for (let g = 0; g < this.queryResult.grid.length; g++) { 225 | this.vec2.fromArray(this.queryResult.grid[g]); 226 | let distance = this.vec2.distanceTo(position); 227 | if (distance < closestDistance) { 228 | closestDistance = distance; 229 | closestIndex = g; 230 | } 231 | } 232 | 233 | // If the magnet point is closer, 234 | // use it instead of the grid point! 235 | if (closestIndex >= 0) { this.vec1.fromArray(this.queryResult.grid[closestIndex]); } 236 | } 237 | } 238 | 239 | position.copy(this.vec1); 240 | return position; 241 | } 242 | 243 | /** @param {number} length */ 244 | snapToGrid1D(length, incrementOverride) { 245 | return (Math.round((length + Number.EPSILON) / 246 | (incrementOverride || this.gridPitch)) * (incrementOverride || this.gridPitch)); 247 | } 248 | 249 | /** @param {boolean} visible */ 250 | setVisible(visible) { 251 | this.space.visible = visible; 252 | if (!visible) { 253 | this.space.position.set(0, 0, 0); 254 | this.space.quaternion.identity(); 255 | this.updateCount = 0; 256 | } 257 | } 258 | } 259 | 260 | export { Grid }; 261 | -------------------------------------------------------------------------------- /src/Frontend/Tools/General/Menu.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../../../node_modules/three/build/three.module.js'; 18 | import { Tools } from '../Tools.js'; 19 | import { World } from '../../World/World.js'; 20 | import { InteractionRay } from '../../Input/Input.js'; 21 | 22 | /** The menu system for selecting tools and configuring behavior. */ 23 | class Menu { 24 | 25 | /** Create the menu scaffolding 26 | * @param {Tools} tools */ 27 | constructor(tools) { 28 | this.tools = tools; 29 | this.world = tools.world; 30 | 31 | this.normalColor = new THREE.Color(0.4, 0.4, 0.4); 32 | this.highlightedColor = new THREE.Color(0.5, 0.6, 0.5); 33 | this.pressedColor = new THREE.Color(1.0, 0.6, 0.5); 34 | this.heldColor = new THREE.Color(1.0, 0.3, 0.3); 35 | this.activeColor = new THREE.Color(0.4, 0.5, 0.5); 36 | this.tempV3 = new THREE.Vector3(); 37 | this.menuHeld = false; 38 | this.cameraWorldPos = new THREE.Vector3(); 39 | this.cameraWorldRot = new THREE.Quaternion(); 40 | this.cameraWorldScale = new THREE.Vector3(); 41 | this.halfSpacing = 25; 42 | 43 | this.menuSphereGeo = new THREE.SphereGeometry(0.020, 20, 20); 44 | this.menuPlaneGeo = new THREE.PlaneGeometry (0.025, 0.025); 45 | 46 | // Menu Container 47 | this.menu = new THREE.Group(); this.menuItems = []; 48 | for (let i = 0; i < tools.tools.length; i++) { 49 | let menuItem = new THREE.Mesh(this.menuSphereGeo, 50 | new THREE.MeshToonMaterial({ color: 0x999999, transparent: true, opacity: 0.5, depthTest: false })); 51 | menuItem.name = "Menu Item - "+i; 52 | menuItem.receiveShadow = false; 53 | menuItem.castShadow = false; 54 | menuItem.frustumCulled = false; 55 | 56 | let menuItemIcon = new THREE.Mesh(this.menuPlaneGeo, 57 | new THREE.MeshBasicMaterial( 58 | { color: 0x999999, transparent: true, map: tools.tools[i].descriptor.icon, depthTest:false })); 59 | menuItemIcon.name = "Menu Item Icon - "+i; 60 | menuItemIcon.receiveShadow = false; 61 | menuItemIcon.castShadow = false; 62 | menuItemIcon.frustumCulled = false; 63 | menuItem.icon = menuItemIcon; 64 | menuItem.add(menuItemIcon); 65 | 66 | menuItem.tool = tools.tools[i]; 67 | 68 | this.menuItems.push(menuItem); 69 | this.menu.add(menuItem); 70 | } 71 | this.world.scene.add(this.menu); 72 | 73 | // Define a series of slot objects for the menu items to lerp towards... 74 | this.slots = []; 75 | for (let i = 0; i < 15; i++) { 76 | let slot = new THREE.Group(); 77 | slot.name = "Slot #" + i; 78 | slot.canonicalPosition = new THREE.Vector3( 79 | (i * this.halfSpacing * 2) - (this.halfSpacing * 6), 80 | 25 * 6, (-75 * 6)).multiplyScalar(0.001); 81 | slot.position.copy(slot.canonicalPosition); 82 | this.slots.push(slot); 83 | this.world.camera.add(slot); 84 | } 85 | } 86 | 87 | /** Update the menu motion and interactive state 88 | * @param {InteractionRay} ray The Current Input Ray */ 89 | update(ray) { 90 | this.world.camera.getWorldPosition (this.cameraWorldPos); 91 | this.world.camera.getWorldQuaternion(this.cameraWorldRot); 92 | this.world.camera.getWorldScale (this.cameraWorldScale); 93 | 94 | // Check to see if the interaction ray intersects one of these items 95 | this.world.raycaster.set(ray.ray.origin, ray.ray.direction); 96 | let intersects = this.world.raycaster.intersectObject(this.menu, true); 97 | 98 | let activeMenuIndex = 0; 99 | for (let i = 0; i < this.menuItems.length; i++){ 100 | // Hide/Show Contextual Menu Items 101 | if (!this.menuItems[i].tool.shouldShow || this.menuItems[i].tool.shouldShow()) { 102 | if (!this.menu.children.includes(this.menuItems[i])) { this.menu.add(this.menuItems[i]); } 103 | } else { 104 | if (this.menu.children.includes(this.menuItems[i])) { this.menu.remove(this.menuItems[i]); } 105 | continue; 106 | } 107 | 108 | // Hover highlight the menu spheres 109 | if (intersects.length > 0 && intersects[0].object === this.menuItems[i]) { 110 | if (ray.justDeactivated && this.menuHeld) { 111 | // Activate the tool associated with this ID 112 | this.tools.tools[i].activate(); 113 | this.menuItems[i].material.color.copy(this.pressedColor); 114 | this.menuHeld = false; 115 | } else if (ray.justActivated || this.menuHeld) { 116 | this.menuHeld = true; 117 | this.menuItems[i].material.color.lerp(this.heldColor, 0.15); 118 | } else { 119 | this.menuHeld = false; 120 | this.menuItems[i].material.color.lerp(this.highlightedColor, 0.15); 121 | if (!ray.active) { ray.hovering = true; } 122 | } 123 | } else { 124 | if (this.menuItems[i].tool === this.tools.activeTool) { 125 | this.menuItems[i].material.color.lerp(this.activeColor, 0.15); 126 | } else { 127 | this.menuItems[i].material.color.lerp(this.normalColor, 0.15); 128 | } 129 | } 130 | 131 | // Lerp the Spheres to their Target Slot's position 132 | this.menuItems[i].position.lerp(this.slots[activeMenuIndex].getWorldPosition(this.tempV3), 0.15); 133 | 134 | // Lerp the Spheres to their Target Slot's position 135 | this.menuItems[i].scale.copy(this.cameraWorldScale); 136 | 137 | // Make the Icon Face the Camera 138 | this.menuItems[i].icon.quaternion.copy(this.cameraWorldRot);//slerp(this.cameraWorldRot, 0.1); 139 | 140 | activeMenuIndex += 1; 141 | } 142 | 143 | // Update the slot positions based on the camera's aspect 144 | // Updating them here allows them to be overridden by other 145 | // subsystems (like the hands) later or earlier in the frame 146 | let minAspect = this.world.inVR ? 1.0 : Math.min(this.world.camera.aspect, 1.5); 147 | for (let i = 0; i < this.slots.length; i++) { 148 | this.slots[i].canonicalPosition.y = (this.world.inVR ? 1 : -1) * 25 * 6 * 0.001; 149 | 150 | this.slots[i].position.x = this.slots[i].canonicalPosition.x; 151 | this.slots[i].position.y = this.slots[i].canonicalPosition.y / minAspect; 152 | this.slots[i].position.z = this.slots[i].canonicalPosition.z / minAspect; 153 | } 154 | 155 | if (!ray.active) { this.menuHeld = false; } 156 | 157 | ray.hovering = ray.hovering || this.menuHeld; 158 | } 159 | 160 | } 161 | 162 | export { Menu }; 163 | -------------------------------------------------------------------------------- /src/Frontend/Tools/General/ToolUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../../../node_modules/three/build/three.module.js'; 18 | import { World } from '../../World/World.js'; 19 | import oc from '../../../../node_modules/opencascade.js/dist/opencascade.wasm.module.js'; 20 | 21 | /** Returns whether Fragment Depth Buffers Exist on this hardware 22 | * @param {World} world */ 23 | export function hasFragmentDepth(world) { 24 | // Inject some spicy stochastic depth logic into this object's material 25 | let safari = /(Safari)/g.test(navigator.userAgent) && ! /(Chrome)/g.test(navigator.userAgent); 26 | return (world.renderer.capabilities.isWebGL2 || (world.renderer.extensions.has('EXT_frag_depth'))) && !safari; 27 | } 28 | 29 | /** Creates a modified shader that dithers when occluded. Useful for CSG Previews. 30 | * @param {World} world 31 | * @param {boolean} hasFragDepth 32 | * @param {string} origShader */ 33 | export function createDitherDepthFragmentShader(hasFragDepth, origShader) { 34 | let bodyStart = origShader.indexOf('void main() {'); 35 | return origShader.slice(0, bodyStart) + 36 | ` 37 | // From https://github.com/Rudranil-Sarkar/Ordered-Dithering-Shader-GLSL/blob/master/Dither8x8.frag#L29 38 | const int[64] dither_table = int[]( 39 | 0, 48, 12, 60, 3, 51, 15, 63, 40 | 32, 16, 44, 28, 35, 19, 47, 31, 41 | 8, 56, 4, 52, 11, 59, 7, 55, 42 | 40, 24, 36, 20, 43, 27, 39, 23, 43 | 2, 50, 14, 62, 1, 49, 13, 61, 44 | 34, 18, 46, 30, 33, 17, 45, 29, 45 | 10, 58, 6, 54, 9, 57, 5, 53, 46 | 42, 26, 38, 22, 41, 25, 37, 21 47 | ); 48 | ` + 49 | origShader.slice(bodyStart - 1, - 1) + 50 | (hasFragDepth ? ` 51 | int x = int(mod(gl_FragCoord.x, 8.)); 52 | int y = int(mod(gl_FragCoord.y, 8.)); 53 | float limit = (float(dither_table[x + y * 8]) + 1.) / 64.; 54 | gl_FragDepthEXT = gl_FragCoord.z - (limit*0.0001); 55 | ` : '\n') + '}'; 56 | } 57 | 58 | /** Creates a modified material that dithers when occluded. Useful for CSG Previews. 59 | * @param {World} world 60 | * @param {THREE.Material} inputMaterial */ 61 | export function createDitherDepthMaterial(world) { 62 | let hasFragDepth = hasFragmentDepth(world); 63 | let stochasticDepthMaterial = new THREE.MeshPhongMaterial(); 64 | stochasticDepthMaterial.color.setRGB(0.5, 0.5, 0.5); 65 | if (!hasFragDepth) { return stochasticDepthMaterial; } 66 | 67 | stochasticDepthMaterial.uniforms = {}; 68 | stochasticDepthMaterial.extensions = { fragDepth: hasFragDepth }; // set to use fragment depth values 69 | stochasticDepthMaterial.onBeforeCompile = (shader) => { 70 | shader.fragmentShader = createDitherDepthFragmentShader(hasFragDepth, shader.fragmentShader); 71 | stochasticDepthMaterial.uniforms = shader.uniforms; 72 | stochasticDepthMaterial.userData.shader = shader; 73 | }; 74 | return stochasticDepthMaterial; 75 | } 76 | 77 | /** Snaps this Vector3 to the global grid of gridPitch cell size 78 | * @param {THREE.Vector3} vecToSnap The vector to snap 79 | * @param {number} gridPitch The grid size to snap to*/ 80 | export function snapToGrid(vecToSnap, gridPitch) { 81 | if (gridPitch > 0) { 82 | vecToSnap.set( 83 | (Math.round((vecToSnap.x + Number.EPSILON) / gridPitch) * gridPitch), 84 | (Math.round((vecToSnap.y + Number.EPSILON) / gridPitch) * gridPitch), 85 | (Math.round((vecToSnap.z + Number.EPSILON) / gridPitch) * gridPitch)); 86 | } 87 | return vecToSnap; 88 | } 89 | 90 | /** Callbacks info from the CAD engine about the surface. 91 | * @param {LeapShapeEngine} engine 92 | * @param {any} raycastHit, 93 | * @param {Function} callback */ 94 | export function querySurface(engine, raycastHit, callback) { 95 | // Match the triangle index to the face index from the face metadata 96 | let faceID = -1; let faceMetadata = raycastHit.object.faceMetadata; 97 | for (let i = 0; i < faceMetadata.length; i++){ 98 | if (raycastHit.faceIndex >= faceMetadata[i].start && 99 | raycastHit.faceIndex < faceMetadata[i].end) { 100 | faceID = faceMetadata[i].index; break; 101 | } 102 | } 103 | 104 | let queryArgs = [raycastHit.object.shapeName, faceID, 105 | raycastHit.uv2.x, raycastHit.uv2.y, 106 | raycastHit.point.x, raycastHit.point.y, raycastHit.point.z, 107 | raycastHit.uvBounds]; 108 | 109 | // Query the CAD Engine Thread for Info 110 | engine.execute("SurfaceQuery", BackendFunctions.querySurfaceBackend, queryArgs, callback); 111 | } 112 | 113 | /** *If engine is not busy*, callbacks info from the CAD engine about the surface. 114 | * Use this version for frequent updates that don't queue up and spiral of death. 115 | * @param {LeapShapeEngine} engine 116 | * @param {any} raycastHit, 117 | * @param {Function} callback */ 118 | export function safeQuerySurface(engine, raycastHit, callback) { 119 | if (!engine.workerWorking) { return querySurface(engine, raycastHit, callback); } 120 | } 121 | 122 | class BackendFunctions { 123 | 124 | /** This function is called in the backend and returns information about the surface. 125 | * @param {string} shapeName @param {number} faceIndex @param {number} u @param {number} v */ 126 | static querySurfaceBackend(shapeName, faceIndex, u, v, x, y, z, uvBounds) { 127 | if (false) { // This fools the intellisense into working 128 | /** @type {Object.} */ this.shapes; this.oc = oc; } 129 | 130 | let toReturn = { isMetadata: true }; 131 | let shape = this.shapes[shapeName]; 132 | let faceName = shapeName + " Face #" + faceIndex; 133 | 134 | // Get the BRepAdaptor_Surface associated with this face 135 | if (!this.surfaces) { this.surfaces = {}; } 136 | if (!(faceName in this.surfaces)) { 137 | let face = null; let face_index = 0; 138 | let anExplorer = new this.oc.TopExp_Explorer(shape, this.oc.TopAbs_FACE); 139 | for (anExplorer.Init(shape, this.oc.TopAbs_FACE); anExplorer.More(); anExplorer.Next()) { 140 | if (face_index === faceIndex) { 141 | face = this.oc.TopoDS.prototype.Face(anExplorer.Current()); 142 | break; 143 | } else { 144 | face_index += 1; 145 | } 146 | } 147 | 148 | // Cache the Adapter Surface in surfaces 149 | this.surfaces[faceName] = new this.oc.BRepAdaptor_Surface(face, false); 150 | } 151 | /** @type {oc.BRepAdaptor_Surface} */ 152 | let adapter = this.surfaces[faceName]; 153 | 154 | // Get the true implicit point 155 | let truePnt = adapter.Value(u, v); 156 | toReturn.x = truePnt.X(); toReturn.y = truePnt.Y(); toReturn.z = truePnt.Z(); 157 | this.oc._free(truePnt); 158 | 159 | // Get the type of face 160 | toReturn.faceType = adapter.GetType(); // https://dev.opencascade.org/doc/occt-7.4.0/refman/html/_geom_abs___surface_type_8hxx.html 161 | 162 | // Get the point in the middle of the face if it's flat 163 | let UMin = 0.0, UMax = 0.0; 164 | let VMin = 0.0, VMax = 0.0; 165 | if (uvBounds) { 166 | UMin = uvBounds[0]; UMax = uvBounds[1]; 167 | VMin = uvBounds[2]; VMax = uvBounds[3]; 168 | } 169 | let UMid = (UMax + UMin) * 0.5, VMid = (VMax + VMin) * 0.5; 170 | let UMinMidMax = [UMin, UMid, UMax]; 171 | let VMinMidMax = [VMin, VMid, VMax]; 172 | let grid = [], uvs = []; 173 | for (let u = 0; u < 3; u++){ 174 | for (let v = 0; v < 3; v++){ 175 | uvs.push([UMinMidMax[u], VMinMidMax[v]]); 176 | let pnt = adapter.Value(UMinMidMax[u], VMinMidMax[v]); 177 | grid.push([pnt.X(), pnt.Y(), pnt.Z()]); 178 | this.oc._free(pnt); 179 | } 180 | } 181 | 182 | // Add the UV origin at the end for good measure 183 | let pnt = adapter.Value(0, 0); 184 | grid.push([pnt.X(), pnt.Y(), pnt.Z()]); 185 | this.oc._free(pnt); 186 | 187 | toReturn.grid = grid; 188 | toReturn.uvs = uvs; 189 | 190 | // Get Surface Normal, Tangent, and Curvature Info 191 | let surfaceHandle = this.oc.BRep_Tool.prototype.Surface(this.surfaces[faceName].Face()); 192 | if (!this.props) { 193 | this.props = new this.oc.GeomLProp_SLProps(surfaceHandle, u, v, 1, 1); 194 | 195 | } 196 | this.props.SetSurface(surfaceHandle); this.props.SetParameters(u, v); 197 | 198 | // Capture Normal Direction 199 | if (this.props.IsNormalDefined()) { 200 | let normal = this.props.Normal() 201 | toReturn.nX = normal.X(); toReturn.nY = normal.Y(); toReturn.nZ = normal.Z(); 202 | this.oc._free(normal); 203 | } 204 | 205 | // Capture Tangent Directions 206 | let tempDir = new this.oc.gp_Dir(); 207 | if (this.props.IsTangentUDefined()) { 208 | this.props.TangentU(tempDir); 209 | toReturn.tU = [0, 0, 0]; 210 | toReturn.tU[0] = tempDir.X(); toReturn.tU[1] = tempDir.Y(); toReturn.tU[2] = tempDir.Z(); 211 | } 212 | if (this.props.IsTangentVDefined()) { 213 | this.props.TangentV(tempDir); 214 | toReturn.tV = [0, 0, 0]; 215 | toReturn.tV[0] = tempDir.X(); toReturn.tV[1] = tempDir.Y(); toReturn.tV[2] = tempDir.Z(); 216 | } 217 | this.oc._free(tempDir); 218 | 219 | return toReturn; 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/Frontend/Tools/OffsetTool.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../../node_modules/three/build/three.module.js'; 18 | import oc from '../../../node_modules/opencascade.js/dist/opencascade.wasm.module.js'; 19 | import { Tools } from './Tools.js'; 20 | import { InteractionRay } from '../Input/Input.js'; 21 | import { Grid } from './General/Grid.js'; 22 | import { Cursor } from './General/Cursor.js'; 23 | import { hasFragmentDepth, createDitherDepthFragmentShader } from "./General/ToolUtils.js"; 24 | 25 | /** This class controls all of the OffsetTool behavior */ 26 | class OffsetTool { 27 | 28 | /** Create the OffsetTool 29 | * @param {Tools} tools */ 30 | constructor(tools) { 31 | this.tools = tools; 32 | this.world = this.tools.world; 33 | this.engine = this.tools.engine; 34 | this.oc = oc; this.shapes = {}; 35 | 36 | this.state = -1; // -1 is Deactivated 37 | this.numOffsets = 0; 38 | this.distance = 1; 39 | this.vec = new THREE.Vector3(); this.quat = new THREE.Quaternion(); 40 | this.point = new THREE.Vector3(); 41 | this.snappedPoint = new THREE.Vector3(); 42 | this.cameraRelativeMovement = new THREE.Vector3(); 43 | this.rayPlane = new THREE.Mesh(new THREE.PlaneGeometry(1000, 1000), 44 | new THREE.MeshBasicMaterial()); 45 | let hasFragDepth = hasFragmentDepth(this.world); 46 | this.offsetMaterial = this.world.previewMaterial.clone(); 47 | this.offsetMaterial.uniforms = {}; 48 | if (hasFragDepth) { this.offsetMaterial.extensions = { fragDepth: hasFragDepth }; } // set to use fragment depth values 49 | this.offsetMaterial.onBeforeCompile = ( shader ) => { 50 | // Vertex Shader: Dilate Vertex positions by the normals 51 | let insertionPoint = shader.vertexShader.indexOf("#include "); 52 | shader.vertexShader = 53 | '\nuniform float dilation;\n' + 54 | shader.vertexShader.slice(0, insertionPoint) + 55 | 'transformed += dilation * objectNormal;\n ' + 56 | shader.vertexShader.slice( insertionPoint); 57 | 58 | if (hasFragDepth) { 59 | shader.fragmentShader = createDitherDepthFragmentShader(hasFragDepth, shader.fragmentShader); 60 | } 61 | 62 | shader.uniforms.dilation = { value: 0.0 }; 63 | this.offsetMaterial.uniforms = shader.uniforms; 64 | this.offsetMaterial.userData.shader = shader; 65 | }; 66 | 67 | // Create Metadata for the Menu System 68 | this.loader = new THREE.TextureLoader(); this.loader.setCrossOrigin (''); 69 | this.icon = this.loader.load ((typeof ESBUILD !== 'undefined') ? './textures/Offset.png' : '../../../textures/Offset.png'); 70 | this.descriptor = { 71 | name: "Offset Tool", 72 | icon: this.icon 73 | } 74 | } 75 | 76 | /** Update the OffsetTool's State Machine 77 | * @param {InteractionRay} ray The Current Input Ray */ 78 | update(ray) { 79 | if (ray.hovering || this.state === -1) { 80 | return; // Tool is currently deactivated 81 | } else if(this.state === 0) { 82 | // Wait for the ray to be active and pointing at a drawable surface 83 | this.world.raycaster.set(ray.ray.origin, ray.ray.direction); 84 | let intersects = this.world.raycaster.intersectObject(this.world.scene, true); 85 | 86 | if (intersects.length > 0 && !ray.justDeactivated && 87 | (intersects[0].object.shapeName || intersects[0].object.isGround)) { 88 | 89 | this.hit = intersects[0]; 90 | // Shoot through the floor if necessary 91 | for (let i = 0; i < intersects.length; i++){ 92 | if (intersects[i].object.shapeName || intersects[i].object.isGround) { 93 | this.hit = intersects[i]; break; 94 | } 95 | } 96 | 97 | // Record the hit object and plane... 98 | if (this.hit.object.shapeName) { 99 | if (ray.justActivated) { 100 | this.hitObject = this.hit.object; 101 | this.point.copy(this.hit.point); 102 | //this.hitObject.material = this.offsetMaterial; 103 | 104 | // Spawn the Offset 105 | this.currentOffset = new THREE.Mesh(this.hitObject.geometry, this.offsetMaterial); 106 | this.offsetMaterial.emissive.setRGB(0, 0.25, 0.25); 107 | this.currentOffset.name = "Waiting..."; 108 | this.world.scene.add(this.currentOffset); 109 | 110 | // Creates an expected offset 111 | this.createPreviewOffsetGeometry([this.hitObject.shapeName, 0.002]); 112 | 113 | this.rayPlane.position.copy(this.point); 114 | this.rayPlane.lookAt(this.world.camera.getWorldPosition(this.vec)); 115 | this.rayPlane.updateMatrixWorld(true); 116 | 117 | this.state += 1; 118 | } 119 | ray.hovering = true; 120 | } 121 | } 122 | } else if(this.state === 1) { 123 | // While holding, resize the Offset 124 | this.world.raycaster.set(ray.ray.origin, ray.ray.direction); 125 | let intersects = this.world.raycaster.intersectObject(this.rayPlane); 126 | if (intersects.length > 0) { 127 | // Get camera-space position to determine union or subtraction 128 | this.cameraRelativeMovement.copy(intersects[0].point.clone().sub(this.point)); 129 | this.cameraRelativeMovement.applyQuaternion(this.world.camera.getWorldQuaternion(this.quat).invert()); 130 | 131 | this.distance = this.cameraRelativeMovement.x; 132 | this.distance = this.tools.grid.snapToGrid1D(this.distance, this.tools.grid.gridPitch/5); 133 | 134 | // Update the Visual Feedback 135 | this.offsetMaterial.uniforms.dilation = { value: this.currentOffset.name === "Waiting..." ? this.distance : this.distance - 0.002 }; 136 | this.offsetMaterial.side = this.distance < 0 ? THREE.BackSide : THREE.FrontSide; 137 | this.offsetMaterial.needsUpdate = true; 138 | if (this.currentOffset.name !== "Waiting...") { 139 | if (this.distance < 0 && this.currentOffset.geometry === this.currentOffset.dilatedGeometry) { 140 | this.currentOffset.geometry = this.currentOffset.contractedGeometry; 141 | this.currentOffset.geometry.needsUpdate = true; 142 | } else if (this.distance > 0 && this.currentOffset.geometry === this.currentOffset.contractedGeometry) { 143 | this.currentOffset.geometry = this.currentOffset.dilatedGeometry; 144 | this.currentOffset.geometry.needsUpdate = true; 145 | } 146 | } 147 | 148 | this.tools.cursor.updateTarget(this.point); 149 | this.tools.cursor.updateLabelNumbers(this.distance); 150 | 151 | //this.currentOffset.scale.x = this.distance; 152 | //this.currentOffset.scale.y = this.distance; 153 | //this.currentOffset.scale.z = this.distance; 154 | this.offsetMaterial.emissive.setRGB( 155 | this.distance > 0 ? 0.0 : 0.25, 156 | this.distance > 0 ? 0.25 : 0.0 , 0.0); 157 | } 158 | ray.hovering = true; 159 | 160 | // When let go, deactivate and add to Undo! 161 | if (!ray.active) { 162 | this.createOffsetGeometry(this.hitObject, 163 | [this.hitObject.shapeName, this.distance]); 164 | this.numOffsets += 1; 165 | //this.currentOffset = null; 166 | this.deactivate(); 167 | } 168 | } 169 | } 170 | 171 | /** @param {THREE.Mesh} offsetMesh */ 172 | createOffsetGeometry(offsetMesh, createOffsetArgs) { 173 | let shapeName = "Offset " + offsetMesh.shapeName; 174 | this.engine.execute(shapeName, this.createOffset, createOffsetArgs, 175 | (mesh) => { 176 | if (this.currentOffset) { 177 | this.world.scene.remove(this.currentOffset); 178 | } 179 | 180 | if (mesh) { 181 | mesh.name = offsetMesh.name; 182 | mesh.shapeName = shapeName; 183 | let friendlyName = (createOffsetArgs[1] > 0) ? "Expansion" : "Hollowing"; 184 | if (this.hitObject.shapeName) { 185 | this.world.history.addToUndo(mesh, this.hitObject, friendlyName); 186 | this.hitObject = null; 187 | } else { 188 | this.world.history.addToUndo(mesh, null, friendlyName); 189 | } 190 | } 191 | 192 | offsetMesh.material = this.world.shapeMaterial; 193 | this.world.dirty = true; 194 | }); 195 | } 196 | 197 | /** Creates an unit offset for previewing */ 198 | createPreviewOffsetGeometry(createOffsetArgs) { 199 | let shapeName = "Offset #" + this.numOffsets; 200 | this.engine.execute(shapeName, this.createOffset, createOffsetArgs, 201 | (mesh) => { 202 | if (mesh) { 203 | if (this.currentOffset) { 204 | this.world.scene.remove(this.currentOffset); 205 | } 206 | 207 | mesh.shapeName = shapeName; 208 | mesh.material = this.offsetMaterial; 209 | mesh.dilatedGeometry = mesh.geometry; 210 | mesh.contractedGeometry = this.currentOffset.geometry; 211 | this.currentOffset = mesh; 212 | this.currentOffset.children[0].visible = false; 213 | this.world.scene.add(this.currentOffset); 214 | } 215 | this.world.dirty = true; 216 | }); 217 | } 218 | 219 | /** Create a Offset in OpenCascade; to be executed on the Worker Thread */ 220 | createOffset(hitObjectName, offsetDistance) { 221 | let inShape = this.shapes[hitObjectName]; 222 | //if (offsetDistance === 0) { return inShape; } 223 | if (offsetDistance !== 0) { 224 | let offsetOp = new this.oc.BRepOffsetAPI_MakeOffsetShape(); 225 | offsetOp.PerformByJoin(inShape, offsetDistance, 0.00001); 226 | let outShape = new this.oc.TopoDS_Shape(offsetOp.Shape()); 227 | 228 | // Convert Shell to Solid as is expected 229 | if (outShape.ShapeType() == 3) { 230 | let solidOffset = new this.oc.BRepBuilderAPI_MakeSolid(); 231 | solidOffset.Add(outShape); 232 | outShape = new this.oc.TopoDS_Solid(solidOffset.Solid()); 233 | } 234 | 235 | if (offsetDistance > 0) { 236 | return outShape; 237 | } else { 238 | // Doesn't seem to work; not sure why... 239 | //let emptyList = new this.oc.TopTools_ListOfShape(); 240 | //let hollowOp = new this.oc.BRepOffsetAPI_MakeThickSolid(); 241 | //hollowOp.MakeThickSolidByJoin(inShape, emptyList, offsetDistance, 0.00001); 242 | //hollowOp.Build(); 243 | //return hollowOp.Shape(); 244 | let differenceCut = new this.oc.BRepAlgoAPI_Cut(inShape, outShape); 245 | differenceCut.SetFuzzyValue(0.00001); 246 | differenceCut.Build(); 247 | return differenceCut.Shape(); 248 | } 249 | } 250 | } 251 | 252 | activate() { 253 | if (this.tools.activeTool) { 254 | this.tools.activeTool.deactivate(); 255 | } 256 | this.state = 0; 257 | this.tools.activeTool = this; 258 | this.tools.grid.updateCount = 0; 259 | } 260 | 261 | deactivate() { 262 | this.state = -1; 263 | this.tools.activeTool = null; 264 | //if (this.currentOffset && this.currentOffset.parent) { 265 | // this.currentOffset.parent.remove(this.currentOffset); 266 | //} 267 | this.tools.grid.updateCount = 0; 268 | this.tools.grid.setVisible(false); 269 | } 270 | 271 | /** Whether or not to show this tool in the menu 272 | * Only Show when no objects are selected */ 273 | shouldShow() { return this.tools.tools[0].selected.length == 0; } 274 | } 275 | 276 | export { OffsetTool }; 277 | -------------------------------------------------------------------------------- /src/Frontend/Tools/RedoTool.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../../node_modules/three/build/three.module.js'; 18 | import oc from '../../../node_modules/opencascade.js/dist/opencascade.wasm.module.js'; 19 | import { Tools } from './Tools.js'; 20 | import { InteractionRay } from '../Input/Input.js'; 21 | import { snapToGrid } from './General/ToolUtils.js'; 22 | 23 | /** This class controls all of the RemoveTool behavior */ 24 | class RedoTool { 25 | 26 | /** Create the RedoTool 27 | * @param {Tools} tools */ 28 | constructor(tools) { 29 | this.tools = tools; 30 | this.world = this.tools.world; 31 | this.engine = this.tools.engine; 32 | 33 | // Create Metadata for the Menu System 34 | this.loader = new THREE.TextureLoader(); this.loader.setCrossOrigin (''); 35 | this.icon = this.loader.load ((typeof ESBUILD !== 'undefined') ? './textures/Redo.png' : '../../../textures/Redo.png' ); 36 | this.descriptor = { 37 | name: "Redo Tool", 38 | icon: this.icon 39 | } 40 | } 41 | 42 | activate() { 43 | this.world.history.Redo(); 44 | this.deactivate(); 45 | } 46 | 47 | deactivate() { 48 | this.tools.activeTool = null; 49 | } 50 | 51 | /** Update the RemoveTool's State Machine 52 | * @param {InteractionRay} ray The Current Input Ray */ 53 | update(ray) { return; } 54 | 55 | /** Whether or not to show this tool in the menu */ 56 | shouldShow() { return this.world.inVR; }// && this.world.history.redoObjects.children.length > 0; } 57 | } 58 | 59 | export { RedoTool }; 60 | -------------------------------------------------------------------------------- /src/Frontend/Tools/RemoveTool.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../../node_modules/three/build/three.module.js'; 18 | import oc from '../../../node_modules/opencascade.js/dist/opencascade.wasm.module.js'; 19 | import { Tools } from './Tools.js'; 20 | import { InteractionRay } from '../Input/Input.js'; 21 | import { snapToGrid } from './General/ToolUtils.js'; 22 | 23 | /** This class controls all of the RemoveTool behavior */ 24 | class RemoveTool { 25 | 26 | /** Create the RemoveTool 27 | * @param {Tools} tools */ 28 | constructor(tools) { 29 | this.tools = tools; 30 | this.world = this.tools.world; 31 | this.engine = this.tools.engine; 32 | this.oc = oc; this.shapes = {}; 33 | 34 | this.state = -1; // -1 is Deactivated 35 | this.numRemoves = 0; 36 | 37 | // Create Metadata for the Menu System 38 | this.loader = new THREE.TextureLoader(); this.loader.setCrossOrigin (''); 39 | this.icon = this.loader.load ((typeof ESBUILD !== 'undefined') ? './textures/Remove.png' : '../../../textures/Remove.png' ); 40 | this.descriptor = { 41 | name: "Remove Tool", 42 | icon: this.icon 43 | } 44 | } 45 | 46 | activate() { 47 | // Get Selected Objects 48 | this.selected = this.tools.tools[0].selected; 49 | this.tools.tools[0].clearSelection(); 50 | for (let i = 0; i < this.selected.length; i++) { 51 | this.world.history.removeShape(this.selected[i], "Object"); 52 | } 53 | 54 | this.deactivate(); 55 | } 56 | 57 | deactivate() { 58 | this.state = -1; 59 | this.tools.activeTool = null; 60 | } 61 | 62 | /** Update the RemoveTool's State Machine 63 | * @param {InteractionRay} ray The Current Input Ray */ 64 | update(ray) { return; } 65 | 66 | /** Whether or not to show this tool in the menu */ 67 | shouldShow() { return this.tools.tools[0].selected.length >= 1; } 68 | } 69 | 70 | export { RemoveTool }; 71 | -------------------------------------------------------------------------------- /src/Frontend/Tools/RotateTool.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../../node_modules/three/build/three.module.js'; 18 | import oc from '../../../node_modules/opencascade.js/dist/opencascade.wasm.module.js'; 19 | import { Tools } from './Tools.js'; 20 | import { InteractionRay } from '../Input/Input.js'; 21 | 22 | /** This class controls all of the RotateTool behavior */ 23 | class RotateTool { 24 | 25 | /** Create the RotateTool 26 | * @param {Tools} tools */ 27 | constructor(tools) { 28 | this.tools = tools; 29 | this.world = this.tools.world; 30 | this.engine = this.tools.engine; 31 | this.oc = oc; this.shapes = {}; 32 | 33 | // Create Metadata for the Menu System 34 | this.loader = new THREE.TextureLoader(); this.loader.setCrossOrigin (''); 35 | this.icon = this.loader.load ((typeof ESBUILD !== 'undefined') ? './textures/Rotate.png' : '../../../textures/Rotate.png' ); 36 | this.descriptor = { 37 | name: "Rotate Tool", 38 | icon: this.icon 39 | } 40 | } 41 | 42 | activate() { 43 | // Set the Transform Gizmo to Rotation Mode 44 | this.tools.tools[0].gizmo.setMode( "rotate" ); 45 | 46 | this.deactivate(); 47 | } 48 | 49 | deactivate() { 50 | this.tools.activeTool = null; 51 | } 52 | 53 | /** Update the RotateTool's State Machine 54 | * @param {InteractionRay} ray The Current Input Ray */ 55 | update(ray) { return; } 56 | 57 | /** Whether or not to show this tool in the menu */ 58 | shouldShow() { return this.tools.tools[0].selected.length >= 1; } 59 | } 60 | 61 | export { RotateTool }; 62 | -------------------------------------------------------------------------------- /src/Frontend/Tools/ScaleTool.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../../node_modules/three/build/three.module.js'; 18 | import oc from '../../../node_modules/opencascade.js/dist/opencascade.wasm.module.js'; 19 | import { Tools } from './Tools.js'; 20 | import { InteractionRay } from '../Input/Input.js'; 21 | 22 | /** This class controls all of the ScaleTool behavior */ 23 | class ScaleTool { 24 | 25 | /** Create the ScaleTool 26 | * @param {Tools} tools */ 27 | constructor(tools) { 28 | this.tools = tools; 29 | this.world = this.tools.world; 30 | this.engine = this.tools.engine; 31 | this.oc = oc; this.shapes = {}; 32 | 33 | // Create Metadata for the Menu System 34 | this.loader = new THREE.TextureLoader(); this.loader.setCrossOrigin (''); 35 | this.icon = this.loader.load ((typeof ESBUILD !== 'undefined') ? './textures/Scale.png' : '../../../textures/Scale.png' ); 36 | this.descriptor = { 37 | name: "Scale Tool", 38 | icon: this.icon 39 | } 40 | } 41 | 42 | activate() { 43 | // Set the Transform Gizmo to Scaling Mode 44 | this.tools.tools[0].gizmo.setMode( "scale" ); 45 | 46 | this.deactivate(); 47 | } 48 | 49 | deactivate() { 50 | this.tools.activeTool = null; 51 | } 52 | 53 | /** Update the ScaleTool's State Machine 54 | * @param {InteractionRay} ray The Current Input Ray */ 55 | update(ray) { return; } 56 | 57 | /** Whether or not to show this tool in the menu */ 58 | shouldShow() { return this.tools.tools[0].selected.length >= 1; } 59 | } 60 | 61 | export { ScaleTool }; 62 | -------------------------------------------------------------------------------- /src/Frontend/Tools/SphereTool.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../../node_modules/three/build/three.module.js'; 18 | import oc from '../../../node_modules/opencascade.js/dist/opencascade.wasm.module.js'; 19 | import { Tools } from './Tools.js'; 20 | import { InteractionRay } from '../Input/Input.js'; 21 | import { Grid } from './General/Grid.js'; 22 | import { Cursor } from './General/Cursor.js'; 23 | 24 | /** This class controls all of the SphereTool behavior */ 25 | class SphereTool { 26 | 27 | /** Create the SphereTool 28 | * @param {Tools} tools */ 29 | constructor(tools) { 30 | this.tools = tools; 31 | this.world = this.tools.world; 32 | this.engine = this.tools.engine; 33 | this.oc = oc; this.shapes = {}; 34 | 35 | this.state = -1; // -1 is Deactivated 36 | this.numSpheres = 0; 37 | this.distance = 0.001; 38 | this.point = new THREE.Vector3(); 39 | this.snappedPoint = new THREE.Vector3(); 40 | this.cameraRelativeMovement = new THREE.Vector3(); 41 | this.rayPlane = new THREE.Mesh(new THREE.PlaneGeometry(1000, 1000), 42 | new THREE.MeshBasicMaterial()); 43 | 44 | // Create Metadata for the Menu System 45 | this.loader = new THREE.TextureLoader(); this.loader.setCrossOrigin (''); 46 | this.icon = this.loader.load ((typeof ESBUILD !== 'undefined') ? './textures/Sphere.png' : '../../../textures/Sphere.png'); 47 | this.descriptor = { 48 | name: "Sphere Tool", 49 | icon: this.icon 50 | } 51 | } 52 | 53 | /** Update the SphereTool's State Machine 54 | * @param {InteractionRay} ray The Current Input Ray */ 55 | update(ray) { 56 | if (ray.hovering || this.state === -1) { 57 | return; // Tool is currently deactivated 58 | } else if(this.state === 0) { 59 | // Wait for the ray to be active and pointing at a drawable surface 60 | this.world.raycaster.set(ray.ray.origin, ray.ray.direction); 61 | let intersects = this.world.raycaster.intersectObject(this.world.scene, true); 62 | 63 | if (intersects.length > 0 && !ray.justDeactivated && 64 | (intersects[0].object.shapeName || intersects[0].object.isGround)) { 65 | 66 | this.hit = intersects[0]; 67 | // Shoot through the floor if necessary 68 | for (let i = 0; i < intersects.length; i++){ 69 | if (intersects[i].object.shapeName || intersects[i].object.isGround) { 70 | this.hit = intersects[i]; break; 71 | } 72 | } 73 | 74 | // Update the grid origin 75 | this.tools.grid.setVisible(true); 76 | this.tools.grid.updateWithHit(this.hit); 77 | this.tools.grid.snapToGrid(this.snappedPoint.copy(this.hit.point)); 78 | this.tools.cursor.updateTarget(this.snappedPoint); 79 | let relativeSnapped = this.tools.grid.space.worldToLocal(this.snappedPoint.clone()); 80 | this.tools.cursor.updateLabelNumbers(Math.abs(relativeSnapped.x), Math.abs(relativeSnapped.z)); 81 | 82 | if (ray.active && this.tools.grid.updateCount > 1) {// iPhones need more than one frame 83 | // Record the hit object and plane... 84 | this.hitObject = this.hit.object; 85 | 86 | this.point.copy(this.snappedPoint); 87 | 88 | // Spawn the Sphere 89 | this.currentSphere = new THREE.Mesh(new THREE.SphereGeometry(1, 10, 10), this.world.previewMaterial); 90 | this.currentSphere.material.color.setRGB(0.5, 0.5, 0.5); 91 | this.currentSphere.material.emissive.setRGB(0, 0.25, 0.25); 92 | this.currentSphere.name = "Sphere #" + this.numSpheres; 93 | this.currentSphere.position.copy(this.point); 94 | this.currentSphere.frustumCulled = false; 95 | this.currentSphere.scale.set(0.00001,0.00001,0.00001); 96 | this.world.scene.add(this.currentSphere); 97 | this.rayPlane.position.copy(this.point); 98 | this.rayPlane.lookAt(this.hit.face.normal.clone().transformDirection(this.hit.object.matrixWorld).add(this.rayPlane.position)); 99 | this.rayPlane.updateMatrixWorld(true); 100 | 101 | this.state += 1; 102 | } 103 | ray.hovering = true; 104 | } 105 | } else if(this.state === 1) { 106 | // While holding, resize the Sphere 107 | this.world.raycaster.set(ray.ray.origin, ray.ray.direction); 108 | let intersects = this.world.raycaster.intersectObject(this.rayPlane); 109 | if (intersects.length > 0) { 110 | // Get camera-space position to determine union or subtraction 111 | this.cameraRelativeMovement.copy(intersects[0].point.clone().sub(this.point)); 112 | this.cameraRelativeMovement.transformDirection(this.world.camera.matrixWorld.invert()); 113 | 114 | this.distance = Math.max(0.001, intersects[0].point.clone().sub(this.point).length()); 115 | //if (this.tools.gridPitch > 0) { this.distance = Math.round(this.distance / this.tools.gridPitch) * this.tools.gridPitch; } 116 | this.distance = this.tools.grid.snapToGrid1D(this.distance); 117 | this.tools.cursor.updateTarget(this.point); 118 | this.tools.cursor.updateLabelNumbers(this.distance); 119 | 120 | this.currentSphere.scale.x = this.distance; 121 | this.currentSphere.scale.y = this.distance; 122 | this.currentSphere.scale.z = this.distance; 123 | this.distance *= Math.sign(this.cameraRelativeMovement.x); 124 | this.currentSphere.material.emissive.setRGB( 125 | this.distance > 0 ? 0.0 : 0.25, 126 | this.distance > 0 ? 0.25 : 0.0 , 0.0); 127 | } 128 | ray.hovering = true; 129 | 130 | // When let go, deactivate and add to Undo! 131 | if (!ray.active) { 132 | this.tools.grid.setVisible(false); 133 | this.createSphereGeometry(this.currentSphere, 134 | [this.point.x, this.point.y, this.point.z, this.distance, this.hitObject.shapeName]); 135 | this.numSpheres += 1; 136 | this.currentSphere = null; 137 | this.deactivate(); 138 | } 139 | } 140 | } 141 | 142 | /** @param {THREE.Mesh} sphereMesh */ 143 | createSphereGeometry(sphereMesh, createSphereArgs) { 144 | // Early Exit if the Sphere is Trivially Invalid 145 | if (createSphereArgs[3] === 0.0) { 146 | this.tools.alerts.displayError("Zero Volume Sphere is Invalid!"); 147 | sphereMesh.parent.remove(sphereMesh); 148 | this.world.dirty = true; 149 | return; 150 | } 151 | 152 | let shapeName = "Sphere #" + this.numSpheres; 153 | this.engine.execute(shapeName, this.createSphere, createSphereArgs, 154 | (mesh) => { 155 | if (mesh) { 156 | mesh.name = sphereMesh.name; 157 | mesh.shapeName = shapeName; 158 | if (this.hitObject.shapeName) { 159 | this.world.history.addToUndo(mesh, this.hitObject, "Sphere CSG"); 160 | this.hitObject = null; 161 | } else { 162 | this.world.history.addToUndo(mesh, null, "Sphere"); 163 | } 164 | } 165 | 166 | sphereMesh.parent.remove(sphereMesh); 167 | this.world.dirty = true; 168 | }); 169 | } 170 | 171 | /** Create a Sphere in OpenCascade; to be executed on the Worker Thread */ 172 | createSphere(x, y, z, radius, hitObjectName) { 173 | if (radius != 0) { 174 | let spherePlane = new this.oc.gp_Ax2(new this.oc.gp_Pnt(x, y, z), this.oc.gp.prototype.DZ()); 175 | let shape = new this.oc.BRepPrimAPI_MakeSphere(spherePlane, Math.abs(radius)).Shape(); 176 | 177 | if (hitObjectName in this.shapes) { 178 | let hitObject = this.shapes[hitObjectName]; 179 | if (radius > 0) { 180 | let union = new this.oc.BRepAlgoAPI_Fuse(hitObject, shape); 181 | union.SetFuzzyValue(0.00000001); 182 | union.Build(); 183 | return union.Shape(); 184 | } else { 185 | let differenceCut = new this.oc.BRepAlgoAPI_Cut(hitObject, shape); 186 | differenceCut.SetFuzzyValue(0.00000001); 187 | differenceCut.Build(); 188 | return differenceCut.Shape(); 189 | } 190 | } 191 | return shape; 192 | } 193 | } 194 | 195 | activate() { 196 | if (this.tools.activeTool) { 197 | this.tools.activeTool.deactivate(); 198 | } 199 | this.state = 0; 200 | this.tools.activeTool = this; 201 | this.tools.grid.updateCount = 0; 202 | } 203 | 204 | deactivate() { 205 | this.state = -1; 206 | this.tools.activeTool = null; 207 | if (this.currentSphere && this.currentSphere.parent) { 208 | this.currentSphere.parent.remove(this.currentSphere); 209 | } 210 | this.tools.grid.updateCount = 0; 211 | this.tools.grid.setVisible(false); 212 | } 213 | 214 | /** Whether or not to show this tool in the menu 215 | * Only Show when no objects are selected */ 216 | shouldShow() { return this.tools.tools[0].selected.length == 0; } 217 | } 218 | 219 | export { SphereTool }; 220 | -------------------------------------------------------------------------------- /src/Frontend/Tools/Tools.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 { LeapShapeEngine } from '../../Backend/main.js'; 18 | import { Menu } from './General/Menu.js'; 19 | import { World } from '../World/World.js'; 20 | 21 | import { Grid } from './General/Grid.js'; 22 | import { Cursor } from './General/Cursor.js'; 23 | import { Alerts } from './General/Alerts.js'; 24 | 25 | import { DefaultTool } from './DefaultTool.js'; 26 | import { UnionTool } from './UnionTool.js'; 27 | import { DifferenceTool } from './DifferenceTool.js'; 28 | import { CopyTool } from './CopyTool.js'; 29 | import { RemoveTool } from './RemoveTool.js'; 30 | import { CleanEdgesTool } from './CleanEdgesTool.js'; 31 | import { BoxTool } from './BoxTool.js'; 32 | import { SphereTool } from './SphereTool.js'; 33 | import { CylinderTool } from './CylinderTool.js'; 34 | import { ExtrusionTool } from './ExtrusionTool.js'; 35 | import { FilletTool } from './FilletTool.js'; 36 | import { OffsetTool } from './OffsetTool.js'; 37 | import { UndoTool } from './UndoTool.js'; 38 | import { RedoTool } from './RedoTool.js'; 39 | import { TranslateTool } from './TranslateTool.js'; 40 | import { RotateTool } from './RotateTool.js'; 41 | import { ScaleTool } from './ScaleTool.js'; 42 | 43 | /** This class controls all of the Tool and Menu State Machines */ 44 | class Tools { 45 | 46 | /** Initialize the Main-Thread App Context 47 | * @param {World} world 48 | * @param {LeapShapeEngine} engine */ 49 | constructor(world, engine) { 50 | this.world = world; this.engine = engine; 51 | 52 | this.grid = new Grid(this); 53 | this.cursor = new Cursor(this); 54 | this.alerts = new Alerts(this); 55 | 56 | this.tools = [ 57 | new DefaultTool (this), 58 | new UndoTool (this), 59 | new RedoTool (this), 60 | new TranslateTool (this), 61 | new RotateTool (this), 62 | new ScaleTool (this), 63 | new CopyTool (this), 64 | new RemoveTool (this), 65 | new CleanEdgesTool(this), 66 | new UnionTool (this), 67 | new DifferenceTool(this), 68 | new BoxTool (this), 69 | new SphereTool (this), 70 | new CylinderTool (this), 71 | new ExtrusionTool (this), 72 | new FilletTool (this), 73 | new OffsetTool (this) 74 | ]; 75 | 76 | this.activeTool = null; 77 | } 78 | 79 | /** Update the Tool and Menu State Machines 80 | * @param {THREE.Ray} ray The Current Input Ray */ 81 | update(ray) { 82 | if (this.menu) { 83 | // Let the user/menus set the activeTool 84 | this.menu.update(ray); 85 | } else if(this.engine.started) { 86 | // Create the menu system, which will 87 | // be populated from the List of Tools 88 | this.menu = new Menu(this); 89 | this.world.dirty = true; // Update the rendered view 90 | } 91 | 92 | if (!this.activeTool) { 93 | this.tools[0].activate(); 94 | } 95 | this.activeTool.update(ray); 96 | this.cursor.update(); 97 | this.alerts.update(); 98 | } 99 | 100 | } 101 | 102 | export { Tools }; 103 | -------------------------------------------------------------------------------- /src/Frontend/Tools/TranslateTool.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../../node_modules/three/build/three.module.js'; 18 | import oc from '../../../node_modules/opencascade.js/dist/opencascade.wasm.module.js'; 19 | import { Tools } from './Tools.js'; 20 | import { InteractionRay } from '../Input/Input.js'; 21 | 22 | /** This class controls all of the TranslateTool behavior */ 23 | class TranslateTool { 24 | 25 | /** Create the TranslateTool 26 | * @param {Tools} tools */ 27 | constructor(tools) { 28 | this.tools = tools; 29 | this.world = this.tools.world; 30 | this.engine = this.tools.engine; 31 | this.oc = oc; this.shapes = {}; 32 | 33 | // Create Metadata for the Menu System 34 | this.loader = new THREE.TextureLoader(); this.loader.setCrossOrigin (''); 35 | this.icon = this.loader.load ((typeof ESBUILD !== 'undefined') ? './textures/Translate.png' : '../../../textures/Translate.png' ); 36 | this.descriptor = { 37 | name: "Translate Tool", 38 | icon: this.icon 39 | } 40 | } 41 | 42 | activate() { 43 | // Set the Transform Gizmo to Translation Mode 44 | this.tools.tools[0].gizmo.setMode( "translate" ); 45 | 46 | this.deactivate(); 47 | } 48 | 49 | deactivate() { 50 | this.tools.activeTool = null; 51 | } 52 | 53 | /** Update the TranslateTool's State Machine 54 | * @param {InteractionRay} ray The Current Input Ray */ 55 | update(ray) { return; } 56 | 57 | /** Whether or not to show this tool in the menu */ 58 | shouldShow() { return this.tools.tools[0].selected.length >= 1; } 59 | } 60 | 61 | export { TranslateTool }; 62 | -------------------------------------------------------------------------------- /src/Frontend/Tools/UndoTool.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../../node_modules/three/build/three.module.js'; 18 | import oc from '../../../node_modules/opencascade.js/dist/opencascade.wasm.module.js'; 19 | import { Tools } from './Tools.js'; 20 | import { InteractionRay } from '../Input/Input.js'; 21 | import { snapToGrid } from './General/ToolUtils.js'; 22 | 23 | /** This class controls all of the RemoveTool behavior */ 24 | class UndoTool { 25 | 26 | /** Create the RedoTool 27 | * @param {Tools} tools */ 28 | constructor(tools) { 29 | this.tools = tools; 30 | this.world = this.tools.world; 31 | this.engine = this.tools.engine; 32 | 33 | // Create Metadata for the Menu System 34 | this.loader = new THREE.TextureLoader(); this.loader.setCrossOrigin (''); 35 | this.icon = this.loader.load ((typeof ESBUILD !== 'undefined') ? './textures/Undo.png' : '../../../textures/Undo.png' ); 36 | this.descriptor = { 37 | name: "Undo Tool", 38 | icon: this.icon 39 | } 40 | } 41 | 42 | activate() { 43 | this.world.history.Undo(); 44 | this.deactivate(); 45 | } 46 | 47 | deactivate() { 48 | this.tools.activeTool = null; 49 | } 50 | 51 | /** Update the RemoveTool's State Machine 52 | * @param {InteractionRay} ray The Current Input Ray */ 53 | update(ray) { return; } 54 | 55 | /** Whether or not to show this tool in the menu */ 56 | shouldShow() { return this.world.inVR; }// && this.world.history.undoObjects.children.length > 0; } 57 | } 58 | 59 | export { UndoTool }; 60 | -------------------------------------------------------------------------------- /src/Frontend/Tools/UnionTool.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../../node_modules/three/build/three.module.js'; 18 | import oc from '../../../node_modules/opencascade.js/dist/opencascade.wasm.module.js'; 19 | import { Tools } from './Tools.js'; 20 | import { InteractionRay } from '../Input/Input.js'; 21 | import { snapToGrid } from './General/ToolUtils.js'; 22 | 23 | /** This class controls all of the UnionTool behavior */ 24 | class UnionTool { 25 | 26 | /** Create the UnionTool 27 | * @param {Tools} tools */ 28 | constructor(tools) { 29 | this.tools = tools; 30 | this.world = this.tools.world; 31 | this.engine = this.tools.engine; 32 | this.oc = oc; this.shapes = {}; 33 | 34 | this.state = -1; // -1 is Deactivated 35 | this.numUnions = 0; 36 | 37 | // Create Metadata for the Menu System 38 | this.loader = new THREE.TextureLoader(); this.loader.setCrossOrigin (''); 39 | this.icon = this.loader.load ((typeof ESBUILD !== 'undefined') ? './textures/Union.png' : '../../../textures/Union.png' ); 40 | this.descriptor = { 41 | name: "Union Tool", 42 | icon: this.icon 43 | } 44 | } 45 | 46 | activate() { 47 | // Get Selected Objects 48 | this.selected = this.tools.tools[0].selected; 49 | if (this.selected.length > 1) { 50 | this.selectedShapes = []; 51 | for (let i = 0; i < this.selected.length; i++) { 52 | this.selectedShapes.push(this.selected[i].shapeName); 53 | } 54 | 55 | this.createUnionGeometry(this.selected, [this.selectedShapes]); 56 | this.numUnions += 1; 57 | } 58 | 59 | this.deactivate(); 60 | } 61 | 62 | deactivate() { 63 | this.state = -1; 64 | this.tools.activeTool = null; 65 | } 66 | 67 | /** Update the UnionTool's State Machine 68 | * @param {InteractionRay} ray The Current Input Ray */ 69 | update(ray) { return; } 70 | 71 | /** @param {THREE.Mesh[]} unionMeshes */ 72 | createUnionGeometry(unionMeshes, createUnionArgs) { 73 | let shapeName = "Union #" + this.numUnions; 74 | this.engine.execute(shapeName, this.createUnion, createUnionArgs, 75 | (mesh) => { 76 | if (mesh) { 77 | mesh.name = shapeName; 78 | mesh.shapeName = shapeName; 79 | this.tools.tools[0].clearSelection(); 80 | 81 | // Creation of the Final Composite Unioned Object 82 | this.world.history.addToUndo(mesh, null, "Union Object"); 83 | 84 | // Individually Undoable Removal of Union Constituents 85 | for (let s = 0; s < unionMeshes.length; s++){ 86 | this.world.history.removeShape(unionMeshes[s], "Original Shape"); 87 | } 88 | } 89 | this.world.dirty = true; 90 | }); 91 | } 92 | 93 | /** Create a Union in OpenCascade; to be executed on the Worker Thread */ 94 | createUnion(unionObjects) { 95 | if (unionObjects.length >= 2) { 96 | let fused = false; 97 | let shape = this.shapes[unionObjects[0]]; 98 | console.log(unionObjects); 99 | 100 | for (let i = 1; i < unionObjects.length; i++){ 101 | let fuseTool = this.shapes[unionObjects[i]]; 102 | 103 | console.log(shape, fuseTool); 104 | 105 | // Check to see if shape and fuseTool are touching 106 | //let overlapChecker = new this.oc.BRepExtrema_DistShapeShape(shape, fuseTool); 107 | //overlapChecker.Perform(); 108 | 109 | //if (overlapChecker.InnerSolution()) { 110 | let union = new this.oc.BRepAlgoAPI_Fuse(shape, fuseTool); 111 | union.SetFuzzyValue(0.00001); union.Build(); 112 | shape = union.Shape(); 113 | fused = true; 114 | //} 115 | } 116 | 117 | return fused ? shape : null; 118 | } 119 | } 120 | 121 | /** Whether or not to show this tool in the menu */ 122 | shouldShow() { return this.tools.tools[0].selected.length >= 2; } 123 | } 124 | 125 | export { UnionTool }; 126 | -------------------------------------------------------------------------------- /src/Frontend/World/History.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../../node_modules/three/build/three.module.js'; 18 | 19 | /** With every action, an object and its anti-object are created. The two are time-reversals of each other. 20 | * When a shape is created into the "shapeObjects", its symmetric "Remove-" shape is added to "undoObjects" 21 | * When it is undone, the shape is moved to "redoObjects" and the "Remove-" shape is destroyed. 22 | * When it is redone, the shape is moved back to "shapeObjects", and a "Remove-" shape is added to "undoObjects" all over again. 23 | * If an object with equivalent name already exists, then it is replaced, and the older object is banished to its shadow realm. 24 | * This is the cycle. Of Birth and Destruction. Change and Stagnation. The flow and ebb of time. This is the Undo/Redo system. */ 25 | class History { 26 | 27 | /** Initialize the Modelling History + Undo/Redo Shortcuts 28 | * @param {World} world */ 29 | constructor(world) { 30 | // Store a reference to the World 31 | this.world = world; 32 | 33 | this.shapeObjects = new THREE.Group(); 34 | this.undoObjects = new THREE.Group(); 35 | this.redoObjects = new THREE.Group(); 36 | this.removeCmd = "Remove-"; 37 | 38 | this.curState = 0; this.curFriendlyName = ''; 39 | window.history.pushState({ state: this.curState, title: "Cleared!" }, null, null); 40 | 41 | this.world.scene.add(this.shapeObjects); 42 | 43 | // Handle Keyboard events 44 | window.addEventListener("keydown", event => { 45 | if (event.isComposing || event.keyCode === 229) { return; } 46 | if (event.ctrlKey || event.metaKey) { 47 | if (event.key == "z") { this.Undo(); } 48 | if (event.key == "y") { this.Redo(); } 49 | } 50 | }); 51 | 52 | // Handle Browser Back/Forth Events 53 | window.onpopstate = (event) => { 54 | // Check to see if this state comes from the past or future 55 | while (event.state && typeof event.state.state === "number" && event.state.state < this.curState && (this.undoObjects.children.length > 0)) { 56 | this.InternalUndo(); 57 | 58 | this.world.parent.tools.alerts.displayInfo("- " + this.curFriendlyName); 59 | this.curFriendlyName = event.state.title || "Action"; 60 | } 61 | while (event.state && typeof event.state.state === "number" && event.state.state > this.curState && (this.redoObjects.children.length > 0)) { 62 | this.InternalRedo(); 63 | 64 | this.world.parent.tools.alerts.displayInfo("+ " + (event.state.title || "Action")); 65 | this.curFriendlyName = event.state.title || "Action"; 66 | } 67 | 68 | this.world.dirty = true; 69 | }; 70 | } 71 | 72 | Undo() { if((this.undoObjects.children.length > 0)) { history.go(-1); } } 73 | Redo() { if((this.redoObjects.children.length > 0)) { history.go( 1); } } 74 | InternalUndo() { if (this.undoObjects.children.length > 0) { this.processDoCommand( this.shapeObjects, this.undoObjects, this.redoObjects); this.curState -= 1; } } 75 | InternalRedo() { if (this.redoObjects.children.length > 0) { this.processDoCommand( this.shapeObjects, this.redoObjects, this.undoObjects); this.curState += 1; } } 76 | 77 | /** Dequeue a do element, and queue its reverse into the ...reverse queue 78 | * @param {THREE.Object3D} drawingLayer 79 | * @param {THREE.Object3D} commandLayer 80 | * @param {THREE.Object3D} reverseLayer */ 81 | processDoCommand(drawingLayer, commandLayer, reverseLayer) { 82 | // Deactivate the current tool since we're messing up their state 83 | if (this.world.parent.tools.activeTool) { this.world.parent.tools.activeTool.deactivate(); } 84 | 85 | let command = commandLayer.children[commandLayer.children.length - 1]; 86 | if (command) { 87 | // If this item's name starts with the removeCmd... 88 | if (command.name.startsWith(this.removeCmd)) { 89 | // Find this item and "delete" it... 90 | let condemnedName = command.name.substring(this.removeCmd.length); 91 | let condemnedStroke = null; 92 | for (let i = 0; i < drawingLayer.children.length; i++){ 93 | if (drawingLayer.children[i].name == condemnedName) { condemnedStroke = drawingLayer.children[i]; } 94 | } 95 | if (condemnedStroke) { 96 | reverseLayer.add(condemnedStroke); 97 | } else { 98 | console.error("Undo/Redo History is corrupt; " + 99 | "couldn't find " + condemnedName + " to delete it..."); 100 | } 101 | commandLayer.remove(command); 102 | } else { 103 | // Check and see if this item already exists 104 | let strokeToReplace = null; let i = 0; 105 | for (i = 0; i < drawingLayer.children.length; i++){ 106 | if (drawingLayer.children[i].name == command.name) { strokeToReplace = drawingLayer.children[i]; } //break; 107 | } 108 | if (strokeToReplace) { 109 | // If it *does* exist, just replace it 110 | reverseLayer.add(strokeToReplace); 111 | 112 | // Use 'replaceWith' to preserve layer order! 113 | drawingLayer.add(command); 114 | } else { 115 | // If it does not exist, create it 116 | drawingLayer.add(command); 117 | 118 | let removeCommand = new THREE.Group(); 119 | removeCommand.name = this.removeCmd + command.name; 120 | reverseLayer.add(removeCommand); 121 | } 122 | } 123 | } 124 | } 125 | 126 | /** Store this item's current state in the Undo Queue 127 | * @param {THREE.Object3D} item Object to add into the scene 128 | * @param {THREE.Object3D} toReplace Object to replace with item */ 129 | addToUndo(item, toReplace, friendlyName) { 130 | if (toReplace) { 131 | if (friendlyName) { toReplace.friendlyName = friendlyName; } 132 | this.undoObjects.add(toReplace); 133 | item.name = toReplace.name; 134 | }else{ 135 | let removeCommand = new THREE.Group(); 136 | removeCommand.name = this.removeCmd + item.name; 137 | if (friendlyName) { 138 | removeCommand.friendlyName = friendlyName; 139 | item.friendlyName = friendlyName; 140 | } 141 | this.undoObjects.add(removeCommand); 142 | } 143 | 144 | this.shapeObjects.add(item); 145 | 146 | this.curState += 1; 147 | window.history.pushState({ state: this.curState, title: (friendlyName||"Action") }, null, null); 148 | 149 | if (friendlyName) { 150 | item.friendlyName = friendlyName; 151 | this.curFriendlyName = friendlyName; 152 | this.world.parent.tools.alerts.displayInfo("+ "+friendlyName); 153 | } else { 154 | this.world.parent.tools.alerts.displayInfo("Action Complete!"); 155 | } 156 | 157 | // Clear the redo "history" (it's technically invalid now...) 158 | this.ClearRedoHistory(); 159 | } 160 | 161 | /** Removes this shape from the scene */ 162 | removeShape(item, friendlyName) { 163 | this.undoObjects.add(item); 164 | 165 | let removeCommand = new THREE.Group(); 166 | removeCommand.name = this.removeCmd + item.name; 167 | this.redoObjects.add(removeCommand); 168 | 169 | this.curState += 1; 170 | window.history.pushState({ state: this.curState, title: "Removed " + (friendlyName||"Object") }, null, null); 171 | this.world.parent.tools.alerts.displayInfo("- " + (friendlyName||"Object")); 172 | // Clear the redo "history" (it's technically invalid now...) 173 | this.ClearRedoHistory(); 174 | } 175 | 176 | /** Clear the Redo Queue */ 177 | ClearRedoHistory() { this.redoObjects.clear(); } 178 | /** Clear the Undo Queue */ 179 | ClearUndoHistory() { this.undoObjects.clear(); } 180 | /** Undo all actions up to this point (can be redone individually) */ 181 | ClearAll() { 182 | for (let i = 0; i < this.undoObjects.length; i++) { this.Undo(); } 183 | } 184 | 185 | } 186 | 187 | export { History }; 188 | -------------------------------------------------------------------------------- /src/Frontend/World/TextMesh.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 THREE from '../../../node_modules/three/build/three.module.js'; 18 | 19 | class TextMesh extends THREE.Mesh { 20 | 21 | constructor(text) { 22 | 23 | let texture = new TextTexture(text); 24 | let geometry = new THREE.PlaneGeometry(1, 1); 25 | let material = new THREE.MeshBasicMaterial({ map: texture, toneMapped: false, transparent: true, depthTest: false }); 26 | 27 | super(geometry, material); 28 | 29 | this.texture = texture; 30 | this.vec = new THREE.Vector3(); 31 | this.canonicalPosition = new THREE.Vector3(); 32 | this.text = ''; 33 | 34 | } 35 | 36 | update(text, r = 0, g = 0, b = 0) { 37 | this.texture.update(text, r, g, b); 38 | this.text = text; 39 | this.scale .set(this.texture.image.width * 0.0003, 40 | this.texture.image.height * 0.0003); 41 | this.position.set(this.texture.image.width * 0.00016, 42 | this.texture.image.height * 0.00016, 0); 43 | this.canonicalPosition.copy(this.position); 44 | } 45 | 46 | } 47 | 48 | class TextTexture extends THREE.CanvasTexture { 49 | 50 | constructor(text) { 51 | 52 | let canvas = document.createElement('canvas'); 53 | let context = canvas.getContext('2d'/*, { alpha: false }*/); 54 | 55 | super(canvas); 56 | 57 | this.canvas = canvas; 58 | this.canvas.width = 404; 59 | this.canvas.height = 150; 60 | this.context = context; 61 | 62 | this.text = text; 63 | 64 | this.anisotropy = 16; 65 | this.encoding = THREE.sRGBEncoding; 66 | this.minFilter = THREE.LinearFilter; 67 | this.magFilter = THREE.LinearFilter; 68 | 69 | } 70 | 71 | update(string = '', r, g, b) { 72 | if (string !== this.text) { 73 | let font = '8.25em Lato, "Helvetica Neue", Helvetica, Arial, sans-serif'; 74 | // Set the Canvas Width/Height 75 | if (this.context.font !== font) { this.context.font = font; } 76 | let rect = this.context.measureText(string); 77 | 78 | let newWidth = rect.width; 79 | let newHeight = rect.actualBoundingBoxDescent - rect.actualBoundingBoxAscent; 80 | 81 | if (this.canvas.width !== newWidth || this.canvas.height !== newHeight) { 82 | this.canvas.width = Math.max(this.canvas.width , newWidth ); 83 | this.canvas.height = Math.max(this.canvas.height, newHeight); 84 | } 85 | this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); 86 | 87 | // Set the Text Style, Clear the Canvas, and Render the Text 88 | this.context.font = font; 89 | this.context.textBaseline = 'bottom'; 90 | this.context.fillStyle = "rgba(" + r + ", " + g + ", " + b + ", 0.8)"; 91 | this.context.fillText(string, 0, this.canvas.height); 92 | } 93 | 94 | this.image = this.canvas; 95 | this.needsUpdate = true; 96 | this.text = string; 97 | 98 | } 99 | 100 | } 101 | 102 | export { TextMesh, TextTexture }; 103 | -------------------------------------------------------------------------------- /src/Frontend/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 { LeapShapeEngine } from '../Backend/main.js'; 18 | import { World } from './World/World.js'; 19 | import { FileIO } from './World/FileIO.js'; 20 | import { Input } from './Input/Input.js'; 21 | import { Tools } from './Tools/Tools.js'; 22 | import { Debug } from './Debug/Debug.js'; 23 | 24 | /** 25 | * This is the visualization entrypoint for LeapShape; 26 | * all visualization and interactivity are managed through here. */ 27 | class LeapShapeRenderer { 28 | 29 | /** Initialize the Main-Thread App Context 30 | * @param {LeapShapeEngine} engine */ 31 | constructor(engine) { 32 | // Store a reference to the CAD Engine 33 | this.engine = engine; 34 | 35 | // Create the world and set its update loop 36 | this.world = new World(this, this.update.bind(this)); 37 | 38 | // Handles Saving and Loading Assets 39 | this.fileIO = new FileIO(this.world, engine); 40 | 41 | // Create the input abstraction for Mice, Hands, and Controllers 42 | this.input = new Input(this.world); 43 | 44 | // Create the menu system, which is populated from the List of Tools 45 | this.tools = new Tools(this.world, engine); 46 | 47 | this.debug = new Debug(this.world, engine); // Print Errors to screen for iOS Debugging 48 | } 49 | 50 | update() { 51 | // Update the Input Abstraction 52 | this.input.update(); 53 | 54 | // Update the Tool and Menu State Machines 55 | this.tools.update(this.input.ray); 56 | 57 | // Render the World 58 | this.world.update(this.input.ray); 59 | } 60 | 61 | } 62 | 63 | export { LeapShapeRenderer }; 64 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Ultraleap, Inc. 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 { LeapShapeRenderer } from './Frontend/main.js'; 18 | import { LeapShapeEngine } from './Backend/main.js'; 19 | 20 | /** 21 | * This is the main entrypoint for LeapShape, unifying both 22 | * the visualization frontend and CAD construction backend. */ 23 | class LeapShape { 24 | constructor() { 25 | this.engine = new LeapShapeEngine (); 26 | this.renderer = new LeapShapeRenderer(this.engine); 27 | } 28 | } 29 | 30 | window.mainApplication = new LeapShape(); 31 | -------------------------------------------------------------------------------- /textures/Box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leapmotion/LeapShape/65870a7463405557d0a5a8800bd31149012bcc6b/textures/Box.png -------------------------------------------------------------------------------- /textures/CleanEdges.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leapmotion/LeapShape/65870a7463405557d0a5a8800bd31149012bcc6b/textures/CleanEdges.png -------------------------------------------------------------------------------- /textures/Copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leapmotion/LeapShape/65870a7463405557d0a5a8800bd31149012bcc6b/textures/Copy.png -------------------------------------------------------------------------------- /textures/Cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leapmotion/LeapShape/65870a7463405557d0a5a8800bd31149012bcc6b/textures/Cursor.png -------------------------------------------------------------------------------- /textures/Cylinder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leapmotion/LeapShape/65870a7463405557d0a5a8800bd31149012bcc6b/textures/Cylinder.png -------------------------------------------------------------------------------- /textures/Difference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leapmotion/LeapShape/65870a7463405557d0a5a8800bd31149012bcc6b/textures/Difference.png -------------------------------------------------------------------------------- /textures/Extrusion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leapmotion/LeapShape/65870a7463405557d0a5a8800bd31149012bcc6b/textures/Extrusion.png -------------------------------------------------------------------------------- /textures/Fillet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leapmotion/LeapShape/65870a7463405557d0a5a8800bd31149012bcc6b/textures/Fillet.png -------------------------------------------------------------------------------- /textures/Offset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leapmotion/LeapShape/65870a7463405557d0a5a8800bd31149012bcc6b/textures/Offset.png -------------------------------------------------------------------------------- /textures/Redo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leapmotion/LeapShape/65870a7463405557d0a5a8800bd31149012bcc6b/textures/Redo.png -------------------------------------------------------------------------------- /textures/Remove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leapmotion/LeapShape/65870a7463405557d0a5a8800bd31149012bcc6b/textures/Remove.png -------------------------------------------------------------------------------- /textures/Rotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leapmotion/LeapShape/65870a7463405557d0a5a8800bd31149012bcc6b/textures/Rotate.png -------------------------------------------------------------------------------- /textures/Scale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leapmotion/LeapShape/65870a7463405557d0a5a8800bd31149012bcc6b/textures/Scale.png -------------------------------------------------------------------------------- /textures/Sphere.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leapmotion/LeapShape/65870a7463405557d0a5a8800bd31149012bcc6b/textures/Sphere.png -------------------------------------------------------------------------------- /textures/Translate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leapmotion/LeapShape/65870a7463405557d0a5a8800bd31149012bcc6b/textures/Translate.png -------------------------------------------------------------------------------- /textures/Undo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leapmotion/LeapShape/65870a7463405557d0a5a8800bd31149012bcc6b/textures/Undo.png -------------------------------------------------------------------------------- /textures/Union.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leapmotion/LeapShape/65870a7463405557d0a5a8800bd31149012bcc6b/textures/Union.png --------------------------------------------------------------------------------