├── .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 |
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
--------------------------------------------------------------------------------