├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .nojekyll ├── LICENSE ├── README.md ├── assets ├── normalize.css └── skeleton.css ├── index.html ├── jsconfig.json ├── node_modules └── three │ └── examples │ └── jsm │ └── controls │ ├── ArcballControls.js │ ├── DragControls.js │ ├── FirstPersonControls.js │ ├── FlyControls.js │ ├── MapControls.js │ ├── OrbitControls.js │ ├── PointerLockControls.js │ ├── TrackballControls.js │ └── TransformControls.js ├── package-lock.json ├── package.json └── src ├── TimeResampler.js ├── World.js └── main.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | # Ideally this makes the next step run faster 35 | # But I haven't noticed a speed difference 36 | # May want to cache the ./node_modules directory directly... 37 | - name: Cache Node Modules 38 | uses: actions/cache@v2 39 | env: 40 | cache-name: cache-node-modules 41 | with: 42 | # npm cache files are stored in `~/.npm` on Linux/macOS 43 | path: ~/.npm 44 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 45 | restore-keys: | 46 | ${{ runner.os }}-build-${{ env.cache-name }}- 47 | ${{ runner.os }}-build- 48 | ${{ runner.os }}- 49 | - name: Install Node Modules 50 | run: npm install 51 | # Switching the HTML entrypoint over to the build 52 | - name: Pre-Process Site 53 | run: | 54 | sed -e 's/\/src\//\/build\//g' index.html > index.html.tmp 55 | mv index.html.tmp index.html 56 | - name: Run esbuild 57 | run: npm run build 58 | - name: Setup Pages 59 | uses: actions/configure-pages@v4 60 | - name: Upload artifact 61 | uses: actions/upload-pages-artifact@v3 62 | with: 63 | # Upload entire repository 64 | path: '.' 65 | - name: Deploy to GitHub Pages 66 | id: deployment 67 | uses: actions/deploy-pages@v4 68 | -------------------------------------------------------------------------------- /.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 | # Three.js Build 40 | !node_modules/three/ 41 | !node_modules/three/*/ 42 | !node_modules/three/build/* 43 | # Three.js Controls 44 | !node_modules/three/examples/*/ 45 | !node_modules/three/examples/jsm/ 46 | !node_modules/three/examples/jsm/*/ 47 | !node_modules/three/examples/jsm/controls/ 48 | !node_modules/three/examples/jsm/controls/* 49 | 50 | # TypeScript v1 declaration files 51 | typings/ 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | 71 | # parcel-bundler cache (https://parceljs.org/) 72 | .cache 73 | 74 | # next.js build output 75 | .next 76 | 77 | # nuxt.js build output 78 | .nuxt 79 | 80 | # vuepress build output 81 | .vuepress/dist 82 | 83 | # Serverless directories 84 | .serverless 85 | 86 | # FuseBox cache 87 | .fusebox/ 88 | 89 | [Bb]uild -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zalo/ThreeOverlay/de56e643e1ec9da7e3b8b418c8f6f8d5e4e729bc/.nojekyll -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Johnathon Selstad 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [ThreeOverlay - WIP](https://zalo.github.io/ThreeOverlay/) 2 | 3 |

4 | 5 | 6 | 7 | 8 | 10 |

11 | 12 | Framework for Rendering 3D Content Inline with Normal Text Content for a more whimsical internet. 13 | 14 | # Building 15 | 16 | This demo can either be run without building (in Chrome/Edge/Opera since raw three.js examples need [Import Maps](https://caniuse.com/import-maps)), or built with: 17 | ``` 18 | npm install 19 | npm run build 20 | ``` 21 | After building, make sure to edit the index .html to point from `"./src/main.js"` to `"./build/main.js"`. 22 | 23 | # Dependencies 24 | - [three.js](https://github.com/mrdoob/three.js/) (3D Rendering Engine) 25 | - [esbuild](https://github.com/evanw/esbuild/) (Bundler) 26 | -------------------------------------------------------------------------------- /assets/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | -moz-box-sizing: content-box; 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type="search"] { 358 | -webkit-appearance: textfield; /* 1 */ 359 | -moz-box-sizing: content-box; 360 | -webkit-box-sizing: content-box; /* 2 */ 361 | box-sizing: content-box; 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | border: 1px solid #c0c0c0; 381 | margin: 0 2px; 382 | padding: 0.35em 0.625em 0.75em; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | border: 0; /* 1 */ 392 | padding: 0; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-collapse: collapse; 421 | border-spacing: 0; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } -------------------------------------------------------------------------------- /assets/skeleton.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Skeleton V2.0.4 3 | * Copyright 2014, Dave Gamache 4 | * www.getskeleton.com 5 | * Free to use under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 12/29/2014 8 | */ 9 | 10 | 11 | /* Table of contents 12 | –––––––––––––––––––––––––––––––––––––––––––––––––– 13 | - Grid 14 | - Base Styles 15 | - Typography 16 | - Links 17 | - Buttons 18 | - Forms 19 | - Lists 20 | - Code 21 | - Tables 22 | - Spacing 23 | - Utilities 24 | - Clearing 25 | - Media Queries 26 | */ 27 | 28 | 29 | /* Grid 30 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 31 | .container { 32 | position: relative; 33 | width: 100%; 34 | max-width: 960px; 35 | margin: 0 auto; 36 | padding: 0 20px; 37 | box-sizing: border-box; } 38 | .column, 39 | .columns { 40 | width: 100%; 41 | float: left; 42 | box-sizing: border-box; } 43 | 44 | /* For devices larger than 400px */ 45 | @media (min-width: 400px) { 46 | .container { 47 | width: 85%; 48 | padding: 0; } 49 | } 50 | 51 | /* For devices larger than 550px */ 52 | @media (min-width: 550px) { 53 | .container { 54 | width: 80%; } 55 | .column, 56 | .columns { 57 | margin-left: 4%; } 58 | .column:first-child, 59 | .columns:first-child { 60 | margin-left: 0; } 61 | 62 | .one.column, 63 | .one.columns { width: 4.66666666667%; } 64 | .two.columns { width: 13.3333333333%; } 65 | .three.columns { width: 22%; } 66 | .four.columns { width: 30.6666666667%; } 67 | .five.columns { width: 39.3333333333%; } 68 | .six.columns { width: 48%; } 69 | .seven.columns { width: 56.6666666667%; } 70 | .eight.columns { width: 65.3333333333%; } 71 | .nine.columns { width: 74.0%; } 72 | .ten.columns { width: 82.6666666667%; } 73 | .eleven.columns { width: 91.3333333333%; } 74 | .twelve.columns { width: 100%; margin-left: 0; } 75 | 76 | .one-third.column { width: 30.6666666667%; } 77 | .two-thirds.column { width: 65.3333333333%; } 78 | 79 | .one-half.column { width: 48%; } 80 | 81 | /* Offsets */ 82 | .offset-by-one.column, 83 | .offset-by-one.columns { margin-left: 8.66666666667%; } 84 | .offset-by-two.column, 85 | .offset-by-two.columns { margin-left: 17.3333333333%; } 86 | .offset-by-three.column, 87 | .offset-by-three.columns { margin-left: 26%; } 88 | .offset-by-four.column, 89 | .offset-by-four.columns { margin-left: 34.6666666667%; } 90 | .offset-by-five.column, 91 | .offset-by-five.columns { margin-left: 43.3333333333%; } 92 | .offset-by-six.column, 93 | .offset-by-six.columns { margin-left: 52%; } 94 | .offset-by-seven.column, 95 | .offset-by-seven.columns { margin-left: 60.6666666667%; } 96 | .offset-by-eight.column, 97 | .offset-by-eight.columns { margin-left: 69.3333333333%; } 98 | .offset-by-nine.column, 99 | .offset-by-nine.columns { margin-left: 78.0%; } 100 | .offset-by-ten.column, 101 | .offset-by-ten.columns { margin-left: 86.6666666667%; } 102 | .offset-by-eleven.column, 103 | .offset-by-eleven.columns { margin-left: 95.3333333333%; } 104 | 105 | .offset-by-one-third.column, 106 | .offset-by-one-third.columns { margin-left: 34.6666666667%; } 107 | .offset-by-two-thirds.column, 108 | .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } 109 | 110 | .offset-by-one-half.column, 111 | .offset-by-one-half.columns { margin-left: 52%; } 112 | 113 | } 114 | 115 | 116 | /* Base Styles 117 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 118 | /* NOTE 119 | html is set to 62.5% so that all the REM measurements throughout Skeleton 120 | are based on 10px sizing. So basically 1.5rem = 15px :) */ 121 | html { 122 | font-size: 62.5%; } 123 | body { 124 | font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ 125 | line-height: 1.6; 126 | font-weight: 400; 127 | font-display: swap; 128 | font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; 129 | color: #222; } 130 | 131 | 132 | /* Typography 133 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 134 | h1, h2, h3, h4, h5, h6 { 135 | margin-top: 0; 136 | margin-bottom: 2rem; 137 | font-weight: 300; } 138 | h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;} 139 | h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; } 140 | h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; } 141 | h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; } 142 | h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; } 143 | h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; } 144 | 145 | /* Larger than phablet */ 146 | @media (min-width: 550px) { 147 | h1 { font-size: 5.0rem; } 148 | h2 { font-size: 4.2rem; } 149 | h3 { font-size: 3.6rem; } 150 | h4 { font-size: 3.0rem; } 151 | h5 { font-size: 2.4rem; } 152 | h6 { font-size: 1.5rem; } 153 | } 154 | 155 | p { 156 | margin-top: 0; } 157 | 158 | 159 | /* Links 160 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 161 | a { 162 | color: #1EAEDB; } 163 | a:hover { 164 | color: #0FA0CE; } 165 | 166 | 167 | /* Buttons 168 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 169 | .button, 170 | button, 171 | input[type="submit"], 172 | input[type="reset"], 173 | input[type="button"] { 174 | display: inline-block; 175 | height: 38px; 176 | padding: 0 30px; 177 | color: #555; 178 | text-align: center; 179 | font-size: 11px; 180 | font-weight: 600; 181 | line-height: 38px; 182 | letter-spacing: .1rem; 183 | text-transform: uppercase; 184 | text-decoration: none; 185 | white-space: nowrap; 186 | background-color: transparent; 187 | border-radius: 4px; 188 | border: 1px solid #bbb; 189 | cursor: pointer; 190 | box-sizing: border-box; } 191 | .button:hover, 192 | button:hover, 193 | input[type="submit"]:hover, 194 | input[type="reset"]:hover, 195 | input[type="button"]:hover, 196 | .button:focus, 197 | button:focus, 198 | input[type="submit"]:focus, 199 | input[type="reset"]:focus, 200 | input[type="button"]:focus { 201 | color: #333; 202 | border-color: #888; 203 | outline: 0; } 204 | .button.button-primary, 205 | button.button-primary, 206 | input[type="submit"].button-primary, 207 | input[type="reset"].button-primary, 208 | input[type="button"].button-primary { 209 | color: #FFF; 210 | background-color: #33C3F0; 211 | border-color: #33C3F0; } 212 | .button.button-primary:hover, 213 | button.button-primary:hover, 214 | input[type="submit"].button-primary:hover, 215 | input[type="reset"].button-primary:hover, 216 | input[type="button"].button-primary:hover, 217 | .button.button-primary:focus, 218 | button.button-primary:focus, 219 | input[type="submit"].button-primary:focus, 220 | input[type="reset"].button-primary:focus, 221 | input[type="button"].button-primary:focus { 222 | color: #FFF; 223 | background-color: #1EAEDB; 224 | border-color: #1EAEDB; } 225 | 226 | 227 | /* Forms 228 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 229 | input[type="email"], 230 | input[type="number"], 231 | input[type="search"], 232 | input[type="text"], 233 | input[type="tel"], 234 | input[type="url"], 235 | input[type="password"], 236 | textarea, 237 | select { 238 | height: 38px; 239 | padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ 240 | background-color: #fff; 241 | border: 1px solid #D1D1D1; 242 | border-radius: 4px; 243 | box-shadow: none; 244 | box-sizing: border-box; } 245 | /* Removes awkward default styles on some inputs for iOS */ 246 | input[type="email"], 247 | input[type="number"], 248 | input[type="search"], 249 | input[type="text"], 250 | input[type="tel"], 251 | input[type="url"], 252 | input[type="password"], 253 | textarea { 254 | -webkit-appearance: none; 255 | -moz-appearance: none; 256 | appearance: none; } 257 | textarea { 258 | min-height: 65px; 259 | padding-top: 6px; 260 | padding-bottom: 6px; } 261 | input[type="email"]:focus, 262 | input[type="number"]:focus, 263 | input[type="search"]:focus, 264 | input[type="text"]:focus, 265 | input[type="tel"]:focus, 266 | input[type="url"]:focus, 267 | input[type="password"]:focus, 268 | textarea:focus, 269 | select:focus { 270 | border: 1px solid #33C3F0; 271 | outline: 0; } 272 | label, 273 | legend { 274 | display: block; 275 | margin-bottom: .5rem; 276 | font-weight: 600; } 277 | fieldset { 278 | padding: 0; 279 | border-width: 0; } 280 | input[type="checkbox"], 281 | input[type="radio"] { 282 | display: inline; } 283 | label > .label-body { 284 | display: inline-block; 285 | margin-left: .5rem; 286 | font-weight: normal; } 287 | 288 | 289 | /* Lists 290 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 291 | ul { 292 | list-style: circle inside; } 293 | ol { 294 | list-style: decimal inside; } 295 | ol, ul { 296 | padding-left: 0; 297 | margin-top: 0; } 298 | ul ul, 299 | ul ol, 300 | ol ol, 301 | ol ul { 302 | margin: 1.5rem 0 1.5rem 3rem; 303 | font-size: 90%; } 304 | li { 305 | margin-bottom: 1rem; } 306 | 307 | 308 | /* Code 309 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 310 | code { 311 | padding: .2rem .5rem; 312 | margin: 0 .2rem; 313 | font-size: 90%; 314 | white-space: nowrap; 315 | background: #F1F1F1; 316 | border: 1px solid #E1E1E1; 317 | border-radius: 4px; } 318 | pre > code { 319 | display: block; 320 | padding: 1rem 1.5rem; 321 | white-space: pre; } 322 | 323 | 324 | /* Tables 325 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 326 | th, 327 | td { 328 | padding: 12px 15px; 329 | text-align: left; 330 | border-bottom: 1px solid #E1E1E1; } 331 | th:first-child, 332 | td:first-child { 333 | padding-left: 0; } 334 | th:last-child, 335 | td:last-child { 336 | padding-right: 0; } 337 | 338 | 339 | /* Spacing 340 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 341 | button, 342 | .button { 343 | margin-bottom: 1rem; } 344 | input, 345 | textarea, 346 | select, 347 | fieldset { 348 | margin-bottom: 1.5rem; } 349 | pre, 350 | blockquote, 351 | dl, 352 | figure, 353 | table, 354 | p, 355 | ul, 356 | ol, 357 | form { 358 | margin-bottom: 2.5rem; } 359 | 360 | 361 | /* Utilities 362 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 363 | .u-full-width { 364 | width: 100%; 365 | box-sizing: border-box; } 366 | .u-max-full-width { 367 | max-width: 100%; 368 | box-sizing: border-box; } 369 | .u-pull-right { 370 | float: right; } 371 | .u-pull-left { 372 | float: left; } 373 | 374 | 375 | /* Misc 376 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 377 | hr { 378 | margin-top: 3rem; 379 | margin-bottom: 3.5rem; 380 | border-width: 0; 381 | border-top: 1px solid #E1E1E1; } 382 | 383 | 384 | /* Clearing 385 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 386 | 387 | /* Self Clearing Goodness */ 388 | .container:after, 389 | .row:after, 390 | .u-cf { 391 | content: ""; 392 | display: table; 393 | clear: both; } 394 | 395 | 396 | /* Media Queries 397 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 398 | /* 399 | Note: The best way to structure the use of media queries is to create the queries 400 | near the relevant code. For example, if you wanted to change the styles for buttons 401 | on small devices, paste the mobile query code up in the buttons section and style it 402 | there. 403 | */ 404 | 405 | 406 | /* Larger than mobile */ 407 | @media (min-width: 400px) {} 408 | 409 | /* Larger than phablet (also point when grid becomes active) */ 410 | @media (min-width: 550px) {} 411 | 412 | /* Larger than tablet */ 413 | @media (min-width: 750px) {} 414 | 415 | /* Larger than desktop */ 416 | @media (min-width: 1000px) {} 417 | 418 | /* Larger than Desktop HD */ 419 | @media (min-width: 1200px) {} 420 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | ThreeOverlay Demo 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 35 |

36 | 37 | 38 | 41 | 42 |
43 |
44 | 45 | 46 | 48 |
49 |
50 |
51 |

ThreeOverlay Demo

52 |

ThreeOverlay is a system that overlays a 3D rendering canvas onto web pages.
53 |

Instructions
54 |

Download the template and modify it to create your ideal webpage ornaments and buddies.

55 |
How it works
56 |

ThreeOverlay creates a transparent canvas that spans your webpage, allowing you to render dynamic 3D elements inline with your text.

57 |

Specific locations and features of your page will be injected into the 3D space as interactable elements via a bidirectional mapping.

58 |
Keymappings
59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 |
Controller ButtonKeypress
Upw
Lefta
Downs
Rightd
ASpace
BShift
StartEnter
97 |

These keymappings aren't used, but provide extra flavor to this page.

98 |
Open Source
99 |

The source code to ThreeOverlay is available under the MIT License at Github.

100 |
Credits
101 |

ThreeOverlay is based on three.js, and Skeleton.css.

102 |
Raw Events
103 |
ThreeOverlay Events will appear here upon connection.
104 |
105 |
106 |
107 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /node_modules/three/examples/jsm/controls/DragControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | EventDispatcher, 3 | Matrix4, 4 | Plane, 5 | Raycaster, 6 | Vector2, 7 | Vector3 8 | } from 'three'; 9 | 10 | const _plane = new Plane(); 11 | const _raycaster = new Raycaster(); 12 | 13 | const _pointer = new Vector2(); 14 | const _offset = new Vector3(); 15 | const _diff = new Vector2(); 16 | const _previousPointer = new Vector2(); 17 | const _intersection = new Vector3(); 18 | const _worldPosition = new Vector3(); 19 | const _inverseMatrix = new Matrix4(); 20 | 21 | const _up = new Vector3(); 22 | const _right = new Vector3(); 23 | 24 | class DragControls extends EventDispatcher { 25 | 26 | constructor( _objects, _camera, _domElement ) { 27 | 28 | super(); 29 | 30 | _domElement.style.touchAction = 'none'; // disable touch scroll 31 | 32 | let _selected = null, _hovered = null; 33 | 34 | const _intersections = []; 35 | 36 | this.mode = 'translate'; 37 | 38 | this.rotateSpeed = 1; 39 | 40 | // 41 | 42 | const scope = this; 43 | 44 | function activate() { 45 | 46 | _domElement.addEventListener( 'pointermove', onPointerMove ); 47 | _domElement.addEventListener( 'pointerdown', onPointerDown ); 48 | _domElement.addEventListener( 'pointerup', onPointerCancel ); 49 | _domElement.addEventListener( 'pointerleave', onPointerCancel ); 50 | 51 | } 52 | 53 | function deactivate() { 54 | 55 | _domElement.removeEventListener( 'pointermove', onPointerMove ); 56 | _domElement.removeEventListener( 'pointerdown', onPointerDown ); 57 | _domElement.removeEventListener( 'pointerup', onPointerCancel ); 58 | _domElement.removeEventListener( 'pointerleave', onPointerCancel ); 59 | 60 | _domElement.style.cursor = ''; 61 | 62 | } 63 | 64 | function dispose() { 65 | 66 | deactivate(); 67 | 68 | } 69 | 70 | function getObjects() { 71 | 72 | return _objects; 73 | 74 | } 75 | 76 | function setObjects( objects ) { 77 | 78 | _objects = objects; 79 | 80 | } 81 | 82 | function getRaycaster() { 83 | 84 | return _raycaster; 85 | 86 | } 87 | 88 | function onPointerMove( event ) { 89 | 90 | if ( scope.enabled === false ) return; 91 | 92 | updatePointer( event ); 93 | 94 | _raycaster.setFromCamera( _pointer, _camera ); 95 | 96 | if ( _selected ) { 97 | 98 | if ( scope.mode === 'translate' ) { 99 | 100 | if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) { 101 | 102 | _selected.position.copy( _intersection.sub( _offset ).applyMatrix4( _inverseMatrix ) ); 103 | 104 | } 105 | 106 | } else if ( scope.mode === 'rotate' ) { 107 | 108 | _diff.subVectors( _pointer, _previousPointer ).multiplyScalar( scope.rotateSpeed ); 109 | _selected.rotateOnWorldAxis( _up, _diff.x ); 110 | _selected.rotateOnWorldAxis( _right.normalize(), - _diff.y ); 111 | 112 | } 113 | 114 | scope.dispatchEvent( { type: 'drag', object: _selected } ); 115 | 116 | _previousPointer.copy( _pointer ); 117 | 118 | } else { 119 | 120 | // hover support 121 | 122 | if ( event.pointerType === 'mouse' || event.pointerType === 'pen' ) { 123 | 124 | _intersections.length = 0; 125 | 126 | _raycaster.setFromCamera( _pointer, _camera ); 127 | _raycaster.intersectObjects( _objects, scope.recursive, _intersections ); 128 | 129 | if ( _intersections.length > 0 ) { 130 | 131 | const object = _intersections[ 0 ].object; 132 | 133 | _plane.setFromNormalAndCoplanarPoint( _camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( object.matrixWorld ) ); 134 | 135 | if ( _hovered !== object && _hovered !== null ) { 136 | 137 | scope.dispatchEvent( { type: 'hoveroff', object: _hovered } ); 138 | 139 | _domElement.style.cursor = 'auto'; 140 | _hovered = null; 141 | 142 | } 143 | 144 | if ( _hovered !== object ) { 145 | 146 | scope.dispatchEvent( { type: 'hoveron', object: object } ); 147 | 148 | _domElement.style.cursor = 'pointer'; 149 | _hovered = object; 150 | 151 | } 152 | 153 | } else { 154 | 155 | if ( _hovered !== null ) { 156 | 157 | scope.dispatchEvent( { type: 'hoveroff', object: _hovered } ); 158 | 159 | _domElement.style.cursor = 'auto'; 160 | _hovered = null; 161 | 162 | } 163 | 164 | } 165 | 166 | } 167 | 168 | } 169 | 170 | _previousPointer.copy( _pointer ); 171 | 172 | } 173 | 174 | function onPointerDown( event ) { 175 | 176 | if ( scope.enabled === false ) return; 177 | 178 | updatePointer( event ); 179 | 180 | _intersections.length = 0; 181 | 182 | _raycaster.setFromCamera( _pointer, _camera ); 183 | _raycaster.intersectObjects( _objects, scope.recursive, _intersections ); 184 | 185 | if ( _intersections.length > 0 ) { 186 | 187 | if ( scope.transformGroup === true ) { 188 | 189 | // look for the outermost group in the object's upper hierarchy 190 | 191 | _selected = findGroup( _intersections[ 0 ].object ); 192 | 193 | } else { 194 | 195 | _selected = _intersections[ 0 ].object; 196 | 197 | } 198 | 199 | _plane.setFromNormalAndCoplanarPoint( _camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) ); 200 | 201 | if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) { 202 | 203 | if ( scope.mode === 'translate' ) { 204 | 205 | _inverseMatrix.copy( _selected.parent.matrixWorld ).invert(); 206 | _offset.copy( _intersection ).sub( _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) ); 207 | 208 | } else if ( scope.mode === 'rotate' ) { 209 | 210 | // the controls only support Y+ up 211 | _up.set( 0, 1, 0 ).applyQuaternion( _camera.quaternion ).normalize(); 212 | _right.set( 1, 0, 0 ).applyQuaternion( _camera.quaternion ).normalize(); 213 | 214 | } 215 | 216 | } 217 | 218 | _domElement.style.cursor = 'move'; 219 | 220 | scope.dispatchEvent( { type: 'dragstart', object: _selected } ); 221 | 222 | } 223 | 224 | _previousPointer.copy( _pointer ); 225 | 226 | } 227 | 228 | function onPointerCancel() { 229 | 230 | if ( scope.enabled === false ) return; 231 | 232 | if ( _selected ) { 233 | 234 | scope.dispatchEvent( { type: 'dragend', object: _selected } ); 235 | 236 | _selected = null; 237 | 238 | } 239 | 240 | _domElement.style.cursor = _hovered ? 'pointer' : 'auto'; 241 | 242 | } 243 | 244 | function updatePointer( event ) { 245 | 246 | const rect = _domElement.getBoundingClientRect(); 247 | 248 | _pointer.x = ( event.clientX - rect.left ) / rect.width * 2 - 1; 249 | _pointer.y = - ( event.clientY - rect.top ) / rect.height * 2 + 1; 250 | 251 | } 252 | 253 | function findGroup( obj, group = null ) { 254 | 255 | if ( obj.isGroup ) group = obj; 256 | 257 | if ( obj.parent === null ) return group; 258 | 259 | return findGroup( obj.parent, group ); 260 | 261 | } 262 | 263 | activate(); 264 | 265 | // API 266 | 267 | this.enabled = true; 268 | this.recursive = true; 269 | this.transformGroup = false; 270 | 271 | this.activate = activate; 272 | this.deactivate = deactivate; 273 | this.dispose = dispose; 274 | this.getObjects = getObjects; 275 | this.getRaycaster = getRaycaster; 276 | this.setObjects = setObjects; 277 | 278 | } 279 | 280 | } 281 | 282 | export { DragControls }; 283 | -------------------------------------------------------------------------------- /node_modules/three/examples/jsm/controls/FirstPersonControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | MathUtils, 3 | Spherical, 4 | Vector3 5 | } from 'three'; 6 | 7 | const _lookDirection = new Vector3(); 8 | const _spherical = new Spherical(); 9 | const _target = new Vector3(); 10 | 11 | class FirstPersonControls { 12 | 13 | constructor( object, domElement ) { 14 | 15 | this.object = object; 16 | this.domElement = domElement; 17 | 18 | // API 19 | 20 | this.enabled = true; 21 | 22 | this.movementSpeed = 1.0; 23 | this.lookSpeed = 0.005; 24 | 25 | this.lookVertical = true; 26 | this.autoForward = false; 27 | 28 | this.activeLook = true; 29 | 30 | this.heightSpeed = false; 31 | this.heightCoef = 1.0; 32 | this.heightMin = 0.0; 33 | this.heightMax = 1.0; 34 | 35 | this.constrainVertical = false; 36 | this.verticalMin = 0; 37 | this.verticalMax = Math.PI; 38 | 39 | this.mouseDragOn = false; 40 | 41 | // internals 42 | 43 | this.autoSpeedFactor = 0.0; 44 | 45 | this.pointerX = 0; 46 | this.pointerY = 0; 47 | 48 | this.moveForward = false; 49 | this.moveBackward = false; 50 | this.moveLeft = false; 51 | this.moveRight = false; 52 | 53 | this.viewHalfX = 0; 54 | this.viewHalfY = 0; 55 | 56 | // private variables 57 | 58 | let lat = 0; 59 | let lon = 0; 60 | 61 | // 62 | 63 | this.handleResize = function () { 64 | 65 | if ( this.domElement === document ) { 66 | 67 | this.viewHalfX = window.innerWidth / 2; 68 | this.viewHalfY = window.innerHeight / 2; 69 | 70 | } else { 71 | 72 | this.viewHalfX = this.domElement.offsetWidth / 2; 73 | this.viewHalfY = this.domElement.offsetHeight / 2; 74 | 75 | } 76 | 77 | }; 78 | 79 | this.onPointerDown = function ( event ) { 80 | 81 | if ( this.domElement !== document ) { 82 | 83 | this.domElement.focus(); 84 | 85 | } 86 | 87 | if ( this.activeLook ) { 88 | 89 | switch ( event.button ) { 90 | 91 | case 0: this.moveForward = true; break; 92 | case 2: this.moveBackward = true; break; 93 | 94 | } 95 | 96 | } 97 | 98 | this.mouseDragOn = true; 99 | 100 | }; 101 | 102 | this.onPointerUp = function ( event ) { 103 | 104 | if ( this.activeLook ) { 105 | 106 | switch ( event.button ) { 107 | 108 | case 0: this.moveForward = false; break; 109 | case 2: this.moveBackward = false; break; 110 | 111 | } 112 | 113 | } 114 | 115 | this.mouseDragOn = false; 116 | 117 | }; 118 | 119 | this.onPointerMove = function ( event ) { 120 | 121 | if ( this.domElement === document ) { 122 | 123 | this.pointerX = event.pageX - this.viewHalfX; 124 | this.pointerY = event.pageY - this.viewHalfY; 125 | 126 | } else { 127 | 128 | this.pointerX = event.pageX - this.domElement.offsetLeft - this.viewHalfX; 129 | this.pointerY = event.pageY - this.domElement.offsetTop - this.viewHalfY; 130 | 131 | } 132 | 133 | }; 134 | 135 | this.onKeyDown = function ( event ) { 136 | 137 | switch ( event.code ) { 138 | 139 | case 'ArrowUp': 140 | case 'KeyW': this.moveForward = true; break; 141 | 142 | case 'ArrowLeft': 143 | case 'KeyA': this.moveLeft = true; break; 144 | 145 | case 'ArrowDown': 146 | case 'KeyS': this.moveBackward = true; break; 147 | 148 | case 'ArrowRight': 149 | case 'KeyD': this.moveRight = true; break; 150 | 151 | case 'KeyR': this.moveUp = true; break; 152 | case 'KeyF': this.moveDown = true; break; 153 | 154 | } 155 | 156 | }; 157 | 158 | this.onKeyUp = function ( event ) { 159 | 160 | switch ( event.code ) { 161 | 162 | case 'ArrowUp': 163 | case 'KeyW': this.moveForward = false; break; 164 | 165 | case 'ArrowLeft': 166 | case 'KeyA': this.moveLeft = false; break; 167 | 168 | case 'ArrowDown': 169 | case 'KeyS': this.moveBackward = false; break; 170 | 171 | case 'ArrowRight': 172 | case 'KeyD': this.moveRight = false; break; 173 | 174 | case 'KeyR': this.moveUp = false; break; 175 | case 'KeyF': this.moveDown = false; break; 176 | 177 | } 178 | 179 | }; 180 | 181 | this.lookAt = function ( x, y, z ) { 182 | 183 | if ( x.isVector3 ) { 184 | 185 | _target.copy( x ); 186 | 187 | } else { 188 | 189 | _target.set( x, y, z ); 190 | 191 | } 192 | 193 | this.object.lookAt( _target ); 194 | 195 | setOrientation( this ); 196 | 197 | return this; 198 | 199 | }; 200 | 201 | this.update = function () { 202 | 203 | const targetPosition = new Vector3(); 204 | 205 | return function update( delta ) { 206 | 207 | if ( this.enabled === false ) return; 208 | 209 | if ( this.heightSpeed ) { 210 | 211 | const y = MathUtils.clamp( this.object.position.y, this.heightMin, this.heightMax ); 212 | const heightDelta = y - this.heightMin; 213 | 214 | this.autoSpeedFactor = delta * ( heightDelta * this.heightCoef ); 215 | 216 | } else { 217 | 218 | this.autoSpeedFactor = 0.0; 219 | 220 | } 221 | 222 | const actualMoveSpeed = delta * this.movementSpeed; 223 | 224 | if ( this.moveForward || ( this.autoForward && ! this.moveBackward ) ) this.object.translateZ( - ( actualMoveSpeed + this.autoSpeedFactor ) ); 225 | if ( this.moveBackward ) this.object.translateZ( actualMoveSpeed ); 226 | 227 | if ( this.moveLeft ) this.object.translateX( - actualMoveSpeed ); 228 | if ( this.moveRight ) this.object.translateX( actualMoveSpeed ); 229 | 230 | if ( this.moveUp ) this.object.translateY( actualMoveSpeed ); 231 | if ( this.moveDown ) this.object.translateY( - actualMoveSpeed ); 232 | 233 | let actualLookSpeed = delta * this.lookSpeed; 234 | 235 | if ( ! this.activeLook ) { 236 | 237 | actualLookSpeed = 0; 238 | 239 | } 240 | 241 | let verticalLookRatio = 1; 242 | 243 | if ( this.constrainVertical ) { 244 | 245 | verticalLookRatio = Math.PI / ( this.verticalMax - this.verticalMin ); 246 | 247 | } 248 | 249 | lon -= this.pointerX * actualLookSpeed; 250 | if ( this.lookVertical ) lat -= this.pointerY * actualLookSpeed * verticalLookRatio; 251 | 252 | lat = Math.max( - 85, Math.min( 85, lat ) ); 253 | 254 | let phi = MathUtils.degToRad( 90 - lat ); 255 | const theta = MathUtils.degToRad( lon ); 256 | 257 | if ( this.constrainVertical ) { 258 | 259 | phi = MathUtils.mapLinear( phi, 0, Math.PI, this.verticalMin, this.verticalMax ); 260 | 261 | } 262 | 263 | const position = this.object.position; 264 | 265 | targetPosition.setFromSphericalCoords( 1, phi, theta ).add( position ); 266 | 267 | this.object.lookAt( targetPosition ); 268 | 269 | }; 270 | 271 | }(); 272 | 273 | this.dispose = function () { 274 | 275 | this.domElement.removeEventListener( 'contextmenu', contextmenu ); 276 | this.domElement.removeEventListener( 'pointerdown', _onPointerDown ); 277 | this.domElement.removeEventListener( 'pointermove', _onPointerMove ); 278 | this.domElement.removeEventListener( 'pointerup', _onPointerUp ); 279 | 280 | window.removeEventListener( 'keydown', _onKeyDown ); 281 | window.removeEventListener( 'keyup', _onKeyUp ); 282 | 283 | }; 284 | 285 | const _onPointerMove = this.onPointerMove.bind( this ); 286 | const _onPointerDown = this.onPointerDown.bind( this ); 287 | const _onPointerUp = this.onPointerUp.bind( this ); 288 | const _onKeyDown = this.onKeyDown.bind( this ); 289 | const _onKeyUp = this.onKeyUp.bind( this ); 290 | 291 | this.domElement.addEventListener( 'contextmenu', contextmenu ); 292 | this.domElement.addEventListener( 'pointerdown', _onPointerDown ); 293 | this.domElement.addEventListener( 'pointermove', _onPointerMove ); 294 | this.domElement.addEventListener( 'pointerup', _onPointerUp ); 295 | 296 | window.addEventListener( 'keydown', _onKeyDown ); 297 | window.addEventListener( 'keyup', _onKeyUp ); 298 | 299 | function setOrientation( controls ) { 300 | 301 | const quaternion = controls.object.quaternion; 302 | 303 | _lookDirection.set( 0, 0, - 1 ).applyQuaternion( quaternion ); 304 | _spherical.setFromVector3( _lookDirection ); 305 | 306 | lat = 90 - MathUtils.radToDeg( _spherical.phi ); 307 | lon = MathUtils.radToDeg( _spherical.theta ); 308 | 309 | } 310 | 311 | this.handleResize(); 312 | 313 | setOrientation( this ); 314 | 315 | } 316 | 317 | } 318 | 319 | function contextmenu( event ) { 320 | 321 | event.preventDefault(); 322 | 323 | } 324 | 325 | export { FirstPersonControls }; 326 | -------------------------------------------------------------------------------- /node_modules/three/examples/jsm/controls/FlyControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | EventDispatcher, 3 | Quaternion, 4 | Vector3 5 | } from 'three'; 6 | 7 | const _changeEvent = { type: 'change' }; 8 | 9 | class FlyControls extends EventDispatcher { 10 | 11 | constructor( object, domElement ) { 12 | 13 | super(); 14 | 15 | this.object = object; 16 | this.domElement = domElement; 17 | 18 | // API 19 | 20 | // Set to false to disable this control 21 | this.enabled = true; 22 | 23 | this.movementSpeed = 1.0; 24 | this.rollSpeed = 0.005; 25 | 26 | this.dragToLook = false; 27 | this.autoForward = false; 28 | 29 | // disable default target object behavior 30 | 31 | // internals 32 | 33 | const scope = this; 34 | 35 | const EPS = 0.000001; 36 | 37 | const lastQuaternion = new Quaternion(); 38 | const lastPosition = new Vector3(); 39 | 40 | this.tmpQuaternion = new Quaternion(); 41 | 42 | this.status = 0; 43 | 44 | this.moveState = { up: 0, down: 0, left: 0, right: 0, forward: 0, back: 0, pitchUp: 0, pitchDown: 0, yawLeft: 0, yawRight: 0, rollLeft: 0, rollRight: 0 }; 45 | this.moveVector = new Vector3( 0, 0, 0 ); 46 | this.rotationVector = new Vector3( 0, 0, 0 ); 47 | 48 | this.keydown = function ( event ) { 49 | 50 | if ( event.altKey || this.enabled === false ) { 51 | 52 | return; 53 | 54 | } 55 | 56 | switch ( event.code ) { 57 | 58 | case 'ShiftLeft': 59 | case 'ShiftRight': this.movementSpeedMultiplier = .1; break; 60 | 61 | case 'KeyW': this.moveState.forward = 1; break; 62 | case 'KeyS': this.moveState.back = 1; break; 63 | 64 | case 'KeyA': this.moveState.left = 1; break; 65 | case 'KeyD': this.moveState.right = 1; break; 66 | 67 | case 'KeyR': this.moveState.up = 1; break; 68 | case 'KeyF': this.moveState.down = 1; break; 69 | 70 | case 'ArrowUp': this.moveState.pitchUp = 1; break; 71 | case 'ArrowDown': this.moveState.pitchDown = 1; break; 72 | 73 | case 'ArrowLeft': this.moveState.yawLeft = 1; break; 74 | case 'ArrowRight': this.moveState.yawRight = 1; break; 75 | 76 | case 'KeyQ': this.moveState.rollLeft = 1; break; 77 | case 'KeyE': this.moveState.rollRight = 1; break; 78 | 79 | } 80 | 81 | this.updateMovementVector(); 82 | this.updateRotationVector(); 83 | 84 | }; 85 | 86 | this.keyup = function ( event ) { 87 | 88 | if ( this.enabled === false ) return; 89 | 90 | switch ( event.code ) { 91 | 92 | case 'ShiftLeft': 93 | case 'ShiftRight': this.movementSpeedMultiplier = 1; break; 94 | 95 | case 'KeyW': this.moveState.forward = 0; break; 96 | case 'KeyS': this.moveState.back = 0; break; 97 | 98 | case 'KeyA': this.moveState.left = 0; break; 99 | case 'KeyD': this.moveState.right = 0; break; 100 | 101 | case 'KeyR': this.moveState.up = 0; break; 102 | case 'KeyF': this.moveState.down = 0; break; 103 | 104 | case 'ArrowUp': this.moveState.pitchUp = 0; break; 105 | case 'ArrowDown': this.moveState.pitchDown = 0; break; 106 | 107 | case 'ArrowLeft': this.moveState.yawLeft = 0; break; 108 | case 'ArrowRight': this.moveState.yawRight = 0; break; 109 | 110 | case 'KeyQ': this.moveState.rollLeft = 0; break; 111 | case 'KeyE': this.moveState.rollRight = 0; break; 112 | 113 | } 114 | 115 | this.updateMovementVector(); 116 | this.updateRotationVector(); 117 | 118 | }; 119 | 120 | this.pointerdown = function ( event ) { 121 | 122 | if ( this.enabled === false ) return; 123 | 124 | if ( this.dragToLook ) { 125 | 126 | this.status ++; 127 | 128 | } else { 129 | 130 | switch ( event.button ) { 131 | 132 | case 0: this.moveState.forward = 1; break; 133 | case 2: this.moveState.back = 1; break; 134 | 135 | } 136 | 137 | this.updateMovementVector(); 138 | 139 | } 140 | 141 | }; 142 | 143 | this.pointermove = function ( event ) { 144 | 145 | if ( this.enabled === false ) return; 146 | 147 | if ( ! this.dragToLook || this.status > 0 ) { 148 | 149 | const container = this.getContainerDimensions(); 150 | const halfWidth = container.size[ 0 ] / 2; 151 | const halfHeight = container.size[ 1 ] / 2; 152 | 153 | this.moveState.yawLeft = - ( ( event.pageX - container.offset[ 0 ] ) - halfWidth ) / halfWidth; 154 | this.moveState.pitchDown = ( ( event.pageY - container.offset[ 1 ] ) - halfHeight ) / halfHeight; 155 | 156 | this.updateRotationVector(); 157 | 158 | } 159 | 160 | }; 161 | 162 | this.pointerup = function ( event ) { 163 | 164 | if ( this.enabled === false ) return; 165 | 166 | if ( this.dragToLook ) { 167 | 168 | this.status --; 169 | 170 | this.moveState.yawLeft = this.moveState.pitchDown = 0; 171 | 172 | } else { 173 | 174 | switch ( event.button ) { 175 | 176 | case 0: this.moveState.forward = 0; break; 177 | case 2: this.moveState.back = 0; break; 178 | 179 | } 180 | 181 | this.updateMovementVector(); 182 | 183 | } 184 | 185 | this.updateRotationVector(); 186 | 187 | }; 188 | 189 | this.pointercancel = function () { 190 | 191 | if ( this.enabled === false ) return; 192 | 193 | if ( this.dragToLook ) { 194 | 195 | this.status = 0; 196 | 197 | this.moveState.yawLeft = this.moveState.pitchDown = 0; 198 | 199 | } else { 200 | 201 | this.moveState.forward = 0; 202 | this.moveState.back = 0; 203 | 204 | this.updateMovementVector(); 205 | 206 | } 207 | 208 | this.updateRotationVector(); 209 | 210 | }; 211 | 212 | this.contextMenu = function ( event ) { 213 | 214 | if ( this.enabled === false ) return; 215 | 216 | event.preventDefault(); 217 | 218 | }; 219 | 220 | this.update = function ( delta ) { 221 | 222 | if ( this.enabled === false ) return; 223 | 224 | const moveMult = delta * scope.movementSpeed; 225 | const rotMult = delta * scope.rollSpeed; 226 | 227 | scope.object.translateX( scope.moveVector.x * moveMult ); 228 | scope.object.translateY( scope.moveVector.y * moveMult ); 229 | scope.object.translateZ( scope.moveVector.z * moveMult ); 230 | 231 | scope.tmpQuaternion.set( scope.rotationVector.x * rotMult, scope.rotationVector.y * rotMult, scope.rotationVector.z * rotMult, 1 ).normalize(); 232 | scope.object.quaternion.multiply( scope.tmpQuaternion ); 233 | 234 | if ( 235 | lastPosition.distanceToSquared( scope.object.position ) > EPS || 236 | 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS 237 | ) { 238 | 239 | scope.dispatchEvent( _changeEvent ); 240 | lastQuaternion.copy( scope.object.quaternion ); 241 | lastPosition.copy( scope.object.position ); 242 | 243 | } 244 | 245 | }; 246 | 247 | this.updateMovementVector = function () { 248 | 249 | const forward = ( this.moveState.forward || ( this.autoForward && ! this.moveState.back ) ) ? 1 : 0; 250 | 251 | this.moveVector.x = ( - this.moveState.left + this.moveState.right ); 252 | this.moveVector.y = ( - this.moveState.down + this.moveState.up ); 253 | this.moveVector.z = ( - forward + this.moveState.back ); 254 | 255 | //console.log( 'move:', [ this.moveVector.x, this.moveVector.y, this.moveVector.z ] ); 256 | 257 | }; 258 | 259 | this.updateRotationVector = function () { 260 | 261 | this.rotationVector.x = ( - this.moveState.pitchDown + this.moveState.pitchUp ); 262 | this.rotationVector.y = ( - this.moveState.yawRight + this.moveState.yawLeft ); 263 | this.rotationVector.z = ( - this.moveState.rollRight + this.moveState.rollLeft ); 264 | 265 | //console.log( 'rotate:', [ this.rotationVector.x, this.rotationVector.y, this.rotationVector.z ] ); 266 | 267 | }; 268 | 269 | this.getContainerDimensions = function () { 270 | 271 | if ( this.domElement != document ) { 272 | 273 | return { 274 | size: [ this.domElement.offsetWidth, this.domElement.offsetHeight ], 275 | offset: [ this.domElement.offsetLeft, this.domElement.offsetTop ] 276 | }; 277 | 278 | } else { 279 | 280 | return { 281 | size: [ window.innerWidth, window.innerHeight ], 282 | offset: [ 0, 0 ] 283 | }; 284 | 285 | } 286 | 287 | }; 288 | 289 | this.dispose = function () { 290 | 291 | this.domElement.removeEventListener( 'contextmenu', _contextmenu ); 292 | this.domElement.removeEventListener( 'pointerdown', _pointerdown ); 293 | this.domElement.removeEventListener( 'pointermove', _pointermove ); 294 | this.domElement.removeEventListener( 'pointerup', _pointerup ); 295 | this.domElement.removeEventListener( 'pointercancel', _pointercancel ); 296 | 297 | window.removeEventListener( 'keydown', _keydown ); 298 | window.removeEventListener( 'keyup', _keyup ); 299 | 300 | }; 301 | 302 | const _contextmenu = this.contextMenu.bind( this ); 303 | const _pointermove = this.pointermove.bind( this ); 304 | const _pointerdown = this.pointerdown.bind( this ); 305 | const _pointerup = this.pointerup.bind( this ); 306 | const _pointercancel = this.pointercancel.bind( this ); 307 | const _keydown = this.keydown.bind( this ); 308 | const _keyup = this.keyup.bind( this ); 309 | 310 | this.domElement.addEventListener( 'contextmenu', _contextmenu ); 311 | this.domElement.addEventListener( 'pointerdown', _pointerdown ); 312 | this.domElement.addEventListener( 'pointermove', _pointermove ); 313 | this.domElement.addEventListener( 'pointerup', _pointerup ); 314 | this.domElement.addEventListener( 'pointercancel', _pointercancel ); 315 | 316 | window.addEventListener( 'keydown', _keydown ); 317 | window.addEventListener( 'keyup', _keyup ); 318 | 319 | this.updateMovementVector(); 320 | this.updateRotationVector(); 321 | 322 | } 323 | 324 | } 325 | 326 | export { FlyControls }; 327 | -------------------------------------------------------------------------------- /node_modules/three/examples/jsm/controls/MapControls.js: -------------------------------------------------------------------------------- 1 | import { MOUSE, TOUCH } from 'three'; 2 | 3 | import { OrbitControls } from './OrbitControls.js'; 4 | 5 | // MapControls performs orbiting, dollying (zooming), and panning. 6 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 7 | // 8 | // Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate 9 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 10 | // Pan - left mouse, or arrow keys / touch: one-finger move 11 | 12 | class MapControls extends OrbitControls { 13 | 14 | constructor( object, domElement ) { 15 | 16 | super( object, domElement ); 17 | 18 | this.screenSpacePanning = false; // pan orthogonal to world-space direction camera.up 19 | 20 | this.mouseButtons = { LEFT: MOUSE.PAN, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.ROTATE }; 21 | 22 | this.touches = { ONE: TOUCH.PAN, TWO: TOUCH.DOLLY_ROTATE }; 23 | 24 | } 25 | 26 | } 27 | 28 | export { MapControls }; 29 | -------------------------------------------------------------------------------- /node_modules/three/examples/jsm/controls/OrbitControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | EventDispatcher, 3 | MOUSE, 4 | Quaternion, 5 | Spherical, 6 | TOUCH, 7 | Vector2, 8 | Vector3, 9 | Plane, 10 | Ray, 11 | MathUtils 12 | } from 'three'; 13 | 14 | // OrbitControls performs orbiting, dollying (zooming), and panning. 15 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 16 | // 17 | // Orbit - left mouse / touch: one-finger move 18 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 19 | // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move 20 | 21 | const _changeEvent = { type: 'change' }; 22 | const _startEvent = { type: 'start' }; 23 | const _endEvent = { type: 'end' }; 24 | const _ray = new Ray(); 25 | const _plane = new Plane(); 26 | const TILT_LIMIT = Math.cos( 70 * MathUtils.DEG2RAD ); 27 | 28 | class OrbitControls extends EventDispatcher { 29 | 30 | constructor( object, domElement ) { 31 | 32 | super(); 33 | 34 | this.object = object; 35 | this.domElement = domElement; 36 | this.domElement.style.touchAction = 'none'; // disable touch scroll 37 | 38 | // Set to false to disable this control 39 | this.enabled = true; 40 | 41 | // "target" sets the location of focus, where the object orbits around 42 | this.target = new Vector3(); 43 | 44 | // Sets the 3D cursor (similar to Blender), from which the maxTargetRadius takes effect 45 | this.cursor = new Vector3(); 46 | 47 | // How far you can dolly in and out ( PerspectiveCamera only ) 48 | this.minDistance = 0; 49 | this.maxDistance = Infinity; 50 | 51 | // How far you can zoom in and out ( OrthographicCamera only ) 52 | this.minZoom = 0; 53 | this.maxZoom = Infinity; 54 | 55 | // Limit camera target within a spherical area around the cursor 56 | this.minTargetRadius = 0; 57 | this.maxTargetRadius = Infinity; 58 | 59 | // How far you can orbit vertically, upper and lower limits. 60 | // Range is 0 to Math.PI radians. 61 | this.minPolarAngle = 0; // radians 62 | this.maxPolarAngle = Math.PI; // radians 63 | 64 | // How far you can orbit horizontally, upper and lower limits. 65 | // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ) 66 | this.minAzimuthAngle = - Infinity; // radians 67 | this.maxAzimuthAngle = Infinity; // radians 68 | 69 | // Set to true to enable damping (inertia) 70 | // If damping is enabled, you must call controls.update() in your animation loop 71 | this.enableDamping = false; 72 | this.dampingFactor = 0.05; 73 | 74 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 75 | // Set to false to disable zooming 76 | this.enableZoom = true; 77 | this.zoomSpeed = 1.0; 78 | 79 | // Set to false to disable rotating 80 | this.enableRotate = true; 81 | this.rotateSpeed = 1.0; 82 | 83 | // Set to false to disable panning 84 | this.enablePan = true; 85 | this.panSpeed = 1.0; 86 | this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up 87 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 88 | this.zoomToCursor = false; 89 | 90 | // Set to true to automatically rotate around the target 91 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 92 | this.autoRotate = false; 93 | this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60 94 | 95 | // The four arrow keys 96 | this.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' }; 97 | 98 | // Mouse buttons 99 | this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }; 100 | 101 | // Touch fingers 102 | this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }; 103 | 104 | // for reset 105 | this.target0 = this.target.clone(); 106 | this.position0 = this.object.position.clone(); 107 | this.zoom0 = this.object.zoom; 108 | 109 | // the target DOM element for key events 110 | this._domElementKeyEvents = null; 111 | 112 | // 113 | // public methods 114 | // 115 | 116 | this.getPolarAngle = function () { 117 | 118 | return spherical.phi; 119 | 120 | }; 121 | 122 | this.getAzimuthalAngle = function () { 123 | 124 | return spherical.theta; 125 | 126 | }; 127 | 128 | this.getDistance = function () { 129 | 130 | return this.object.position.distanceTo( this.target ); 131 | 132 | }; 133 | 134 | this.listenToKeyEvents = function ( domElement ) { 135 | 136 | domElement.addEventListener( 'keydown', onKeyDown ); 137 | this._domElementKeyEvents = domElement; 138 | 139 | }; 140 | 141 | this.stopListenToKeyEvents = function () { 142 | 143 | this._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown ); 144 | this._domElementKeyEvents = null; 145 | 146 | }; 147 | 148 | this.saveState = function () { 149 | 150 | scope.target0.copy( scope.target ); 151 | scope.position0.copy( scope.object.position ); 152 | scope.zoom0 = scope.object.zoom; 153 | 154 | }; 155 | 156 | this.reset = function () { 157 | 158 | scope.target.copy( scope.target0 ); 159 | scope.object.position.copy( scope.position0 ); 160 | scope.object.zoom = scope.zoom0; 161 | 162 | scope.object.updateProjectionMatrix(); 163 | scope.dispatchEvent( _changeEvent ); 164 | 165 | scope.update(); 166 | 167 | state = STATE.NONE; 168 | 169 | }; 170 | 171 | // this method is exposed, but perhaps it would be better if we can make it private... 172 | this.update = function () { 173 | 174 | const offset = new Vector3(); 175 | 176 | // so camera.up is the orbit axis 177 | const quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) ); 178 | const quatInverse = quat.clone().invert(); 179 | 180 | const lastPosition = new Vector3(); 181 | const lastQuaternion = new Quaternion(); 182 | const lastTargetPosition = new Vector3(); 183 | 184 | const twoPI = 2 * Math.PI; 185 | 186 | return function update( deltaTime = null ) { 187 | 188 | const position = scope.object.position; 189 | 190 | offset.copy( position ).sub( scope.target ); 191 | 192 | // rotate offset to "y-axis-is-up" space 193 | offset.applyQuaternion( quat ); 194 | 195 | // angle from z-axis around y-axis 196 | spherical.setFromVector3( offset ); 197 | 198 | if ( scope.autoRotate && state === STATE.NONE ) { 199 | 200 | rotateLeft( getAutoRotationAngle( deltaTime ) ); 201 | 202 | } 203 | 204 | if ( scope.enableDamping ) { 205 | 206 | spherical.theta += sphericalDelta.theta * scope.dampingFactor; 207 | spherical.phi += sphericalDelta.phi * scope.dampingFactor; 208 | 209 | } else { 210 | 211 | spherical.theta += sphericalDelta.theta; 212 | spherical.phi += sphericalDelta.phi; 213 | 214 | } 215 | 216 | // restrict theta to be between desired limits 217 | 218 | let min = scope.minAzimuthAngle; 219 | let max = scope.maxAzimuthAngle; 220 | 221 | if ( isFinite( min ) && isFinite( max ) ) { 222 | 223 | if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI; 224 | 225 | if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI; 226 | 227 | if ( min <= max ) { 228 | 229 | spherical.theta = Math.max( min, Math.min( max, spherical.theta ) ); 230 | 231 | } else { 232 | 233 | spherical.theta = ( spherical.theta > ( min + max ) / 2 ) ? 234 | Math.max( min, spherical.theta ) : 235 | Math.min( max, spherical.theta ); 236 | 237 | } 238 | 239 | } 240 | 241 | // restrict phi to be between desired limits 242 | spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); 243 | 244 | spherical.makeSafe(); 245 | 246 | 247 | // move target to panned location 248 | 249 | if ( scope.enableDamping === true ) { 250 | 251 | scope.target.addScaledVector( panOffset, scope.dampingFactor ); 252 | 253 | } else { 254 | 255 | scope.target.add( panOffset ); 256 | 257 | } 258 | 259 | // Limit the target distance from the cursor to create a sphere around the center of interest 260 | scope.target.sub( scope.cursor ); 261 | scope.target.clampLength( scope.minTargetRadius, scope.maxTargetRadius ); 262 | scope.target.add( scope.cursor ); 263 | 264 | let zoomChanged = false; 265 | // adjust the camera position based on zoom only if we're not zooming to the cursor or if it's an ortho camera 266 | // we adjust zoom later in these cases 267 | if ( scope.zoomToCursor && performCursorZoom || scope.object.isOrthographicCamera ) { 268 | 269 | spherical.radius = clampDistance( spherical.radius ); 270 | 271 | } else { 272 | 273 | const prevRadius = spherical.radius; 274 | spherical.radius = clampDistance( spherical.radius * scale ); 275 | zoomChanged = prevRadius != spherical.radius; 276 | 277 | } 278 | 279 | offset.setFromSpherical( spherical ); 280 | 281 | // rotate offset back to "camera-up-vector-is-up" space 282 | offset.applyQuaternion( quatInverse ); 283 | 284 | position.copy( scope.target ).add( offset ); 285 | 286 | scope.object.lookAt( scope.target ); 287 | 288 | if ( scope.enableDamping === true ) { 289 | 290 | sphericalDelta.theta *= ( 1 - scope.dampingFactor ); 291 | sphericalDelta.phi *= ( 1 - scope.dampingFactor ); 292 | 293 | panOffset.multiplyScalar( 1 - scope.dampingFactor ); 294 | 295 | } else { 296 | 297 | sphericalDelta.set( 0, 0, 0 ); 298 | 299 | panOffset.set( 0, 0, 0 ); 300 | 301 | } 302 | 303 | // adjust camera position 304 | if ( scope.zoomToCursor && performCursorZoom ) { 305 | 306 | let newRadius = null; 307 | if ( scope.object.isPerspectiveCamera ) { 308 | 309 | // move the camera down the pointer ray 310 | // this method avoids floating point error 311 | const prevRadius = offset.length(); 312 | newRadius = clampDistance( prevRadius * scale ); 313 | 314 | const radiusDelta = prevRadius - newRadius; 315 | scope.object.position.addScaledVector( dollyDirection, radiusDelta ); 316 | scope.object.updateMatrixWorld(); 317 | 318 | zoomChanged = !! radiusDelta; 319 | 320 | } else if ( scope.object.isOrthographicCamera ) { 321 | 322 | // adjust the ortho camera position based on zoom changes 323 | const mouseBefore = new Vector3( mouse.x, mouse.y, 0 ); 324 | mouseBefore.unproject( scope.object ); 325 | 326 | const prevZoom = scope.object.zoom; 327 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / scale ) ); 328 | scope.object.updateProjectionMatrix(); 329 | 330 | zoomChanged = prevZoom !== scope.object.zoom; 331 | 332 | const mouseAfter = new Vector3( mouse.x, mouse.y, 0 ); 333 | mouseAfter.unproject( scope.object ); 334 | 335 | scope.object.position.sub( mouseAfter ).add( mouseBefore ); 336 | scope.object.updateMatrixWorld(); 337 | 338 | newRadius = offset.length(); 339 | 340 | } else { 341 | 342 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled.' ); 343 | scope.zoomToCursor = false; 344 | 345 | } 346 | 347 | // handle the placement of the target 348 | if ( newRadius !== null ) { 349 | 350 | if ( this.screenSpacePanning ) { 351 | 352 | // position the orbit target in front of the new camera position 353 | scope.target.set( 0, 0, - 1 ) 354 | .transformDirection( scope.object.matrix ) 355 | .multiplyScalar( newRadius ) 356 | .add( scope.object.position ); 357 | 358 | } else { 359 | 360 | // get the ray and translation plane to compute target 361 | _ray.origin.copy( scope.object.position ); 362 | _ray.direction.set( 0, 0, - 1 ).transformDirection( scope.object.matrix ); 363 | 364 | // if the camera is 20 degrees above the horizon then don't adjust the focus target to avoid 365 | // extremely large values 366 | if ( Math.abs( scope.object.up.dot( _ray.direction ) ) < TILT_LIMIT ) { 367 | 368 | object.lookAt( scope.target ); 369 | 370 | } else { 371 | 372 | _plane.setFromNormalAndCoplanarPoint( scope.object.up, scope.target ); 373 | _ray.intersectPlane( _plane, scope.target ); 374 | 375 | } 376 | 377 | } 378 | 379 | } 380 | 381 | } else if ( scope.object.isOrthographicCamera ) { 382 | 383 | const prevZoom = scope.object.zoom; 384 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / scale ) ); 385 | 386 | if ( prevZoom !== scope.object.zoom ) { 387 | 388 | scope.object.updateProjectionMatrix(); 389 | zoomChanged = true; 390 | 391 | } 392 | 393 | } 394 | 395 | scale = 1; 396 | performCursorZoom = false; 397 | 398 | // update condition is: 399 | // min(camera displacement, camera rotation in radians)^2 > EPS 400 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 401 | 402 | if ( zoomChanged || 403 | lastPosition.distanceToSquared( scope.object.position ) > EPS || 404 | 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS || 405 | lastTargetPosition.distanceToSquared( scope.target ) > EPS ) { 406 | 407 | scope.dispatchEvent( _changeEvent ); 408 | 409 | lastPosition.copy( scope.object.position ); 410 | lastQuaternion.copy( scope.object.quaternion ); 411 | lastTargetPosition.copy( scope.target ); 412 | 413 | return true; 414 | 415 | } 416 | 417 | return false; 418 | 419 | }; 420 | 421 | }(); 422 | 423 | this.dispose = function () { 424 | 425 | scope.domElement.removeEventListener( 'contextmenu', onContextMenu ); 426 | 427 | scope.domElement.removeEventListener( 'pointerdown', onPointerDown ); 428 | scope.domElement.removeEventListener( 'pointercancel', onPointerUp ); 429 | scope.domElement.removeEventListener( 'wheel', onMouseWheel ); 430 | 431 | scope.domElement.removeEventListener( 'pointermove', onPointerMove ); 432 | scope.domElement.removeEventListener( 'pointerup', onPointerUp ); 433 | 434 | const document = scope.domElement.getRootNode(); // offscreen canvas compatibility 435 | 436 | document.removeEventListener( 'keydown', interceptControlDown, { capture: true } ); 437 | 438 | if ( scope._domElementKeyEvents !== null ) { 439 | 440 | scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown ); 441 | scope._domElementKeyEvents = null; 442 | 443 | } 444 | 445 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 446 | 447 | }; 448 | 449 | // 450 | // internals 451 | // 452 | 453 | const scope = this; 454 | 455 | const STATE = { 456 | NONE: - 1, 457 | ROTATE: 0, 458 | DOLLY: 1, 459 | PAN: 2, 460 | TOUCH_ROTATE: 3, 461 | TOUCH_PAN: 4, 462 | TOUCH_DOLLY_PAN: 5, 463 | TOUCH_DOLLY_ROTATE: 6 464 | }; 465 | 466 | let state = STATE.NONE; 467 | 468 | const EPS = 0.000001; 469 | 470 | // current position in spherical coordinates 471 | const spherical = new Spherical(); 472 | const sphericalDelta = new Spherical(); 473 | 474 | let scale = 1; 475 | const panOffset = new Vector3(); 476 | 477 | const rotateStart = new Vector2(); 478 | const rotateEnd = new Vector2(); 479 | const rotateDelta = new Vector2(); 480 | 481 | const panStart = new Vector2(); 482 | const panEnd = new Vector2(); 483 | const panDelta = new Vector2(); 484 | 485 | const dollyStart = new Vector2(); 486 | const dollyEnd = new Vector2(); 487 | const dollyDelta = new Vector2(); 488 | 489 | const dollyDirection = new Vector3(); 490 | const mouse = new Vector2(); 491 | let performCursorZoom = false; 492 | 493 | const pointers = []; 494 | const pointerPositions = {}; 495 | 496 | let controlActive = false; 497 | 498 | function getAutoRotationAngle( deltaTime ) { 499 | 500 | if ( deltaTime !== null ) { 501 | 502 | return ( 2 * Math.PI / 60 * scope.autoRotateSpeed ) * deltaTime; 503 | 504 | } else { 505 | 506 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 507 | 508 | } 509 | 510 | } 511 | 512 | function getZoomScale( delta ) { 513 | 514 | const normalizedDelta = Math.abs( delta * 0.01 ); 515 | return Math.pow( 0.95, scope.zoomSpeed * normalizedDelta ); 516 | 517 | } 518 | 519 | function rotateLeft( angle ) { 520 | 521 | sphericalDelta.theta -= angle; 522 | 523 | } 524 | 525 | function rotateUp( angle ) { 526 | 527 | sphericalDelta.phi -= angle; 528 | 529 | } 530 | 531 | const panLeft = function () { 532 | 533 | const v = new Vector3(); 534 | 535 | return function panLeft( distance, objectMatrix ) { 536 | 537 | v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix 538 | v.multiplyScalar( - distance ); 539 | 540 | panOffset.add( v ); 541 | 542 | }; 543 | 544 | }(); 545 | 546 | const panUp = function () { 547 | 548 | const v = new Vector3(); 549 | 550 | return function panUp( distance, objectMatrix ) { 551 | 552 | if ( scope.screenSpacePanning === true ) { 553 | 554 | v.setFromMatrixColumn( objectMatrix, 1 ); 555 | 556 | } else { 557 | 558 | v.setFromMatrixColumn( objectMatrix, 0 ); 559 | v.crossVectors( scope.object.up, v ); 560 | 561 | } 562 | 563 | v.multiplyScalar( distance ); 564 | 565 | panOffset.add( v ); 566 | 567 | }; 568 | 569 | }(); 570 | 571 | // deltaX and deltaY are in pixels; right and down are positive 572 | const pan = function () { 573 | 574 | const offset = new Vector3(); 575 | 576 | return function pan( deltaX, deltaY ) { 577 | 578 | const element = scope.domElement; 579 | 580 | if ( scope.object.isPerspectiveCamera ) { 581 | 582 | // perspective 583 | const position = scope.object.position; 584 | offset.copy( position ).sub( scope.target ); 585 | let targetDistance = offset.length(); 586 | 587 | // half of the fov is center to top of screen 588 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 589 | 590 | // we use only clientHeight here so aspect ratio does not distort speed 591 | panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); 592 | panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); 593 | 594 | } else if ( scope.object.isOrthographicCamera ) { 595 | 596 | // orthographic 597 | panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); 598 | panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); 599 | 600 | } else { 601 | 602 | // camera neither orthographic nor perspective 603 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 604 | scope.enablePan = false; 605 | 606 | } 607 | 608 | }; 609 | 610 | }(); 611 | 612 | function dollyOut( dollyScale ) { 613 | 614 | if ( scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera ) { 615 | 616 | scale /= dollyScale; 617 | 618 | } else { 619 | 620 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 621 | scope.enableZoom = false; 622 | 623 | } 624 | 625 | } 626 | 627 | function dollyIn( dollyScale ) { 628 | 629 | if ( scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera ) { 630 | 631 | scale *= dollyScale; 632 | 633 | } else { 634 | 635 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 636 | scope.enableZoom = false; 637 | 638 | } 639 | 640 | } 641 | 642 | function updateZoomParameters( x, y ) { 643 | 644 | if ( ! scope.zoomToCursor ) { 645 | 646 | return; 647 | 648 | } 649 | 650 | performCursorZoom = true; 651 | 652 | const rect = scope.domElement.getBoundingClientRect(); 653 | const dx = x - rect.left; 654 | const dy = y - rect.top; 655 | const w = rect.width; 656 | const h = rect.height; 657 | 658 | mouse.x = ( dx / w ) * 2 - 1; 659 | mouse.y = - ( dy / h ) * 2 + 1; 660 | 661 | dollyDirection.set( mouse.x, mouse.y, 1 ).unproject( scope.object ).sub( scope.object.position ).normalize(); 662 | 663 | } 664 | 665 | function clampDistance( dist ) { 666 | 667 | return Math.max( scope.minDistance, Math.min( scope.maxDistance, dist ) ); 668 | 669 | } 670 | 671 | // 672 | // event callbacks - update the object state 673 | // 674 | 675 | function handleMouseDownRotate( event ) { 676 | 677 | rotateStart.set( event.clientX, event.clientY ); 678 | 679 | } 680 | 681 | function handleMouseDownDolly( event ) { 682 | 683 | updateZoomParameters( event.clientX, event.clientX ); 684 | dollyStart.set( event.clientX, event.clientY ); 685 | 686 | } 687 | 688 | function handleMouseDownPan( event ) { 689 | 690 | panStart.set( event.clientX, event.clientY ); 691 | 692 | } 693 | 694 | function handleMouseMoveRotate( event ) { 695 | 696 | rotateEnd.set( event.clientX, event.clientY ); 697 | 698 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 699 | 700 | const element = scope.domElement; 701 | 702 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 703 | 704 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 705 | 706 | rotateStart.copy( rotateEnd ); 707 | 708 | scope.update(); 709 | 710 | } 711 | 712 | function handleMouseMoveDolly( event ) { 713 | 714 | dollyEnd.set( event.clientX, event.clientY ); 715 | 716 | dollyDelta.subVectors( dollyEnd, dollyStart ); 717 | 718 | if ( dollyDelta.y > 0 ) { 719 | 720 | dollyOut( getZoomScale( dollyDelta.y ) ); 721 | 722 | } else if ( dollyDelta.y < 0 ) { 723 | 724 | dollyIn( getZoomScale( dollyDelta.y ) ); 725 | 726 | } 727 | 728 | dollyStart.copy( dollyEnd ); 729 | 730 | scope.update(); 731 | 732 | } 733 | 734 | function handleMouseMovePan( event ) { 735 | 736 | panEnd.set( event.clientX, event.clientY ); 737 | 738 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 739 | 740 | pan( panDelta.x, panDelta.y ); 741 | 742 | panStart.copy( panEnd ); 743 | 744 | scope.update(); 745 | 746 | } 747 | 748 | function handleMouseWheel( event ) { 749 | 750 | updateZoomParameters( event.clientX, event.clientY ); 751 | 752 | if ( event.deltaY < 0 ) { 753 | 754 | dollyIn( getZoomScale( event.deltaY ) ); 755 | 756 | } else if ( event.deltaY > 0 ) { 757 | 758 | dollyOut( getZoomScale( event.deltaY ) ); 759 | 760 | } 761 | 762 | scope.update(); 763 | 764 | } 765 | 766 | function handleKeyDown( event ) { 767 | 768 | let needsUpdate = false; 769 | 770 | switch ( event.code ) { 771 | 772 | case scope.keys.UP: 773 | 774 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 775 | 776 | rotateUp( 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight ); 777 | 778 | } else { 779 | 780 | pan( 0, scope.keyPanSpeed ); 781 | 782 | } 783 | 784 | needsUpdate = true; 785 | break; 786 | 787 | case scope.keys.BOTTOM: 788 | 789 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 790 | 791 | rotateUp( - 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight ); 792 | 793 | } else { 794 | 795 | pan( 0, - scope.keyPanSpeed ); 796 | 797 | } 798 | 799 | needsUpdate = true; 800 | break; 801 | 802 | case scope.keys.LEFT: 803 | 804 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 805 | 806 | rotateLeft( 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight ); 807 | 808 | } else { 809 | 810 | pan( scope.keyPanSpeed, 0 ); 811 | 812 | } 813 | 814 | needsUpdate = true; 815 | break; 816 | 817 | case scope.keys.RIGHT: 818 | 819 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 820 | 821 | rotateLeft( - 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight ); 822 | 823 | } else { 824 | 825 | pan( - scope.keyPanSpeed, 0 ); 826 | 827 | } 828 | 829 | needsUpdate = true; 830 | break; 831 | 832 | } 833 | 834 | if ( needsUpdate ) { 835 | 836 | // prevent the browser from scrolling on cursor keys 837 | event.preventDefault(); 838 | 839 | scope.update(); 840 | 841 | } 842 | 843 | 844 | } 845 | 846 | function handleTouchStartRotate( event ) { 847 | 848 | if ( pointers.length === 1 ) { 849 | 850 | rotateStart.set( event.pageX, event.pageY ); 851 | 852 | } else { 853 | 854 | const position = getSecondPointerPosition( event ); 855 | 856 | const x = 0.5 * ( event.pageX + position.x ); 857 | const y = 0.5 * ( event.pageY + position.y ); 858 | 859 | rotateStart.set( x, y ); 860 | 861 | } 862 | 863 | } 864 | 865 | function handleTouchStartPan( event ) { 866 | 867 | if ( pointers.length === 1 ) { 868 | 869 | panStart.set( event.pageX, event.pageY ); 870 | 871 | } else { 872 | 873 | const position = getSecondPointerPosition( event ); 874 | 875 | const x = 0.5 * ( event.pageX + position.x ); 876 | const y = 0.5 * ( event.pageY + position.y ); 877 | 878 | panStart.set( x, y ); 879 | 880 | } 881 | 882 | } 883 | 884 | function handleTouchStartDolly( event ) { 885 | 886 | const position = getSecondPointerPosition( event ); 887 | 888 | const dx = event.pageX - position.x; 889 | const dy = event.pageY - position.y; 890 | 891 | const distance = Math.sqrt( dx * dx + dy * dy ); 892 | 893 | dollyStart.set( 0, distance ); 894 | 895 | } 896 | 897 | function handleTouchStartDollyPan( event ) { 898 | 899 | if ( scope.enableZoom ) handleTouchStartDolly( event ); 900 | 901 | if ( scope.enablePan ) handleTouchStartPan( event ); 902 | 903 | } 904 | 905 | function handleTouchStartDollyRotate( event ) { 906 | 907 | if ( scope.enableZoom ) handleTouchStartDolly( event ); 908 | 909 | if ( scope.enableRotate ) handleTouchStartRotate( event ); 910 | 911 | } 912 | 913 | function handleTouchMoveRotate( event ) { 914 | 915 | if ( pointers.length == 1 ) { 916 | 917 | rotateEnd.set( event.pageX, event.pageY ); 918 | 919 | } else { 920 | 921 | const position = getSecondPointerPosition( event ); 922 | 923 | const x = 0.5 * ( event.pageX + position.x ); 924 | const y = 0.5 * ( event.pageY + position.y ); 925 | 926 | rotateEnd.set( x, y ); 927 | 928 | } 929 | 930 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 931 | 932 | const element = scope.domElement; 933 | 934 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 935 | 936 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 937 | 938 | rotateStart.copy( rotateEnd ); 939 | 940 | } 941 | 942 | function handleTouchMovePan( event ) { 943 | 944 | if ( pointers.length === 1 ) { 945 | 946 | panEnd.set( event.pageX, event.pageY ); 947 | 948 | } else { 949 | 950 | const position = getSecondPointerPosition( event ); 951 | 952 | const x = 0.5 * ( event.pageX + position.x ); 953 | const y = 0.5 * ( event.pageY + position.y ); 954 | 955 | panEnd.set( x, y ); 956 | 957 | } 958 | 959 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 960 | 961 | pan( panDelta.x, panDelta.y ); 962 | 963 | panStart.copy( panEnd ); 964 | 965 | } 966 | 967 | function handleTouchMoveDolly( event ) { 968 | 969 | const position = getSecondPointerPosition( event ); 970 | 971 | const dx = event.pageX - position.x; 972 | const dy = event.pageY - position.y; 973 | 974 | const distance = Math.sqrt( dx * dx + dy * dy ); 975 | 976 | dollyEnd.set( 0, distance ); 977 | 978 | dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); 979 | 980 | dollyOut( dollyDelta.y ); 981 | 982 | dollyStart.copy( dollyEnd ); 983 | 984 | const centerX = ( event.pageX + position.x ) * 0.5; 985 | const centerY = ( event.pageY + position.y ) * 0.5; 986 | 987 | updateZoomParameters( centerX, centerY ); 988 | 989 | } 990 | 991 | function handleTouchMoveDollyPan( event ) { 992 | 993 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 994 | 995 | if ( scope.enablePan ) handleTouchMovePan( event ); 996 | 997 | } 998 | 999 | function handleTouchMoveDollyRotate( event ) { 1000 | 1001 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 1002 | 1003 | if ( scope.enableRotate ) handleTouchMoveRotate( event ); 1004 | 1005 | } 1006 | 1007 | // 1008 | // event handlers - FSM: listen for events and reset state 1009 | // 1010 | 1011 | function onPointerDown( event ) { 1012 | 1013 | if ( scope.enabled === false ) return; 1014 | 1015 | if ( pointers.length === 0 ) { 1016 | 1017 | scope.domElement.setPointerCapture( event.pointerId ); 1018 | 1019 | scope.domElement.addEventListener( 'pointermove', onPointerMove ); 1020 | scope.domElement.addEventListener( 'pointerup', onPointerUp ); 1021 | 1022 | } 1023 | 1024 | // 1025 | 1026 | if ( isTrackingPointer( event ) ) return; 1027 | 1028 | // 1029 | 1030 | addPointer( event ); 1031 | 1032 | if ( event.pointerType === 'touch' ) { 1033 | 1034 | onTouchStart( event ); 1035 | 1036 | } else { 1037 | 1038 | onMouseDown( event ); 1039 | 1040 | } 1041 | 1042 | } 1043 | 1044 | function onPointerMove( event ) { 1045 | 1046 | if ( scope.enabled === false ) return; 1047 | 1048 | if ( event.pointerType === 'touch' ) { 1049 | 1050 | onTouchMove( event ); 1051 | 1052 | } else { 1053 | 1054 | onMouseMove( event ); 1055 | 1056 | } 1057 | 1058 | } 1059 | 1060 | function onPointerUp( event ) { 1061 | 1062 | removePointer( event ); 1063 | 1064 | switch ( pointers.length ) { 1065 | 1066 | case 0: 1067 | 1068 | scope.domElement.releasePointerCapture( event.pointerId ); 1069 | 1070 | scope.domElement.removeEventListener( 'pointermove', onPointerMove ); 1071 | scope.domElement.removeEventListener( 'pointerup', onPointerUp ); 1072 | 1073 | scope.dispatchEvent( _endEvent ); 1074 | 1075 | state = STATE.NONE; 1076 | 1077 | break; 1078 | 1079 | case 1: 1080 | 1081 | const pointerId = pointers[ 0 ]; 1082 | const position = pointerPositions[ pointerId ]; 1083 | 1084 | // minimal placeholder event - allows state correction on pointer-up 1085 | onTouchStart( { pointerId: pointerId, pageX: position.x, pageY: position.y } ); 1086 | 1087 | break; 1088 | 1089 | } 1090 | 1091 | } 1092 | 1093 | function onMouseDown( event ) { 1094 | 1095 | let mouseAction; 1096 | 1097 | switch ( event.button ) { 1098 | 1099 | case 0: 1100 | 1101 | mouseAction = scope.mouseButtons.LEFT; 1102 | break; 1103 | 1104 | case 1: 1105 | 1106 | mouseAction = scope.mouseButtons.MIDDLE; 1107 | break; 1108 | 1109 | case 2: 1110 | 1111 | mouseAction = scope.mouseButtons.RIGHT; 1112 | break; 1113 | 1114 | default: 1115 | 1116 | mouseAction = - 1; 1117 | 1118 | } 1119 | 1120 | switch ( mouseAction ) { 1121 | 1122 | case MOUSE.DOLLY: 1123 | 1124 | if ( scope.enableZoom === false ) return; 1125 | 1126 | handleMouseDownDolly( event ); 1127 | 1128 | state = STATE.DOLLY; 1129 | 1130 | break; 1131 | 1132 | case MOUSE.ROTATE: 1133 | 1134 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 1135 | 1136 | if ( scope.enablePan === false ) return; 1137 | 1138 | handleMouseDownPan( event ); 1139 | 1140 | state = STATE.PAN; 1141 | 1142 | } else { 1143 | 1144 | if ( scope.enableRotate === false ) return; 1145 | 1146 | handleMouseDownRotate( event ); 1147 | 1148 | state = STATE.ROTATE; 1149 | 1150 | } 1151 | 1152 | break; 1153 | 1154 | case MOUSE.PAN: 1155 | 1156 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 1157 | 1158 | if ( scope.enableRotate === false ) return; 1159 | 1160 | handleMouseDownRotate( event ); 1161 | 1162 | state = STATE.ROTATE; 1163 | 1164 | } else { 1165 | 1166 | if ( scope.enablePan === false ) return; 1167 | 1168 | handleMouseDownPan( event ); 1169 | 1170 | state = STATE.PAN; 1171 | 1172 | } 1173 | 1174 | break; 1175 | 1176 | default: 1177 | 1178 | state = STATE.NONE; 1179 | 1180 | } 1181 | 1182 | if ( state !== STATE.NONE ) { 1183 | 1184 | scope.dispatchEvent( _startEvent ); 1185 | 1186 | } 1187 | 1188 | } 1189 | 1190 | function onMouseMove( event ) { 1191 | 1192 | switch ( state ) { 1193 | 1194 | case STATE.ROTATE: 1195 | 1196 | if ( scope.enableRotate === false ) return; 1197 | 1198 | handleMouseMoveRotate( event ); 1199 | 1200 | break; 1201 | 1202 | case STATE.DOLLY: 1203 | 1204 | if ( scope.enableZoom === false ) return; 1205 | 1206 | handleMouseMoveDolly( event ); 1207 | 1208 | break; 1209 | 1210 | case STATE.PAN: 1211 | 1212 | if ( scope.enablePan === false ) return; 1213 | 1214 | handleMouseMovePan( event ); 1215 | 1216 | break; 1217 | 1218 | } 1219 | 1220 | } 1221 | 1222 | function onMouseWheel( event ) { 1223 | 1224 | if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE ) return; 1225 | 1226 | event.preventDefault(); 1227 | 1228 | scope.dispatchEvent( _startEvent ); 1229 | 1230 | handleMouseWheel( customWheelEvent( event ) ); 1231 | 1232 | scope.dispatchEvent( _endEvent ); 1233 | 1234 | } 1235 | 1236 | function customWheelEvent( event ) { 1237 | 1238 | const mode = event.deltaMode; 1239 | 1240 | // minimal wheel event altered to meet delta-zoom demand 1241 | const newEvent = { 1242 | clientX: event.clientX, 1243 | clientY: event.clientY, 1244 | deltaY: event.deltaY, 1245 | }; 1246 | 1247 | switch ( mode ) { 1248 | 1249 | case 1: // LINE_MODE 1250 | newEvent.deltaY *= 16; 1251 | break; 1252 | 1253 | case 2: // PAGE_MODE 1254 | newEvent.deltaY *= 100; 1255 | break; 1256 | 1257 | } 1258 | 1259 | // detect if event was triggered by pinching 1260 | if ( event.ctrlKey && ! controlActive ) { 1261 | 1262 | newEvent.deltaY *= 10; 1263 | 1264 | } 1265 | 1266 | return newEvent; 1267 | 1268 | } 1269 | 1270 | function interceptControlDown( event ) { 1271 | 1272 | if ( event.key === 'Control' ) { 1273 | 1274 | controlActive = true; 1275 | 1276 | 1277 | const document = scope.domElement.getRootNode(); // offscreen canvas compatibility 1278 | 1279 | document.addEventListener( 'keyup', interceptControlUp, { passive: true, capture: true } ); 1280 | 1281 | } 1282 | 1283 | } 1284 | 1285 | function interceptControlUp( event ) { 1286 | 1287 | if ( event.key === 'Control' ) { 1288 | 1289 | controlActive = false; 1290 | 1291 | 1292 | const document = scope.domElement.getRootNode(); // offscreen canvas compatibility 1293 | 1294 | document.removeEventListener( 'keyup', interceptControlUp, { passive: true, capture: true } ); 1295 | 1296 | } 1297 | 1298 | } 1299 | 1300 | function onKeyDown( event ) { 1301 | 1302 | if ( scope.enabled === false || scope.enablePan === false ) return; 1303 | 1304 | handleKeyDown( event ); 1305 | 1306 | } 1307 | 1308 | function onTouchStart( event ) { 1309 | 1310 | trackPointer( event ); 1311 | 1312 | switch ( pointers.length ) { 1313 | 1314 | case 1: 1315 | 1316 | switch ( scope.touches.ONE ) { 1317 | 1318 | case TOUCH.ROTATE: 1319 | 1320 | if ( scope.enableRotate === false ) return; 1321 | 1322 | handleTouchStartRotate( event ); 1323 | 1324 | state = STATE.TOUCH_ROTATE; 1325 | 1326 | break; 1327 | 1328 | case TOUCH.PAN: 1329 | 1330 | if ( scope.enablePan === false ) return; 1331 | 1332 | handleTouchStartPan( event ); 1333 | 1334 | state = STATE.TOUCH_PAN; 1335 | 1336 | break; 1337 | 1338 | default: 1339 | 1340 | state = STATE.NONE; 1341 | 1342 | } 1343 | 1344 | break; 1345 | 1346 | case 2: 1347 | 1348 | switch ( scope.touches.TWO ) { 1349 | 1350 | case TOUCH.DOLLY_PAN: 1351 | 1352 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 1353 | 1354 | handleTouchStartDollyPan( event ); 1355 | 1356 | state = STATE.TOUCH_DOLLY_PAN; 1357 | 1358 | break; 1359 | 1360 | case TOUCH.DOLLY_ROTATE: 1361 | 1362 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 1363 | 1364 | handleTouchStartDollyRotate( event ); 1365 | 1366 | state = STATE.TOUCH_DOLLY_ROTATE; 1367 | 1368 | break; 1369 | 1370 | default: 1371 | 1372 | state = STATE.NONE; 1373 | 1374 | } 1375 | 1376 | break; 1377 | 1378 | default: 1379 | 1380 | state = STATE.NONE; 1381 | 1382 | } 1383 | 1384 | if ( state !== STATE.NONE ) { 1385 | 1386 | scope.dispatchEvent( _startEvent ); 1387 | 1388 | } 1389 | 1390 | } 1391 | 1392 | function onTouchMove( event ) { 1393 | 1394 | trackPointer( event ); 1395 | 1396 | switch ( state ) { 1397 | 1398 | case STATE.TOUCH_ROTATE: 1399 | 1400 | if ( scope.enableRotate === false ) return; 1401 | 1402 | handleTouchMoveRotate( event ); 1403 | 1404 | scope.update(); 1405 | 1406 | break; 1407 | 1408 | case STATE.TOUCH_PAN: 1409 | 1410 | if ( scope.enablePan === false ) return; 1411 | 1412 | handleTouchMovePan( event ); 1413 | 1414 | scope.update(); 1415 | 1416 | break; 1417 | 1418 | case STATE.TOUCH_DOLLY_PAN: 1419 | 1420 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 1421 | 1422 | handleTouchMoveDollyPan( event ); 1423 | 1424 | scope.update(); 1425 | 1426 | break; 1427 | 1428 | case STATE.TOUCH_DOLLY_ROTATE: 1429 | 1430 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 1431 | 1432 | handleTouchMoveDollyRotate( event ); 1433 | 1434 | scope.update(); 1435 | 1436 | break; 1437 | 1438 | default: 1439 | 1440 | state = STATE.NONE; 1441 | 1442 | } 1443 | 1444 | } 1445 | 1446 | function onContextMenu( event ) { 1447 | 1448 | if ( scope.enabled === false ) return; 1449 | 1450 | event.preventDefault(); 1451 | 1452 | } 1453 | 1454 | function addPointer( event ) { 1455 | 1456 | pointers.push( event.pointerId ); 1457 | 1458 | } 1459 | 1460 | function removePointer( event ) { 1461 | 1462 | delete pointerPositions[ event.pointerId ]; 1463 | 1464 | for ( let i = 0; i < pointers.length; i ++ ) { 1465 | 1466 | if ( pointers[ i ] == event.pointerId ) { 1467 | 1468 | pointers.splice( i, 1 ); 1469 | return; 1470 | 1471 | } 1472 | 1473 | } 1474 | 1475 | } 1476 | 1477 | function isTrackingPointer( event ) { 1478 | 1479 | for ( let i = 0; i < pointers.length; i ++ ) { 1480 | 1481 | if ( pointers[ i ] == event.pointerId ) return true; 1482 | 1483 | } 1484 | 1485 | return false; 1486 | 1487 | } 1488 | 1489 | function trackPointer( event ) { 1490 | 1491 | let position = pointerPositions[ event.pointerId ]; 1492 | 1493 | if ( position === undefined ) { 1494 | 1495 | position = new Vector2(); 1496 | pointerPositions[ event.pointerId ] = position; 1497 | 1498 | } 1499 | 1500 | position.set( event.pageX, event.pageY ); 1501 | 1502 | } 1503 | 1504 | function getSecondPointerPosition( event ) { 1505 | 1506 | const pointerId = ( event.pointerId === pointers[ 0 ] ) ? pointers[ 1 ] : pointers[ 0 ]; 1507 | 1508 | return pointerPositions[ pointerId ]; 1509 | 1510 | } 1511 | 1512 | // 1513 | 1514 | scope.domElement.addEventListener( 'contextmenu', onContextMenu ); 1515 | 1516 | scope.domElement.addEventListener( 'pointerdown', onPointerDown ); 1517 | scope.domElement.addEventListener( 'pointercancel', onPointerUp ); 1518 | scope.domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } ); 1519 | 1520 | const document = scope.domElement.getRootNode(); // offscreen canvas compatibility 1521 | 1522 | document.addEventListener( 'keydown', interceptControlDown, { passive: true, capture: true } ); 1523 | 1524 | // force an update at start 1525 | 1526 | this.update(); 1527 | 1528 | } 1529 | 1530 | } 1531 | 1532 | export { OrbitControls }; 1533 | -------------------------------------------------------------------------------- /node_modules/three/examples/jsm/controls/PointerLockControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | Euler, 3 | EventDispatcher, 4 | Vector3 5 | } from 'three'; 6 | 7 | const _euler = new Euler( 0, 0, 0, 'YXZ' ); 8 | const _vector = new Vector3(); 9 | 10 | const _changeEvent = { type: 'change' }; 11 | const _lockEvent = { type: 'lock' }; 12 | const _unlockEvent = { type: 'unlock' }; 13 | 14 | const _PI_2 = Math.PI / 2; 15 | 16 | class PointerLockControls extends EventDispatcher { 17 | 18 | constructor( camera, domElement ) { 19 | 20 | super(); 21 | 22 | this.camera = camera; 23 | this.domElement = domElement; 24 | 25 | this.isLocked = false; 26 | 27 | // Set to constrain the pitch of the camera 28 | // Range is 0 to Math.PI radians 29 | this.minPolarAngle = 0; // radians 30 | this.maxPolarAngle = Math.PI; // radians 31 | 32 | this.pointerSpeed = 1.0; 33 | 34 | this._onMouseMove = onMouseMove.bind( this ); 35 | this._onPointerlockChange = onPointerlockChange.bind( this ); 36 | this._onPointerlockError = onPointerlockError.bind( this ); 37 | 38 | this.connect(); 39 | 40 | } 41 | 42 | connect() { 43 | 44 | this.domElement.ownerDocument.addEventListener( 'mousemove', this._onMouseMove ); 45 | this.domElement.ownerDocument.addEventListener( 'pointerlockchange', this._onPointerlockChange ); 46 | this.domElement.ownerDocument.addEventListener( 'pointerlockerror', this._onPointerlockError ); 47 | 48 | } 49 | 50 | disconnect() { 51 | 52 | this.domElement.ownerDocument.removeEventListener( 'mousemove', this._onMouseMove ); 53 | this.domElement.ownerDocument.removeEventListener( 'pointerlockchange', this._onPointerlockChange ); 54 | this.domElement.ownerDocument.removeEventListener( 'pointerlockerror', this._onPointerlockError ); 55 | 56 | } 57 | 58 | dispose() { 59 | 60 | this.disconnect(); 61 | 62 | } 63 | 64 | getObject() { // retaining this method for backward compatibility 65 | 66 | return this.camera; 67 | 68 | } 69 | 70 | getDirection( v ) { 71 | 72 | return v.set( 0, 0, - 1 ).applyQuaternion( this.camera.quaternion ); 73 | 74 | } 75 | 76 | moveForward( distance ) { 77 | 78 | // move forward parallel to the xz-plane 79 | // assumes camera.up is y-up 80 | 81 | const camera = this.camera; 82 | 83 | _vector.setFromMatrixColumn( camera.matrix, 0 ); 84 | 85 | _vector.crossVectors( camera.up, _vector ); 86 | 87 | camera.position.addScaledVector( _vector, distance ); 88 | 89 | } 90 | 91 | moveRight( distance ) { 92 | 93 | const camera = this.camera; 94 | 95 | _vector.setFromMatrixColumn( camera.matrix, 0 ); 96 | 97 | camera.position.addScaledVector( _vector, distance ); 98 | 99 | } 100 | 101 | lock() { 102 | 103 | this.domElement.requestPointerLock(); 104 | 105 | } 106 | 107 | unlock() { 108 | 109 | this.domElement.ownerDocument.exitPointerLock(); 110 | 111 | } 112 | 113 | } 114 | 115 | // event listeners 116 | 117 | function onMouseMove( event ) { 118 | 119 | if ( this.isLocked === false ) return; 120 | 121 | const movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0; 122 | const movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0; 123 | 124 | const camera = this.camera; 125 | _euler.setFromQuaternion( camera.quaternion ); 126 | 127 | _euler.y -= movementX * 0.002 * this.pointerSpeed; 128 | _euler.x -= movementY * 0.002 * this.pointerSpeed; 129 | 130 | _euler.x = Math.max( _PI_2 - this.maxPolarAngle, Math.min( _PI_2 - this.minPolarAngle, _euler.x ) ); 131 | 132 | camera.quaternion.setFromEuler( _euler ); 133 | 134 | this.dispatchEvent( _changeEvent ); 135 | 136 | } 137 | 138 | function onPointerlockChange() { 139 | 140 | if ( this.domElement.ownerDocument.pointerLockElement === this.domElement ) { 141 | 142 | this.dispatchEvent( _lockEvent ); 143 | 144 | this.isLocked = true; 145 | 146 | } else { 147 | 148 | this.dispatchEvent( _unlockEvent ); 149 | 150 | this.isLocked = false; 151 | 152 | } 153 | 154 | } 155 | 156 | function onPointerlockError() { 157 | 158 | console.error( 'THREE.PointerLockControls: Unable to use Pointer Lock API' ); 159 | 160 | } 161 | 162 | export { PointerLockControls }; 163 | -------------------------------------------------------------------------------- /node_modules/three/examples/jsm/controls/TrackballControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | EventDispatcher, 3 | MathUtils, 4 | MOUSE, 5 | Quaternion, 6 | Vector2, 7 | Vector3 8 | } from 'three'; 9 | 10 | const _changeEvent = { type: 'change' }; 11 | const _startEvent = { type: 'start' }; 12 | const _endEvent = { type: 'end' }; 13 | 14 | class TrackballControls extends EventDispatcher { 15 | 16 | constructor( object, domElement ) { 17 | 18 | super(); 19 | 20 | const scope = this; 21 | const STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 }; 22 | 23 | this.object = object; 24 | this.domElement = domElement; 25 | this.domElement.style.touchAction = 'none'; // disable touch scroll 26 | 27 | // API 28 | 29 | this.enabled = true; 30 | 31 | this.screen = { left: 0, top: 0, width: 0, height: 0 }; 32 | 33 | this.rotateSpeed = 1.0; 34 | this.zoomSpeed = 1.2; 35 | this.panSpeed = 0.3; 36 | 37 | this.noRotate = false; 38 | this.noZoom = false; 39 | this.noPan = false; 40 | 41 | this.staticMoving = false; 42 | this.dynamicDampingFactor = 0.2; 43 | 44 | this.minDistance = 0; 45 | this.maxDistance = Infinity; 46 | 47 | this.minZoom = 0; 48 | this.maxZoom = Infinity; 49 | 50 | this.keys = [ 'KeyA' /*A*/, 'KeyS' /*S*/, 'KeyD' /*D*/ ]; 51 | 52 | this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }; 53 | 54 | // internals 55 | 56 | this.target = new Vector3(); 57 | 58 | const EPS = 0.000001; 59 | 60 | const lastPosition = new Vector3(); 61 | let lastZoom = 1; 62 | 63 | let _state = STATE.NONE, 64 | _keyState = STATE.NONE, 65 | 66 | _touchZoomDistanceStart = 0, 67 | _touchZoomDistanceEnd = 0, 68 | 69 | _lastAngle = 0; 70 | 71 | const _eye = new Vector3(), 72 | 73 | _movePrev = new Vector2(), 74 | _moveCurr = new Vector2(), 75 | 76 | _lastAxis = new Vector3(), 77 | 78 | _zoomStart = new Vector2(), 79 | _zoomEnd = new Vector2(), 80 | 81 | _panStart = new Vector2(), 82 | _panEnd = new Vector2(), 83 | 84 | _pointers = [], 85 | _pointerPositions = {}; 86 | 87 | // for reset 88 | 89 | this.target0 = this.target.clone(); 90 | this.position0 = this.object.position.clone(); 91 | this.up0 = this.object.up.clone(); 92 | this.zoom0 = this.object.zoom; 93 | 94 | // methods 95 | 96 | this.handleResize = function () { 97 | 98 | const box = scope.domElement.getBoundingClientRect(); 99 | // adjustments come from similar code in the jquery offset() function 100 | const d = scope.domElement.ownerDocument.documentElement; 101 | scope.screen.left = box.left + window.pageXOffset - d.clientLeft; 102 | scope.screen.top = box.top + window.pageYOffset - d.clientTop; 103 | scope.screen.width = box.width; 104 | scope.screen.height = box.height; 105 | 106 | }; 107 | 108 | const getMouseOnScreen = ( function () { 109 | 110 | const vector = new Vector2(); 111 | 112 | return function getMouseOnScreen( pageX, pageY ) { 113 | 114 | vector.set( 115 | ( pageX - scope.screen.left ) / scope.screen.width, 116 | ( pageY - scope.screen.top ) / scope.screen.height 117 | ); 118 | 119 | return vector; 120 | 121 | }; 122 | 123 | }() ); 124 | 125 | const getMouseOnCircle = ( function () { 126 | 127 | const vector = new Vector2(); 128 | 129 | return function getMouseOnCircle( pageX, pageY ) { 130 | 131 | vector.set( 132 | ( ( pageX - scope.screen.width * 0.5 - scope.screen.left ) / ( scope.screen.width * 0.5 ) ), 133 | ( ( scope.screen.height + 2 * ( scope.screen.top - pageY ) ) / scope.screen.width ) // screen.width intentional 134 | ); 135 | 136 | return vector; 137 | 138 | }; 139 | 140 | }() ); 141 | 142 | this.rotateCamera = ( function () { 143 | 144 | const axis = new Vector3(), 145 | quaternion = new Quaternion(), 146 | eyeDirection = new Vector3(), 147 | objectUpDirection = new Vector3(), 148 | objectSidewaysDirection = new Vector3(), 149 | moveDirection = new Vector3(); 150 | 151 | return function rotateCamera() { 152 | 153 | moveDirection.set( _moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0 ); 154 | let angle = moveDirection.length(); 155 | 156 | if ( angle ) { 157 | 158 | _eye.copy( scope.object.position ).sub( scope.target ); 159 | 160 | eyeDirection.copy( _eye ).normalize(); 161 | objectUpDirection.copy( scope.object.up ).normalize(); 162 | objectSidewaysDirection.crossVectors( objectUpDirection, eyeDirection ).normalize(); 163 | 164 | objectUpDirection.setLength( _moveCurr.y - _movePrev.y ); 165 | objectSidewaysDirection.setLength( _moveCurr.x - _movePrev.x ); 166 | 167 | moveDirection.copy( objectUpDirection.add( objectSidewaysDirection ) ); 168 | 169 | axis.crossVectors( moveDirection, _eye ).normalize(); 170 | 171 | angle *= scope.rotateSpeed; 172 | quaternion.setFromAxisAngle( axis, angle ); 173 | 174 | _eye.applyQuaternion( quaternion ); 175 | scope.object.up.applyQuaternion( quaternion ); 176 | 177 | _lastAxis.copy( axis ); 178 | _lastAngle = angle; 179 | 180 | } else if ( ! scope.staticMoving && _lastAngle ) { 181 | 182 | _lastAngle *= Math.sqrt( 1.0 - scope.dynamicDampingFactor ); 183 | _eye.copy( scope.object.position ).sub( scope.target ); 184 | quaternion.setFromAxisAngle( _lastAxis, _lastAngle ); 185 | _eye.applyQuaternion( quaternion ); 186 | scope.object.up.applyQuaternion( quaternion ); 187 | 188 | } 189 | 190 | _movePrev.copy( _moveCurr ); 191 | 192 | }; 193 | 194 | }() ); 195 | 196 | 197 | this.zoomCamera = function () { 198 | 199 | let factor; 200 | 201 | if ( _state === STATE.TOUCH_ZOOM_PAN ) { 202 | 203 | factor = _touchZoomDistanceStart / _touchZoomDistanceEnd; 204 | _touchZoomDistanceStart = _touchZoomDistanceEnd; 205 | 206 | if ( scope.object.isPerspectiveCamera ) { 207 | 208 | _eye.multiplyScalar( factor ); 209 | 210 | } else if ( scope.object.isOrthographicCamera ) { 211 | 212 | scope.object.zoom = MathUtils.clamp( scope.object.zoom / factor, scope.minZoom, scope.maxZoom ); 213 | 214 | if ( lastZoom !== scope.object.zoom ) { 215 | 216 | scope.object.updateProjectionMatrix(); 217 | 218 | } 219 | 220 | } else { 221 | 222 | console.warn( 'THREE.TrackballControls: Unsupported camera type' ); 223 | 224 | } 225 | 226 | } else { 227 | 228 | factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * scope.zoomSpeed; 229 | 230 | if ( factor !== 1.0 && factor > 0.0 ) { 231 | 232 | if ( scope.object.isPerspectiveCamera ) { 233 | 234 | _eye.multiplyScalar( factor ); 235 | 236 | } else if ( scope.object.isOrthographicCamera ) { 237 | 238 | scope.object.zoom = MathUtils.clamp( scope.object.zoom / factor, scope.minZoom, scope.maxZoom ); 239 | 240 | if ( lastZoom !== scope.object.zoom ) { 241 | 242 | scope.object.updateProjectionMatrix(); 243 | 244 | } 245 | 246 | } else { 247 | 248 | console.warn( 'THREE.TrackballControls: Unsupported camera type' ); 249 | 250 | } 251 | 252 | } 253 | 254 | if ( scope.staticMoving ) { 255 | 256 | _zoomStart.copy( _zoomEnd ); 257 | 258 | } else { 259 | 260 | _zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor; 261 | 262 | } 263 | 264 | } 265 | 266 | }; 267 | 268 | this.panCamera = ( function () { 269 | 270 | const mouseChange = new Vector2(), 271 | objectUp = new Vector3(), 272 | pan = new Vector3(); 273 | 274 | return function panCamera() { 275 | 276 | mouseChange.copy( _panEnd ).sub( _panStart ); 277 | 278 | if ( mouseChange.lengthSq() ) { 279 | 280 | if ( scope.object.isOrthographicCamera ) { 281 | 282 | const scale_x = ( scope.object.right - scope.object.left ) / scope.object.zoom / scope.domElement.clientWidth; 283 | const scale_y = ( scope.object.top - scope.object.bottom ) / scope.object.zoom / scope.domElement.clientWidth; 284 | 285 | mouseChange.x *= scale_x; 286 | mouseChange.y *= scale_y; 287 | 288 | } 289 | 290 | mouseChange.multiplyScalar( _eye.length() * scope.panSpeed ); 291 | 292 | pan.copy( _eye ).cross( scope.object.up ).setLength( mouseChange.x ); 293 | pan.add( objectUp.copy( scope.object.up ).setLength( mouseChange.y ) ); 294 | 295 | scope.object.position.add( pan ); 296 | scope.target.add( pan ); 297 | 298 | if ( scope.staticMoving ) { 299 | 300 | _panStart.copy( _panEnd ); 301 | 302 | } else { 303 | 304 | _panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( scope.dynamicDampingFactor ) ); 305 | 306 | } 307 | 308 | } 309 | 310 | }; 311 | 312 | }() ); 313 | 314 | this.checkDistances = function () { 315 | 316 | if ( ! scope.noZoom || ! scope.noPan ) { 317 | 318 | if ( _eye.lengthSq() > scope.maxDistance * scope.maxDistance ) { 319 | 320 | scope.object.position.addVectors( scope.target, _eye.setLength( scope.maxDistance ) ); 321 | _zoomStart.copy( _zoomEnd ); 322 | 323 | } 324 | 325 | if ( _eye.lengthSq() < scope.minDistance * scope.minDistance ) { 326 | 327 | scope.object.position.addVectors( scope.target, _eye.setLength( scope.minDistance ) ); 328 | _zoomStart.copy( _zoomEnd ); 329 | 330 | } 331 | 332 | } 333 | 334 | }; 335 | 336 | this.update = function () { 337 | 338 | _eye.subVectors( scope.object.position, scope.target ); 339 | 340 | if ( ! scope.noRotate ) { 341 | 342 | scope.rotateCamera(); 343 | 344 | } 345 | 346 | if ( ! scope.noZoom ) { 347 | 348 | scope.zoomCamera(); 349 | 350 | } 351 | 352 | if ( ! scope.noPan ) { 353 | 354 | scope.panCamera(); 355 | 356 | } 357 | 358 | scope.object.position.addVectors( scope.target, _eye ); 359 | 360 | if ( scope.object.isPerspectiveCamera ) { 361 | 362 | scope.checkDistances(); 363 | 364 | scope.object.lookAt( scope.target ); 365 | 366 | if ( lastPosition.distanceToSquared( scope.object.position ) > EPS ) { 367 | 368 | scope.dispatchEvent( _changeEvent ); 369 | 370 | lastPosition.copy( scope.object.position ); 371 | 372 | } 373 | 374 | } else if ( scope.object.isOrthographicCamera ) { 375 | 376 | scope.object.lookAt( scope.target ); 377 | 378 | if ( lastPosition.distanceToSquared( scope.object.position ) > EPS || lastZoom !== scope.object.zoom ) { 379 | 380 | scope.dispatchEvent( _changeEvent ); 381 | 382 | lastPosition.copy( scope.object.position ); 383 | lastZoom = scope.object.zoom; 384 | 385 | } 386 | 387 | } else { 388 | 389 | console.warn( 'THREE.TrackballControls: Unsupported camera type' ); 390 | 391 | } 392 | 393 | }; 394 | 395 | this.reset = function () { 396 | 397 | _state = STATE.NONE; 398 | _keyState = STATE.NONE; 399 | 400 | scope.target.copy( scope.target0 ); 401 | scope.object.position.copy( scope.position0 ); 402 | scope.object.up.copy( scope.up0 ); 403 | scope.object.zoom = scope.zoom0; 404 | 405 | scope.object.updateProjectionMatrix(); 406 | 407 | _eye.subVectors( scope.object.position, scope.target ); 408 | 409 | scope.object.lookAt( scope.target ); 410 | 411 | scope.dispatchEvent( _changeEvent ); 412 | 413 | lastPosition.copy( scope.object.position ); 414 | lastZoom = scope.object.zoom; 415 | 416 | }; 417 | 418 | // listeners 419 | 420 | function onPointerDown( event ) { 421 | 422 | if ( scope.enabled === false ) return; 423 | 424 | if ( _pointers.length === 0 ) { 425 | 426 | scope.domElement.setPointerCapture( event.pointerId ); 427 | 428 | scope.domElement.addEventListener( 'pointermove', onPointerMove ); 429 | scope.domElement.addEventListener( 'pointerup', onPointerUp ); 430 | 431 | } 432 | 433 | // 434 | 435 | addPointer( event ); 436 | 437 | if ( event.pointerType === 'touch' ) { 438 | 439 | onTouchStart( event ); 440 | 441 | } else { 442 | 443 | onMouseDown( event ); 444 | 445 | } 446 | 447 | } 448 | 449 | function onPointerMove( event ) { 450 | 451 | if ( scope.enabled === false ) return; 452 | 453 | if ( event.pointerType === 'touch' ) { 454 | 455 | onTouchMove( event ); 456 | 457 | } else { 458 | 459 | onMouseMove( event ); 460 | 461 | } 462 | 463 | } 464 | 465 | function onPointerUp( event ) { 466 | 467 | if ( scope.enabled === false ) return; 468 | 469 | if ( event.pointerType === 'touch' ) { 470 | 471 | onTouchEnd( event ); 472 | 473 | } else { 474 | 475 | onMouseUp(); 476 | 477 | } 478 | 479 | // 480 | 481 | removePointer( event ); 482 | 483 | if ( _pointers.length === 0 ) { 484 | 485 | scope.domElement.releasePointerCapture( event.pointerId ); 486 | 487 | scope.domElement.removeEventListener( 'pointermove', onPointerMove ); 488 | scope.domElement.removeEventListener( 'pointerup', onPointerUp ); 489 | 490 | } 491 | 492 | 493 | } 494 | 495 | function onPointerCancel( event ) { 496 | 497 | removePointer( event ); 498 | 499 | } 500 | 501 | function keydown( event ) { 502 | 503 | if ( scope.enabled === false ) return; 504 | 505 | window.removeEventListener( 'keydown', keydown ); 506 | 507 | if ( _keyState !== STATE.NONE ) { 508 | 509 | return; 510 | 511 | } else if ( event.code === scope.keys[ STATE.ROTATE ] && ! scope.noRotate ) { 512 | 513 | _keyState = STATE.ROTATE; 514 | 515 | } else if ( event.code === scope.keys[ STATE.ZOOM ] && ! scope.noZoom ) { 516 | 517 | _keyState = STATE.ZOOM; 518 | 519 | } else if ( event.code === scope.keys[ STATE.PAN ] && ! scope.noPan ) { 520 | 521 | _keyState = STATE.PAN; 522 | 523 | } 524 | 525 | } 526 | 527 | function keyup() { 528 | 529 | if ( scope.enabled === false ) return; 530 | 531 | _keyState = STATE.NONE; 532 | 533 | window.addEventListener( 'keydown', keydown ); 534 | 535 | } 536 | 537 | function onMouseDown( event ) { 538 | 539 | if ( _state === STATE.NONE ) { 540 | 541 | switch ( event.button ) { 542 | 543 | case scope.mouseButtons.LEFT: 544 | _state = STATE.ROTATE; 545 | break; 546 | 547 | case scope.mouseButtons.MIDDLE: 548 | _state = STATE.ZOOM; 549 | break; 550 | 551 | case scope.mouseButtons.RIGHT: 552 | _state = STATE.PAN; 553 | break; 554 | 555 | } 556 | 557 | } 558 | 559 | const state = ( _keyState !== STATE.NONE ) ? _keyState : _state; 560 | 561 | if ( state === STATE.ROTATE && ! scope.noRotate ) { 562 | 563 | _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) ); 564 | _movePrev.copy( _moveCurr ); 565 | 566 | } else if ( state === STATE.ZOOM && ! scope.noZoom ) { 567 | 568 | _zoomStart.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 569 | _zoomEnd.copy( _zoomStart ); 570 | 571 | } else if ( state === STATE.PAN && ! scope.noPan ) { 572 | 573 | _panStart.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 574 | _panEnd.copy( _panStart ); 575 | 576 | } 577 | 578 | scope.dispatchEvent( _startEvent ); 579 | 580 | } 581 | 582 | function onMouseMove( event ) { 583 | 584 | const state = ( _keyState !== STATE.NONE ) ? _keyState : _state; 585 | 586 | if ( state === STATE.ROTATE && ! scope.noRotate ) { 587 | 588 | _movePrev.copy( _moveCurr ); 589 | _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) ); 590 | 591 | } else if ( state === STATE.ZOOM && ! scope.noZoom ) { 592 | 593 | _zoomEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 594 | 595 | } else if ( state === STATE.PAN && ! scope.noPan ) { 596 | 597 | _panEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 598 | 599 | } 600 | 601 | } 602 | 603 | function onMouseUp() { 604 | 605 | _state = STATE.NONE; 606 | 607 | scope.dispatchEvent( _endEvent ); 608 | 609 | } 610 | 611 | function onMouseWheel( event ) { 612 | 613 | if ( scope.enabled === false ) return; 614 | 615 | if ( scope.noZoom === true ) return; 616 | 617 | event.preventDefault(); 618 | 619 | switch ( event.deltaMode ) { 620 | 621 | case 2: 622 | // Zoom in pages 623 | _zoomStart.y -= event.deltaY * 0.025; 624 | break; 625 | 626 | case 1: 627 | // Zoom in lines 628 | _zoomStart.y -= event.deltaY * 0.01; 629 | break; 630 | 631 | default: 632 | // undefined, 0, assume pixels 633 | _zoomStart.y -= event.deltaY * 0.00025; 634 | break; 635 | 636 | } 637 | 638 | scope.dispatchEvent( _startEvent ); 639 | scope.dispatchEvent( _endEvent ); 640 | 641 | } 642 | 643 | function onTouchStart( event ) { 644 | 645 | trackPointer( event ); 646 | 647 | switch ( _pointers.length ) { 648 | 649 | case 1: 650 | _state = STATE.TOUCH_ROTATE; 651 | _moveCurr.copy( getMouseOnCircle( _pointers[ 0 ].pageX, _pointers[ 0 ].pageY ) ); 652 | _movePrev.copy( _moveCurr ); 653 | break; 654 | 655 | default: // 2 or more 656 | _state = STATE.TOUCH_ZOOM_PAN; 657 | const dx = _pointers[ 0 ].pageX - _pointers[ 1 ].pageX; 658 | const dy = _pointers[ 0 ].pageY - _pointers[ 1 ].pageY; 659 | _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy ); 660 | 661 | const x = ( _pointers[ 0 ].pageX + _pointers[ 1 ].pageX ) / 2; 662 | const y = ( _pointers[ 0 ].pageY + _pointers[ 1 ].pageY ) / 2; 663 | _panStart.copy( getMouseOnScreen( x, y ) ); 664 | _panEnd.copy( _panStart ); 665 | break; 666 | 667 | } 668 | 669 | scope.dispatchEvent( _startEvent ); 670 | 671 | } 672 | 673 | function onTouchMove( event ) { 674 | 675 | trackPointer( event ); 676 | 677 | switch ( _pointers.length ) { 678 | 679 | case 1: 680 | _movePrev.copy( _moveCurr ); 681 | _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) ); 682 | break; 683 | 684 | default: // 2 or more 685 | 686 | const position = getSecondPointerPosition( event ); 687 | 688 | const dx = event.pageX - position.x; 689 | const dy = event.pageY - position.y; 690 | _touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy ); 691 | 692 | const x = ( event.pageX + position.x ) / 2; 693 | const y = ( event.pageY + position.y ) / 2; 694 | _panEnd.copy( getMouseOnScreen( x, y ) ); 695 | break; 696 | 697 | } 698 | 699 | } 700 | 701 | function onTouchEnd( event ) { 702 | 703 | switch ( _pointers.length ) { 704 | 705 | case 0: 706 | _state = STATE.NONE; 707 | break; 708 | 709 | case 1: 710 | _state = STATE.TOUCH_ROTATE; 711 | _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) ); 712 | _movePrev.copy( _moveCurr ); 713 | break; 714 | 715 | case 2: 716 | _state = STATE.TOUCH_ZOOM_PAN; 717 | 718 | for ( let i = 0; i < _pointers.length; i ++ ) { 719 | 720 | if ( _pointers[ i ].pointerId !== event.pointerId ) { 721 | 722 | const position = _pointerPositions[ _pointers[ i ].pointerId ]; 723 | _moveCurr.copy( getMouseOnCircle( position.x, position.y ) ); 724 | _movePrev.copy( _moveCurr ); 725 | break; 726 | 727 | } 728 | 729 | } 730 | 731 | break; 732 | 733 | } 734 | 735 | scope.dispatchEvent( _endEvent ); 736 | 737 | } 738 | 739 | function contextmenu( event ) { 740 | 741 | if ( scope.enabled === false ) return; 742 | 743 | event.preventDefault(); 744 | 745 | } 746 | 747 | function addPointer( event ) { 748 | 749 | _pointers.push( event ); 750 | 751 | } 752 | 753 | function removePointer( event ) { 754 | 755 | delete _pointerPositions[ event.pointerId ]; 756 | 757 | for ( let i = 0; i < _pointers.length; i ++ ) { 758 | 759 | if ( _pointers[ i ].pointerId == event.pointerId ) { 760 | 761 | _pointers.splice( i, 1 ); 762 | return; 763 | 764 | } 765 | 766 | } 767 | 768 | } 769 | 770 | function trackPointer( event ) { 771 | 772 | let position = _pointerPositions[ event.pointerId ]; 773 | 774 | if ( position === undefined ) { 775 | 776 | position = new Vector2(); 777 | _pointerPositions[ event.pointerId ] = position; 778 | 779 | } 780 | 781 | position.set( event.pageX, event.pageY ); 782 | 783 | } 784 | 785 | function getSecondPointerPosition( event ) { 786 | 787 | const pointer = ( event.pointerId === _pointers[ 0 ].pointerId ) ? _pointers[ 1 ] : _pointers[ 0 ]; 788 | 789 | return _pointerPositions[ pointer.pointerId ]; 790 | 791 | } 792 | 793 | this.dispose = function () { 794 | 795 | scope.domElement.removeEventListener( 'contextmenu', contextmenu ); 796 | 797 | scope.domElement.removeEventListener( 'pointerdown', onPointerDown ); 798 | scope.domElement.removeEventListener( 'pointercancel', onPointerCancel ); 799 | scope.domElement.removeEventListener( 'wheel', onMouseWheel ); 800 | 801 | scope.domElement.removeEventListener( 'pointermove', onPointerMove ); 802 | scope.domElement.removeEventListener( 'pointerup', onPointerUp ); 803 | 804 | window.removeEventListener( 'keydown', keydown ); 805 | window.removeEventListener( 'keyup', keyup ); 806 | 807 | }; 808 | 809 | this.domElement.addEventListener( 'contextmenu', contextmenu ); 810 | 811 | this.domElement.addEventListener( 'pointerdown', onPointerDown ); 812 | this.domElement.addEventListener( 'pointercancel', onPointerCancel ); 813 | this.domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } ); 814 | 815 | 816 | window.addEventListener( 'keydown', keydown ); 817 | window.addEventListener( 'keyup', keyup ); 818 | 819 | this.handleResize(); 820 | 821 | // force an update at start 822 | this.update(); 823 | 824 | } 825 | 826 | } 827 | 828 | export { TrackballControls }; 829 | -------------------------------------------------------------------------------- /node_modules/three/examples/jsm/controls/TransformControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | BoxGeometry, 3 | BufferGeometry, 4 | CylinderGeometry, 5 | DoubleSide, 6 | Euler, 7 | Float32BufferAttribute, 8 | Line, 9 | LineBasicMaterial, 10 | Matrix4, 11 | Mesh, 12 | MeshBasicMaterial, 13 | Object3D, 14 | OctahedronGeometry, 15 | PlaneGeometry, 16 | Quaternion, 17 | Raycaster, 18 | SphereGeometry, 19 | TorusGeometry, 20 | Vector3 21 | } from 'three'; 22 | 23 | const _raycaster = new Raycaster(); 24 | 25 | const _tempVector = new Vector3(); 26 | const _tempVector2 = new Vector3(); 27 | const _tempQuaternion = new Quaternion(); 28 | const _unit = { 29 | X: new Vector3( 1, 0, 0 ), 30 | Y: new Vector3( 0, 1, 0 ), 31 | Z: new Vector3( 0, 0, 1 ) 32 | }; 33 | 34 | const _changeEvent = { type: 'change' }; 35 | const _mouseDownEvent = { type: 'mouseDown' }; 36 | const _mouseUpEvent = { type: 'mouseUp', mode: null }; 37 | const _objectChangeEvent = { type: 'objectChange' }; 38 | 39 | class TransformControls extends Object3D { 40 | 41 | constructor( camera, domElement ) { 42 | 43 | super(); 44 | 45 | if ( domElement === undefined ) { 46 | 47 | console.warn( 'THREE.TransformControls: The second parameter "domElement" is now mandatory.' ); 48 | domElement = document; 49 | 50 | } 51 | 52 | this.isTransformControls = true; 53 | 54 | this.visible = false; 55 | this.domElement = domElement; 56 | this.domElement.style.touchAction = 'none'; // disable touch scroll 57 | 58 | const _gizmo = new TransformControlsGizmo(); 59 | this._gizmo = _gizmo; 60 | this.add( _gizmo ); 61 | 62 | const _plane = new TransformControlsPlane(); 63 | this._plane = _plane; 64 | this.add( _plane ); 65 | 66 | const scope = this; 67 | 68 | // Defined getter, setter and store for a property 69 | function defineProperty( propName, defaultValue ) { 70 | 71 | let propValue = defaultValue; 72 | 73 | Object.defineProperty( scope, propName, { 74 | 75 | get: function () { 76 | 77 | return propValue !== undefined ? propValue : defaultValue; 78 | 79 | }, 80 | 81 | set: function ( value ) { 82 | 83 | if ( propValue !== value ) { 84 | 85 | propValue = value; 86 | _plane[ propName ] = value; 87 | _gizmo[ propName ] = value; 88 | 89 | scope.dispatchEvent( { type: propName + '-changed', value: value } ); 90 | scope.dispatchEvent( _changeEvent ); 91 | 92 | } 93 | 94 | } 95 | 96 | } ); 97 | 98 | scope[ propName ] = defaultValue; 99 | _plane[ propName ] = defaultValue; 100 | _gizmo[ propName ] = defaultValue; 101 | 102 | } 103 | 104 | // Define properties with getters/setter 105 | // Setting the defined property will automatically trigger change event 106 | // Defined properties are passed down to gizmo and plane 107 | 108 | defineProperty( 'camera', camera ); 109 | defineProperty( 'object', undefined ); 110 | defineProperty( 'enabled', true ); 111 | defineProperty( 'axis', null ); 112 | defineProperty( 'mode', 'translate' ); 113 | defineProperty( 'translationSnap', null ); 114 | defineProperty( 'rotationSnap', null ); 115 | defineProperty( 'scaleSnap', null ); 116 | defineProperty( 'space', 'world' ); 117 | defineProperty( 'size', 1 ); 118 | defineProperty( 'dragging', false ); 119 | defineProperty( 'showX', true ); 120 | defineProperty( 'showY', true ); 121 | defineProperty( 'showZ', true ); 122 | 123 | // Reusable utility variables 124 | 125 | const worldPosition = new Vector3(); 126 | const worldPositionStart = new Vector3(); 127 | const worldQuaternion = new Quaternion(); 128 | const worldQuaternionStart = new Quaternion(); 129 | const cameraPosition = new Vector3(); 130 | const cameraQuaternion = new Quaternion(); 131 | const pointStart = new Vector3(); 132 | const pointEnd = new Vector3(); 133 | const rotationAxis = new Vector3(); 134 | const rotationAngle = 0; 135 | const eye = new Vector3(); 136 | 137 | // TODO: remove properties unused in plane and gizmo 138 | 139 | defineProperty( 'worldPosition', worldPosition ); 140 | defineProperty( 'worldPositionStart', worldPositionStart ); 141 | defineProperty( 'worldQuaternion', worldQuaternion ); 142 | defineProperty( 'worldQuaternionStart', worldQuaternionStart ); 143 | defineProperty( 'cameraPosition', cameraPosition ); 144 | defineProperty( 'cameraQuaternion', cameraQuaternion ); 145 | defineProperty( 'pointStart', pointStart ); 146 | defineProperty( 'pointEnd', pointEnd ); 147 | defineProperty( 'rotationAxis', rotationAxis ); 148 | defineProperty( 'rotationAngle', rotationAngle ); 149 | defineProperty( 'eye', eye ); 150 | 151 | this._offset = new Vector3(); 152 | this._startNorm = new Vector3(); 153 | this._endNorm = new Vector3(); 154 | this._cameraScale = new Vector3(); 155 | 156 | this._parentPosition = new Vector3(); 157 | this._parentQuaternion = new Quaternion(); 158 | this._parentQuaternionInv = new Quaternion(); 159 | this._parentScale = new Vector3(); 160 | 161 | this._worldScaleStart = new Vector3(); 162 | this._worldQuaternionInv = new Quaternion(); 163 | this._worldScale = new Vector3(); 164 | 165 | this._positionStart = new Vector3(); 166 | this._quaternionStart = new Quaternion(); 167 | this._scaleStart = new Vector3(); 168 | 169 | this._getPointer = getPointer.bind( this ); 170 | this._onPointerDown = onPointerDown.bind( this ); 171 | this._onPointerHover = onPointerHover.bind( this ); 172 | this._onPointerMove = onPointerMove.bind( this ); 173 | this._onPointerUp = onPointerUp.bind( this ); 174 | 175 | this.domElement.addEventListener( 'pointerdown', this._onPointerDown ); 176 | this.domElement.addEventListener( 'pointermove', this._onPointerHover ); 177 | this.domElement.addEventListener( 'pointerup', this._onPointerUp ); 178 | 179 | } 180 | 181 | // updateMatrixWorld updates key transformation variables 182 | updateMatrixWorld( force ) { 183 | 184 | if ( this.object !== undefined ) { 185 | 186 | this.object.updateMatrixWorld(); 187 | 188 | if ( this.object.parent === null ) { 189 | 190 | console.error( 'TransformControls: The attached 3D object must be a part of the scene graph.' ); 191 | 192 | } else { 193 | 194 | this.object.parent.matrixWorld.decompose( this._parentPosition, this._parentQuaternion, this._parentScale ); 195 | 196 | } 197 | 198 | this.object.matrixWorld.decompose( this.worldPosition, this.worldQuaternion, this._worldScale ); 199 | 200 | this._parentQuaternionInv.copy( this._parentQuaternion ).invert(); 201 | this._worldQuaternionInv.copy( this.worldQuaternion ).invert(); 202 | 203 | } 204 | 205 | this.camera.updateMatrixWorld(); 206 | this.camera.matrixWorld.decompose( this.cameraPosition, this.cameraQuaternion, this._cameraScale ); 207 | 208 | if ( this.camera.isOrthographicCamera ) { 209 | 210 | this.camera.getWorldDirection( this.eye ).negate(); 211 | 212 | } else { 213 | 214 | this.eye.copy( this.cameraPosition ).sub( this.worldPosition ).normalize(); 215 | 216 | } 217 | 218 | super.updateMatrixWorld( force ); 219 | 220 | } 221 | 222 | pointerHover( pointer ) { 223 | 224 | if ( this.object === undefined || this.dragging === true ) return; 225 | 226 | if ( pointer !== null ) _raycaster.setFromCamera( pointer, this.camera ); 227 | 228 | const intersect = intersectObjectWithRay( this._gizmo.picker[ this.mode ], _raycaster ); 229 | 230 | if ( intersect ) { 231 | 232 | this.axis = intersect.object.name; 233 | 234 | } else { 235 | 236 | this.axis = null; 237 | 238 | } 239 | 240 | } 241 | 242 | pointerDown( pointer ) { 243 | 244 | if ( this.object === undefined || this.dragging === true || ( pointer != null && pointer.button !== 0 ) ) return; 245 | 246 | if ( this.axis !== null ) { 247 | 248 | if ( pointer !== null ) _raycaster.setFromCamera( pointer, this.camera ); 249 | 250 | const planeIntersect = intersectObjectWithRay( this._plane, _raycaster, true ); 251 | 252 | if ( planeIntersect ) { 253 | 254 | this.object.updateMatrixWorld(); 255 | this.object.parent.updateMatrixWorld(); 256 | 257 | this._positionStart.copy( this.object.position ); 258 | this._quaternionStart.copy( this.object.quaternion ); 259 | this._scaleStart.copy( this.object.scale ); 260 | 261 | this.object.matrixWorld.decompose( this.worldPositionStart, this.worldQuaternionStart, this._worldScaleStart ); 262 | 263 | this.pointStart.copy( planeIntersect.point ).sub( this.worldPositionStart ); 264 | 265 | } 266 | 267 | this.dragging = true; 268 | _mouseDownEvent.mode = this.mode; 269 | this.dispatchEvent( _mouseDownEvent ); 270 | 271 | } 272 | 273 | } 274 | 275 | pointerMove( pointer ) { 276 | 277 | const axis = this.axis; 278 | const mode = this.mode; 279 | const object = this.object; 280 | let space = this.space; 281 | 282 | if ( mode === 'scale' ) { 283 | 284 | space = 'local'; 285 | 286 | } else if ( axis === 'E' || axis === 'XYZE' || axis === 'XYZ' ) { 287 | 288 | space = 'world'; 289 | 290 | } 291 | 292 | if ( object === undefined || axis === null || this.dragging === false || ( pointer !== null && pointer.button !== - 1 ) ) return; 293 | 294 | if ( pointer !== null ) _raycaster.setFromCamera( pointer, this.camera ); 295 | 296 | const planeIntersect = intersectObjectWithRay( this._plane, _raycaster, true ); 297 | 298 | if ( ! planeIntersect ) return; 299 | 300 | this.pointEnd.copy( planeIntersect.point ).sub( this.worldPositionStart ); 301 | 302 | if ( mode === 'translate' ) { 303 | 304 | // Apply translate 305 | 306 | this._offset.copy( this.pointEnd ).sub( this.pointStart ); 307 | 308 | if ( space === 'local' && axis !== 'XYZ' ) { 309 | 310 | this._offset.applyQuaternion( this._worldQuaternionInv ); 311 | 312 | } 313 | 314 | if ( axis.indexOf( 'X' ) === - 1 ) this._offset.x = 0; 315 | if ( axis.indexOf( 'Y' ) === - 1 ) this._offset.y = 0; 316 | if ( axis.indexOf( 'Z' ) === - 1 ) this._offset.z = 0; 317 | 318 | if ( space === 'local' && axis !== 'XYZ' ) { 319 | 320 | this._offset.applyQuaternion( this._quaternionStart ).divide( this._parentScale ); 321 | 322 | } else { 323 | 324 | this._offset.applyQuaternion( this._parentQuaternionInv ).divide( this._parentScale ); 325 | 326 | } 327 | 328 | object.position.copy( this._offset ).add( this._positionStart ); 329 | 330 | // Apply translation snap 331 | 332 | if ( this.translationSnap ) { 333 | 334 | if ( space === 'local' ) { 335 | 336 | object.position.applyQuaternion( _tempQuaternion.copy( this._quaternionStart ).invert() ); 337 | 338 | if ( axis.search( 'X' ) !== - 1 ) { 339 | 340 | object.position.x = Math.round( object.position.x / this.translationSnap ) * this.translationSnap; 341 | 342 | } 343 | 344 | if ( axis.search( 'Y' ) !== - 1 ) { 345 | 346 | object.position.y = Math.round( object.position.y / this.translationSnap ) * this.translationSnap; 347 | 348 | } 349 | 350 | if ( axis.search( 'Z' ) !== - 1 ) { 351 | 352 | object.position.z = Math.round( object.position.z / this.translationSnap ) * this.translationSnap; 353 | 354 | } 355 | 356 | object.position.applyQuaternion( this._quaternionStart ); 357 | 358 | } 359 | 360 | if ( space === 'world' ) { 361 | 362 | if ( object.parent ) { 363 | 364 | object.position.add( _tempVector.setFromMatrixPosition( object.parent.matrixWorld ) ); 365 | 366 | } 367 | 368 | if ( axis.search( 'X' ) !== - 1 ) { 369 | 370 | object.position.x = Math.round( object.position.x / this.translationSnap ) * this.translationSnap; 371 | 372 | } 373 | 374 | if ( axis.search( 'Y' ) !== - 1 ) { 375 | 376 | object.position.y = Math.round( object.position.y / this.translationSnap ) * this.translationSnap; 377 | 378 | } 379 | 380 | if ( axis.search( 'Z' ) !== - 1 ) { 381 | 382 | object.position.z = Math.round( object.position.z / this.translationSnap ) * this.translationSnap; 383 | 384 | } 385 | 386 | if ( object.parent ) { 387 | 388 | object.position.sub( _tempVector.setFromMatrixPosition( object.parent.matrixWorld ) ); 389 | 390 | } 391 | 392 | } 393 | 394 | } 395 | 396 | } else if ( mode === 'scale' ) { 397 | 398 | if ( axis.search( 'XYZ' ) !== - 1 ) { 399 | 400 | let d = this.pointEnd.length() / this.pointStart.length(); 401 | 402 | if ( this.pointEnd.dot( this.pointStart ) < 0 ) d *= - 1; 403 | 404 | _tempVector2.set( d, d, d ); 405 | 406 | } else { 407 | 408 | _tempVector.copy( this.pointStart ); 409 | _tempVector2.copy( this.pointEnd ); 410 | 411 | _tempVector.applyQuaternion( this._worldQuaternionInv ); 412 | _tempVector2.applyQuaternion( this._worldQuaternionInv ); 413 | 414 | _tempVector2.divide( _tempVector ); 415 | 416 | if ( axis.search( 'X' ) === - 1 ) { 417 | 418 | _tempVector2.x = 1; 419 | 420 | } 421 | 422 | if ( axis.search( 'Y' ) === - 1 ) { 423 | 424 | _tempVector2.y = 1; 425 | 426 | } 427 | 428 | if ( axis.search( 'Z' ) === - 1 ) { 429 | 430 | _tempVector2.z = 1; 431 | 432 | } 433 | 434 | } 435 | 436 | // Apply scale 437 | 438 | object.scale.copy( this._scaleStart ).multiply( _tempVector2 ); 439 | 440 | if ( this.scaleSnap ) { 441 | 442 | if ( axis.search( 'X' ) !== - 1 ) { 443 | 444 | object.scale.x = Math.round( object.scale.x / this.scaleSnap ) * this.scaleSnap || this.scaleSnap; 445 | 446 | } 447 | 448 | if ( axis.search( 'Y' ) !== - 1 ) { 449 | 450 | object.scale.y = Math.round( object.scale.y / this.scaleSnap ) * this.scaleSnap || this.scaleSnap; 451 | 452 | } 453 | 454 | if ( axis.search( 'Z' ) !== - 1 ) { 455 | 456 | object.scale.z = Math.round( object.scale.z / this.scaleSnap ) * this.scaleSnap || this.scaleSnap; 457 | 458 | } 459 | 460 | } 461 | 462 | } else if ( mode === 'rotate' ) { 463 | 464 | this._offset.copy( this.pointEnd ).sub( this.pointStart ); 465 | 466 | const ROTATION_SPEED = 20 / this.worldPosition.distanceTo( _tempVector.setFromMatrixPosition( this.camera.matrixWorld ) ); 467 | 468 | let _inPlaneRotation = false; 469 | 470 | if ( axis === 'XYZE' ) { 471 | 472 | this.rotationAxis.copy( this._offset ).cross( this.eye ).normalize(); 473 | this.rotationAngle = this._offset.dot( _tempVector.copy( this.rotationAxis ).cross( this.eye ) ) * ROTATION_SPEED; 474 | 475 | } else if ( axis === 'X' || axis === 'Y' || axis === 'Z' ) { 476 | 477 | this.rotationAxis.copy( _unit[ axis ] ); 478 | 479 | _tempVector.copy( _unit[ axis ] ); 480 | 481 | if ( space === 'local' ) { 482 | 483 | _tempVector.applyQuaternion( this.worldQuaternion ); 484 | 485 | } 486 | 487 | _tempVector.cross( this.eye ); 488 | 489 | // When _tempVector is 0 after cross with this.eye the vectors are parallel and should use in-plane rotation logic. 490 | if ( _tempVector.length() === 0 ) { 491 | 492 | _inPlaneRotation = true; 493 | 494 | } else { 495 | 496 | this.rotationAngle = this._offset.dot( _tempVector.normalize() ) * ROTATION_SPEED; 497 | 498 | } 499 | 500 | 501 | } 502 | 503 | if ( axis === 'E' || _inPlaneRotation ) { 504 | 505 | this.rotationAxis.copy( this.eye ); 506 | this.rotationAngle = this.pointEnd.angleTo( this.pointStart ); 507 | 508 | this._startNorm.copy( this.pointStart ).normalize(); 509 | this._endNorm.copy( this.pointEnd ).normalize(); 510 | 511 | this.rotationAngle *= ( this._endNorm.cross( this._startNorm ).dot( this.eye ) < 0 ? 1 : - 1 ); 512 | 513 | } 514 | 515 | // Apply rotation snap 516 | 517 | if ( this.rotationSnap ) this.rotationAngle = Math.round( this.rotationAngle / this.rotationSnap ) * this.rotationSnap; 518 | 519 | // Apply rotate 520 | if ( space === 'local' && axis !== 'E' && axis !== 'XYZE' ) { 521 | 522 | object.quaternion.copy( this._quaternionStart ); 523 | object.quaternion.multiply( _tempQuaternion.setFromAxisAngle( this.rotationAxis, this.rotationAngle ) ).normalize(); 524 | 525 | } else { 526 | 527 | this.rotationAxis.applyQuaternion( this._parentQuaternionInv ); 528 | object.quaternion.copy( _tempQuaternion.setFromAxisAngle( this.rotationAxis, this.rotationAngle ) ); 529 | object.quaternion.multiply( this._quaternionStart ).normalize(); 530 | 531 | } 532 | 533 | } 534 | 535 | this.dispatchEvent( _changeEvent ); 536 | this.dispatchEvent( _objectChangeEvent ); 537 | 538 | } 539 | 540 | pointerUp( pointer ) { 541 | 542 | if ( pointer !== null && pointer.button !== 0 ) return; 543 | 544 | if ( this.dragging && ( this.axis !== null ) ) { 545 | 546 | _mouseUpEvent.mode = this.mode; 547 | this.dispatchEvent( _mouseUpEvent ); 548 | 549 | } 550 | 551 | this.dragging = false; 552 | this.axis = null; 553 | 554 | } 555 | 556 | dispose() { 557 | 558 | this.domElement.removeEventListener( 'pointerdown', this._onPointerDown ); 559 | this.domElement.removeEventListener( 'pointermove', this._onPointerHover ); 560 | this.domElement.removeEventListener( 'pointermove', this._onPointerMove ); 561 | this.domElement.removeEventListener( 'pointerup', this._onPointerUp ); 562 | 563 | this.traverse( function ( child ) { 564 | 565 | if ( child.geometry ) child.geometry.dispose(); 566 | if ( child.material ) child.material.dispose(); 567 | 568 | } ); 569 | 570 | } 571 | 572 | // Set current object 573 | attach( object ) { 574 | 575 | this.object = object; 576 | this.visible = true; 577 | 578 | return this; 579 | 580 | } 581 | 582 | // Detach from object 583 | detach() { 584 | 585 | this.object = undefined; 586 | this.visible = false; 587 | this.axis = null; 588 | 589 | return this; 590 | 591 | } 592 | 593 | reset() { 594 | 595 | if ( ! this.enabled ) return; 596 | 597 | if ( this.dragging ) { 598 | 599 | this.object.position.copy( this._positionStart ); 600 | this.object.quaternion.copy( this._quaternionStart ); 601 | this.object.scale.copy( this._scaleStart ); 602 | 603 | this.dispatchEvent( _changeEvent ); 604 | this.dispatchEvent( _objectChangeEvent ); 605 | 606 | this.pointStart.copy( this.pointEnd ); 607 | 608 | } 609 | 610 | } 611 | 612 | getRaycaster() { 613 | 614 | return _raycaster; 615 | 616 | } 617 | 618 | // TODO: deprecate 619 | 620 | getMode() { 621 | 622 | return this.mode; 623 | 624 | } 625 | 626 | setMode( mode ) { 627 | 628 | this.mode = mode; 629 | 630 | } 631 | 632 | setTranslationSnap( translationSnap ) { 633 | 634 | this.translationSnap = translationSnap; 635 | 636 | } 637 | 638 | setRotationSnap( rotationSnap ) { 639 | 640 | this.rotationSnap = rotationSnap; 641 | 642 | } 643 | 644 | setScaleSnap( scaleSnap ) { 645 | 646 | this.scaleSnap = scaleSnap; 647 | 648 | } 649 | 650 | setSize( size ) { 651 | 652 | this.size = size; 653 | 654 | } 655 | 656 | setSpace( space ) { 657 | 658 | this.space = space; 659 | 660 | } 661 | 662 | } 663 | 664 | // mouse / touch event handlers 665 | 666 | function getPointer( event ) { 667 | 668 | if ( this.domElement.ownerDocument.pointerLockElement ) { 669 | 670 | return { 671 | x: 0, 672 | y: 0, 673 | button: event.button 674 | }; 675 | 676 | } else { 677 | 678 | const rect = this.domElement.getBoundingClientRect(); 679 | 680 | return { 681 | x: ( event.clientX - rect.left ) / rect.width * 2 - 1, 682 | y: - ( event.clientY - rect.top ) / rect.height * 2 + 1, 683 | button: event.button 684 | }; 685 | 686 | } 687 | 688 | } 689 | 690 | function onPointerHover( event ) { 691 | 692 | if ( ! this.enabled ) return; 693 | 694 | switch ( event.pointerType ) { 695 | 696 | case 'mouse': 697 | case 'pen': 698 | this.pointerHover( this._getPointer( event ) ); 699 | break; 700 | 701 | } 702 | 703 | } 704 | 705 | function onPointerDown( event ) { 706 | 707 | if ( ! this.enabled ) return; 708 | 709 | if ( ! document.pointerLockElement ) { 710 | 711 | this.domElement.setPointerCapture( event.pointerId ); 712 | 713 | } 714 | 715 | this.domElement.addEventListener( 'pointermove', this._onPointerMove ); 716 | 717 | this.pointerHover( this._getPointer( event ) ); 718 | this.pointerDown( this._getPointer( event ) ); 719 | 720 | } 721 | 722 | function onPointerMove( event ) { 723 | 724 | if ( ! this.enabled ) return; 725 | 726 | this.pointerMove( this._getPointer( event ) ); 727 | 728 | } 729 | 730 | function onPointerUp( event ) { 731 | 732 | if ( ! this.enabled ) return; 733 | 734 | this.domElement.releasePointerCapture( event.pointerId ); 735 | 736 | this.domElement.removeEventListener( 'pointermove', this._onPointerMove ); 737 | 738 | this.pointerUp( this._getPointer( event ) ); 739 | 740 | } 741 | 742 | function intersectObjectWithRay( object, raycaster, includeInvisible ) { 743 | 744 | const allIntersections = raycaster.intersectObject( object, true ); 745 | 746 | for ( let i = 0; i < allIntersections.length; i ++ ) { 747 | 748 | if ( allIntersections[ i ].object.visible || includeInvisible ) { 749 | 750 | return allIntersections[ i ]; 751 | 752 | } 753 | 754 | } 755 | 756 | return false; 757 | 758 | } 759 | 760 | // 761 | 762 | // Reusable utility variables 763 | 764 | const _tempEuler = new Euler(); 765 | const _alignVector = new Vector3( 0, 1, 0 ); 766 | const _zeroVector = new Vector3( 0, 0, 0 ); 767 | const _lookAtMatrix = new Matrix4(); 768 | const _tempQuaternion2 = new Quaternion(); 769 | const _identityQuaternion = new Quaternion(); 770 | const _dirVector = new Vector3(); 771 | const _tempMatrix = new Matrix4(); 772 | 773 | const _unitX = new Vector3( 1, 0, 0 ); 774 | const _unitY = new Vector3( 0, 1, 0 ); 775 | const _unitZ = new Vector3( 0, 0, 1 ); 776 | 777 | const _v1 = new Vector3(); 778 | const _v2 = new Vector3(); 779 | const _v3 = new Vector3(); 780 | 781 | class TransformControlsGizmo extends Object3D { 782 | 783 | constructor() { 784 | 785 | super(); 786 | 787 | this.isTransformControlsGizmo = true; 788 | 789 | this.type = 'TransformControlsGizmo'; 790 | 791 | // shared materials 792 | 793 | const gizmoMaterial = new MeshBasicMaterial( { 794 | depthTest: false, 795 | depthWrite: false, 796 | fog: false, 797 | toneMapped: false, 798 | transparent: true 799 | } ); 800 | 801 | const gizmoLineMaterial = new LineBasicMaterial( { 802 | depthTest: false, 803 | depthWrite: false, 804 | fog: false, 805 | toneMapped: false, 806 | transparent: true 807 | } ); 808 | 809 | // Make unique material for each axis/color 810 | 811 | const matInvisible = gizmoMaterial.clone(); 812 | matInvisible.opacity = 0.15; 813 | 814 | const matHelper = gizmoLineMaterial.clone(); 815 | matHelper.opacity = 0.5; 816 | 817 | const matRed = gizmoMaterial.clone(); 818 | matRed.color.setHex( 0xff0000 ); 819 | 820 | const matGreen = gizmoMaterial.clone(); 821 | matGreen.color.setHex( 0x00ff00 ); 822 | 823 | const matBlue = gizmoMaterial.clone(); 824 | matBlue.color.setHex( 0x0000ff ); 825 | 826 | const matRedTransparent = gizmoMaterial.clone(); 827 | matRedTransparent.color.setHex( 0xff0000 ); 828 | matRedTransparent.opacity = 0.5; 829 | 830 | const matGreenTransparent = gizmoMaterial.clone(); 831 | matGreenTransparent.color.setHex( 0x00ff00 ); 832 | matGreenTransparent.opacity = 0.5; 833 | 834 | const matBlueTransparent = gizmoMaterial.clone(); 835 | matBlueTransparent.color.setHex( 0x0000ff ); 836 | matBlueTransparent.opacity = 0.5; 837 | 838 | const matWhiteTransparent = gizmoMaterial.clone(); 839 | matWhiteTransparent.opacity = 0.25; 840 | 841 | const matYellowTransparent = gizmoMaterial.clone(); 842 | matYellowTransparent.color.setHex( 0xffff00 ); 843 | matYellowTransparent.opacity = 0.25; 844 | 845 | const matYellow = gizmoMaterial.clone(); 846 | matYellow.color.setHex( 0xffff00 ); 847 | 848 | const matGray = gizmoMaterial.clone(); 849 | matGray.color.setHex( 0x787878 ); 850 | 851 | // reusable geometry 852 | 853 | const arrowGeometry = new CylinderGeometry( 0, 0.04, 0.1, 12 ); 854 | arrowGeometry.translate( 0, 0.05, 0 ); 855 | 856 | const scaleHandleGeometry = new BoxGeometry( 0.08, 0.08, 0.08 ); 857 | scaleHandleGeometry.translate( 0, 0.04, 0 ); 858 | 859 | const lineGeometry = new BufferGeometry(); 860 | lineGeometry.setAttribute( 'position', new Float32BufferAttribute( [ 0, 0, 0, 1, 0, 0 ], 3 ) ); 861 | 862 | const lineGeometry2 = new CylinderGeometry( 0.0075, 0.0075, 0.5, 3 ); 863 | lineGeometry2.translate( 0, 0.25, 0 ); 864 | 865 | function CircleGeometry( radius, arc ) { 866 | 867 | const geometry = new TorusGeometry( radius, 0.0075, 3, 64, arc * Math.PI * 2 ); 868 | geometry.rotateY( Math.PI / 2 ); 869 | geometry.rotateX( Math.PI / 2 ); 870 | return geometry; 871 | 872 | } 873 | 874 | // Special geometry for transform helper. If scaled with position vector it spans from [0,0,0] to position 875 | 876 | function TranslateHelperGeometry() { 877 | 878 | const geometry = new BufferGeometry(); 879 | 880 | geometry.setAttribute( 'position', new Float32BufferAttribute( [ 0, 0, 0, 1, 1, 1 ], 3 ) ); 881 | 882 | return geometry; 883 | 884 | } 885 | 886 | // Gizmo definitions - custom hierarchy definitions for setupGizmo() function 887 | 888 | const gizmoTranslate = { 889 | X: [ 890 | [ new Mesh( arrowGeometry, matRed ), [ 0.5, 0, 0 ], [ 0, 0, - Math.PI / 2 ]], 891 | [ new Mesh( arrowGeometry, matRed ), [ - 0.5, 0, 0 ], [ 0, 0, Math.PI / 2 ]], 892 | [ new Mesh( lineGeometry2, matRed ), [ 0, 0, 0 ], [ 0, 0, - Math.PI / 2 ]] 893 | ], 894 | Y: [ 895 | [ new Mesh( arrowGeometry, matGreen ), [ 0, 0.5, 0 ]], 896 | [ new Mesh( arrowGeometry, matGreen ), [ 0, - 0.5, 0 ], [ Math.PI, 0, 0 ]], 897 | [ new Mesh( lineGeometry2, matGreen ) ] 898 | ], 899 | Z: [ 900 | [ new Mesh( arrowGeometry, matBlue ), [ 0, 0, 0.5 ], [ Math.PI / 2, 0, 0 ]], 901 | [ new Mesh( arrowGeometry, matBlue ), [ 0, 0, - 0.5 ], [ - Math.PI / 2, 0, 0 ]], 902 | [ new Mesh( lineGeometry2, matBlue ), null, [ Math.PI / 2, 0, 0 ]] 903 | ], 904 | XYZ: [ 905 | [ new Mesh( new OctahedronGeometry( 0.1, 0 ), matWhiteTransparent.clone() ), [ 0, 0, 0 ]] 906 | ], 907 | XY: [ 908 | [ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matBlueTransparent.clone() ), [ 0.15, 0.15, 0 ]] 909 | ], 910 | YZ: [ 911 | [ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matRedTransparent.clone() ), [ 0, 0.15, 0.15 ], [ 0, Math.PI / 2, 0 ]] 912 | ], 913 | XZ: [ 914 | [ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matGreenTransparent.clone() ), [ 0.15, 0, 0.15 ], [ - Math.PI / 2, 0, 0 ]] 915 | ] 916 | }; 917 | 918 | const pickerTranslate = { 919 | X: [ 920 | [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0.3, 0, 0 ], [ 0, 0, - Math.PI / 2 ]], 921 | [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ - 0.3, 0, 0 ], [ 0, 0, Math.PI / 2 ]] 922 | ], 923 | Y: [ 924 | [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, 0.3, 0 ]], 925 | [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, - 0.3, 0 ], [ 0, 0, Math.PI ]] 926 | ], 927 | Z: [ 928 | [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, 0, 0.3 ], [ Math.PI / 2, 0, 0 ]], 929 | [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, 0, - 0.3 ], [ - Math.PI / 2, 0, 0 ]] 930 | ], 931 | XYZ: [ 932 | [ new Mesh( new OctahedronGeometry( 0.2, 0 ), matInvisible ) ] 933 | ], 934 | XY: [ 935 | [ new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), [ 0.15, 0.15, 0 ]] 936 | ], 937 | YZ: [ 938 | [ new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), [ 0, 0.15, 0.15 ], [ 0, Math.PI / 2, 0 ]] 939 | ], 940 | XZ: [ 941 | [ new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), [ 0.15, 0, 0.15 ], [ - Math.PI / 2, 0, 0 ]] 942 | ] 943 | }; 944 | 945 | const helperTranslate = { 946 | START: [ 947 | [ new Mesh( new OctahedronGeometry( 0.01, 2 ), matHelper ), null, null, null, 'helper' ] 948 | ], 949 | END: [ 950 | [ new Mesh( new OctahedronGeometry( 0.01, 2 ), matHelper ), null, null, null, 'helper' ] 951 | ], 952 | DELTA: [ 953 | [ new Line( TranslateHelperGeometry(), matHelper ), null, null, null, 'helper' ] 954 | ], 955 | X: [ 956 | [ new Line( lineGeometry, matHelper.clone() ), [ - 1e3, 0, 0 ], null, [ 1e6, 1, 1 ], 'helper' ] 957 | ], 958 | Y: [ 959 | [ new Line( lineGeometry, matHelper.clone() ), [ 0, - 1e3, 0 ], [ 0, 0, Math.PI / 2 ], [ 1e6, 1, 1 ], 'helper' ] 960 | ], 961 | Z: [ 962 | [ new Line( lineGeometry, matHelper.clone() ), [ 0, 0, - 1e3 ], [ 0, - Math.PI / 2, 0 ], [ 1e6, 1, 1 ], 'helper' ] 963 | ] 964 | }; 965 | 966 | const gizmoRotate = { 967 | XYZE: [ 968 | [ new Mesh( CircleGeometry( 0.5, 1 ), matGray ), null, [ 0, Math.PI / 2, 0 ]] 969 | ], 970 | X: [ 971 | [ new Mesh( CircleGeometry( 0.5, 0.5 ), matRed ) ] 972 | ], 973 | Y: [ 974 | [ new Mesh( CircleGeometry( 0.5, 0.5 ), matGreen ), null, [ 0, 0, - Math.PI / 2 ]] 975 | ], 976 | Z: [ 977 | [ new Mesh( CircleGeometry( 0.5, 0.5 ), matBlue ), null, [ 0, Math.PI / 2, 0 ]] 978 | ], 979 | E: [ 980 | [ new Mesh( CircleGeometry( 0.75, 1 ), matYellowTransparent ), null, [ 0, Math.PI / 2, 0 ]] 981 | ] 982 | }; 983 | 984 | const helperRotate = { 985 | AXIS: [ 986 | [ new Line( lineGeometry, matHelper.clone() ), [ - 1e3, 0, 0 ], null, [ 1e6, 1, 1 ], 'helper' ] 987 | ] 988 | }; 989 | 990 | const pickerRotate = { 991 | XYZE: [ 992 | [ new Mesh( new SphereGeometry( 0.25, 10, 8 ), matInvisible ) ] 993 | ], 994 | X: [ 995 | [ new Mesh( new TorusGeometry( 0.5, 0.1, 4, 24 ), matInvisible ), [ 0, 0, 0 ], [ 0, - Math.PI / 2, - Math.PI / 2 ]], 996 | ], 997 | Y: [ 998 | [ new Mesh( new TorusGeometry( 0.5, 0.1, 4, 24 ), matInvisible ), [ 0, 0, 0 ], [ Math.PI / 2, 0, 0 ]], 999 | ], 1000 | Z: [ 1001 | [ new Mesh( new TorusGeometry( 0.5, 0.1, 4, 24 ), matInvisible ), [ 0, 0, 0 ], [ 0, 0, - Math.PI / 2 ]], 1002 | ], 1003 | E: [ 1004 | [ new Mesh( new TorusGeometry( 0.75, 0.1, 2, 24 ), matInvisible ) ] 1005 | ] 1006 | }; 1007 | 1008 | const gizmoScale = { 1009 | X: [ 1010 | [ new Mesh( scaleHandleGeometry, matRed ), [ 0.5, 0, 0 ], [ 0, 0, - Math.PI / 2 ]], 1011 | [ new Mesh( lineGeometry2, matRed ), [ 0, 0, 0 ], [ 0, 0, - Math.PI / 2 ]], 1012 | [ new Mesh( scaleHandleGeometry, matRed ), [ - 0.5, 0, 0 ], [ 0, 0, Math.PI / 2 ]], 1013 | ], 1014 | Y: [ 1015 | [ new Mesh( scaleHandleGeometry, matGreen ), [ 0, 0.5, 0 ]], 1016 | [ new Mesh( lineGeometry2, matGreen ) ], 1017 | [ new Mesh( scaleHandleGeometry, matGreen ), [ 0, - 0.5, 0 ], [ 0, 0, Math.PI ]], 1018 | ], 1019 | Z: [ 1020 | [ new Mesh( scaleHandleGeometry, matBlue ), [ 0, 0, 0.5 ], [ Math.PI / 2, 0, 0 ]], 1021 | [ new Mesh( lineGeometry2, matBlue ), [ 0, 0, 0 ], [ Math.PI / 2, 0, 0 ]], 1022 | [ new Mesh( scaleHandleGeometry, matBlue ), [ 0, 0, - 0.5 ], [ - Math.PI / 2, 0, 0 ]] 1023 | ], 1024 | XY: [ 1025 | [ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matBlueTransparent ), [ 0.15, 0.15, 0 ]] 1026 | ], 1027 | YZ: [ 1028 | [ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matRedTransparent ), [ 0, 0.15, 0.15 ], [ 0, Math.PI / 2, 0 ]] 1029 | ], 1030 | XZ: [ 1031 | [ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matGreenTransparent ), [ 0.15, 0, 0.15 ], [ - Math.PI / 2, 0, 0 ]] 1032 | ], 1033 | XYZ: [ 1034 | [ new Mesh( new BoxGeometry( 0.1, 0.1, 0.1 ), matWhiteTransparent.clone() ) ], 1035 | ] 1036 | }; 1037 | 1038 | const pickerScale = { 1039 | X: [ 1040 | [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0.3, 0, 0 ], [ 0, 0, - Math.PI / 2 ]], 1041 | [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ - 0.3, 0, 0 ], [ 0, 0, Math.PI / 2 ]] 1042 | ], 1043 | Y: [ 1044 | [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, 0.3, 0 ]], 1045 | [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, - 0.3, 0 ], [ 0, 0, Math.PI ]] 1046 | ], 1047 | Z: [ 1048 | [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, 0, 0.3 ], [ Math.PI / 2, 0, 0 ]], 1049 | [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, 0, - 0.3 ], [ - Math.PI / 2, 0, 0 ]] 1050 | ], 1051 | XY: [ 1052 | [ new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), [ 0.15, 0.15, 0 ]], 1053 | ], 1054 | YZ: [ 1055 | [ new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), [ 0, 0.15, 0.15 ], [ 0, Math.PI / 2, 0 ]], 1056 | ], 1057 | XZ: [ 1058 | [ new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), [ 0.15, 0, 0.15 ], [ - Math.PI / 2, 0, 0 ]], 1059 | ], 1060 | XYZ: [ 1061 | [ new Mesh( new BoxGeometry( 0.2, 0.2, 0.2 ), matInvisible ), [ 0, 0, 0 ]], 1062 | ] 1063 | }; 1064 | 1065 | const helperScale = { 1066 | X: [ 1067 | [ new Line( lineGeometry, matHelper.clone() ), [ - 1e3, 0, 0 ], null, [ 1e6, 1, 1 ], 'helper' ] 1068 | ], 1069 | Y: [ 1070 | [ new Line( lineGeometry, matHelper.clone() ), [ 0, - 1e3, 0 ], [ 0, 0, Math.PI / 2 ], [ 1e6, 1, 1 ], 'helper' ] 1071 | ], 1072 | Z: [ 1073 | [ new Line( lineGeometry, matHelper.clone() ), [ 0, 0, - 1e3 ], [ 0, - Math.PI / 2, 0 ], [ 1e6, 1, 1 ], 'helper' ] 1074 | ] 1075 | }; 1076 | 1077 | // Creates an Object3D with gizmos described in custom hierarchy definition. 1078 | 1079 | function setupGizmo( gizmoMap ) { 1080 | 1081 | const gizmo = new Object3D(); 1082 | 1083 | for ( const name in gizmoMap ) { 1084 | 1085 | for ( let i = gizmoMap[ name ].length; i --; ) { 1086 | 1087 | const object = gizmoMap[ name ][ i ][ 0 ].clone(); 1088 | const position = gizmoMap[ name ][ i ][ 1 ]; 1089 | const rotation = gizmoMap[ name ][ i ][ 2 ]; 1090 | const scale = gizmoMap[ name ][ i ][ 3 ]; 1091 | const tag = gizmoMap[ name ][ i ][ 4 ]; 1092 | 1093 | // name and tag properties are essential for picking and updating logic. 1094 | object.name = name; 1095 | object.tag = tag; 1096 | 1097 | if ( position ) { 1098 | 1099 | object.position.set( position[ 0 ], position[ 1 ], position[ 2 ] ); 1100 | 1101 | } 1102 | 1103 | if ( rotation ) { 1104 | 1105 | object.rotation.set( rotation[ 0 ], rotation[ 1 ], rotation[ 2 ] ); 1106 | 1107 | } 1108 | 1109 | if ( scale ) { 1110 | 1111 | object.scale.set( scale[ 0 ], scale[ 1 ], scale[ 2 ] ); 1112 | 1113 | } 1114 | 1115 | object.updateMatrix(); 1116 | 1117 | const tempGeometry = object.geometry.clone(); 1118 | tempGeometry.applyMatrix4( object.matrix ); 1119 | object.geometry = tempGeometry; 1120 | object.renderOrder = Infinity; 1121 | 1122 | object.position.set( 0, 0, 0 ); 1123 | object.rotation.set( 0, 0, 0 ); 1124 | object.scale.set( 1, 1, 1 ); 1125 | 1126 | gizmo.add( object ); 1127 | 1128 | } 1129 | 1130 | } 1131 | 1132 | return gizmo; 1133 | 1134 | } 1135 | 1136 | // Gizmo creation 1137 | 1138 | this.gizmo = {}; 1139 | this.picker = {}; 1140 | this.helper = {}; 1141 | 1142 | this.add( this.gizmo[ 'translate' ] = setupGizmo( gizmoTranslate ) ); 1143 | this.add( this.gizmo[ 'rotate' ] = setupGizmo( gizmoRotate ) ); 1144 | this.add( this.gizmo[ 'scale' ] = setupGizmo( gizmoScale ) ); 1145 | this.add( this.picker[ 'translate' ] = setupGizmo( pickerTranslate ) ); 1146 | this.add( this.picker[ 'rotate' ] = setupGizmo( pickerRotate ) ); 1147 | this.add( this.picker[ 'scale' ] = setupGizmo( pickerScale ) ); 1148 | this.add( this.helper[ 'translate' ] = setupGizmo( helperTranslate ) ); 1149 | this.add( this.helper[ 'rotate' ] = setupGizmo( helperRotate ) ); 1150 | this.add( this.helper[ 'scale' ] = setupGizmo( helperScale ) ); 1151 | 1152 | // Pickers should be hidden always 1153 | 1154 | this.picker[ 'translate' ].visible = false; 1155 | this.picker[ 'rotate' ].visible = false; 1156 | this.picker[ 'scale' ].visible = false; 1157 | 1158 | } 1159 | 1160 | // updateMatrixWorld will update transformations and appearance of individual handles 1161 | 1162 | updateMatrixWorld( force ) { 1163 | 1164 | const space = ( this.mode === 'scale' ) ? 'local' : this.space; // scale always oriented to local rotation 1165 | 1166 | const quaternion = ( space === 'local' ) ? this.worldQuaternion : _identityQuaternion; 1167 | 1168 | // Show only gizmos for current transform mode 1169 | 1170 | this.gizmo[ 'translate' ].visible = this.mode === 'translate'; 1171 | this.gizmo[ 'rotate' ].visible = this.mode === 'rotate'; 1172 | this.gizmo[ 'scale' ].visible = this.mode === 'scale'; 1173 | 1174 | this.helper[ 'translate' ].visible = this.mode === 'translate'; 1175 | this.helper[ 'rotate' ].visible = this.mode === 'rotate'; 1176 | this.helper[ 'scale' ].visible = this.mode === 'scale'; 1177 | 1178 | 1179 | let handles = []; 1180 | handles = handles.concat( this.picker[ this.mode ].children ); 1181 | handles = handles.concat( this.gizmo[ this.mode ].children ); 1182 | handles = handles.concat( this.helper[ this.mode ].children ); 1183 | 1184 | for ( let i = 0; i < handles.length; i ++ ) { 1185 | 1186 | const handle = handles[ i ]; 1187 | 1188 | // hide aligned to camera 1189 | 1190 | handle.visible = true; 1191 | handle.rotation.set( 0, 0, 0 ); 1192 | handle.position.copy( this.worldPosition ); 1193 | 1194 | let factor; 1195 | 1196 | if ( this.camera.isOrthographicCamera ) { 1197 | 1198 | factor = ( this.camera.top - this.camera.bottom ) / this.camera.zoom; 1199 | 1200 | } else { 1201 | 1202 | factor = this.worldPosition.distanceTo( this.cameraPosition ) * Math.min( 1.9 * Math.tan( Math.PI * this.camera.fov / 360 ) / this.camera.zoom, 7 ); 1203 | 1204 | } 1205 | 1206 | handle.scale.set( 1, 1, 1 ).multiplyScalar( factor * this.size / 4 ); 1207 | 1208 | // TODO: simplify helpers and consider decoupling from gizmo 1209 | 1210 | if ( handle.tag === 'helper' ) { 1211 | 1212 | handle.visible = false; 1213 | 1214 | if ( handle.name === 'AXIS' ) { 1215 | 1216 | handle.visible = !! this.axis; 1217 | 1218 | if ( this.axis === 'X' ) { 1219 | 1220 | _tempQuaternion.setFromEuler( _tempEuler.set( 0, 0, 0 ) ); 1221 | handle.quaternion.copy( quaternion ).multiply( _tempQuaternion ); 1222 | 1223 | if ( Math.abs( _alignVector.copy( _unitX ).applyQuaternion( quaternion ).dot( this.eye ) ) > 0.9 ) { 1224 | 1225 | handle.visible = false; 1226 | 1227 | } 1228 | 1229 | } 1230 | 1231 | if ( this.axis === 'Y' ) { 1232 | 1233 | _tempQuaternion.setFromEuler( _tempEuler.set( 0, 0, Math.PI / 2 ) ); 1234 | handle.quaternion.copy( quaternion ).multiply( _tempQuaternion ); 1235 | 1236 | if ( Math.abs( _alignVector.copy( _unitY ).applyQuaternion( quaternion ).dot( this.eye ) ) > 0.9 ) { 1237 | 1238 | handle.visible = false; 1239 | 1240 | } 1241 | 1242 | } 1243 | 1244 | if ( this.axis === 'Z' ) { 1245 | 1246 | _tempQuaternion.setFromEuler( _tempEuler.set( 0, Math.PI / 2, 0 ) ); 1247 | handle.quaternion.copy( quaternion ).multiply( _tempQuaternion ); 1248 | 1249 | if ( Math.abs( _alignVector.copy( _unitZ ).applyQuaternion( quaternion ).dot( this.eye ) ) > 0.9 ) { 1250 | 1251 | handle.visible = false; 1252 | 1253 | } 1254 | 1255 | } 1256 | 1257 | if ( this.axis === 'XYZE' ) { 1258 | 1259 | _tempQuaternion.setFromEuler( _tempEuler.set( 0, Math.PI / 2, 0 ) ); 1260 | _alignVector.copy( this.rotationAxis ); 1261 | handle.quaternion.setFromRotationMatrix( _lookAtMatrix.lookAt( _zeroVector, _alignVector, _unitY ) ); 1262 | handle.quaternion.multiply( _tempQuaternion ); 1263 | handle.visible = this.dragging; 1264 | 1265 | } 1266 | 1267 | if ( this.axis === 'E' ) { 1268 | 1269 | handle.visible = false; 1270 | 1271 | } 1272 | 1273 | 1274 | } else if ( handle.name === 'START' ) { 1275 | 1276 | handle.position.copy( this.worldPositionStart ); 1277 | handle.visible = this.dragging; 1278 | 1279 | } else if ( handle.name === 'END' ) { 1280 | 1281 | handle.position.copy( this.worldPosition ); 1282 | handle.visible = this.dragging; 1283 | 1284 | } else if ( handle.name === 'DELTA' ) { 1285 | 1286 | handle.position.copy( this.worldPositionStart ); 1287 | handle.quaternion.copy( this.worldQuaternionStart ); 1288 | _tempVector.set( 1e-10, 1e-10, 1e-10 ).add( this.worldPositionStart ).sub( this.worldPosition ).multiplyScalar( - 1 ); 1289 | _tempVector.applyQuaternion( this.worldQuaternionStart.clone().invert() ); 1290 | handle.scale.copy( _tempVector ); 1291 | handle.visible = this.dragging; 1292 | 1293 | } else { 1294 | 1295 | handle.quaternion.copy( quaternion ); 1296 | 1297 | if ( this.dragging ) { 1298 | 1299 | handle.position.copy( this.worldPositionStart ); 1300 | 1301 | } else { 1302 | 1303 | handle.position.copy( this.worldPosition ); 1304 | 1305 | } 1306 | 1307 | if ( this.axis ) { 1308 | 1309 | handle.visible = this.axis.search( handle.name ) !== - 1; 1310 | 1311 | } 1312 | 1313 | } 1314 | 1315 | // If updating helper, skip rest of the loop 1316 | continue; 1317 | 1318 | } 1319 | 1320 | // Align handles to current local or world rotation 1321 | 1322 | handle.quaternion.copy( quaternion ); 1323 | 1324 | if ( this.mode === 'translate' || this.mode === 'scale' ) { 1325 | 1326 | // Hide translate and scale axis facing the camera 1327 | 1328 | const AXIS_HIDE_THRESHOLD = 0.99; 1329 | const PLANE_HIDE_THRESHOLD = 0.2; 1330 | 1331 | if ( handle.name === 'X' ) { 1332 | 1333 | if ( Math.abs( _alignVector.copy( _unitX ).applyQuaternion( quaternion ).dot( this.eye ) ) > AXIS_HIDE_THRESHOLD ) { 1334 | 1335 | handle.scale.set( 1e-10, 1e-10, 1e-10 ); 1336 | handle.visible = false; 1337 | 1338 | } 1339 | 1340 | } 1341 | 1342 | if ( handle.name === 'Y' ) { 1343 | 1344 | if ( Math.abs( _alignVector.copy( _unitY ).applyQuaternion( quaternion ).dot( this.eye ) ) > AXIS_HIDE_THRESHOLD ) { 1345 | 1346 | handle.scale.set( 1e-10, 1e-10, 1e-10 ); 1347 | handle.visible = false; 1348 | 1349 | } 1350 | 1351 | } 1352 | 1353 | if ( handle.name === 'Z' ) { 1354 | 1355 | if ( Math.abs( _alignVector.copy( _unitZ ).applyQuaternion( quaternion ).dot( this.eye ) ) > AXIS_HIDE_THRESHOLD ) { 1356 | 1357 | handle.scale.set( 1e-10, 1e-10, 1e-10 ); 1358 | handle.visible = false; 1359 | 1360 | } 1361 | 1362 | } 1363 | 1364 | if ( handle.name === 'XY' ) { 1365 | 1366 | if ( Math.abs( _alignVector.copy( _unitZ ).applyQuaternion( quaternion ).dot( this.eye ) ) < PLANE_HIDE_THRESHOLD ) { 1367 | 1368 | handle.scale.set( 1e-10, 1e-10, 1e-10 ); 1369 | handle.visible = false; 1370 | 1371 | } 1372 | 1373 | } 1374 | 1375 | if ( handle.name === 'YZ' ) { 1376 | 1377 | if ( Math.abs( _alignVector.copy( _unitX ).applyQuaternion( quaternion ).dot( this.eye ) ) < PLANE_HIDE_THRESHOLD ) { 1378 | 1379 | handle.scale.set( 1e-10, 1e-10, 1e-10 ); 1380 | handle.visible = false; 1381 | 1382 | } 1383 | 1384 | } 1385 | 1386 | if ( handle.name === 'XZ' ) { 1387 | 1388 | if ( Math.abs( _alignVector.copy( _unitY ).applyQuaternion( quaternion ).dot( this.eye ) ) < PLANE_HIDE_THRESHOLD ) { 1389 | 1390 | handle.scale.set( 1e-10, 1e-10, 1e-10 ); 1391 | handle.visible = false; 1392 | 1393 | } 1394 | 1395 | } 1396 | 1397 | } else if ( this.mode === 'rotate' ) { 1398 | 1399 | // Align handles to current local or world rotation 1400 | 1401 | _tempQuaternion2.copy( quaternion ); 1402 | _alignVector.copy( this.eye ).applyQuaternion( _tempQuaternion.copy( quaternion ).invert() ); 1403 | 1404 | if ( handle.name.search( 'E' ) !== - 1 ) { 1405 | 1406 | handle.quaternion.setFromRotationMatrix( _lookAtMatrix.lookAt( this.eye, _zeroVector, _unitY ) ); 1407 | 1408 | } 1409 | 1410 | if ( handle.name === 'X' ) { 1411 | 1412 | _tempQuaternion.setFromAxisAngle( _unitX, Math.atan2( - _alignVector.y, _alignVector.z ) ); 1413 | _tempQuaternion.multiplyQuaternions( _tempQuaternion2, _tempQuaternion ); 1414 | handle.quaternion.copy( _tempQuaternion ); 1415 | 1416 | } 1417 | 1418 | if ( handle.name === 'Y' ) { 1419 | 1420 | _tempQuaternion.setFromAxisAngle( _unitY, Math.atan2( _alignVector.x, _alignVector.z ) ); 1421 | _tempQuaternion.multiplyQuaternions( _tempQuaternion2, _tempQuaternion ); 1422 | handle.quaternion.copy( _tempQuaternion ); 1423 | 1424 | } 1425 | 1426 | if ( handle.name === 'Z' ) { 1427 | 1428 | _tempQuaternion.setFromAxisAngle( _unitZ, Math.atan2( _alignVector.y, _alignVector.x ) ); 1429 | _tempQuaternion.multiplyQuaternions( _tempQuaternion2, _tempQuaternion ); 1430 | handle.quaternion.copy( _tempQuaternion ); 1431 | 1432 | } 1433 | 1434 | } 1435 | 1436 | // Hide disabled axes 1437 | handle.visible = handle.visible && ( handle.name.indexOf( 'X' ) === - 1 || this.showX ); 1438 | handle.visible = handle.visible && ( handle.name.indexOf( 'Y' ) === - 1 || this.showY ); 1439 | handle.visible = handle.visible && ( handle.name.indexOf( 'Z' ) === - 1 || this.showZ ); 1440 | handle.visible = handle.visible && ( handle.name.indexOf( 'E' ) === - 1 || ( this.showX && this.showY && this.showZ ) ); 1441 | 1442 | // highlight selected axis 1443 | 1444 | handle.material._color = handle.material._color || handle.material.color.clone(); 1445 | handle.material._opacity = handle.material._opacity || handle.material.opacity; 1446 | 1447 | handle.material.color.copy( handle.material._color ); 1448 | handle.material.opacity = handle.material._opacity; 1449 | 1450 | if ( this.enabled && this.axis ) { 1451 | 1452 | if ( handle.name === this.axis ) { 1453 | 1454 | handle.material.color.setHex( 0xffff00 ); 1455 | handle.material.opacity = 1.0; 1456 | 1457 | } else if ( this.axis.split( '' ).some( function ( a ) { 1458 | 1459 | return handle.name === a; 1460 | 1461 | } ) ) { 1462 | 1463 | handle.material.color.setHex( 0xffff00 ); 1464 | handle.material.opacity = 1.0; 1465 | 1466 | } 1467 | 1468 | } 1469 | 1470 | } 1471 | 1472 | super.updateMatrixWorld( force ); 1473 | 1474 | } 1475 | 1476 | } 1477 | 1478 | // 1479 | 1480 | class TransformControlsPlane extends Mesh { 1481 | 1482 | constructor() { 1483 | 1484 | super( 1485 | new PlaneGeometry( 100000, 100000, 2, 2 ), 1486 | new MeshBasicMaterial( { visible: false, wireframe: true, side: DoubleSide, transparent: true, opacity: 0.1, toneMapped: false } ) 1487 | ); 1488 | 1489 | this.isTransformControlsPlane = true; 1490 | 1491 | this.type = 'TransformControlsPlane'; 1492 | 1493 | } 1494 | 1495 | updateMatrixWorld( force ) { 1496 | 1497 | let space = this.space; 1498 | 1499 | this.position.copy( this.worldPosition ); 1500 | 1501 | if ( this.mode === 'scale' ) space = 'local'; // scale always oriented to local rotation 1502 | 1503 | _v1.copy( _unitX ).applyQuaternion( space === 'local' ? this.worldQuaternion : _identityQuaternion ); 1504 | _v2.copy( _unitY ).applyQuaternion( space === 'local' ? this.worldQuaternion : _identityQuaternion ); 1505 | _v3.copy( _unitZ ).applyQuaternion( space === 'local' ? this.worldQuaternion : _identityQuaternion ); 1506 | 1507 | // Align the plane for current transform mode, axis and space. 1508 | 1509 | _alignVector.copy( _v2 ); 1510 | 1511 | switch ( this.mode ) { 1512 | 1513 | case 'translate': 1514 | case 'scale': 1515 | switch ( this.axis ) { 1516 | 1517 | case 'X': 1518 | _alignVector.copy( this.eye ).cross( _v1 ); 1519 | _dirVector.copy( _v1 ).cross( _alignVector ); 1520 | break; 1521 | case 'Y': 1522 | _alignVector.copy( this.eye ).cross( _v2 ); 1523 | _dirVector.copy( _v2 ).cross( _alignVector ); 1524 | break; 1525 | case 'Z': 1526 | _alignVector.copy( this.eye ).cross( _v3 ); 1527 | _dirVector.copy( _v3 ).cross( _alignVector ); 1528 | break; 1529 | case 'XY': 1530 | _dirVector.copy( _v3 ); 1531 | break; 1532 | case 'YZ': 1533 | _dirVector.copy( _v1 ); 1534 | break; 1535 | case 'XZ': 1536 | _alignVector.copy( _v3 ); 1537 | _dirVector.copy( _v2 ); 1538 | break; 1539 | case 'XYZ': 1540 | case 'E': 1541 | _dirVector.set( 0, 0, 0 ); 1542 | break; 1543 | 1544 | } 1545 | 1546 | break; 1547 | case 'rotate': 1548 | default: 1549 | // special case for rotate 1550 | _dirVector.set( 0, 0, 0 ); 1551 | 1552 | } 1553 | 1554 | if ( _dirVector.length() === 0 ) { 1555 | 1556 | // If in rotate mode, make the plane parallel to camera 1557 | this.quaternion.copy( this.cameraQuaternion ); 1558 | 1559 | } else { 1560 | 1561 | _tempMatrix.lookAt( _tempVector.set( 0, 0, 0 ), _dirVector, _alignVector ); 1562 | 1563 | this.quaternion.setFromRotationMatrix( _tempMatrix ); 1564 | 1565 | } 1566 | 1567 | super.updateMatrixWorld( force ); 1568 | 1569 | } 1570 | 1571 | } 1572 | 1573 | export { TransformControls, TransformControlsGizmo, TransformControlsPlane }; 1574 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ThreeOverlay", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "esbuild": "^0.21.4", 9 | "three": "^0.164.1" 10 | } 11 | }, 12 | "node_modules/@esbuild/aix-ppc64": { 13 | "version": "0.21.4", 14 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.4.tgz", 15 | "integrity": "sha512-Zrm+B33R4LWPLjDEVnEqt2+SLTATlru1q/xYKVn8oVTbiRBGmK2VIMoIYGJDGyftnGaC788IuzGFAlb7IQ0Y8A==", 16 | "cpu": [ 17 | "ppc64" 18 | ], 19 | "optional": true, 20 | "os": [ 21 | "aix" 22 | ], 23 | "engines": { 24 | "node": ">=12" 25 | } 26 | }, 27 | "node_modules/@esbuild/android-arm": { 28 | "version": "0.21.4", 29 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.4.tgz", 30 | "integrity": "sha512-E7H/yTd8kGQfY4z9t3nRPk/hrhaCajfA3YSQSBrst8B+3uTcgsi8N+ZWYCaeIDsiVs6m65JPCaQN/DxBRclF3A==", 31 | "cpu": [ 32 | "arm" 33 | ], 34 | "optional": true, 35 | "os": [ 36 | "android" 37 | ], 38 | "engines": { 39 | "node": ">=12" 40 | } 41 | }, 42 | "node_modules/@esbuild/android-arm64": { 43 | "version": "0.21.4", 44 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.4.tgz", 45 | "integrity": "sha512-fYFnz+ObClJ3dNiITySBUx+oNalYUT18/AryMxfovLkYWbutXsct3Wz2ZWAcGGppp+RVVX5FiXeLYGi97umisA==", 46 | "cpu": [ 47 | "arm64" 48 | ], 49 | "optional": true, 50 | "os": [ 51 | "android" 52 | ], 53 | "engines": { 54 | "node": ">=12" 55 | } 56 | }, 57 | "node_modules/@esbuild/android-x64": { 58 | "version": "0.21.4", 59 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.4.tgz", 60 | "integrity": "sha512-mDqmlge3hFbEPbCWxp4fM6hqq7aZfLEHZAKGP9viq9wMUBVQx202aDIfc3l+d2cKhUJM741VrCXEzRFhPDKH3Q==", 61 | "cpu": [ 62 | "x64" 63 | ], 64 | "optional": true, 65 | "os": [ 66 | "android" 67 | ], 68 | "engines": { 69 | "node": ">=12" 70 | } 71 | }, 72 | "node_modules/@esbuild/darwin-arm64": { 73 | "version": "0.21.4", 74 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.4.tgz", 75 | "integrity": "sha512-72eaIrDZDSiWqpmCzVaBD58c8ea8cw/U0fq/PPOTqE3c53D0xVMRt2ooIABZ6/wj99Y+h4ksT/+I+srCDLU9TA==", 76 | "cpu": [ 77 | "arm64" 78 | ], 79 | "optional": true, 80 | "os": [ 81 | "darwin" 82 | ], 83 | "engines": { 84 | "node": ">=12" 85 | } 86 | }, 87 | "node_modules/@esbuild/darwin-x64": { 88 | "version": "0.21.4", 89 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.4.tgz", 90 | "integrity": "sha512-uBsuwRMehGmw1JC7Vecu/upOjTsMhgahmDkWhGLWxIgUn2x/Y4tIwUZngsmVb6XyPSTXJYS4YiASKPcm9Zitag==", 91 | "cpu": [ 92 | "x64" 93 | ], 94 | "optional": true, 95 | "os": [ 96 | "darwin" 97 | ], 98 | "engines": { 99 | "node": ">=12" 100 | } 101 | }, 102 | "node_modules/@esbuild/freebsd-arm64": { 103 | "version": "0.21.4", 104 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.4.tgz", 105 | "integrity": "sha512-8JfuSC6YMSAEIZIWNL3GtdUT5NhUA/CMUCpZdDRolUXNAXEE/Vbpe6qlGLpfThtY5NwXq8Hi4nJy4YfPh+TwAg==", 106 | "cpu": [ 107 | "arm64" 108 | ], 109 | "optional": true, 110 | "os": [ 111 | "freebsd" 112 | ], 113 | "engines": { 114 | "node": ">=12" 115 | } 116 | }, 117 | "node_modules/@esbuild/freebsd-x64": { 118 | "version": "0.21.4", 119 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.4.tgz", 120 | "integrity": "sha512-8d9y9eQhxv4ef7JmXny7591P/PYsDFc4+STaxC1GBv0tMyCdyWfXu2jBuqRsyhY8uL2HU8uPyscgE2KxCY9imQ==", 121 | "cpu": [ 122 | "x64" 123 | ], 124 | "optional": true, 125 | "os": [ 126 | "freebsd" 127 | ], 128 | "engines": { 129 | "node": ">=12" 130 | } 131 | }, 132 | "node_modules/@esbuild/linux-arm": { 133 | "version": "0.21.4", 134 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.4.tgz", 135 | "integrity": "sha512-2rqFFefpYmpMs+FWjkzSgXg5vViocqpq5a1PSRgT0AvSgxoXmGF17qfGAzKedg6wAwyM7UltrKVo9kxaJLMF/g==", 136 | "cpu": [ 137 | "arm" 138 | ], 139 | "optional": true, 140 | "os": [ 141 | "linux" 142 | ], 143 | "engines": { 144 | "node": ">=12" 145 | } 146 | }, 147 | "node_modules/@esbuild/linux-arm64": { 148 | "version": "0.21.4", 149 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.4.tgz", 150 | "integrity": "sha512-/GLD2orjNU50v9PcxNpYZi+y8dJ7e7/LhQukN3S4jNDXCKkyyiyAz9zDw3siZ7Eh1tRcnCHAo/WcqKMzmi4eMQ==", 151 | "cpu": [ 152 | "arm64" 153 | ], 154 | "optional": true, 155 | "os": [ 156 | "linux" 157 | ], 158 | "engines": { 159 | "node": ">=12" 160 | } 161 | }, 162 | "node_modules/@esbuild/linux-ia32": { 163 | "version": "0.21.4", 164 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.4.tgz", 165 | "integrity": "sha512-pNftBl7m/tFG3t2m/tSjuYeWIffzwAZT9m08+9DPLizxVOsUl8DdFzn9HvJrTQwe3wvJnwTdl92AonY36w/25g==", 166 | "cpu": [ 167 | "ia32" 168 | ], 169 | "optional": true, 170 | "os": [ 171 | "linux" 172 | ], 173 | "engines": { 174 | "node": ">=12" 175 | } 176 | }, 177 | "node_modules/@esbuild/linux-loong64": { 178 | "version": "0.21.4", 179 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.4.tgz", 180 | "integrity": "sha512-cSD2gzCK5LuVX+hszzXQzlWya6c7hilO71L9h4KHwqI4qeqZ57bAtkgcC2YioXjsbfAv4lPn3qe3b00Zt+jIfQ==", 181 | "cpu": [ 182 | "loong64" 183 | ], 184 | "optional": true, 185 | "os": [ 186 | "linux" 187 | ], 188 | "engines": { 189 | "node": ">=12" 190 | } 191 | }, 192 | "node_modules/@esbuild/linux-mips64el": { 193 | "version": "0.21.4", 194 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.4.tgz", 195 | "integrity": "sha512-qtzAd3BJh7UdbiXCrg6npWLYU0YpufsV9XlufKhMhYMJGJCdfX/G6+PNd0+v877X1JG5VmjBLUiFB0o8EUSicA==", 196 | "cpu": [ 197 | "mips64el" 198 | ], 199 | "optional": true, 200 | "os": [ 201 | "linux" 202 | ], 203 | "engines": { 204 | "node": ">=12" 205 | } 206 | }, 207 | "node_modules/@esbuild/linux-ppc64": { 208 | "version": "0.21.4", 209 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.4.tgz", 210 | "integrity": "sha512-yB8AYzOTaL0D5+2a4xEy7OVvbcypvDR05MsB/VVPVA7nL4hc5w5Dyd/ddnayStDgJE59fAgNEOdLhBxjfx5+dg==", 211 | "cpu": [ 212 | "ppc64" 213 | ], 214 | "optional": true, 215 | "os": [ 216 | "linux" 217 | ], 218 | "engines": { 219 | "node": ">=12" 220 | } 221 | }, 222 | "node_modules/@esbuild/linux-riscv64": { 223 | "version": "0.21.4", 224 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.4.tgz", 225 | "integrity": "sha512-Y5AgOuVzPjQdgU59ramLoqSSiXddu7F3F+LI5hYy/d1UHN7K5oLzYBDZe23QmQJ9PIVUXwOdKJ/jZahPdxzm9w==", 226 | "cpu": [ 227 | "riscv64" 228 | ], 229 | "optional": true, 230 | "os": [ 231 | "linux" 232 | ], 233 | "engines": { 234 | "node": ">=12" 235 | } 236 | }, 237 | "node_modules/@esbuild/linux-s390x": { 238 | "version": "0.21.4", 239 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.4.tgz", 240 | "integrity": "sha512-Iqc/l/FFwtt8FoTK9riYv9zQNms7B8u+vAI/rxKuN10HgQIXaPzKZc479lZ0x6+vKVQbu55GdpYpeNWzjOhgbA==", 241 | "cpu": [ 242 | "s390x" 243 | ], 244 | "optional": true, 245 | "os": [ 246 | "linux" 247 | ], 248 | "engines": { 249 | "node": ">=12" 250 | } 251 | }, 252 | "node_modules/@esbuild/linux-x64": { 253 | "version": "0.21.4", 254 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.4.tgz", 255 | "integrity": "sha512-Td9jv782UMAFsuLZINfUpoF5mZIbAj+jv1YVtE58rFtfvoKRiKSkRGQfHTgKamLVT/fO7203bHa3wU122V/Bdg==", 256 | "cpu": [ 257 | "x64" 258 | ], 259 | "optional": true, 260 | "os": [ 261 | "linux" 262 | ], 263 | "engines": { 264 | "node": ">=12" 265 | } 266 | }, 267 | "node_modules/@esbuild/netbsd-x64": { 268 | "version": "0.21.4", 269 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.4.tgz", 270 | "integrity": "sha512-Awn38oSXxsPMQxaV0Ipb7W/gxZtk5Tx3+W+rAPdZkyEhQ6968r9NvtkjhnhbEgWXYbgV+JEONJ6PcdBS+nlcpA==", 271 | "cpu": [ 272 | "x64" 273 | ], 274 | "optional": true, 275 | "os": [ 276 | "netbsd" 277 | ], 278 | "engines": { 279 | "node": ">=12" 280 | } 281 | }, 282 | "node_modules/@esbuild/openbsd-x64": { 283 | "version": "0.21.4", 284 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.4.tgz", 285 | "integrity": "sha512-IsUmQeCY0aU374R82fxIPu6vkOybWIMc3hVGZ3ChRwL9hA1TwY+tS0lgFWV5+F1+1ssuvvXt3HFqe8roCip8Hg==", 286 | "cpu": [ 287 | "x64" 288 | ], 289 | "optional": true, 290 | "os": [ 291 | "openbsd" 292 | ], 293 | "engines": { 294 | "node": ">=12" 295 | } 296 | }, 297 | "node_modules/@esbuild/sunos-x64": { 298 | "version": "0.21.4", 299 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.4.tgz", 300 | "integrity": "sha512-hsKhgZ4teLUaDA6FG/QIu2q0rI6I36tZVfM4DBZv3BG0mkMIdEnMbhc4xwLvLJSS22uWmaVkFkqWgIS0gPIm+A==", 301 | "cpu": [ 302 | "x64" 303 | ], 304 | "optional": true, 305 | "os": [ 306 | "sunos" 307 | ], 308 | "engines": { 309 | "node": ">=12" 310 | } 311 | }, 312 | "node_modules/@esbuild/win32-arm64": { 313 | "version": "0.21.4", 314 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.4.tgz", 315 | "integrity": "sha512-UUfMgMoXPoA/bvGUNfUBFLCh0gt9dxZYIx9W4rfJr7+hKe5jxxHmfOK8YSH4qsHLLN4Ck8JZ+v7Q5fIm1huErg==", 316 | "cpu": [ 317 | "arm64" 318 | ], 319 | "optional": true, 320 | "os": [ 321 | "win32" 322 | ], 323 | "engines": { 324 | "node": ">=12" 325 | } 326 | }, 327 | "node_modules/@esbuild/win32-ia32": { 328 | "version": "0.21.4", 329 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.4.tgz", 330 | "integrity": "sha512-yIxbspZb5kGCAHWm8dexALQ9en1IYDfErzjSEq1KzXFniHv019VT3mNtTK7t8qdy4TwT6QYHI9sEZabONHg+aw==", 331 | "cpu": [ 332 | "ia32" 333 | ], 334 | "optional": true, 335 | "os": [ 336 | "win32" 337 | ], 338 | "engines": { 339 | "node": ">=12" 340 | } 341 | }, 342 | "node_modules/@esbuild/win32-x64": { 343 | "version": "0.21.4", 344 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.4.tgz", 345 | "integrity": "sha512-sywLRD3UK/qRJt0oBwdpYLBibk7KiRfbswmWRDabuncQYSlf8aLEEUor/oP6KRz8KEG+HoiVLBhPRD5JWjS8Sg==", 346 | "cpu": [ 347 | "x64" 348 | ], 349 | "optional": true, 350 | "os": [ 351 | "win32" 352 | ], 353 | "engines": { 354 | "node": ">=12" 355 | } 356 | }, 357 | "node_modules/esbuild": { 358 | "version": "0.21.4", 359 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.4.tgz", 360 | "integrity": "sha512-sFMcNNrj+Q0ZDolrp5pDhH0nRPN9hLIM3fRPwgbLYJeSHHgnXSnbV3xYgSVuOeLWH9c73VwmEverVzupIv5xuA==", 361 | "hasInstallScript": true, 362 | "bin": { 363 | "esbuild": "bin/esbuild" 364 | }, 365 | "engines": { 366 | "node": ">=12" 367 | }, 368 | "optionalDependencies": { 369 | "@esbuild/aix-ppc64": "0.21.4", 370 | "@esbuild/android-arm": "0.21.4", 371 | "@esbuild/android-arm64": "0.21.4", 372 | "@esbuild/android-x64": "0.21.4", 373 | "@esbuild/darwin-arm64": "0.21.4", 374 | "@esbuild/darwin-x64": "0.21.4", 375 | "@esbuild/freebsd-arm64": "0.21.4", 376 | "@esbuild/freebsd-x64": "0.21.4", 377 | "@esbuild/linux-arm": "0.21.4", 378 | "@esbuild/linux-arm64": "0.21.4", 379 | "@esbuild/linux-ia32": "0.21.4", 380 | "@esbuild/linux-loong64": "0.21.4", 381 | "@esbuild/linux-mips64el": "0.21.4", 382 | "@esbuild/linux-ppc64": "0.21.4", 383 | "@esbuild/linux-riscv64": "0.21.4", 384 | "@esbuild/linux-s390x": "0.21.4", 385 | "@esbuild/linux-x64": "0.21.4", 386 | "@esbuild/netbsd-x64": "0.21.4", 387 | "@esbuild/openbsd-x64": "0.21.4", 388 | "@esbuild/sunos-x64": "0.21.4", 389 | "@esbuild/win32-arm64": "0.21.4", 390 | "@esbuild/win32-ia32": "0.21.4", 391 | "@esbuild/win32-x64": "0.21.4" 392 | } 393 | }, 394 | "node_modules/three": { 395 | "version": "0.164.1", 396 | "resolved": "https://registry.npmjs.org/three/-/three-0.164.1.tgz", 397 | "integrity": "sha512-iC/hUBbl1vzFny7f5GtqzVXYjMJKaTPxiCxXfrvVdBi1Sf+jhd1CAkitiFwC7mIBFCo3MrDLJG97yisoaWig0w==" 398 | } 399 | }, 400 | "dependencies": { 401 | "@esbuild/aix-ppc64": { 402 | "version": "0.21.4", 403 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.4.tgz", 404 | "integrity": "sha512-Zrm+B33R4LWPLjDEVnEqt2+SLTATlru1q/xYKVn8oVTbiRBGmK2VIMoIYGJDGyftnGaC788IuzGFAlb7IQ0Y8A==", 405 | "optional": true 406 | }, 407 | "@esbuild/android-arm": { 408 | "version": "0.21.4", 409 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.4.tgz", 410 | "integrity": "sha512-E7H/yTd8kGQfY4z9t3nRPk/hrhaCajfA3YSQSBrst8B+3uTcgsi8N+ZWYCaeIDsiVs6m65JPCaQN/DxBRclF3A==", 411 | "optional": true 412 | }, 413 | "@esbuild/android-arm64": { 414 | "version": "0.21.4", 415 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.4.tgz", 416 | "integrity": "sha512-fYFnz+ObClJ3dNiITySBUx+oNalYUT18/AryMxfovLkYWbutXsct3Wz2ZWAcGGppp+RVVX5FiXeLYGi97umisA==", 417 | "optional": true 418 | }, 419 | "@esbuild/android-x64": { 420 | "version": "0.21.4", 421 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.4.tgz", 422 | "integrity": "sha512-mDqmlge3hFbEPbCWxp4fM6hqq7aZfLEHZAKGP9viq9wMUBVQx202aDIfc3l+d2cKhUJM741VrCXEzRFhPDKH3Q==", 423 | "optional": true 424 | }, 425 | "@esbuild/darwin-arm64": { 426 | "version": "0.21.4", 427 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.4.tgz", 428 | "integrity": "sha512-72eaIrDZDSiWqpmCzVaBD58c8ea8cw/U0fq/PPOTqE3c53D0xVMRt2ooIABZ6/wj99Y+h4ksT/+I+srCDLU9TA==", 429 | "optional": true 430 | }, 431 | "@esbuild/darwin-x64": { 432 | "version": "0.21.4", 433 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.4.tgz", 434 | "integrity": "sha512-uBsuwRMehGmw1JC7Vecu/upOjTsMhgahmDkWhGLWxIgUn2x/Y4tIwUZngsmVb6XyPSTXJYS4YiASKPcm9Zitag==", 435 | "optional": true 436 | }, 437 | "@esbuild/freebsd-arm64": { 438 | "version": "0.21.4", 439 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.4.tgz", 440 | "integrity": "sha512-8JfuSC6YMSAEIZIWNL3GtdUT5NhUA/CMUCpZdDRolUXNAXEE/Vbpe6qlGLpfThtY5NwXq8Hi4nJy4YfPh+TwAg==", 441 | "optional": true 442 | }, 443 | "@esbuild/freebsd-x64": { 444 | "version": "0.21.4", 445 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.4.tgz", 446 | "integrity": "sha512-8d9y9eQhxv4ef7JmXny7591P/PYsDFc4+STaxC1GBv0tMyCdyWfXu2jBuqRsyhY8uL2HU8uPyscgE2KxCY9imQ==", 447 | "optional": true 448 | }, 449 | "@esbuild/linux-arm": { 450 | "version": "0.21.4", 451 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.4.tgz", 452 | "integrity": "sha512-2rqFFefpYmpMs+FWjkzSgXg5vViocqpq5a1PSRgT0AvSgxoXmGF17qfGAzKedg6wAwyM7UltrKVo9kxaJLMF/g==", 453 | "optional": true 454 | }, 455 | "@esbuild/linux-arm64": { 456 | "version": "0.21.4", 457 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.4.tgz", 458 | "integrity": "sha512-/GLD2orjNU50v9PcxNpYZi+y8dJ7e7/LhQukN3S4jNDXCKkyyiyAz9zDw3siZ7Eh1tRcnCHAo/WcqKMzmi4eMQ==", 459 | "optional": true 460 | }, 461 | "@esbuild/linux-ia32": { 462 | "version": "0.21.4", 463 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.4.tgz", 464 | "integrity": "sha512-pNftBl7m/tFG3t2m/tSjuYeWIffzwAZT9m08+9DPLizxVOsUl8DdFzn9HvJrTQwe3wvJnwTdl92AonY36w/25g==", 465 | "optional": true 466 | }, 467 | "@esbuild/linux-loong64": { 468 | "version": "0.21.4", 469 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.4.tgz", 470 | "integrity": "sha512-cSD2gzCK5LuVX+hszzXQzlWya6c7hilO71L9h4KHwqI4qeqZ57bAtkgcC2YioXjsbfAv4lPn3qe3b00Zt+jIfQ==", 471 | "optional": true 472 | }, 473 | "@esbuild/linux-mips64el": { 474 | "version": "0.21.4", 475 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.4.tgz", 476 | "integrity": "sha512-qtzAd3BJh7UdbiXCrg6npWLYU0YpufsV9XlufKhMhYMJGJCdfX/G6+PNd0+v877X1JG5VmjBLUiFB0o8EUSicA==", 477 | "optional": true 478 | }, 479 | "@esbuild/linux-ppc64": { 480 | "version": "0.21.4", 481 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.4.tgz", 482 | "integrity": "sha512-yB8AYzOTaL0D5+2a4xEy7OVvbcypvDR05MsB/VVPVA7nL4hc5w5Dyd/ddnayStDgJE59fAgNEOdLhBxjfx5+dg==", 483 | "optional": true 484 | }, 485 | "@esbuild/linux-riscv64": { 486 | "version": "0.21.4", 487 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.4.tgz", 488 | "integrity": "sha512-Y5AgOuVzPjQdgU59ramLoqSSiXddu7F3F+LI5hYy/d1UHN7K5oLzYBDZe23QmQJ9PIVUXwOdKJ/jZahPdxzm9w==", 489 | "optional": true 490 | }, 491 | "@esbuild/linux-s390x": { 492 | "version": "0.21.4", 493 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.4.tgz", 494 | "integrity": "sha512-Iqc/l/FFwtt8FoTK9riYv9zQNms7B8u+vAI/rxKuN10HgQIXaPzKZc479lZ0x6+vKVQbu55GdpYpeNWzjOhgbA==", 495 | "optional": true 496 | }, 497 | "@esbuild/linux-x64": { 498 | "version": "0.21.4", 499 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.4.tgz", 500 | "integrity": "sha512-Td9jv782UMAFsuLZINfUpoF5mZIbAj+jv1YVtE58rFtfvoKRiKSkRGQfHTgKamLVT/fO7203bHa3wU122V/Bdg==", 501 | "optional": true 502 | }, 503 | "@esbuild/netbsd-x64": { 504 | "version": "0.21.4", 505 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.4.tgz", 506 | "integrity": "sha512-Awn38oSXxsPMQxaV0Ipb7W/gxZtk5Tx3+W+rAPdZkyEhQ6968r9NvtkjhnhbEgWXYbgV+JEONJ6PcdBS+nlcpA==", 507 | "optional": true 508 | }, 509 | "@esbuild/openbsd-x64": { 510 | "version": "0.21.4", 511 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.4.tgz", 512 | "integrity": "sha512-IsUmQeCY0aU374R82fxIPu6vkOybWIMc3hVGZ3ChRwL9hA1TwY+tS0lgFWV5+F1+1ssuvvXt3HFqe8roCip8Hg==", 513 | "optional": true 514 | }, 515 | "@esbuild/sunos-x64": { 516 | "version": "0.21.4", 517 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.4.tgz", 518 | "integrity": "sha512-hsKhgZ4teLUaDA6FG/QIu2q0rI6I36tZVfM4DBZv3BG0mkMIdEnMbhc4xwLvLJSS22uWmaVkFkqWgIS0gPIm+A==", 519 | "optional": true 520 | }, 521 | "@esbuild/win32-arm64": { 522 | "version": "0.21.4", 523 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.4.tgz", 524 | "integrity": "sha512-UUfMgMoXPoA/bvGUNfUBFLCh0gt9dxZYIx9W4rfJr7+hKe5jxxHmfOK8YSH4qsHLLN4Ck8JZ+v7Q5fIm1huErg==", 525 | "optional": true 526 | }, 527 | "@esbuild/win32-ia32": { 528 | "version": "0.21.4", 529 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.4.tgz", 530 | "integrity": "sha512-yIxbspZb5kGCAHWm8dexALQ9en1IYDfErzjSEq1KzXFniHv019VT3mNtTK7t8qdy4TwT6QYHI9sEZabONHg+aw==", 531 | "optional": true 532 | }, 533 | "@esbuild/win32-x64": { 534 | "version": "0.21.4", 535 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.4.tgz", 536 | "integrity": "sha512-sywLRD3UK/qRJt0oBwdpYLBibk7KiRfbswmWRDabuncQYSlf8aLEEUor/oP6KRz8KEG+HoiVLBhPRD5JWjS8Sg==", 537 | "optional": true 538 | }, 539 | "esbuild": { 540 | "version": "0.21.4", 541 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.4.tgz", 542 | "integrity": "sha512-sFMcNNrj+Q0ZDolrp5pDhH0nRPN9hLIM3fRPwgbLYJeSHHgnXSnbV3xYgSVuOeLWH9c73VwmEverVzupIv5xuA==", 543 | "requires": { 544 | "@esbuild/aix-ppc64": "0.21.4", 545 | "@esbuild/android-arm": "0.21.4", 546 | "@esbuild/android-arm64": "0.21.4", 547 | "@esbuild/android-x64": "0.21.4", 548 | "@esbuild/darwin-arm64": "0.21.4", 549 | "@esbuild/darwin-x64": "0.21.4", 550 | "@esbuild/freebsd-arm64": "0.21.4", 551 | "@esbuild/freebsd-x64": "0.21.4", 552 | "@esbuild/linux-arm": "0.21.4", 553 | "@esbuild/linux-arm64": "0.21.4", 554 | "@esbuild/linux-ia32": "0.21.4", 555 | "@esbuild/linux-loong64": "0.21.4", 556 | "@esbuild/linux-mips64el": "0.21.4", 557 | "@esbuild/linux-ppc64": "0.21.4", 558 | "@esbuild/linux-riscv64": "0.21.4", 559 | "@esbuild/linux-s390x": "0.21.4", 560 | "@esbuild/linux-x64": "0.21.4", 561 | "@esbuild/netbsd-x64": "0.21.4", 562 | "@esbuild/openbsd-x64": "0.21.4", 563 | "@esbuild/sunos-x64": "0.21.4", 564 | "@esbuild/win32-arm64": "0.21.4", 565 | "@esbuild/win32-ia32": "0.21.4", 566 | "@esbuild/win32-x64": "0.21.4" 567 | } 568 | }, 569 | "three": { 570 | "version": "0.164.1", 571 | "resolved": "https://registry.npmjs.org/three/-/three-0.164.1.tgz", 572 | "integrity": "sha512-iC/hUBbl1vzFny7f5GtqzVXYjMJKaTPxiCxXfrvVdBi1Sf+jhd1CAkitiFwC7mIBFCo3MrDLJG97yisoaWig0w==" 573 | } 574 | } 575 | } 576 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "esbuild ./src/main.js --bundle --minify --sourcemap --format=esm --outdir=./build" 4 | }, 5 | "dependencies": { 6 | "esbuild": "^0.21.4", 7 | "three": "^0.164.1" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/TimeResampler.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../node_modules/three/build/three.module.js'; 2 | import Stats from '../node_modules/three/examples/jsm/libs/stats.module.js'; 3 | 4 | /** Dequeue and resample time series values with linear interpolation */ 5 | export default class TimeResampler { 6 | 7 | /** Construct a new TimeResampler with numSamples in the circular buffer */ 8 | constructor(numSamples) { 9 | this.numSamples = numSamples; 10 | this.values = new Array(this.numSamples).fill(0); 11 | this.times = new Array(this.numSamples).fill(10000); 12 | this.head = 0; 13 | this.tail = 1; 14 | } 15 | 16 | /** Enqueue a new value */ 17 | enqueue(timeMS, value){ 18 | this.values[this.tail] = value; 19 | this.times[this.tail] = timeMS; 20 | this.tail = (this.tail + 1) % this.numSamples; 21 | } 22 | 23 | /** Resample the time series - BROKEN */ 24 | interpolate(timeMS){ 25 | let nextIdx = (this.head + 1) % this.numSamples; 26 | while(true){ 27 | if((this.times[this.head] > timeMS && this.times[nextIdx] < timeMS) || nextIdx == this.tail){ 28 | break; 29 | } else { 30 | this.head = nextIdx; 31 | nextIdx = (this.head + 1) % this.numSamples; 32 | } 33 | } 34 | if(timeMS > this.times[nextIdx] + 16.0){ return this.values[nextIdx]; } 35 | 36 | let alpha = (timeMS - this.times[this.head]) / (this.times[nextIdx] - this.times[this.head]); 37 | return this.values[this.head] + alpha * (this.values[nextIdx] - this.values[this.head]); 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /src/World.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../node_modules/three/build/three.module.js'; 2 | 3 | /** The fundamental set up and animation structures for 3D Visualization */ 4 | export default class World { 5 | 6 | constructor(mainObject) { 7 | this.container_bg = document.getElementById('appbody-bg'); 8 | this.container_fg = document.getElementById('appbody-fg'); 9 | 10 | // camera and world 11 | this.scene = new THREE.Scene(); 12 | 13 | this.forcedPixelsPerMeter = 100.0; 14 | this.cameraDepth = 5.0; 15 | this.cameraFoV = 60.0; 16 | 17 | this.camera = new THREE.PerspectiveCamera( this.cameraFoV, window.innerWidth / window.innerHeight, 2.0, 1000 ); 18 | this.camera.position.set( 0.0, 0.0, this.cameraDepth ); 19 | this.camera.layers.enableAll(); 20 | this.scene.add(this.camera); 21 | 22 | this._forcePixelsPerMeter(); 23 | 24 | this.spotLight = new THREE.SpotLight( 0xffffff, Math.PI * 10.0 ); 25 | this.spotLight.angle = Math.PI / 5; 26 | this.spotLight.penumbra = 0.2; 27 | this.spotLight.position.set( -2, 3, 3 ); 28 | this.spotLight.castShadow = true; 29 | this.spotLight.shadow.camera.near = 1; 30 | this.spotLight.shadow.camera.far = 20; 31 | this.spotLight.shadow.mapSize.width = 1024; 32 | this.spotLight.shadow.mapSize.height = 1024; 33 | this.scene.add( this.spotLight ); 34 | 35 | this.dirLight = new THREE.DirectionalLight( 0x55505a, Math.PI * 10.0 ); 36 | this.dirLight.position.set( 0, 3, 0 ); 37 | this.dirLight.castShadow = true; 38 | this.dirLight.shadow.camera.near = -10; 39 | this.dirLight.shadow.camera.far = 10; 40 | 41 | this.dirLight.shadow.camera.right = 3; 42 | this.dirLight.shadow.camera.left = - 3; 43 | this.dirLight.shadow.camera.top = 3; 44 | this.dirLight.shadow.camera.bottom = - 3; 45 | 46 | this.dirLight.shadow.mapSize.width = 1024; 47 | this.dirLight.shadow.mapSize.height = 1024; 48 | this.scene.add( this.dirLight ); 49 | 50 | // Geometry 51 | 52 | for(let i = 0; i < 10; i++){ 53 | this.helper0 = new THREE.GridHelper( 20, 20 ); 54 | this.helper0.material.opacity = 0.2; 55 | this.helper0.material.transparent = true; 56 | this.helper0.position.set((window.innerWidth * 0.5) / this.pixelsPerMeter, i * -5.0, 0); 57 | this.scene.add( this.helper0 ); 58 | } 59 | 60 | this.renderingMode = 2; 61 | 62 | // renderer 63 | this.renderer_bg = new THREE.WebGLRenderer( { antialias: true } ); //, alpha: true 64 | this.renderer_bg.setPixelRatio(1.0);//window.devicePixelRatio > 1.5 ? 1.0 : 1.0); 65 | this.renderer_bg.shadowMap.enabled = true; 66 | this.renderer_bg.setAnimationLoop(mainObject.update.bind(mainObject)); 67 | this.renderer_bg.setClearColor( 0x000000, 0 ); // the default 68 | 69 | this.renderer_fg = new THREE.WebGLRenderer( { antialias: true } ); //, alpha: true 70 | this.renderer_fg.setPixelRatio(1.0);//window.devicePixelRatio > 1.5 ? 1.0 : 1.0); 71 | this.renderer_fg.shadowMap.enabled = true; 72 | //this.renderer_fg.setAnimationLoop(mainObject.update.bind(mainObject)); 73 | this.renderer_fg.setClearColor( 0x000000, 0 ); // the default 74 | 75 | window.addEventListener('resize', this._onWindowResize.bind(this), false); 76 | window.addEventListener('orientationchange', this._onWindowResize.bind(this), false); 77 | this._onWindowResize(); 78 | 79 | this.container_bg.appendChild(this.renderer_bg.domElement); 80 | this.container_fg.appendChild(this.renderer_fg.domElement); 81 | 82 | // raycaster 83 | this.raycaster = new THREE.Raycaster(); 84 | this.raycaster.layers.set(0); 85 | 86 | // Enqueue Scroll Events, since sometimes multiple scroll events are fired in a single frame 87 | this.scrollQueue = []; 88 | this.curScrollY = window.scrollY; 89 | this.positioningMode = 1; // 0 is Fixed, 1 is Absolute 90 | 91 | this.boxGeometry = new THREE.BoxGeometry(1, 1, 1) 92 | this.elementMaterial = new THREE.MeshPhysicalMaterial({ color: 0xffffff, wireframe: true, opacity: 0.05, transparent: true}); 93 | 94 | this.elementBoxes = []; 95 | this._recomputeElementBoxes(); 96 | this._setScroll(); 97 | 98 | this.cube = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshPhysicalMaterial({ color: 0x00ff00, wireframe: false })); 99 | this.cube.position.set((window.innerWidth * 0.5) / this.pixelsPerMeter, -5.0, 0.0); 100 | this.scene.add(this.cube); 101 | } 102 | 103 | 104 | _render() { 105 | // Trigger a resize event if the window size has changed 106 | // iOS Pinch to Zoom does not trigger a resize event 107 | if( window.innerWidth != this.lastWidth || 108 | window.innerHeight != this.lastHeight){ 109 | this._onWindowResize(); 110 | } 111 | 112 | if(this.renderingMode == 2){ 113 | this.camera.near = this.cameraDepth; 114 | this.camera.far = 1000; 115 | this.camera.updateProjectionMatrix(); 116 | this.renderer_bg.render(this.scene, this.camera); 117 | 118 | this.camera.near = 2.0; 119 | this.camera.far = this.cameraDepth + 0.01; 120 | this.camera.updateProjectionMatrix(); 121 | this.renderer_fg.render(this.scene, this.camera); 122 | }else{ 123 | this.camera.near = 2.0; 124 | this.camera.far = 1000; 125 | this.camera.updateProjectionMatrix(); 126 | if(this.renderingMode == 0){ 127 | this.renderer_bg.render(this.scene, this.camera); 128 | this.renderer_fg.clear(); 129 | }else{ 130 | this.renderer_bg.clear(); 131 | this.renderer_fg.render(this.scene, this.camera); 132 | } 133 | } 134 | } 135 | 136 | _setScroll(){ 137 | this.curScrollX = window.scrollX; 138 | this.curScrollY = window.scrollY; 139 | this.camera.position.set( (this.curScrollX + (window.innerWidth * 0.5)) / this.pixelsPerMeter, 140 | -(this.curScrollY + (window.innerHeight * 0.5)) / this.pixelsPerMeter, this.cameraDepth); // - (window.innerWidth * 0.5) 141 | 142 | this._render(this.scene, this.camera); 143 | if(this.positioningMode == 1){ 144 | this.container_bg.style.position = "absolute"; 145 | this.container_fg.style.position = "absolute"; 146 | this.container_bg.style.transform = "translate("+this.curScrollX+"px, "+this.curScrollY+"px)"; 147 | this.container_fg.style.transform = "translate("+this.curScrollX+"px, "+this.curScrollY+"px)"; 148 | }else{ 149 | this.container_bg.style.position = "fixed"; 150 | this.container_fg.style.position = "fixed"; 151 | this.container_bg.style.transform = "translate(0px, 0px)"; 152 | this.container_fg.style.transform = "translate(0px, 0px)"; 153 | } 154 | } 155 | 156 | _recomputeElementBoxes(){ 157 | if(!this.elementBoxes){ this.elementBoxes = []; } 158 | 159 | let elements = document.getElementsByTagName('p'); 160 | for(let i = 0; i < elements.length; i++){ 161 | if(i >= this.elementBoxes.length){ 162 | let cube = new THREE.Mesh(this.boxGeometry, this.elementMaterial); 163 | this.scene.add(cube); 164 | this.elementBoxes.push(cube); 165 | } 166 | 167 | let rect = elements[i].getBoundingClientRect(); 168 | this.elementBoxes[i].position.set( 169 | (rect.left + window.scrollX + (rect.width * 0.5)) / this.pixelsPerMeter, 170 | (rect.top + window.scrollY + (rect.height * 0.5)) / -this.pixelsPerMeter, 0.0); 171 | this.elementBoxes[i].scale.set(rect.width / this.pixelsPerMeter, rect.height / this.pixelsPerMeter, 0.5); 172 | this.elementBoxes[i].element = elements[i]; 173 | } 174 | } 175 | 176 | _recomputePixelsPerMeter(){ 177 | // Calculate the camera movement required to follow the scroll 178 | let oldPosition = this.camera.position.clone(); 179 | this.camera.position.set(0.0, 0.0, this.cameraDepth); 180 | this.camera.updateMatrixWorld(); 181 | this.camera.updateProjectionMatrix(); 182 | this.derp = new THREE.Vector3(0.0, 0, 0.0); 183 | this.derp.project(this.camera); 184 | this.derp.y = 1.0/window.innerHeight; 185 | this.derp.unproject(this.camera); 186 | this.camera.position.copy(oldPosition); 187 | this.camera.updateMatrixWorld(); 188 | this.camera.updateProjectionMatrix(); 189 | this.pixelsPerMeter = 1.0 / (this.derp.y * 2.0); 190 | return this.pixelsPerMeter; 191 | } 192 | 193 | _forcePixelsPerMeter(){ 194 | for(let i = 0; i < 10; i++){ 195 | let curPixelsPerMeter = this._recomputePixelsPerMeter(); 196 | // This is more compelling, but breaks the near clipping plane when zoomed in... 197 | //this.cameraDepth *= curPixelsPerMeter / this.forcedPixelsPerMeter; 198 | this.camera.fov *= curPixelsPerMeter / this.forcedPixelsPerMeter; 199 | this.camera.updateProjectionMatrix(); 200 | } 201 | } 202 | 203 | /** **INTERNAL**: This function recalculates the viewport based on the new window size. */ 204 | _onWindowResize() { 205 | let width = window.innerWidth, height = window.innerHeight; 206 | if(this.lastWidth != width || this.lastHeight != height){ 207 | this.camera.aspect = width / height; 208 | this.camera.updateProjectionMatrix(); 209 | this.renderer_bg.setSize(width, height); 210 | this.renderer_fg.setSize(width, height); 211 | this.lastWidth = width; 212 | this.lastHeight = height; 213 | this._forcePixelsPerMeter(); 214 | this._recomputeElementBoxes(); 215 | } 216 | this._setScroll(); 217 | } 218 | 219 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../node_modules/three/build/three.module.js'; 2 | import { GUI } from '../node_modules/three/examples/jsm/libs/lil-gui.module.min.js'; 3 | import World from './World.js'; 4 | import { OBJLoader } from '../node_modules/three/examples/jsm/loaders/OBJLoader.js'; 5 | 6 | /** The fundamental set up and animation structures for 3D Visualization */ 7 | export default class Main { 8 | 9 | constructor() { 10 | // Intercept Main Window Errors 11 | window.realConsoleError = console.error; 12 | window.addEventListener('error', (event) => { 13 | let path = event.filename.split("/"); 14 | this.display((path[path.length - 1] + ":" + event.lineno + " - " + event.message)); 15 | }); 16 | console.error = this.fakeError.bind(this); 17 | this.physicsScene = { softBodies: [] }; 18 | this.deferredConstructor(); 19 | } 20 | async deferredConstructor() { 21 | // Construct the render world 22 | this.world = new World(this); 23 | 24 | // Configure Settings 25 | this.overlayParams = { 26 | renderingMode: this.world.renderingMode, 27 | positioningMode: this.world.positioningMode, 28 | }; 29 | //this.gui = new GUI(); 30 | //this.gui.add( this.overlayParams, 'renderingMode', { Background: 0, Foreground: 1, ForegroundAndBackground: 2 } ) 31 | // .onFinishChange((value) => { this.world.renderingMode = value; }); 32 | //this.gui.add( this.overlayParams, 'positioningMode', { Fixed: 0, Absolute: 1 } ) 33 | // .onFinishChange((value) => { this.world.positioningMode = value; }); 34 | 35 | //this.gui.add(this.overlayParams, 'RemeshResolution', 0, 50, 1).onFinishChange((value) => { 36 | // if(this.mesh){ this.generateTetMesh(this.mesh); }}); 37 | //this.gui.add(this.overlayParams, 'TargetTriangles', 100, 5000, 100).onFinishChange((value) => { 38 | // if(this.mesh){ this.generateTetMesh(this.mesh); }}); 39 | //this.gui.add(this.overlayParams, 'MaxTriangleEdgeLength').onFinishChange((value) => { 40 | // if(this.mesh){ this.generateTetMesh(this.mesh); }}); 41 | //this.gui.add(this.overlayParams, 'MinTetVolume').onFinishChange((value) => { 42 | // if(this.mesh){ this.generateTetMesh(this.mesh); }}); 43 | 44 | } 45 | 46 | /** Update the simulation */ 47 | update(timeMS) { 48 | this.world._setScroll(timeMS); 49 | //this.world.stats.update(); 50 | } 51 | 52 | // Log Errors as
s over the main viewport 53 | fakeError(...args) { 54 | if (args.length > 0 && args[0]) { this.display(JSON.stringify(args[0])); } 55 | window.realConsoleError.apply(console, arguments); 56 | } 57 | 58 | display(text) { 59 | let errorNode = window.document.createElement("div"); 60 | errorNode.innerHTML = text.fontcolor("red"); 61 | window.document.getElementById("info").appendChild(errorNode); 62 | } 63 | } 64 | 65 | var main = new Main(); 66 | --------------------------------------------------------------------------------