├── icon.png ├── files ├── favicon.ico ├── favicon_white.ico ├── Inter-Regular.woff2 ├── Inter-SemiBold.woff2 ├── RobotoMono-Medium.woff2 ├── RobotoMono-Regular.woff2 ├── ic_arrow_drop_down_black_24dp.svg ├── ic_menu_black_24dp.svg ├── ic_code_black_24dp.svg ├── ic_close_black_24dp.svg ├── ic_mode_edit_black_24dp.svg ├── ic_search_black_24dp.svg ├── icon.svg └── main.css ├── examples ├── js │ └── libs │ │ └── ammo.wasm.wasm ├── models │ └── ldraw │ │ ├── ldraw_org_logo │ │ ├── Stamp145.png │ │ └── LDraw.org_logo_LICENSE.txt │ │ └── officialLibrary │ │ ├── CAreadme.txt │ │ ├── Readme.txt │ │ └── CAlicense.txt ├── main.css ├── jsm │ ├── environments │ │ └── RoomEnvironment.js │ ├── libs │ │ └── lil-gui.module.min.js │ ├── controls │ │ └── OrbitControls.js │ └── loaders │ │ └── LDrawLoader.js └── ldraw_animation.html ├── README.md └── LICENSE /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yomboprime/LDrawAnimation/HEAD/icon.png -------------------------------------------------------------------------------- /files/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yomboprime/LDrawAnimation/HEAD/files/favicon.ico -------------------------------------------------------------------------------- /files/favicon_white.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yomboprime/LDrawAnimation/HEAD/files/favicon_white.ico -------------------------------------------------------------------------------- /files/Inter-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yomboprime/LDrawAnimation/HEAD/files/Inter-Regular.woff2 -------------------------------------------------------------------------------- /files/Inter-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yomboprime/LDrawAnimation/HEAD/files/Inter-SemiBold.woff2 -------------------------------------------------------------------------------- /examples/js/libs/ammo.wasm.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yomboprime/LDrawAnimation/HEAD/examples/js/libs/ammo.wasm.wasm -------------------------------------------------------------------------------- /files/RobotoMono-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yomboprime/LDrawAnimation/HEAD/files/RobotoMono-Medium.woff2 -------------------------------------------------------------------------------- /files/RobotoMono-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yomboprime/LDrawAnimation/HEAD/files/RobotoMono-Regular.woff2 -------------------------------------------------------------------------------- /examples/models/ldraw/ldraw_org_logo/Stamp145.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yomboprime/LDrawAnimation/HEAD/examples/models/ldraw/ldraw_org_logo/Stamp145.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LDrawAnimation 2 | Animation with physics based on LDrawLoader by https://threejs.org 3 | 4 | Play online at: https://yomboprime.github.io/LDrawAnimation/examples/ldraw_animation.html 5 | -------------------------------------------------------------------------------- /files/ic_arrow_drop_down_black_24dp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /files/ic_menu_black_24dp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /files/ic_code_black_24dp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /files/ic_close_black_24dp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /files/ic_mode_edit_black_24dp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /files/ic_search_black_24dp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /files/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Juan Jose Luna Espinosa 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 | -------------------------------------------------------------------------------- /examples/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | background-color: #000; 4 | color: #fff; 5 | font-family: Monospace; 6 | font-size: 13px; 7 | line-height: 24px; 8 | overscroll-behavior: none; 9 | } 10 | 11 | a { 12 | color: #ff0; 13 | text-decoration: none; 14 | } 15 | 16 | a:hover { 17 | text-decoration: underline; 18 | } 19 | 20 | button { 21 | cursor: pointer; 22 | text-transform: uppercase; 23 | } 24 | 25 | #info { 26 | position: absolute; 27 | top: 0px; 28 | width: 100%; 29 | padding: 10px; 30 | box-sizing: border-box; 31 | text-align: center; 32 | -moz-user-select: none; 33 | -webkit-user-select: none; 34 | -ms-user-select: none; 35 | user-select: none; 36 | pointer-events: none; 37 | z-index: 1; /* TODO Solve this in HTML */ 38 | } 39 | 40 | a, button, input, select { 41 | pointer-events: auto; 42 | } 43 | 44 | .lil-gui { 45 | z-index: 2 !important; /* TODO Solve this in HTML */ 46 | } 47 | 48 | @media all and ( max-width: 640px ) { 49 | .lil-gui.root { 50 | right: auto; 51 | top: auto; 52 | max-height: 50%; 53 | max-width: 80%; 54 | bottom: 0; 55 | left: 0; 56 | } 57 | } 58 | 59 | #overlay { 60 | position: absolute; 61 | font-size: 16px; 62 | z-index: 2; 63 | top: 0; 64 | left: 0; 65 | width: 100%; 66 | height: 100%; 67 | display: flex; 68 | align-items: center; 69 | justify-content: center; 70 | flex-direction: column; 71 | background: rgba(0,0,0,0.7); 72 | } 73 | 74 | #overlay button { 75 | background: transparent; 76 | border: 0; 77 | border: 1px solid rgb(255, 255, 255); 78 | border-radius: 4px; 79 | color: #ffffff; 80 | padding: 12px 18px; 81 | text-transform: uppercase; 82 | cursor: pointer; 83 | } 84 | 85 | #notSupported { 86 | width: 50%; 87 | margin: auto; 88 | background-color: #f00; 89 | margin-top: 20px; 90 | padding: 10px; 91 | } 92 | -------------------------------------------------------------------------------- /examples/models/ldraw/officialLibrary/CAreadme.txt: -------------------------------------------------------------------------------- 1 | LDRAW.ORG 2 | PARTS LIBRARY AGREEMENT 3 | CAreadme.txt 4 | 5 | 6 | LDRAW.ORG PARTS LIBRARY AGREEMENT: CAreadme.txt 7 | 8 | 1. Introduction 9 | 10 | The following is the README text for the LDraw Parts Library 11 | licenced by the Creative Commons Attribution Licence 2.0 as 12 | specified by the LDraw Contributor Agreement (CA). Parts which 13 | fall under this agreement will contain the following line in 14 | their headers. 15 | 16 | 0 !LICENSE Redistributable under CCAL version 2.0 : see CAreadme.txt 17 | 18 | However this text is not a license! It is simply a handy reference 19 | for understanding the Legal Code (the full license), which can be 20 | found in the CAlicense.txt file - it is a human-readable expression 21 | of some key terms. Think of it as the user-friendly interface to 22 | the Legal Code beneath. This text itself has no legal value and its 23 | contents do not appear in the actual licence. For a deeper insight 24 | please visit: http://creativecommons.org/licenses/by/2.0/ 25 | 26 | 2. Rights 27 | 28 | You are free: 29 | 30 | * to copy, distribute, display, and use the CA approved LDraw 31 | Parts Library 32 | * to make derivative works 33 | * to make commercial use of the CA approved LDraw Parts Library 34 | 35 | Under the following conditions: 36 | 37 | 3. Attribution. 38 | 39 | You must give the original author credit. 40 | 41 | * For any reuse or distribution, you must make clear to others 42 | the licence terms of this library. 43 | * Any of these conditions can be waived if you get permission 44 | from the copyright holder. 45 | 46 | The LDraw Steering Committee (SteerCo) also holds an attribution 47 | to 'The LDraw Parts Library' in such Derivative Works to be sufficient 48 | in lieu of a full list of authors. 49 | 50 | Your fair use and other rights are in no way affected by the above. 51 | 52 | 4. Enforcements 53 | 54 | The single copyright holders are the sole entity responsible for 55 | enforcements of their copyrights. For purposes of enforcement 56 | LDraw.org considers the following good rules of thumb to determine 57 | if a new file is not a Derivative work. Anything else should be 58 | considered a Derivative work and an attribution would be required. 59 | If in doubt contact the current LDraw SteerCo. 60 | 61 | What is not considered a Derivative work? 62 | 63 | * Rendered images generated from the LDraw library. Rendering here 64 | covers any conversion of a 3D model file into a 2D image. 65 | 66 | * Model files containing references only (the source code for the 67 | part may NOT be included in any form) to parts in the official 68 | LDraw Parts Library at the time of their creation. Part files 69 | which are marked as unofficial at the time of creation may be 70 | included in full in the model file. If the sole or main purpose (as 71 | determined by the LDraw SteerCo in consultation with the potentially 72 | offending author and LDraw community at large) of the Model file 73 | is to include large numbers of unofficial parts then it WILL be 74 | considered a derivative work. 75 | 76 | * Alternative libraries of parts which LDraw.org does not own or 77 | enforce the copyright. If some or more of the parts in the library 78 | are converted from the LDraw Parts Library then they must be 79 | considered a Derivative work. 80 | 81 | LDraw.org, July 2007 82 | -------------------------------------------------------------------------------- /examples/jsm/environments/RoomEnvironment.js: -------------------------------------------------------------------------------- 1 | /** 2 | * https://github.com/google/model-viewer/blob/master/packages/model-viewer/src/three-components/EnvironmentScene.ts 3 | */ 4 | 5 | import { 6 | BackSide, 7 | BoxGeometry, 8 | Mesh, 9 | MeshBasicMaterial, 10 | MeshStandardMaterial, 11 | PointLight, 12 | Scene, 13 | } from '../../../build/three.module.js'; 14 | 15 | class RoomEnvironment extends Scene { 16 | 17 | constructor() { 18 | 19 | super(); 20 | 21 | const geometry = new BoxGeometry(); 22 | geometry.deleteAttribute( 'uv' ); 23 | 24 | const roomMaterial = new MeshStandardMaterial( { side: BackSide } ); 25 | const boxMaterial = new MeshStandardMaterial(); 26 | 27 | const mainLight = new PointLight( 0xffffff, 5.0, 28, 2 ); 28 | mainLight.position.set( 0.418, 16.199, 0.300 ); 29 | this.add( mainLight ); 30 | 31 | const room = new Mesh( geometry, roomMaterial ); 32 | room.position.set( - 0.757, 13.219, 0.717 ); 33 | room.scale.set( 31.713, 28.305, 28.591 ); 34 | this.add( room ); 35 | 36 | const box1 = new Mesh( geometry, boxMaterial ); 37 | box1.position.set( - 10.906, 2.009, 1.846 ); 38 | box1.rotation.set( 0, - 0.195, 0 ); 39 | box1.scale.set( 2.328, 7.905, 4.651 ); 40 | this.add( box1 ); 41 | 42 | const box2 = new Mesh( geometry, boxMaterial ); 43 | box2.position.set( - 5.607, - 0.754, - 0.758 ); 44 | box2.rotation.set( 0, 0.994, 0 ); 45 | box2.scale.set( 1.970, 1.534, 3.955 ); 46 | this.add( box2 ); 47 | 48 | const box3 = new Mesh( geometry, boxMaterial ); 49 | box3.position.set( 6.167, 0.857, 7.803 ); 50 | box3.rotation.set( 0, 0.561, 0 ); 51 | box3.scale.set( 3.927, 6.285, 3.687 ); 52 | this.add( box3 ); 53 | 54 | const box4 = new Mesh( geometry, boxMaterial ); 55 | box4.position.set( - 2.017, 0.018, 6.124 ); 56 | box4.rotation.set( 0, 0.333, 0 ); 57 | box4.scale.set( 2.002, 4.566, 2.064 ); 58 | this.add( box4 ); 59 | 60 | const box5 = new Mesh( geometry, boxMaterial ); 61 | box5.position.set( 2.291, - 0.756, - 2.621 ); 62 | box5.rotation.set( 0, - 0.286, 0 ); 63 | box5.scale.set( 1.546, 1.552, 1.496 ); 64 | this.add( box5 ); 65 | 66 | const box6 = new Mesh( geometry, boxMaterial ); 67 | box6.position.set( - 2.193, - 0.369, - 5.547 ); 68 | box6.rotation.set( 0, 0.516, 0 ); 69 | box6.scale.set( 3.875, 3.487, 2.986 ); 70 | this.add( box6 ); 71 | 72 | 73 | // -x right 74 | const light1 = new Mesh( geometry, createAreaLightMaterial( 50 ) ); 75 | light1.position.set( - 16.116, 14.37, 8.208 ); 76 | light1.scale.set( 0.1, 2.428, 2.739 ); 77 | this.add( light1 ); 78 | 79 | // -x left 80 | const light2 = new Mesh( geometry, createAreaLightMaterial( 50 ) ); 81 | light2.position.set( - 16.109, 18.021, - 8.207 ); 82 | light2.scale.set( 0.1, 2.425, 2.751 ); 83 | this.add( light2 ); 84 | 85 | // +x 86 | const light3 = new Mesh( geometry, createAreaLightMaterial( 17 ) ); 87 | light3.position.set( 14.904, 12.198, - 1.832 ); 88 | light3.scale.set( 0.15, 4.265, 6.331 ); 89 | this.add( light3 ); 90 | 91 | // +z 92 | const light4 = new Mesh( geometry, createAreaLightMaterial( 43 ) ); 93 | light4.position.set( - 0.462, 8.89, 14.520 ); 94 | light4.scale.set( 4.38, 5.441, 0.088 ); 95 | this.add( light4 ); 96 | 97 | // -z 98 | const light5 = new Mesh( geometry, createAreaLightMaterial( 20 ) ); 99 | light5.position.set( 3.235, 11.486, - 12.541 ); 100 | light5.scale.set( 2.5, 2.0, 0.1 ); 101 | this.add( light5 ); 102 | 103 | // +y 104 | const light6 = new Mesh( geometry, createAreaLightMaterial( 100 ) ); 105 | light6.position.set( 0.0, 20.0, 0.0 ); 106 | light6.scale.set( 1.0, 0.1, 1.0 ); 107 | this.add( light6 ); 108 | 109 | } 110 | 111 | } 112 | 113 | function createAreaLightMaterial( intensity ) { 114 | 115 | const material = new MeshBasicMaterial(); 116 | material.color.setScalar( intensity ); 117 | return material; 118 | 119 | } 120 | 121 | export { RoomEnvironment }; 122 | -------------------------------------------------------------------------------- /examples/models/ldraw/ldraw_org_logo/LDraw.org_logo_LICENSE.txt: -------------------------------------------------------------------------------- 1 | Content © 2002 LDraw.org. All content created specifically for LDraw.org, including but not limited to images, HTML code, and original unique content falls under the OpenContent Licence (OPL). Individual programs made available for download from this site are subject to their individual respective licenses and are used by permission. 2 | 3 | OpenContent License (OPL) 4 | Version 1.0, July 14, 1998. 5 | 6 | This document outlines the principles underlying the OpenContent (OC) movement and may be redistributed provided it remains unaltered. For legal purposes, this document is the license under which OpenContent is made available for use. 7 | 8 | The original version of this document may be found at http://www.opencontent.org/opl.shtml 9 | 10 | LICENSE 11 | 12 | Terms and Conditions for Copying, Distributing, and Modifying 13 | 14 | Items other than copying, distributing, and modifying the Content with which this license was distributed (such as using, etc.) are outside the scope of this license. 15 | 16 | You may copy and distribute exact replicas of the OpenContent (OC) as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the OC a copy of this License along with the OC. You may at your option charge a fee for the media and/or handling involved in creating a unique copy of the OC for use offline, you may at your option offer instructional support for the OC in exchange for a fee, or you may at your option offer warranty in exchange for a fee. You may not charge a fee for the OC itself. You may not charge a fee for the sole service of providing access to and/or use of the OC via a network (e.g. the Internet), whether it be via the world wide web, FTP, or any other method. 17 | You may modify your copy or copies of the OpenContent or any portion of it, thus forming works based on the Content, and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: 18 | 19 | a) You must cause the modified content to carry prominent notices stating that you changed it, the exact nature and content of the changes, and the date of any change. 20 | 21 | b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the OC or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License, unless otherwise permitted under applicable Fair Use law. 22 | 23 | These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the OC, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the OC, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Exceptions are made to this requirement to release modified works free of charge under this license only in compliance with Fair Use law where applicable. 24 | You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to copy, distribute or modify the OC. These actions are prohibited by law if you do not accept this License. Therefore, by distributing or translating the OC, or by deriving works herefrom, you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or translating the OC. 25 | 26 | NO WARRANTY 27 | 28 | BECAUSE THE OPENCONTENT (OC) IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE OC, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE OC "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK OF USE OF THE OC IS WITH YOU. SHOULD THE OC PROVE FAULTY, INACCURATE, OR OTHERWISE UNACCEPTABLE YOU ASSUME THE COST OF ALL NECESSARY REPAIR OR CORRECTION. 29 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MIRROR AND/OR REDISTRIBUTE THE OC AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE OC, EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 30 | -------------------------------------------------------------------------------- /examples/models/ldraw/officialLibrary/Readme.txt: -------------------------------------------------------------------------------- 1 | LDraw Readme File 2 | 3 | Welcome to LDraw 4 | 5 | This short readme file explains what files and subdirectories are present in 6 | your LDraw installation, described the LDraw library structure and and has links 7 | to some sites on the internet where you can find help and further information. 8 | 9 | * LDraw program directory contents 10 | * LDraw library structure 11 | * Where to find further information 12 | * Parts library updates 13 | 14 | -------------------------------------------------------------------------------- 15 | * LDraw program directory contents: 16 | - Program executables: 17 | mklist.exe - This is a utility that creates a list of available 18 | parts. This list (parts.lst) is used by LDraw tools 19 | as the available parts list. You should re-run mklist 20 | after installing new parts updates. 21 | 22 | - Support Files: 23 | Parts.lst - This is your listing of all usable parts available. 24 | This list is created by running mklist.exe and choosing 25 | to create the list sorted Numerically or by Description. 26 | Most people use Description sorting, but you can 27 | change to whichever way you prefer at any time. 28 | mklist1_6.zip - Zip archive of the MKList source code. 29 | 30 | - Informational Files: 31 | Readme.txt - This file you are currently reading. 32 | 33 | - Subdirectories: 34 | \MODELS\ - This directory is where your model .dat files are stored. 35 | There are two sample model .dat files installed for you 36 | to look at - Car.dat and Pyramid.dat. 37 | \P\ - This directory is where parts primitives are located. 38 | Parts primitives are tyically highly reusable components 39 | used by the part files in the LDraw library. 40 | \P\48\ - This directory is where high resolution parts primitives 41 | are located. These are typically used for large curved 42 | parts where excessive scaling of the regular curved 43 | primitives would produce an undesriable result. 44 | \PARTS\ - This directory holds all the actual parts that can be used 45 | in creating or rendering your models. A list of these 46 | parts can be seen by viewing the parts.lst file. 47 | \PARTS\S\ - This directory holds sub-parts that are used by the LDraw 48 | parts to optimise file size and improve parts development 49 | efficiancy. 50 | 51 | -------------------------------------------------------------------------------- 52 | * LDraw library structure: 53 | 54 | The official LDraw library is segmented into four categories: 55 | 56 | - OfficialCA - The library of officially released parts for which the 57 | authors have agreed to the Contributor Agreement, allowing 58 | their work to be re-distributed. Full details of this 59 | agreement can be found in the CAreadme.txt and 60 | CAlicense.txt files in the same diectory as this file. 61 | This download is restricted to generic colour versions of 62 | each part and does not contain duplicate copies of part 63 | files where different numbers have been used for the same 64 | physical part. This library may be re-distributed, subject 65 | to the conditions laid out in CAreadme.txt. 66 | 67 | - OfficialCA_a - The library of officially released part aliases. This 68 | includes generic colour versions of parts that are 69 | physically identical to parts in the OfficialCA library, 70 | but have a different part number, either because of 71 | production differences between opaque and transparent 72 | parts or due to evolution of the part numbering scheme. 73 | 74 | - OfficialCA_p - The library of officially released physical colour parts. 75 | This includes hard-coded colour versions of parts or 76 | composite parts. 77 | 78 | - OfficialNonCA - The library of officially released parts for which the 79 | authors have not agreed to the Contributor Agreement, or 80 | where we have been unable to contact the original author. 81 | This download is restricted to generic colour versions of 82 | each part and does not contain duplicate copies of part 83 | files where different numbers have been used for the same 84 | physical part. 85 | This library MAY NOT be re-distributed, as detailed in the 86 | conditions laid out in NonCAreadme.txt file. 87 | 88 | -------------------------------------------------------------------------------- 89 | * Where to find further information 90 | 91 | For more information on LDraw, check out these internet resources: 92 | 93 | - LDraw.org - http://www.ldraw.org/ 94 | Centralized LDraw Resources on the internet. 95 | Parts updates, Utility programs for using and enhancing LDraw, and more. 96 | 97 | - LUGNET - http://www.lugnet.com/ 98 | The Lego Users Group NETwork (LUGNET) - A great place for fans of Lego. 99 | LUGNET has many topic-specific newsgroups that discuss LDraw and other forms 100 | of Lego-type CAD. 101 | 102 | - The LDraw Parts Tracker - http://www.ldraw.org/library/tracker/ 103 | The web-based system for managing the development of new LDraw parts. Here 104 | you will find unofficial versions of new parts and updates to existing parts. 105 | As these are unofficial parts, they may be incomplete, or inaccurate, and it 106 | is possible that when they are officially released they may be changed in 107 | ways that could change any model in which you have used them. 108 | 109 | - The LDraw Frequently Asked Questions (FAQ): 110 | http://www.ldraw.org/faq/ 111 | 112 | -------------------------------------------------------------------------------- 113 | * Parts library updates: 114 | 115 | - If you have not already done so, you should visit www.ldraw.org and 116 | download and install the current complete package of LDraw parts. 117 | 118 | - Periodically, new parts and part fixes are released in small updates, 119 | available from www.ldraw.org. These updates should be downloaded and 120 | installed as they become available. Please remember that OLD updates 121 | should not be installed over NEW or NEWER updates. Doing so might 122 | overwrite a fixed version of a part with an older version. 123 | 124 | LDraw Update 2010-02 125 | --end of file-- 126 | -------------------------------------------------------------------------------- /examples/models/ldraw/officialLibrary/CAlicense.txt: -------------------------------------------------------------------------------- 1 | CREATIVE COMMONS 2 | LEGAL CODE 3 | Attribution 2.0 4 | 5 | License 6 | 7 | THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS 8 | CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS 9 | PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE 10 | WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS 11 | PROHIBITED. 12 | 13 | BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND 14 | AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. THE LICENSOR GRANTS 15 | YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF 16 | SUCH TERMS AND CONDITIONS. 17 | 18 | 1. Definitions 19 | 20 | 1. "Collective Work" means a work, such as a periodical issue, 21 | anthology or encyclopedia, in which the Work in its entirety in 22 | unmodified form, along with a number of other contributions, 23 | constituting separate and independent works in themselves, are 24 | assembled into a collective whole. A work that constitutes a 25 | Collective Work will not be considered a Derivative Work (as 26 | defined below) for the purposes of this License. 27 | 28 | 2. "Derivative Work" means a work based upon the Work or upon the 29 | Work and other pre-existing works, such as a translation, 30 | musical arrangement, dramatization, fictionalization, motion 31 | picture version, sound recording, art reproduction, abridgment, 32 | condensation, or any other form in which the Work may be recast, 33 | transformed, or adapted, except that a work that constitutes a 34 | Collective Work will not be considered a Derivative Work for the 35 | purpose of this License. For the avoidance of doubt, where the 36 | Work is a musical composition or sound recording, the 37 | synchronization of the Work in timed-relation with a moving 38 | image ("synching") will be considered a Derivative Work for the 39 | purpose of this License. 40 | 41 | 3. "Licensor" means the individual or entity that offers the Work 42 | under the terms of this License. 43 | 44 | 4. "Original Author" means the individual or entity who created the 45 | Work. 46 | 47 | 5. "Work" means the copyrightable work of authorship offered under 48 | the terms of this License. 49 | 50 | 6. "You" means an individual or entity exercising rights under this 51 | License who has not previously violated the terms of this 52 | License with respect to the Work, or who has received express 53 | permission from the Licensor to exercise rights under this 54 | License despite a previous violation. 55 | 56 | 57 | 2. Fair Use Rights. Nothing in this license is intended to reduce, 58 | limit, or restrict any rights arising from fair use, first sale or 59 | other limitations on the exclusive rights of the copyright owner 60 | under copyright law or other applicable laws. 61 | 62 | 3. License Grant. Subject to the terms and conditions of this License, 63 | Licensor hereby grants You a worldwide, royalty-free, 64 | non-exclusive, perpetual (for the duration of the applicable 65 | copyright) license to exercise the rights in the Work as stated 66 | below: 67 | 68 | 1. to reproduce the Work, to incorporate the Work into one or more 69 | Collective Works, and to reproduce the Work as incorporated in 70 | the Collective Works; 71 | 72 | 2. to create and reproduce Derivative Works; 73 | 74 | 3. to distribute copies or phonorecords of, display publicly, 75 | perform publicly, and perform publicly by means of a digital 76 | audio transmission the Work including as incorporated in 77 | Collective Works; 78 | 79 | 4. to distribute copies or phonorecords of, display publicly, 80 | perform publicly, and perform publicly by means of a digital 81 | audio transmission Derivative Works. 82 | 83 | 5. For the avoidance of doubt, where the work is a musical 84 | composition: 85 | 86 | 1. Performance Royalties Under Blanket Licenses. Licensor 87 | waives the exclusive right to collect, whether 88 | individually or via a performance rights society 89 | (e.g. ASCAP, BMI, SESAC), royalties for the public 90 | performance or public digital performance (e.g. webcast) 91 | of the Work. 92 | 93 | 2. Mechanical Rights and Statutory Royalties. Licensor waives 94 | the exclusive right to collect, whether individually or 95 | via a music rights agency or designated agent (e.g. Harry 96 | Fox Agency), royalties for any phonorecord You create from 97 | the Work ("cover version") and distribute, subject to the 98 | compulsory license created by 17 USC Section 115 of the US 99 | Copyright Act (or the equivalent in other jurisdictions). 100 | 101 | 6. Webcasting Rights and Statutory Royalties. For the avoidance of 102 | doubt, where the Work is a sound recording, Licensor waives the 103 | exclusive right to collect, whether individually or via a 104 | performance-rights society (e.g. SoundExchange), royalties for 105 | the public digital performance (e.g. webcast) of the Work, 106 | subject to the compulsory license created by 17 USC Section 114 107 | of the US Copyright Act (or the equivalent in other 108 | jurisdictions). 109 | 110 | 111 | The above rights may be exercised in all media and formats whether now 112 | known or hereafter devised. The above rights include the right to make 113 | such modifications as are technically necessary to exercise the rights 114 | in other media and formats. All rights not expressly granted by 115 | Licensor are hereby reserved. 116 | 117 | 4. Restrictions.The license granted in Section 3 above is expressly 118 | made subject to and limited by the following restrictions: 119 | 120 | 1. You may distribute, publicly display, publicly perform, or 121 | publicly digitally perform the Work only under the terms of this 122 | License, and You must include a copy of, or the Uniform Resource 123 | Identifier for, this License with every copy or phonorecord of 124 | the Work You distribute, publicly display, publicly perform, or 125 | publicly digitally perform. You may not offer or impose any 126 | terms on the Work that alter or restrict the terms of this 127 | License or the recipients' exercise of the rights granted 128 | hereunder. You may not sublicense the Work. You must keep intact 129 | all notices that refer to this License and to the disclaimer of 130 | warranties. You may not distribute, publicly display, publicly 131 | perform, or publicly digitally perform the Work with any 132 | technological measures that control access or use of the Work in 133 | a manner inconsistent with the terms of this License 134 | Agreement. The above applies to the Work as incorporated in a 135 | Collective Work, but this does not require the Collective Work 136 | apart from the Work itself to be made subject to the terms of 137 | this License. If You create a Collective Work, upon notice from 138 | any Licensor You must, to the extent practicable, remove from 139 | the Collective Work any reference to such Licensor or the 140 | Original Author, as requested. If You create a Derivative Work, 141 | upon notice from any Licensor You must, to the extent 142 | practicable, remove from the Derivative Work any reference to 143 | such Licensor or the Original Author, as requested. 144 | 145 | 2. If you distribute, publicly display, publicly perform, or 146 | publicly digitally perform the Work or any Derivative Works or 147 | Collective Works, You must keep intact all copyright notices for 148 | the Work and give the Original Author credit reasonable to the 149 | medium or means You are utilizing by conveying the name (or 150 | pseudonym if applicable) of the Original Author if supplied; the 151 | title of the Work if supplied; to the extent reasonably 152 | practicable, the Uniform Resource Identifier, if any, that 153 | Licensor specifies to be associated with the Work, unless such 154 | URI does not refer to the copyright notice or licensing 155 | information for the Work; and in the case of a Derivative Work, 156 | a credit identifying the use of the Work in the Derivative Work 157 | (e.g., "French translation of the Work by Original Author," or 158 | "Screenplay based on original Work by Original Author"). Such 159 | credit may be implemented in any reasonable manner; provided, 160 | however, that in the case of a Derivative Work or Collective 161 | Work, at a minimum such credit will appear where any other 162 | comparable authorship credit appears and in a manner at least as 163 | prominent as such other comparable authorship credit. 164 | 165 | 166 | 5. Representations, Warranties and Disclaimer 167 | 168 | UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, 169 | LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR 170 | WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, 171 | STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF 172 | TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, 173 | NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, 174 | OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT 175 | DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED 176 | WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. 177 | 178 | 6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY 179 | APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY 180 | LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE 181 | OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE 182 | WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 183 | DAMAGES. 184 | 185 | 7. Termination 186 | 187 | 1. This License and the rights granted hereunder will terminate 188 | automatically upon any breach by You of the terms of this 189 | License. Individuals or entities who have received Derivative 190 | Works or Collective Works from You under this License, however, 191 | will not have their licenses terminated provided such 192 | individuals or entities remain in full compliance with those 193 | licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any 194 | termination of this License. 195 | 196 | 2. Subject to the above terms and conditions, the license granted 197 | here is perpetual (for the duration of the applicable copyright 198 | in the Work). Notwithstanding the above, Licensor reserves the 199 | right to release the Work under different license terms or to 200 | stop distributing the Work at any time; provided, however that 201 | any such election will not serve to withdraw this License (or 202 | any other license that has been, or is required to be, granted 203 | under the terms of this License), and this License will continue 204 | in full force and effect unless terminated as stated above. 205 | 206 | 207 | 8. Miscellaneous 208 | 209 | 1. Each time You distribute or publicly digitally perform the Work 210 | or a Collective Work, the Licensor offers to the recipient a 211 | license to the Work on the same terms and conditions as the 212 | license granted to You under this License. 213 | 214 | 2. Each time You distribute or publicly digitally perform a 215 | Derivative Work, Licensor offers to the recipient a license to 216 | the original Work on the same terms and conditions as the 217 | license granted to You under this License. 218 | 219 | 3. If any provision of this License is invalid or unenforceable 220 | under applicable law, it shall not affect the validity or 221 | enforceability of the remainder of the terms of this License, 222 | and without further action by the parties to this agreement, 223 | such provision shall be reformed to the minimum extent necessary 224 | to make such provision valid and enforceable. 225 | 226 | 4. No term or provision of this License shall be deemed waived and 227 | no breach consented to unless such waiver or consent shall be in 228 | writing and signed by the party to be charged with such waiver 229 | or consent. 230 | 231 | 5. This License constitutes the entire agreement between the 232 | parties with respect to the Work licensed here. There are no 233 | understandings, agreements or representations with respect to 234 | the Work not specified here. Licensor shall not be bound by any 235 | additional provisions that may appear in any communication from 236 | You. This License may not be modified without the mutual written 237 | agreement of the Licensor and You. 238 | 239 | 240 | 241 | For the purposes of this license The Work is define as the LDraw Parts 242 | Library (and all parts within it) which have fall under the LDRaw 243 | Contributor Agreement. These parts may be identified by the appearance 244 | of the following line in their header as placed their by the LDraw 245 | Library Administrators or the Original Author. 246 | 247 | 0 !LICENSE Redistributable under CCAL version 2.0 : see CAreadme.txt 248 | 249 | -------------------------------------------------------------------------------- /files/main.css: -------------------------------------------------------------------------------- 1 | :root { 2 | color-scheme: light dark; 3 | 4 | --background-color: #fff; 5 | --secondary-background-color: #f7f7f7; 6 | 7 | --color-blue: #049EF4; 8 | --text-color: #444; 9 | --secondary-text-color: #9e9e9e; 10 | 11 | --font-size: 16px; 12 | --line-height: 26px; 13 | 14 | --border-style: 1px solid #E8E8E8; 15 | --header-height: 48px; 16 | --panel-width: 300px; 17 | --panel-padding: 16px; 18 | --icon-size: 20px; 19 | } 20 | 21 | @media (prefers-color-scheme: dark) { 22 | 23 | :root { 24 | --background-color: #222; 25 | --secondary-background-color: #2e2e2e; 26 | 27 | --text-color: #bbb; 28 | --secondary-text-color: #666; 29 | 30 | --border-style: 1px solid #444; 31 | } 32 | 33 | #previewsToggler { 34 | filter: invert(100%); 35 | } 36 | 37 | } 38 | 39 | @font-face { 40 | font-family: 'Roboto Mono'; 41 | src: local('Roboto Mono'), local('RobotoMono-Regular'), url('../files/RobotoMono-Regular.woff2') format('woff2'); 42 | font-style: normal; 43 | font-weight: 400; 44 | } 45 | 46 | @font-face { 47 | font-family: 'Roboto Mono'; 48 | src: local('Roboto Mono Medium'), local('RobotoMono-Medium'), url('../files/RobotoMono-Medium.woff2') format('woff2'); 49 | font-style: normal; 50 | font-weight: 500; 51 | } 52 | 53 | * { 54 | box-sizing: border-box; 55 | -webkit-font-smoothing: antialiased; 56 | -moz-osx-font-smoothing: grayscale; 57 | } 58 | 59 | html, body { 60 | height: 100%; 61 | } 62 | 63 | html { 64 | font-size: calc(var(--font-size) - 1px); 65 | line-height: calc(var(--line-height) - 1px); 66 | } 67 | 68 | body { 69 | font-family: 'Roboto Mono', monospace; 70 | margin: 0px; 71 | color: var(--text-color); 72 | background-color: var(--background-color); 73 | } 74 | 75 | a { 76 | text-decoration: none; 77 | } 78 | 79 | h1 { 80 | font-size: 18px; 81 | line-height: 24px; 82 | font-weight: 500; 83 | } 84 | 85 | h2 { 86 | padding: 0; 87 | margin: 16px 0; 88 | font-size: calc(var(--font-size) - 1px); 89 | line-height: var(--line-height); 90 | font-weight: 500; 91 | color: var(--color-blue); 92 | } 93 | 94 | h3 { 95 | margin: 0; 96 | font-weight: 500; 97 | font-size: calc(var(--font-size) - 1px); 98 | line-height: var(--line-height); 99 | color: var(--secondary-text-color); 100 | } 101 | 102 | h1 a { 103 | color: var(--color-blue); 104 | } 105 | 106 | #header { 107 | display: flex; 108 | height: var(--header-height); 109 | border-bottom: var(--border-style); 110 | align-items: center; 111 | } 112 | #header h1 { 113 | padding-left: var(--panel-padding); 114 | flex: 1; 115 | display: flex; 116 | align-items: center; 117 | color: var(--color-blue); 118 | } 119 | #header #version { 120 | border: 1px solid var(--color-blue); 121 | color: var(--color-blue); 122 | border-radius: 4px; 123 | line-height: 16px; 124 | padding: 0px 2px; 125 | margin-left: 6px; 126 | font-size: .9rem; 127 | } 128 | #panel { 129 | position: fixed; 130 | z-index: 100; 131 | left: 0px; 132 | width: var(--panel-width); 133 | height: 100%; 134 | overflow: auto; 135 | border-right: var(--border-style); 136 | display: flex; 137 | flex-direction: column; 138 | transition: 0s 0s height; 139 | } 140 | 141 | #panel #exitSearchButton { 142 | width: 48px; 143 | height: 48px; 144 | display: none; 145 | background-color: var(--text-color); 146 | background-size: var(--icon-size); 147 | -webkit-mask-image: url(../files/ic_close_black_24dp.svg); 148 | -webkit-mask-position: 50% 50%; 149 | -webkit-mask-repeat: no-repeat; 150 | mask-image: url(../files/ic_close_black_24dp.svg); 151 | mask-position: 50% 50%; 152 | mask-repeat: no-repeat; 153 | cursor: pointer; 154 | margin-right: 0px; 155 | } 156 | 157 | #panel.searchFocused #exitSearchButton { 158 | display: block; 159 | } 160 | 161 | #panel.searchFocused #language { 162 | display: none; 163 | } 164 | 165 | #panel.searchFocused #filterInput { 166 | -webkit-mask-image: none; 167 | mask-image: none; 168 | background-color: inherit; 169 | padding-left: 0; 170 | } 171 | 172 | #panel #expandButton { 173 | width: 48px; 174 | height: 48px; 175 | margin-right: 4px; 176 | margin-left: 4px; 177 | display: none; 178 | cursor: pointer; 179 | background-color: var(--text-color); 180 | background-size: var(--icon-size); 181 | -webkit-mask-image: url(../files/ic_menu_black_24dp.svg); 182 | -webkit-mask-position: 50% 50%; 183 | -webkit-mask-repeat: no-repeat; 184 | mask-image: url(../files/ic_menu_black_24dp.svg); 185 | mask-position: 50% 50%; 186 | mask-repeat: no-repeat; 187 | } 188 | 189 | #panel #sections { 190 | display: flex; 191 | justify-content: center; 192 | z-index: 1000; 193 | position: relative; 194 | height: 100%; 195 | align-items: center; 196 | font-weight: 500; 197 | } 198 | 199 | #panel #sections * { 200 | padding: 0 var(--panel-padding); 201 | height: 100%; 202 | position: relative; 203 | display: flex; 204 | justify-content: center; 205 | align-items: center; 206 | } 207 | #panel #sections .selected:after { 208 | content: ""; 209 | position: absolute; 210 | left: 0; 211 | right: 0; 212 | bottom: -1px; 213 | border-bottom: 1px solid var(--text-color); 214 | } 215 | #panel #sections a { 216 | color: var(--secondary-text-color); 217 | } 218 | 219 | body.home #panel #sections { 220 | display: none; 221 | } 222 | 223 | 224 | #panel #inputWrapper { 225 | display: flex; 226 | align-items: center; 227 | height: var(--header-height); 228 | padding: 0 0 0 var(--panel-padding); 229 | position: relative; 230 | background: var(--background-color); 231 | } 232 | #panel #inputWrapper:after { 233 | position: absolute; 234 | left: 0; 235 | right: 0; 236 | bottom: 0; 237 | border-bottom: var(--border-style); 238 | content: ""; 239 | } 240 | 241 | #panel #filterInput { 242 | flex: 1; 243 | width: 100%; 244 | font-size: 1rem; 245 | font-weight: 500; 246 | color: var(--text-color); 247 | outline: none; 248 | border: 0px; 249 | background-color: var(--text-color); 250 | background-size: var(--icon-size); 251 | -webkit-mask-image: url(../files/ic_search_black_24dp.svg); 252 | -webkit-mask-position: 0 50%; 253 | -webkit-mask-repeat: no-repeat; 254 | mask-image: url(../files/ic_search_black_24dp.svg); 255 | mask-position: 0 50%; 256 | mask-repeat: no-repeat; 257 | font-family: 'Roboto Mono', monospace; 258 | } 259 | 260 | #panel #language { 261 | font-family: 'Roboto Mono', monospace; 262 | font-size: 1rem; 263 | font-weight: 500; 264 | color: var(--text-color); 265 | border: 0px; 266 | background-image: url(ic_arrow_drop_down_black_24dp.svg); 267 | background-size: var(--icon-size); 268 | background-repeat: no-repeat; 269 | background-position: right center; 270 | background-color: var(--background-color); 271 | padding: 2px 24px 4px 24px; 272 | -webkit-appearance: none; 273 | -moz-appearance: none; 274 | appearance: none; 275 | margin-right: 10px; 276 | text-align-last: right; 277 | } 278 | 279 | #panel #language:focus { 280 | outline: none; 281 | } 282 | 283 | #contentWrapper { 284 | flex: 1; 285 | overflow: hidden; 286 | display: flex; 287 | flex-direction: column; 288 | } 289 | 290 | #panel #content { 291 | flex: 1; 292 | overflow-y: auto; 293 | overflow-x: hidden; 294 | -webkit-overflow-scrolling: touch; 295 | padding: 0 var(--panel-padding) var(--panel-padding) var(--panel-padding); 296 | } 297 | 298 | #panel #content ul { 299 | list-style-type: none; 300 | padding: 0px; 301 | margin: 0px 0 20px 0; 302 | } 303 | #panel #content ul li { 304 | margin: 1px 0; 305 | } 306 | 307 | #panel #content h2:not(.hidden) { 308 | margin-top: 16px; 309 | border-top: none; 310 | padding-top: 0; 311 | } 312 | 313 | #panel #content h2:not(.hidden) ~ h2 { 314 | margin-top: 32px; 315 | border-top: var(--border-style); 316 | padding-top: 12px; 317 | } 318 | 319 | #panel #content h3 { 320 | color: var(--text-color); 321 | font-weight: 900; 322 | } 323 | 324 | #panel #content a { 325 | position: relative; 326 | color: var(--text-color); 327 | } 328 | 329 | #panel #content a:hover, 330 | #panel #content a:hover .spacer, 331 | #panel #content .selected { 332 | color: var(--color-blue); 333 | } 334 | 335 | #panel #content .selected { 336 | text-decoration: underline; 337 | } 338 | 339 | #panel #content .hidden { 340 | display: none !important; 341 | } 342 | 343 | #panel #content #previewsToggler { 344 | cursor: pointer; 345 | float: right; 346 | margin-top: 18px; 347 | margin-bottom: -18px; 348 | opacity: 0.25; 349 | } 350 | 351 | #panel #content.minimal .card { 352 | background-color: transparent; 353 | margin-bottom: 4px; 354 | } 355 | 356 | #panel #content.minimal .cover { 357 | display: none; 358 | } 359 | 360 | #panel #content.minimal .title { 361 | padding: 0; 362 | } 363 | 364 | #panel #content.minimal #previewsToggler { 365 | opacity: 1; 366 | } 367 | 368 | body.home #panel #content h2 { 369 | margin-bottom: 2px; 370 | padding-bottom: 0px; 371 | margin-top: 18px; 372 | border-top: none; 373 | padding-top: 6px; 374 | } 375 | 376 | .spacer { 377 | color: var(--secondary-text-color); 378 | margin-left: 2px; 379 | padding-right: 2px; 380 | } 381 | 382 | #viewer, 383 | iframe { 384 | position: absolute; 385 | border: 0px; 386 | left: 0; 387 | right: 0; 388 | width: 100%; 389 | height: 100%; 390 | overflow: auto; 391 | } 392 | 393 | iframe#viewer { 394 | display: none; /* Workaround: The iframe has white background in Chrome for some reason */ 395 | } 396 | 397 | #viewer { 398 | padding-left: var(--panel-width); 399 | } 400 | 401 | #button { 402 | position: fixed; 403 | bottom: 16px; 404 | right: 16px; 405 | 406 | padding: 12px; 407 | border-radius: 50%; 408 | margin-bottom: 0px; 409 | 410 | background-color: #FFF; 411 | opacity: .9; 412 | z-index: 999; 413 | 414 | box-shadow: 0 0 4px rgba(0,0,0,.15); 415 | } 416 | #button:hover { 417 | cursor: pointer; 418 | opacity: 1; 419 | } 420 | #button img { 421 | display: block; 422 | width: var(--icon-size); 423 | } 424 | 425 | #button.text { 426 | border-radius: 25px; 427 | padding-right: 20px; 428 | padding-left: 20px; 429 | color: var(--color-blue); 430 | opacity: 1; 431 | font-weight: 500; 432 | } 433 | 434 | 435 | #projects { 436 | display: grid; 437 | grid-template-columns: repeat(6, 1fr); 438 | line-height: 0; 439 | } 440 | #projects a { 441 | overflow: hidden; 442 | } 443 | #projects a img { 444 | width: 100%; 445 | transform: scale(1.0); 446 | transition: 0.15s transform; 447 | } 448 | #projects a:hover img { 449 | transform: scale(1.08); 450 | } 451 | 452 | 453 | 454 | @media all and ( min-width: 1500px ) { 455 | #projects { 456 | grid-template-columns: repeat(7, 1fr); 457 | } 458 | } 459 | 460 | @media all and ( min-width: 1700px ) { 461 | :root { 462 | --panel-width: 360px; 463 | --font-size: 18px; 464 | --line-height: 28px; 465 | --header-height: 56px; 466 | --icon-size: 24px; 467 | } 468 | #projects { 469 | grid-template-columns: repeat(8, 1fr); 470 | } 471 | } 472 | 473 | @media all and ( min-width: 1900px ) { 474 | 475 | #projects { 476 | grid-template-columns: repeat(9, 1fr); 477 | } 478 | 479 | } 480 | 481 | @media all and ( max-width: 1300px ) { 482 | #projects { 483 | grid-template-columns: repeat(6, 1fr); 484 | } 485 | } 486 | 487 | @media all and ( max-width: 1100px ) { 488 | #projects { 489 | grid-template-columns: repeat(5, 1fr); 490 | } 491 | } 492 | 493 | @media all and ( max-width: 900px ) { 494 | #projects { 495 | grid-template-columns: repeat(4, 1fr); 496 | } 497 | } 498 | 499 | @media all and ( max-width: 700px ) { 500 | #projects { 501 | grid-template-columns: repeat(3, 1fr); 502 | } 503 | } 504 | 505 | 506 | .card { 507 | border-radius: 3px; 508 | overflow: hidden; 509 | background-color: var(--secondary-background-color); 510 | padding-bottom: 6px; 511 | margin-bottom: 16px; 512 | } 513 | 514 | .card.selected { 515 | box-shadow: 0 0 0 3px var(--color-blue); 516 | text-decoration: none !important; 517 | } 518 | 519 | .card .cover { 520 | padding-bottom: 56.25%; /* 16:9 aspect ratio */ 521 | position: relative; 522 | overflow: hidden; 523 | } 524 | 525 | .card .cover img { 526 | position: absolute; 527 | width: 100%; 528 | top: 50%; 529 | left: 50%; 530 | transform: translate(-50%, -50%); 531 | } 532 | 533 | .card .title { 534 | padding: 8px 12px 4px; 535 | font-size: calc(var(--font-size) - 1px); 536 | font-weight: 500; 537 | line-height: calc(var(--line-height) - 6px); 538 | } 539 | 540 | /* mobile */ 541 | 542 | @media all and ( max-width: 640px ) { 543 | 544 | :root { 545 | --header-height: 56px; 546 | --icon-size: 24px; 547 | } 548 | 549 | #projects { 550 | grid-template-columns: repeat(2, 1fr); 551 | } 552 | 553 | #panel #expandButton { 554 | display: block; 555 | } 556 | #panel { 557 | position: absolute; 558 | left: 0; 559 | top: 0; 560 | width: 100%; 561 | right: 0; 562 | z-index: 1000; 563 | overflow-x: hidden; 564 | transition: 0s 0s height; 565 | border: none; 566 | height: var(--header-height); 567 | transition: 0s 0.2s height; 568 | } 569 | #panel.open { 570 | height: 100%; 571 | transition: 0s 0s height; 572 | } 573 | 574 | #panelScrim { 575 | pointer-events: none; 576 | background-color: rgba(0,0,0,0); 577 | position: absolute; 578 | left: 0; 579 | right: 0; 580 | top: 0; 581 | bottom: 0; 582 | z-index: 1000; 583 | pointer-events: none; 584 | transition: .2s background-color; 585 | } 586 | #panel.open #panelScrim { 587 | pointer-events: auto; 588 | background-color: rgba(0,0,0,0.4); 589 | } 590 | 591 | #contentWrapper { 592 | position: absolute; 593 | right: 0; 594 | top: 0; 595 | bottom: 0; 596 | background: var(--background-color); 597 | box-shadow: 0 0 8px rgba(0,0,0,.1); 598 | width: calc(100vw - 60px); 599 | max-width: 360px; 600 | z-index: 10000; 601 | transition: .25s transform; 602 | overflow-x: hidden; 603 | margin-right: -380px; 604 | line-height: 2rem; 605 | } 606 | #panel.open #contentWrapper { 607 | transform: translate3d(-380px, 0 ,0); 608 | } 609 | #viewer, 610 | iframe { 611 | left: 0; 612 | top: var(--header-height); 613 | width: 100%; 614 | height: calc(100% - var(--header-height)); 615 | } 616 | #viewer { 617 | padding-left: 0; 618 | } 619 | } 620 | -------------------------------------------------------------------------------- /examples/ldraw_animation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | LDraw animation 5 | 6 | 7 | 8 | 16 | 17 | 18 | 19 |
20 | three.js - LDraw animation 21 |
22 | 23 | 24 | 25 | 670 | 671 | 672 |
673 |
674 | 675 |
676 | This software uses the LDraw Parts Library 677 |
678 |
679 | 680 | 681 | 682 | -------------------------------------------------------------------------------- /examples/jsm/libs/lil-gui.module.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * lil-gui 3 | * https://lil-gui.georgealways.com 4 | * @version 0.16.0 5 | * @author George Michael Brower 6 | * @license MIT 7 | */ 8 | class t{constructor(i,e,s,n,r="div"){this.parent=i,this.object=e,this.property=s,this._disabled=!1,this.initialValue=this.getValue(),this.domElement=document.createElement("div"),this.domElement.classList.add("controller"),this.domElement.classList.add(n),this.$name=document.createElement("div"),this.$name.classList.add("name"),t.nextNameID=t.nextNameID||0,this.$name.id="lil-gui-name-"+ ++t.nextNameID,this.$widget=document.createElement(r),this.$widget.classList.add("widget"),this.$disable=this.$widget,this.domElement.appendChild(this.$name),this.domElement.appendChild(this.$widget),this.parent.children.push(this),this.parent.controllers.push(this),this.parent.$children.appendChild(this.domElement),this._listenCallback=this._listenCallback.bind(this),this.name(s)}name(t){return this._name=t,this.$name.innerHTML=t,this}onChange(t){return this._onChange=t,this}_callOnChange(){this.parent._callOnChange(this),void 0!==this._onChange&&this._onChange.call(this,this.getValue()),this._changed=!0}onFinishChange(t){return this._onFinishChange=t,this}_callOnFinishChange(){this._changed&&(this.parent._callOnFinishChange(this),void 0!==this._onFinishChange&&this._onFinishChange.call(this,this.getValue())),this._changed=!1}reset(){return this.setValue(this.initialValue),this._callOnFinishChange(),this}enable(t=!0){return this.disable(!t)}disable(t=!0){return t===this._disabled||(this._disabled=t,this.domElement.classList.toggle("disabled",t),this.$disable.toggleAttribute("disabled",t)),this}options(t){const i=this.parent.add(this.object,this.property,t);return i.name(this._name),this.destroy(),i}min(t){return this}max(t){return this}step(t){return this}listen(t=!0){return this._listening=t,void 0!==this._listenCallbackID&&(cancelAnimationFrame(this._listenCallbackID),this._listenCallbackID=void 0),this._listening&&this._listenCallback(),this}_listenCallback(){this._listenCallbackID=requestAnimationFrame(this._listenCallback),this.updateDisplay()}getValue(){return this.object[this.property]}setValue(t){return this.object[this.property]=t,this._callOnChange(),this.updateDisplay(),this}updateDisplay(){return this}load(t){return this.setValue(t),this._callOnFinishChange(),this}save(){return this.getValue()}destroy(){this.parent.children.splice(this.parent.children.indexOf(this),1),this.parent.controllers.splice(this.parent.controllers.indexOf(this),1),this.parent.$children.removeChild(this.domElement)}}class i extends t{constructor(t,i,e){super(t,i,e,"boolean","label"),this.$input=document.createElement("input"),this.$input.setAttribute("type","checkbox"),this.$input.setAttribute("aria-labelledby",this.$name.id),this.$widget.appendChild(this.$input),this.$input.addEventListener("change",()=>{this.setValue(this.$input.checked),this._callOnFinishChange()}),this.$disable=this.$input,this.updateDisplay()}updateDisplay(){return this.$input.checked=this.getValue(),this}}function e(t){let i,e;return(i=t.match(/(#|0x)?([a-f0-9]{6})/i))?e=i[2]:(i=t.match(/rgb\(\s*(\d*)\s*,\s*(\d*)\s*,\s*(\d*)\s*\)/))?e=parseInt(i[1]).toString(16).padStart(2,0)+parseInt(i[2]).toString(16).padStart(2,0)+parseInt(i[3]).toString(16).padStart(2,0):(i=t.match(/^#?([a-f0-9])([a-f0-9])([a-f0-9])$/i))&&(e=i[1]+i[1]+i[2]+i[2]+i[3]+i[3]),!!e&&"#"+e}const s={isPrimitive:!0,match:t=>"string"==typeof t,fromHexString:e,toHexString:e},n={isPrimitive:!0,match:t=>"number"==typeof t,fromHexString:t=>parseInt(t.substring(1),16),toHexString:t=>"#"+t.toString(16).padStart(6,0)},r={isPrimitive:!1,match:Array.isArray,fromHexString(t,i,e=1){const s=n.fromHexString(t);i[0]=(s>>16&255)/255*e,i[1]=(s>>8&255)/255*e,i[2]=(255&s)/255*e},toHexString:([t,i,e],s=1)=>n.toHexString(t*(s=255/s)<<16^i*s<<8^e*s<<0)},l={isPrimitive:!1,match:t=>Object(t)===t,fromHexString(t,i,e=1){const s=n.fromHexString(t);i.r=(s>>16&255)/255*e,i.g=(s>>8&255)/255*e,i.b=(255&s)/255*e},toHexString:({r:t,g:i,b:e},s=1)=>n.toHexString(t*(s=255/s)<<16^i*s<<8^e*s<<0)},o=[s,n,r,l];class a extends t{constructor(t,i,s,n){var r;super(t,i,s,"color"),this.$input=document.createElement("input"),this.$input.setAttribute("type","color"),this.$input.setAttribute("tabindex",-1),this.$input.setAttribute("aria-labelledby",this.$name.id),this.$text=document.createElement("input"),this.$text.setAttribute("type","text"),this.$text.setAttribute("spellcheck","false"),this.$text.setAttribute("aria-labelledby",this.$name.id),this.$display=document.createElement("div"),this.$display.classList.add("display"),this.$display.appendChild(this.$input),this.$widget.appendChild(this.$display),this.$widget.appendChild(this.$text),this._format=(r=this.initialValue,o.find(t=>t.match(r))),this._rgbScale=n,this._initialValueHexString=this.save(),this._textFocused=!1,this.$input.addEventListener("input",()=>{this._setValueFromHexString(this.$input.value)}),this.$input.addEventListener("blur",()=>{this._callOnFinishChange()}),this.$text.addEventListener("input",()=>{const t=e(this.$text.value);t&&this._setValueFromHexString(t)}),this.$text.addEventListener("focus",()=>{this._textFocused=!0,this.$text.select()}),this.$text.addEventListener("blur",()=>{this._textFocused=!1,this.updateDisplay(),this._callOnFinishChange()}),this.$disable=this.$text,this.updateDisplay()}reset(){return this._setValueFromHexString(this._initialValueHexString),this}_setValueFromHexString(t){if(this._format.isPrimitive){const i=this._format.fromHexString(t);this.setValue(i)}else this._format.fromHexString(t,this.getValue(),this._rgbScale),this._callOnChange(),this.updateDisplay()}save(){return this._format.toHexString(this.getValue(),this._rgbScale)}load(t){return this._setValueFromHexString(t),this._callOnFinishChange(),this}updateDisplay(){return this.$input.value=this._format.toHexString(this.getValue(),this._rgbScale),this._textFocused||(this.$text.value=this.$input.value.substring(1)),this.$display.style.backgroundColor=this.$input.value,this}}class h extends t{constructor(t,i,e){super(t,i,e,"function"),this.$button=document.createElement("button"),this.$button.appendChild(this.$name),this.$widget.appendChild(this.$button),this.$button.addEventListener("click",t=>{t.preventDefault(),this.getValue().call(this.object)}),this.$button.addEventListener("touchstart",()=>{}),this.$disable=this.$button}}class d extends t{constructor(t,i,e,s,n,r){super(t,i,e,"number"),this._initInput(),this.min(s),this.max(n);const l=void 0!==r;this.step(l?r:this._getImplicitStep(),l),this.updateDisplay()}min(t){return this._min=t,this._onUpdateMinMax(),this}max(t){return this._max=t,this._onUpdateMinMax(),this}step(t,i=!0){return this._step=t,this._stepExplicit=i,this}updateDisplay(){const t=this.getValue();if(this._hasSlider){let i=(t-this._min)/(this._max-this._min);i=Math.max(0,Math.min(i,1)),this.$fill.style.width=100*i+"%"}return this._inputFocused||(this.$input.value=t),this}_initInput(){this.$input=document.createElement("input"),this.$input.setAttribute("type","number"),this.$input.setAttribute("step","any"),this.$input.setAttribute("aria-labelledby",this.$name.id),this.$widget.appendChild(this.$input),this.$disable=this.$input;const t=t=>{const i=parseFloat(this.$input.value);isNaN(i)||(this._snapClampSetValue(i+t),this.$input.value=this.getValue())};let i,e,s,n,r,l=!1;const o=t=>{if(l){const s=t.clientX-i,n=t.clientY-e;Math.abs(n)>5?(t.preventDefault(),this.$input.blur(),l=!1,this._setDraggingStyle(!0,"vertical")):Math.abs(s)>5&&a()}if(!l){const i=t.clientY-s;r-=i*this._step*this._arrowKeyMultiplier(t),n+r>this._max?r=this._max-n:n+r{this._setDraggingStyle(!1,"vertical"),this._callOnFinishChange(),window.removeEventListener("mousemove",o),window.removeEventListener("mouseup",a)};this.$input.addEventListener("input",()=>{const t=parseFloat(this.$input.value);isNaN(t)||this.setValue(this._clamp(t))}),this.$input.addEventListener("keydown",i=>{"Enter"===i.code&&this.$input.blur(),"ArrowUp"===i.code&&(i.preventDefault(),t(this._step*this._arrowKeyMultiplier(i))),"ArrowDown"===i.code&&(i.preventDefault(),t(this._step*this._arrowKeyMultiplier(i)*-1))}),this.$input.addEventListener("wheel",i=>{this._inputFocused&&(i.preventDefault(),t(this._step*this._normalizeMouseWheel(i)))}),this.$input.addEventListener("mousedown",t=>{i=t.clientX,e=s=t.clientY,l=!0,n=this.getValue(),r=0,window.addEventListener("mousemove",o),window.addEventListener("mouseup",a)}),this.$input.addEventListener("focus",()=>{this._inputFocused=!0}),this.$input.addEventListener("blur",()=>{this._inputFocused=!1,this.updateDisplay(),this._callOnFinishChange()})}_initSlider(){this._hasSlider=!0,this.$slider=document.createElement("div"),this.$slider.classList.add("slider"),this.$fill=document.createElement("div"),this.$fill.classList.add("fill"),this.$slider.appendChild(this.$fill),this.$widget.insertBefore(this.$slider,this.$input),this.domElement.classList.add("hasSlider");const t=t=>{const i=this.$slider.getBoundingClientRect();let e=(s=t,n=i.left,r=i.right,l=this._min,o=this._max,(s-n)/(r-n)*(o-l)+l);var s,n,r,l,o;this._snapClampSetValue(e)},i=i=>{t(i.clientX)},e=()=>{this._callOnFinishChange(),this._setDraggingStyle(!1),window.removeEventListener("mousemove",i),window.removeEventListener("mouseup",e)};let s,n,r=!1;const l=i=>{i.preventDefault(),this._setDraggingStyle(!0),t(i.touches[0].clientX),r=!1},o=i=>{if(r){const t=i.touches[0].clientX-s,e=i.touches[0].clientY-n;Math.abs(t)>Math.abs(e)?l(i):(window.removeEventListener("touchmove",o),window.removeEventListener("touchend",a))}else i.preventDefault(),t(i.touches[0].clientX)},a=()=>{this._callOnFinishChange(),this._setDraggingStyle(!1),window.removeEventListener("touchmove",o),window.removeEventListener("touchend",a)},h=this._callOnFinishChange.bind(this);let d;this.$slider.addEventListener("mousedown",s=>{this._setDraggingStyle(!0),t(s.clientX),window.addEventListener("mousemove",i),window.addEventListener("mouseup",e)}),this.$slider.addEventListener("touchstart",t=>{t.touches.length>1||(this._hasScrollBar?(s=t.touches[0].clientX,n=t.touches[0].clientY,r=!0):l(t),window.addEventListener("touchmove",o),window.addEventListener("touchend",a))}),this.$slider.addEventListener("wheel",t=>{if(Math.abs(t.deltaX)this._max&&(t=this._max),t}_snapClampSetValue(t){this.setValue(this._clamp(this._snap(t)))}get _hasScrollBar(){const t=this.parent.root.$children;return t.scrollHeight>t.clientHeight}get _hasMin(){return void 0!==this._min}get _hasMax(){return void 0!==this._max}}class c extends t{constructor(t,i,e,s){super(t,i,e,"option"),this.$select=document.createElement("select"),this.$select.setAttribute("aria-labelledby",this.$name.id),this.$display=document.createElement("div"),this.$display.classList.add("display"),this._values=Array.isArray(s)?s:Object.values(s),this._names=Array.isArray(s)?s:Object.keys(s),this._names.forEach(t=>{const i=document.createElement("option");i.innerHTML=t,this.$select.appendChild(i)}),this.$select.addEventListener("change",()=>{this.setValue(this._values[this.$select.selectedIndex]),this._callOnFinishChange()}),this.$select.addEventListener("focus",()=>{this.$display.classList.add("focus")}),this.$select.addEventListener("blur",()=>{this.$display.classList.remove("focus")}),this.$widget.appendChild(this.$select),this.$widget.appendChild(this.$display),this.$disable=this.$select,this.updateDisplay()}updateDisplay(){const t=this.getValue(),i=this._values.indexOf(t);return this.$select.selectedIndex=i,this.$display.innerHTML=-1===i?t:this._names[i],this}}class u extends t{constructor(t,i,e){super(t,i,e,"string"),this.$input=document.createElement("input"),this.$input.setAttribute("type","text"),this.$input.setAttribute("aria-labelledby",this.$name.id),this.$input.addEventListener("input",()=>{this.setValue(this.$input.value)}),this.$input.addEventListener("keydown",t=>{"Enter"===t.code&&this.$input.blur()}),this.$input.addEventListener("blur",()=>{this._callOnFinishChange()}),this.$widget.appendChild(this.$input),this.$disable=this.$input,this.updateDisplay()}updateDisplay(){return this.$input.value=this.getValue(),this}}let p=!1;class g{constructor({parent:t,autoPlace:i=void 0===t,container:e,width:s,title:n="Controls",injectStyles:r=!0,touchStyles:l=!0}={}){if(this.parent=t,this.root=t?t.root:this,this.children=[],this.controllers=[],this.folders=[],this._closed=!1,this._hidden=!1,this.domElement=document.createElement("div"),this.domElement.classList.add("lil-gui"),this.$title=document.createElement("div"),this.$title.classList.add("title"),this.$title.setAttribute("role","button"),this.$title.setAttribute("aria-expanded",!0),this.$title.setAttribute("tabindex",0),this.$title.addEventListener("click",()=>this.openAnimated(this._closed)),this.$title.addEventListener("keydown",t=>{"Enter"!==t.code&&"Space"!==t.code||(t.preventDefault(),this.$title.click())}),this.$title.addEventListener("touchstart",()=>{}),this.$children=document.createElement("div"),this.$children.classList.add("children"),this.domElement.appendChild(this.$title),this.domElement.appendChild(this.$children),this.title(n),l&&this.domElement.classList.add("allow-touch-styles"),this.parent)return this.parent.children.push(this),this.parent.folders.push(this),void this.parent.$children.appendChild(this.domElement);this.domElement.classList.add("root"),!p&&r&&(!function(t){const i=document.createElement("style");i.innerHTML=t;const e=document.querySelector("head link[rel=stylesheet], head style");e?document.head.insertBefore(i,e):document.head.appendChild(i)}('.lil-gui{--background-color:#1f1f1f;--text-color:#ebebeb;--title-background-color:#111;--title-text-color:#ebebeb;--widget-color:#424242;--hover-color:#4f4f4f;--focus-color:#595959;--number-color:#2cc9ff;--string-color:#a2db3c;--font-size:11px;--input-font-size:11px;--font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif;--font-family-mono:Menlo,Monaco,Consolas,"Droid Sans Mono",monospace;--padding:4px;--spacing:4px;--widget-height:20px;--name-width:45%;--slider-knob-width:2px;--slider-input-width:27%;--color-input-width:27%;--slider-input-min-width:45px;--color-input-min-width:45px;--folder-indent:7px;--widget-padding:0 0 0 3px;--widget-border-radius:2px;--checkbox-size:calc(var(--widget-height)*0.75);--scrollbar-width:5px;background-color:var(--background-color);color:var(--text-color);font-family:var(--font-family);font-size:var(--font-size);font-style:normal;font-weight:400;line-height:1;text-align:left;touch-action:manipulation;user-select:none;-webkit-user-select:none}.lil-gui,.lil-gui *{box-sizing:border-box;margin:0;padding:0}.lil-gui.root{display:flex;flex-direction:column;width:var(--width,245px)}.lil-gui.root>.title{background:var(--title-background-color);color:var(--title-text-color)}.lil-gui.root>.children{overflow-x:hidden;overflow-y:auto}.lil-gui.root>.children::-webkit-scrollbar{background:var(--background-color);height:var(--scrollbar-width);width:var(--scrollbar-width)}.lil-gui.root>.children::-webkit-scrollbar-thumb{background:var(--focus-color);border-radius:var(--scrollbar-width)}.lil-gui.force-touch-styles{--widget-height:28px;--padding:6px;--spacing:6px;--font-size:13px;--input-font-size:16px;--folder-indent:10px;--scrollbar-width:7px;--slider-input-min-width:50px;--color-input-min-width:65px}.lil-gui.autoPlace{max-height:100%;position:fixed;right:15px;top:0;z-index:1001}.lil-gui .controller{align-items:center;display:flex;margin:var(--spacing) 0;padding:0 var(--padding)}.lil-gui .controller.disabled{opacity:.5}.lil-gui .controller.disabled,.lil-gui .controller.disabled *{pointer-events:none!important}.lil-gui .controller>.name{flex-shrink:0;line-height:var(--widget-height);min-width:var(--name-width);padding-right:var(--spacing);white-space:pre}.lil-gui .controller .widget{align-items:center;display:flex;min-height:var(--widget-height);position:relative;width:100%}.lil-gui .controller.string input{color:var(--string-color)}.lil-gui .controller.boolean .widget{cursor:pointer}.lil-gui .controller.color .display{border-radius:var(--widget-border-radius);height:var(--widget-height);position:relative;width:100%}.lil-gui .controller.color input[type=color]{cursor:pointer;height:100%;opacity:0;width:100%}.lil-gui .controller.color input[type=text]{flex-shrink:0;font-family:var(--font-family-mono);margin-left:var(--spacing);min-width:var(--color-input-min-width);width:var(--color-input-width)}.lil-gui .controller.option select{max-width:100%;opacity:0;position:absolute;width:100%}.lil-gui .controller.option .display{background:var(--widget-color);border-radius:var(--widget-border-radius);height:var(--widget-height);line-height:var(--widget-height);max-width:100%;overflow:hidden;padding-left:.55em;padding-right:1.75em;pointer-events:none;position:relative;word-break:break-all}.lil-gui .controller.option .display.active{background:var(--focus-color)}.lil-gui .controller.option .display:after{bottom:0;content:"↕";font-family:lil-gui;padding-right:.375em;position:absolute;right:0;top:0}.lil-gui .controller.option .widget,.lil-gui .controller.option select{cursor:pointer}.lil-gui .controller.number input{color:var(--number-color)}.lil-gui .controller.number.hasSlider input{flex-shrink:0;margin-left:var(--spacing);min-width:var(--slider-input-min-width);width:var(--slider-input-width)}.lil-gui .controller.number .slider{background-color:var(--widget-color);border-radius:var(--widget-border-radius);cursor:ew-resize;height:var(--widget-height);overflow:hidden;padding-right:var(--slider-knob-width);touch-action:pan-y;width:100%}.lil-gui .controller.number .slider.active{background-color:var(--focus-color)}.lil-gui .controller.number .slider.active .fill{opacity:.95}.lil-gui .controller.number .fill{border-right:var(--slider-knob-width) solid var(--number-color);box-sizing:content-box;height:100%}.lil-gui-dragging .lil-gui{--hover-color:var(--widget-color)}.lil-gui-dragging *{cursor:ew-resize!important}.lil-gui-dragging.lil-gui-vertical *{cursor:ns-resize!important}.lil-gui .title{--title-height:calc(var(--widget-height) + var(--spacing)*1.25);-webkit-tap-highlight-color:transparent;text-decoration-skip:objects;cursor:pointer;font-weight:600;height:var(--title-height);line-height:calc(var(--title-height) - 4px);outline:none;padding:0 var(--padding)}.lil-gui .title:before{content:"▾";display:inline-block;font-family:lil-gui;padding-right:2px}.lil-gui .title:active{background:var(--title-background-color);opacity:.75}.lil-gui.root>.title:focus{text-decoration:none!important}.lil-gui.closed>.title:before{content:"▸"}.lil-gui.closed>.children{opacity:0;transform:translateY(-7px)}.lil-gui.closed:not(.transition)>.children{display:none}.lil-gui.transition>.children{overflow:hidden;pointer-events:none;transition-duration:.3s;transition-property:height,opacity,transform;transition-timing-function:cubic-bezier(.2,.6,.35,1)}.lil-gui .children:empty:before{content:"Empty";display:block;font-style:italic;height:var(--widget-height);line-height:var(--widget-height);margin:var(--spacing) 0;opacity:.5;padding:0 var(--padding)}.lil-gui.root>.children>.lil-gui>.title{border-width:0;border-bottom:1px solid var(--widget-color);border-left:0 solid var(--widget-color);border-right:0 solid var(--widget-color);border-top:1px solid var(--widget-color);transition:border-color .3s}.lil-gui.root>.children>.lil-gui.closed>.title{border-bottom-color:transparent}.lil-gui+.controller{border-top:1px solid var(--widget-color);margin-top:0;padding-top:var(--spacing)}.lil-gui .lil-gui .lil-gui>.title{border:none}.lil-gui .lil-gui .lil-gui>.children{border:none;border-left:2px solid var(--widget-color);margin-left:var(--folder-indent)}.lil-gui .lil-gui .controller{border:none}.lil-gui input{-webkit-tap-highlight-color:transparent;background:var(--widget-color);border:0;border-radius:var(--widget-border-radius);color:var(--text-color);font-family:var(--font-family);font-size:var(--input-font-size);height:var(--widget-height);outline:none;width:100%}.lil-gui input:disabled{opacity:1}.lil-gui input[type=number],.lil-gui input[type=text]{padding:var(--widget-padding)}.lil-gui input[type=number]:focus,.lil-gui input[type=text]:focus{background:var(--focus-color)}.lil-gui input::-webkit-inner-spin-button,.lil-gui input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.lil-gui input[type=number]{-moz-appearance:textfield}.lil-gui input[type=checkbox]{appearance:none;-webkit-appearance:none;border-radius:var(--widget-border-radius);cursor:pointer;height:var(--checkbox-size);text-align:center;width:var(--checkbox-size)}.lil-gui input[type=checkbox]:checked:before{content:"✓";font-family:lil-gui;font-size:var(--checkbox-size);line-height:var(--checkbox-size)}.lil-gui button{-webkit-tap-highlight-color:transparent;background:var(--widget-color);border:1px solid var(--widget-color);border-radius:var(--widget-border-radius);color:var(--text-color);cursor:pointer;font-family:var(--font-family);font-size:var(--font-size);height:var(--widget-height);line-height:calc(var(--widget-height) - 4px);outline:none;text-align:center;text-transform:none;width:100%}.lil-gui button:active{background:var(--focus-color)}@font-face{font-family:lil-gui;src:url("data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAUsAAsAAAAACJwAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAAH4AAADAImwmYE9TLzIAAAGIAAAAPwAAAGBKqH5SY21hcAAAAcgAAAD0AAACrukyyJBnbHlmAAACvAAAAF8AAACEIZpWH2hlYWQAAAMcAAAAJwAAADZfcj2zaGhlYQAAA0QAAAAYAAAAJAC5AHhobXR4AAADXAAAABAAAABMAZAAAGxvY2EAAANsAAAAFAAAACgCEgIybWF4cAAAA4AAAAAeAAAAIAEfABJuYW1lAAADoAAAASIAAAIK9SUU/XBvc3QAAATEAAAAZgAAAJCTcMc2eJxVjbEOgjAURU+hFRBK1dGRL+ALnAiToyMLEzFpnPz/eAshwSa97517c/MwwJmeB9kwPl+0cf5+uGPZXsqPu4nvZabcSZldZ6kfyWnomFY/eScKqZNWupKJO6kXN3K9uCVoL7iInPr1X5baXs3tjuMqCtzEuagm/AAlzQgPAAB4nGNgYRBlnMDAysDAYM/gBiT5oLQBAwuDJAMDEwMrMwNWEJDmmsJwgCFeXZghBcjlZMgFCzOiKOIFAB71Bb8AeJy1kjFuwkAQRZ+DwRAwBtNQRUGKQ8OdKCAWUhAgKLhIuAsVSpWz5Bbkj3dEgYiUIszqWdpZe+Z7/wB1oCYmIoboiwiLT2WjKl/jscrHfGg/pKdMkyklC5Zs2LEfHYpjcRoPzme9MWWmk3dWbK9ObkWkikOetJ554fWyoEsmdSlt+uR0pCJR34b6t/TVg1SY3sYvdf8vuiKrpyaDXDISiegp17p7579Gp3p++y7HPAiY9pmTibljrr85qSidtlg4+l25GLCaS8e6rRxNBmsnERunKbaOObRz7N72ju5vdAjYpBXHgJylOAVsMseDAPEP8LYoUHicY2BiAAEfhiAGJgZWBgZ7RnFRdnVJELCQlBSRlATJMoLV2DK4glSYs6ubq5vbKrJLSbGrgEmovDuDJVhe3VzcXFwNLCOILB/C4IuQ1xTn5FPilBTj5FPmBAB4WwoqAHicY2BkYGAA4sk1sR/j+W2+MnAzpDBgAyEMQUCSg4EJxAEAwUgFHgB4nGNgZGBgSGFggJMhDIwMqEAYAByHATJ4nGNgAIIUNEwmAABl3AGReJxjYAACIQYlBiMGJ3wQAEcQBEV4nGNgZGBgEGZgY2BiAAEQyQWEDAz/wXwGAAsPATIAAHicXdBNSsNAHAXwl35iA0UQXYnMShfS9GPZA7T7LgIu03SSpkwzYTIt1BN4Ak/gKTyAeCxfw39jZkjymzcvAwmAW/wgwHUEGDb36+jQQ3GXGot79L24jxCP4gHzF/EIr4jEIe7wxhOC3g2TMYy4Q7+Lu/SHuEd/ivt4wJd4wPxbPEKMX3GI5+DJFGaSn4qNzk8mcbKSR6xdXdhSzaOZJGtdapd4vVPbi6rP+cL7TGXOHtXKll4bY1Xl7EGnPtp7Xy2n00zyKLVHfkHBa4IcJ2oD3cgggWvt/V/FbDrUlEUJhTn/0azVWbNTNr0Ens8de1tceK9xZmfB1CPjOmPH4kitmvOubcNpmVTN3oFJyjzCvnmrwhJTzqzVj9jiSX911FjeAAB4nG3HMRKCMBBA0f0giiKi4DU8k0V2GWbIZDOh4PoWWvq6J5V8If9NVNQcaDhyouXMhY4rPTcG7jwYmXhKq8Wz+p762aNaeYXom2n3m2dLTVgsrCgFJ7OTmIkYbwIbC6vIB7WmFfAAAA==") format("woff")}@media (pointer:coarse){.lil-gui.allow-touch-styles{--widget-height:28px;--padding:6px;--spacing:6px;--font-size:13px;--input-font-size:16px;--folder-indent:10px;--scrollbar-width:7px;--slider-input-min-width:50px;--color-input-min-width:65px}}@media (hover:hover){.lil-gui .controller.color .display:hover:before{border:1px solid #fff9;border-radius:var(--widget-border-radius);bottom:0;content:" ";display:block;left:0;position:absolute;right:0;top:0}.lil-gui .controller.option .display.focus{background:var(--focus-color)}.lil-gui .controller.option .widget:hover .display{background:var(--hover-color)}.lil-gui .controller.number .slider:hover{background-color:var(--hover-color)}body:not(.lil-gui-dragging) .lil-gui .title:hover{background:var(--title-background-color);opacity:.85}.lil-gui .title:focus{text-decoration:underline var(--focus-color)}.lil-gui input:hover{background:var(--hover-color)}.lil-gui input:active{background:var(--focus-color)}.lil-gui input[type=checkbox]:focus{box-shadow:inset 0 0 0 1px var(--focus-color)}.lil-gui button:hover{background:var(--hover-color);border-color:var(--hover-color)}.lil-gui button:focus{border-color:var(--focus-color)}}'),p=!0),e?e.appendChild(this.domElement):i&&(this.domElement.classList.add("autoPlace"),document.body.appendChild(this.domElement)),s&&this.domElement.style.setProperty("--width",s+"px"),this.domElement.addEventListener("keydown",t=>t.stopPropagation()),this.domElement.addEventListener("keyup",t=>t.stopPropagation())}add(t,e,s,n,r){if(Object(s)===s)return new c(this,t,e,s);const l=t[e];switch(typeof l){case"number":return new d(this,t,e,s,n,r);case"boolean":return new i(this,t,e);case"string":return new u(this,t,e);case"function":return new h(this,t,e)}console.error("gui.add failed\n\tproperty:",e,"\n\tobject:",t,"\n\tvalue:",l)}addColor(t,i,e=1){return new a(this,t,i,e)}addFolder(t){return new g({parent:this,title:t})}load(t,i=!0){return t.controllers&&this.controllers.forEach(i=>{i instanceof h||i._name in t.controllers&&i.load(t.controllers[i._name])}),i&&t.folders&&this.folders.forEach(i=>{i._title in t.folders&&i.load(t.folders[i._title])}),this}save(t=!0){const i={controllers:{},folders:{}};return this.controllers.forEach(t=>{if(!(t instanceof h)){if(t._name in i.controllers)throw new Error(`Cannot save GUI with duplicate property "${t._name}"`);i.controllers[t._name]=t.save()}}),t&&this.folders.forEach(t=>{if(t._title in i.folders)throw new Error(`Cannot save GUI with duplicate folder "${t._title}"`);i.folders[t._title]=t.save()}),i}open(t=!0){return this._closed=!t,this.$title.setAttribute("aria-expanded",!this._closed),this.domElement.classList.toggle("closed",this._closed),this}close(){return this.open(!1)}show(t=!0){return this._hidden=!t,this.domElement.style.display=this._hidden?"none":"",this}hide(){return this.show(!1)}openAnimated(t=!0){return this._closed=!t,this.$title.setAttribute("aria-expanded",!this._closed),requestAnimationFrame(()=>{const i=this.$children.clientHeight;this.$children.style.height=i+"px",this.domElement.classList.add("transition");const e=t=>{t.target===this.$children&&(this.$children.style.height="",this.domElement.classList.remove("transition"),this.$children.removeEventListener("transitionend",e))};this.$children.addEventListener("transitionend",e);const s=t?this.$children.scrollHeight:0;this.domElement.classList.toggle("closed",!t),requestAnimationFrame(()=>{this.$children.style.height=s+"px"})}),this}title(t){return this._title=t,this.$title.innerHTML=t,this}reset(t=!0){return(t?this.controllersRecursive():this.controllers).forEach(t=>t.reset()),this}onChange(t){return this._onChange=t,this}_callOnChange(t){this.parent&&this.parent._callOnChange(t),void 0!==this._onChange&&this._onChange.call(this,{object:t.object,property:t.property,value:t.getValue(),controller:t})}onFinishChange(t){return this._onFinishChange=t,this}_callOnFinishChange(t){this.parent&&this.parent._callOnFinishChange(t),void 0!==this._onFinishChange&&this._onFinishChange.call(this,{object:t.object,property:t.property,value:t.getValue(),controller:t})}destroy(){this.parent&&(this.parent.children.splice(this.parent.children.indexOf(this),1),this.parent.folders.splice(this.parent.folders.indexOf(this),1)),this.domElement.parentElement&&this.domElement.parentElement.removeChild(this.domElement),Array.from(this.children).forEach(t=>t.destroy())}controllersRecursive(){let t=Array.from(this.controllers);return this.folders.forEach(i=>{t=t.concat(i.controllersRecursive())}),t}foldersRecursive(){let t=Array.from(this.folders);return this.folders.forEach(i=>{t=t.concat(i.foldersRecursive())}),t}}export default g;export{i as BooleanController,a as ColorController,t as Controller,h as FunctionController,g as GUI,d as NumberController,c as OptionController,u as StringController}; 9 | -------------------------------------------------------------------------------- /examples/jsm/controls/OrbitControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | EventDispatcher, 3 | MOUSE, 4 | Quaternion, 5 | Spherical, 6 | TOUCH, 7 | Vector2, 8 | Vector3 9 | } from '../../../build/three.module.js'; 10 | 11 | // This set of controls performs orbiting, dollying (zooming), and panning. 12 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 13 | // 14 | // Orbit - left mouse / touch: one-finger move 15 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 16 | // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move 17 | 18 | const _changeEvent = { type: 'change' }; 19 | const _startEvent = { type: 'start' }; 20 | const _endEvent = { type: 'end' }; 21 | 22 | class OrbitControls extends EventDispatcher { 23 | 24 | constructor( object, domElement ) { 25 | 26 | super(); 27 | 28 | if ( domElement === undefined ) console.warn( 'THREE.OrbitControls: The second parameter "domElement" is now mandatory.' ); 29 | if ( domElement === document ) console.error( 'THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' ); 30 | 31 | this.object = object; 32 | this.domElement = domElement; 33 | this.domElement.style.touchAction = 'none'; // disable touch scroll 34 | 35 | // Set to false to disable this control 36 | this.enabled = true; 37 | 38 | // "target" sets the location of focus, where the object orbits around 39 | this.target = new Vector3(); 40 | 41 | // How far you can dolly in and out ( PerspectiveCamera only ) 42 | this.minDistance = 0; 43 | this.maxDistance = Infinity; 44 | 45 | // How far you can zoom in and out ( OrthographicCamera only ) 46 | this.minZoom = 0; 47 | this.maxZoom = Infinity; 48 | 49 | // How far you can orbit vertically, upper and lower limits. 50 | // Range is 0 to Math.PI radians. 51 | this.minPolarAngle = 0; // radians 52 | this.maxPolarAngle = Math.PI; // radians 53 | 54 | // How far you can orbit horizontally, upper and lower limits. 55 | // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ) 56 | this.minAzimuthAngle = - Infinity; // radians 57 | this.maxAzimuthAngle = Infinity; // radians 58 | 59 | // Set to true to enable damping (inertia) 60 | // If damping is enabled, you must call controls.update() in your animation loop 61 | this.enableDamping = false; 62 | this.dampingFactor = 0.05; 63 | 64 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 65 | // Set to false to disable zooming 66 | this.enableZoom = true; 67 | this.zoomSpeed = 1.0; 68 | 69 | // Set to false to disable rotating 70 | this.enableRotate = true; 71 | this.rotateSpeed = 1.0; 72 | 73 | // Set to false to disable panning 74 | this.enablePan = true; 75 | this.panSpeed = 1.0; 76 | this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up 77 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 78 | 79 | // Set to true to automatically rotate around the target 80 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 81 | this.autoRotate = false; 82 | this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60 83 | 84 | // The four arrow keys 85 | this.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' }; 86 | 87 | // Mouse buttons 88 | this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }; 89 | 90 | // Touch fingers 91 | this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }; 92 | 93 | // for reset 94 | this.target0 = this.target.clone(); 95 | this.position0 = this.object.position.clone(); 96 | this.zoom0 = this.object.zoom; 97 | 98 | // the target DOM element for key events 99 | this._domElementKeyEvents = null; 100 | 101 | // 102 | // public methods 103 | // 104 | 105 | this.getPolarAngle = function () { 106 | 107 | return spherical.phi; 108 | 109 | }; 110 | 111 | this.getAzimuthalAngle = function () { 112 | 113 | return spherical.theta; 114 | 115 | }; 116 | 117 | this.getDistance = function () { 118 | 119 | return this.object.position.distanceTo( this.target ); 120 | 121 | }; 122 | 123 | this.listenToKeyEvents = function ( domElement ) { 124 | 125 | domElement.addEventListener( 'keydown', onKeyDown ); 126 | this._domElementKeyEvents = domElement; 127 | 128 | }; 129 | 130 | this.saveState = function () { 131 | 132 | scope.target0.copy( scope.target ); 133 | scope.position0.copy( scope.object.position ); 134 | scope.zoom0 = scope.object.zoom; 135 | 136 | }; 137 | 138 | this.reset = function () { 139 | 140 | scope.target.copy( scope.target0 ); 141 | scope.object.position.copy( scope.position0 ); 142 | scope.object.zoom = scope.zoom0; 143 | 144 | scope.object.updateProjectionMatrix(); 145 | scope.dispatchEvent( _changeEvent ); 146 | 147 | scope.update(); 148 | 149 | state = STATE.NONE; 150 | 151 | }; 152 | 153 | // this method is exposed, but perhaps it would be better if we can make it private... 154 | this.update = function () { 155 | 156 | const offset = new Vector3(); 157 | 158 | // so camera.up is the orbit axis 159 | const quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) ); 160 | const quatInverse = quat.clone().invert(); 161 | 162 | const lastPosition = new Vector3(); 163 | const lastQuaternion = new Quaternion(); 164 | 165 | const twoPI = 2 * Math.PI; 166 | 167 | return function update() { 168 | 169 | const position = scope.object.position; 170 | 171 | offset.copy( position ).sub( scope.target ); 172 | 173 | // rotate offset to "y-axis-is-up" space 174 | offset.applyQuaternion( quat ); 175 | 176 | // angle from z-axis around y-axis 177 | spherical.setFromVector3( offset ); 178 | 179 | if ( scope.autoRotate && state === STATE.NONE ) { 180 | 181 | rotateLeft( getAutoRotationAngle() ); 182 | 183 | } 184 | 185 | if ( scope.enableDamping ) { 186 | 187 | spherical.theta += sphericalDelta.theta * scope.dampingFactor; 188 | spherical.phi += sphericalDelta.phi * scope.dampingFactor; 189 | 190 | } else { 191 | 192 | spherical.theta += sphericalDelta.theta; 193 | spherical.phi += sphericalDelta.phi; 194 | 195 | } 196 | 197 | // restrict theta to be between desired limits 198 | 199 | let min = scope.minAzimuthAngle; 200 | let max = scope.maxAzimuthAngle; 201 | 202 | if ( isFinite( min ) && isFinite( max ) ) { 203 | 204 | if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI; 205 | 206 | if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI; 207 | 208 | if ( min <= max ) { 209 | 210 | spherical.theta = Math.max( min, Math.min( max, spherical.theta ) ); 211 | 212 | } else { 213 | 214 | spherical.theta = ( spherical.theta > ( min + max ) / 2 ) ? 215 | Math.max( min, spherical.theta ) : 216 | Math.min( max, spherical.theta ); 217 | 218 | } 219 | 220 | } 221 | 222 | // restrict phi to be between desired limits 223 | spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); 224 | 225 | spherical.makeSafe(); 226 | 227 | 228 | spherical.radius *= scale; 229 | 230 | // restrict radius to be between desired limits 231 | spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); 232 | 233 | // move target to panned location 234 | 235 | if ( scope.enableDamping === true ) { 236 | 237 | scope.target.addScaledVector( panOffset, scope.dampingFactor ); 238 | 239 | } else { 240 | 241 | scope.target.add( panOffset ); 242 | 243 | } 244 | 245 | offset.setFromSpherical( spherical ); 246 | 247 | // rotate offset back to "camera-up-vector-is-up" space 248 | offset.applyQuaternion( quatInverse ); 249 | 250 | position.copy( scope.target ).add( offset ); 251 | 252 | scope.object.lookAt( scope.target ); 253 | 254 | if ( scope.enableDamping === true ) { 255 | 256 | sphericalDelta.theta *= ( 1 - scope.dampingFactor ); 257 | sphericalDelta.phi *= ( 1 - scope.dampingFactor ); 258 | 259 | panOffset.multiplyScalar( 1 - scope.dampingFactor ); 260 | 261 | } else { 262 | 263 | sphericalDelta.set( 0, 0, 0 ); 264 | 265 | panOffset.set( 0, 0, 0 ); 266 | 267 | } 268 | 269 | scale = 1; 270 | 271 | // update condition is: 272 | // min(camera displacement, camera rotation in radians)^2 > EPS 273 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 274 | 275 | if ( zoomChanged || 276 | lastPosition.distanceToSquared( scope.object.position ) > EPS || 277 | 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { 278 | 279 | scope.dispatchEvent( _changeEvent ); 280 | 281 | lastPosition.copy( scope.object.position ); 282 | lastQuaternion.copy( scope.object.quaternion ); 283 | zoomChanged = false; 284 | 285 | return true; 286 | 287 | } 288 | 289 | return false; 290 | 291 | }; 292 | 293 | }(); 294 | 295 | this.dispose = function () { 296 | 297 | scope.domElement.removeEventListener( 'contextmenu', onContextMenu ); 298 | 299 | scope.domElement.removeEventListener( 'pointerdown', onPointerDown ); 300 | scope.domElement.removeEventListener( 'pointercancel', onPointerCancel ); 301 | scope.domElement.removeEventListener( 'wheel', onMouseWheel ); 302 | 303 | scope.domElement.removeEventListener( 'pointermove', onPointerMove ); 304 | scope.domElement.removeEventListener( 'pointerup', onPointerUp ); 305 | 306 | 307 | if ( scope._domElementKeyEvents !== null ) { 308 | 309 | scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown ); 310 | 311 | } 312 | 313 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 314 | 315 | }; 316 | 317 | // 318 | // internals 319 | // 320 | 321 | const scope = this; 322 | 323 | const STATE = { 324 | NONE: - 1, 325 | ROTATE: 0, 326 | DOLLY: 1, 327 | PAN: 2, 328 | TOUCH_ROTATE: 3, 329 | TOUCH_PAN: 4, 330 | TOUCH_DOLLY_PAN: 5, 331 | TOUCH_DOLLY_ROTATE: 6 332 | }; 333 | 334 | let state = STATE.NONE; 335 | 336 | const EPS = 0.000001; 337 | 338 | // current position in spherical coordinates 339 | const spherical = new Spherical(); 340 | const sphericalDelta = new Spherical(); 341 | 342 | let scale = 1; 343 | const panOffset = new Vector3(); 344 | let zoomChanged = false; 345 | 346 | const rotateStart = new Vector2(); 347 | const rotateEnd = new Vector2(); 348 | const rotateDelta = new Vector2(); 349 | 350 | const panStart = new Vector2(); 351 | const panEnd = new Vector2(); 352 | const panDelta = new Vector2(); 353 | 354 | const dollyStart = new Vector2(); 355 | const dollyEnd = new Vector2(); 356 | const dollyDelta = new Vector2(); 357 | 358 | const pointers = []; 359 | const pointerPositions = {}; 360 | 361 | function getAutoRotationAngle() { 362 | 363 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 364 | 365 | } 366 | 367 | function getZoomScale() { 368 | 369 | return Math.pow( 0.95, scope.zoomSpeed ); 370 | 371 | } 372 | 373 | function rotateLeft( angle ) { 374 | 375 | sphericalDelta.theta -= angle; 376 | 377 | } 378 | 379 | function rotateUp( angle ) { 380 | 381 | sphericalDelta.phi -= angle; 382 | 383 | } 384 | 385 | const panLeft = function () { 386 | 387 | const v = new Vector3(); 388 | 389 | return function panLeft( distance, objectMatrix ) { 390 | 391 | v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix 392 | v.multiplyScalar( - distance ); 393 | 394 | panOffset.add( v ); 395 | 396 | }; 397 | 398 | }(); 399 | 400 | const panUp = function () { 401 | 402 | const v = new Vector3(); 403 | 404 | return function panUp( distance, objectMatrix ) { 405 | 406 | if ( scope.screenSpacePanning === true ) { 407 | 408 | v.setFromMatrixColumn( objectMatrix, 1 ); 409 | 410 | } else { 411 | 412 | v.setFromMatrixColumn( objectMatrix, 0 ); 413 | v.crossVectors( scope.object.up, v ); 414 | 415 | } 416 | 417 | v.multiplyScalar( distance ); 418 | 419 | panOffset.add( v ); 420 | 421 | }; 422 | 423 | }(); 424 | 425 | // deltaX and deltaY are in pixels; right and down are positive 426 | const pan = function () { 427 | 428 | const offset = new Vector3(); 429 | 430 | return function pan( deltaX, deltaY ) { 431 | 432 | const element = scope.domElement; 433 | 434 | if ( scope.object.isPerspectiveCamera ) { 435 | 436 | // perspective 437 | const position = scope.object.position; 438 | offset.copy( position ).sub( scope.target ); 439 | let targetDistance = offset.length(); 440 | 441 | // half of the fov is center to top of screen 442 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 443 | 444 | // we use only clientHeight here so aspect ratio does not distort speed 445 | panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); 446 | panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); 447 | 448 | } else if ( scope.object.isOrthographicCamera ) { 449 | 450 | // orthographic 451 | panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); 452 | panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); 453 | 454 | } else { 455 | 456 | // camera neither orthographic nor perspective 457 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 458 | scope.enablePan = false; 459 | 460 | } 461 | 462 | }; 463 | 464 | }(); 465 | 466 | function dollyOut( dollyScale ) { 467 | 468 | if ( scope.object.isPerspectiveCamera ) { 469 | 470 | scale /= dollyScale; 471 | 472 | } else if ( scope.object.isOrthographicCamera ) { 473 | 474 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); 475 | scope.object.updateProjectionMatrix(); 476 | zoomChanged = true; 477 | 478 | } else { 479 | 480 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 481 | scope.enableZoom = false; 482 | 483 | } 484 | 485 | } 486 | 487 | function dollyIn( dollyScale ) { 488 | 489 | if ( scope.object.isPerspectiveCamera ) { 490 | 491 | scale *= dollyScale; 492 | 493 | } else if ( scope.object.isOrthographicCamera ) { 494 | 495 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); 496 | scope.object.updateProjectionMatrix(); 497 | zoomChanged = true; 498 | 499 | } else { 500 | 501 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 502 | scope.enableZoom = false; 503 | 504 | } 505 | 506 | } 507 | 508 | // 509 | // event callbacks - update the object state 510 | // 511 | 512 | function handleMouseDownRotate( event ) { 513 | 514 | rotateStart.set( event.clientX, event.clientY ); 515 | 516 | } 517 | 518 | function handleMouseDownDolly( event ) { 519 | 520 | dollyStart.set( event.clientX, event.clientY ); 521 | 522 | } 523 | 524 | function handleMouseDownPan( event ) { 525 | 526 | panStart.set( event.clientX, event.clientY ); 527 | 528 | } 529 | 530 | function handleMouseMoveRotate( event ) { 531 | 532 | rotateEnd.set( event.clientX, event.clientY ); 533 | 534 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 535 | 536 | const element = scope.domElement; 537 | 538 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 539 | 540 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 541 | 542 | rotateStart.copy( rotateEnd ); 543 | 544 | scope.update(); 545 | 546 | } 547 | 548 | function handleMouseMoveDolly( event ) { 549 | 550 | dollyEnd.set( event.clientX, event.clientY ); 551 | 552 | dollyDelta.subVectors( dollyEnd, dollyStart ); 553 | 554 | if ( dollyDelta.y > 0 ) { 555 | 556 | dollyOut( getZoomScale() ); 557 | 558 | } else if ( dollyDelta.y < 0 ) { 559 | 560 | dollyIn( getZoomScale() ); 561 | 562 | } 563 | 564 | dollyStart.copy( dollyEnd ); 565 | 566 | scope.update(); 567 | 568 | } 569 | 570 | function handleMouseMovePan( event ) { 571 | 572 | panEnd.set( event.clientX, event.clientY ); 573 | 574 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 575 | 576 | pan( panDelta.x, panDelta.y ); 577 | 578 | panStart.copy( panEnd ); 579 | 580 | scope.update(); 581 | 582 | } 583 | 584 | function handleMouseWheel( event ) { 585 | 586 | if ( event.deltaY < 0 ) { 587 | 588 | dollyIn( getZoomScale() ); 589 | 590 | } else if ( event.deltaY > 0 ) { 591 | 592 | dollyOut( getZoomScale() ); 593 | 594 | } 595 | 596 | scope.update(); 597 | 598 | } 599 | 600 | function handleKeyDown( event ) { 601 | 602 | let needsUpdate = false; 603 | 604 | switch ( event.code ) { 605 | 606 | case scope.keys.UP: 607 | pan( 0, scope.keyPanSpeed ); 608 | needsUpdate = true; 609 | break; 610 | 611 | case scope.keys.BOTTOM: 612 | pan( 0, - scope.keyPanSpeed ); 613 | needsUpdate = true; 614 | break; 615 | 616 | case scope.keys.LEFT: 617 | pan( scope.keyPanSpeed, 0 ); 618 | needsUpdate = true; 619 | break; 620 | 621 | case scope.keys.RIGHT: 622 | pan( - scope.keyPanSpeed, 0 ); 623 | needsUpdate = true; 624 | break; 625 | 626 | } 627 | 628 | if ( needsUpdate ) { 629 | 630 | // prevent the browser from scrolling on cursor keys 631 | event.preventDefault(); 632 | 633 | scope.update(); 634 | 635 | } 636 | 637 | 638 | } 639 | 640 | function handleTouchStartRotate() { 641 | 642 | if ( pointers.length === 1 ) { 643 | 644 | rotateStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY ); 645 | 646 | } else { 647 | 648 | const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX ); 649 | const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY ); 650 | 651 | rotateStart.set( x, y ); 652 | 653 | } 654 | 655 | } 656 | 657 | function handleTouchStartPan() { 658 | 659 | if ( pointers.length === 1 ) { 660 | 661 | panStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY ); 662 | 663 | } else { 664 | 665 | const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX ); 666 | const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY ); 667 | 668 | panStart.set( x, y ); 669 | 670 | } 671 | 672 | } 673 | 674 | function handleTouchStartDolly() { 675 | 676 | const dx = pointers[ 0 ].pageX - pointers[ 1 ].pageX; 677 | const dy = pointers[ 0 ].pageY - pointers[ 1 ].pageY; 678 | 679 | const distance = Math.sqrt( dx * dx + dy * dy ); 680 | 681 | dollyStart.set( 0, distance ); 682 | 683 | } 684 | 685 | function handleTouchStartDollyPan() { 686 | 687 | if ( scope.enableZoom ) handleTouchStartDolly(); 688 | 689 | if ( scope.enablePan ) handleTouchStartPan(); 690 | 691 | } 692 | 693 | function handleTouchStartDollyRotate() { 694 | 695 | if ( scope.enableZoom ) handleTouchStartDolly(); 696 | 697 | if ( scope.enableRotate ) handleTouchStartRotate(); 698 | 699 | } 700 | 701 | function handleTouchMoveRotate( event ) { 702 | 703 | if ( pointers.length == 1 ) { 704 | 705 | rotateEnd.set( event.pageX, event.pageY ); 706 | 707 | } else { 708 | 709 | const position = getSecondPointerPosition( event ); 710 | 711 | const x = 0.5 * ( event.pageX + position.x ); 712 | const y = 0.5 * ( event.pageY + position.y ); 713 | 714 | rotateEnd.set( x, y ); 715 | 716 | } 717 | 718 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 719 | 720 | const element = scope.domElement; 721 | 722 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 723 | 724 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 725 | 726 | rotateStart.copy( rotateEnd ); 727 | 728 | } 729 | 730 | function handleTouchMovePan( event ) { 731 | 732 | if ( pointers.length === 1 ) { 733 | 734 | panEnd.set( event.pageX, event.pageY ); 735 | 736 | } else { 737 | 738 | const position = getSecondPointerPosition( event ); 739 | 740 | const x = 0.5 * ( event.pageX + position.x ); 741 | const y = 0.5 * ( event.pageY + position.y ); 742 | 743 | panEnd.set( x, y ); 744 | 745 | } 746 | 747 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 748 | 749 | pan( panDelta.x, panDelta.y ); 750 | 751 | panStart.copy( panEnd ); 752 | 753 | } 754 | 755 | function handleTouchMoveDolly( event ) { 756 | 757 | const position = getSecondPointerPosition( event ); 758 | 759 | const dx = event.pageX - position.x; 760 | const dy = event.pageY - position.y; 761 | 762 | const distance = Math.sqrt( dx * dx + dy * dy ); 763 | 764 | dollyEnd.set( 0, distance ); 765 | 766 | dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); 767 | 768 | dollyOut( dollyDelta.y ); 769 | 770 | dollyStart.copy( dollyEnd ); 771 | 772 | } 773 | 774 | function handleTouchMoveDollyPan( event ) { 775 | 776 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 777 | 778 | if ( scope.enablePan ) handleTouchMovePan( event ); 779 | 780 | } 781 | 782 | function handleTouchMoveDollyRotate( event ) { 783 | 784 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 785 | 786 | if ( scope.enableRotate ) handleTouchMoveRotate( event ); 787 | 788 | } 789 | 790 | // 791 | // event handlers - FSM: listen for events and reset state 792 | // 793 | 794 | function onPointerDown( event ) { 795 | 796 | if ( scope.enabled === false ) return; 797 | 798 | if ( pointers.length === 0 ) { 799 | 800 | scope.domElement.setPointerCapture( event.pointerId ); 801 | 802 | scope.domElement.addEventListener( 'pointermove', onPointerMove ); 803 | scope.domElement.addEventListener( 'pointerup', onPointerUp ); 804 | 805 | } 806 | 807 | // 808 | 809 | addPointer( event ); 810 | 811 | if ( event.pointerType === 'touch' ) { 812 | 813 | onTouchStart( event ); 814 | 815 | } else { 816 | 817 | onMouseDown( event ); 818 | 819 | } 820 | 821 | } 822 | 823 | function onPointerMove( event ) { 824 | 825 | if ( scope.enabled === false ) return; 826 | 827 | if ( event.pointerType === 'touch' ) { 828 | 829 | onTouchMove( event ); 830 | 831 | } else { 832 | 833 | onMouseMove( event ); 834 | 835 | } 836 | 837 | } 838 | 839 | function onPointerUp( event ) { 840 | 841 | removePointer( event ); 842 | 843 | if ( pointers.length === 0 ) { 844 | 845 | scope.domElement.releasePointerCapture( event.pointerId ); 846 | 847 | scope.domElement.removeEventListener( 'pointermove', onPointerMove ); 848 | scope.domElement.removeEventListener( 'pointerup', onPointerUp ); 849 | 850 | } 851 | 852 | scope.dispatchEvent( _endEvent ); 853 | 854 | state = STATE.NONE; 855 | 856 | } 857 | 858 | function onPointerCancel( event ) { 859 | 860 | removePointer( event ); 861 | 862 | } 863 | 864 | function onMouseDown( event ) { 865 | 866 | let mouseAction; 867 | 868 | switch ( event.button ) { 869 | 870 | case 0: 871 | 872 | mouseAction = scope.mouseButtons.LEFT; 873 | break; 874 | 875 | case 1: 876 | 877 | mouseAction = scope.mouseButtons.MIDDLE; 878 | break; 879 | 880 | case 2: 881 | 882 | mouseAction = scope.mouseButtons.RIGHT; 883 | break; 884 | 885 | default: 886 | 887 | mouseAction = - 1; 888 | 889 | } 890 | 891 | switch ( mouseAction ) { 892 | 893 | case MOUSE.DOLLY: 894 | 895 | if ( scope.enableZoom === false ) return; 896 | 897 | handleMouseDownDolly( event ); 898 | 899 | state = STATE.DOLLY; 900 | 901 | break; 902 | 903 | case MOUSE.ROTATE: 904 | 905 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 906 | 907 | if ( scope.enablePan === false ) return; 908 | 909 | handleMouseDownPan( event ); 910 | 911 | state = STATE.PAN; 912 | 913 | } else { 914 | 915 | if ( scope.enableRotate === false ) return; 916 | 917 | handleMouseDownRotate( event ); 918 | 919 | state = STATE.ROTATE; 920 | 921 | } 922 | 923 | break; 924 | 925 | case MOUSE.PAN: 926 | 927 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 928 | 929 | if ( scope.enableRotate === false ) return; 930 | 931 | handleMouseDownRotate( event ); 932 | 933 | state = STATE.ROTATE; 934 | 935 | } else { 936 | 937 | if ( scope.enablePan === false ) return; 938 | 939 | handleMouseDownPan( event ); 940 | 941 | state = STATE.PAN; 942 | 943 | } 944 | 945 | break; 946 | 947 | default: 948 | 949 | state = STATE.NONE; 950 | 951 | } 952 | 953 | if ( state !== STATE.NONE ) { 954 | 955 | scope.dispatchEvent( _startEvent ); 956 | 957 | } 958 | 959 | } 960 | 961 | function onMouseMove( event ) { 962 | 963 | if ( scope.enabled === false ) return; 964 | 965 | switch ( state ) { 966 | 967 | case STATE.ROTATE: 968 | 969 | if ( scope.enableRotate === false ) return; 970 | 971 | handleMouseMoveRotate( event ); 972 | 973 | break; 974 | 975 | case STATE.DOLLY: 976 | 977 | if ( scope.enableZoom === false ) return; 978 | 979 | handleMouseMoveDolly( event ); 980 | 981 | break; 982 | 983 | case STATE.PAN: 984 | 985 | if ( scope.enablePan === false ) return; 986 | 987 | handleMouseMovePan( event ); 988 | 989 | break; 990 | 991 | } 992 | 993 | } 994 | 995 | function onMouseWheel( event ) { 996 | 997 | if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE ) return; 998 | 999 | event.preventDefault(); 1000 | 1001 | scope.dispatchEvent( _startEvent ); 1002 | 1003 | handleMouseWheel( event ); 1004 | 1005 | scope.dispatchEvent( _endEvent ); 1006 | 1007 | } 1008 | 1009 | function onKeyDown( event ) { 1010 | 1011 | if ( scope.enabled === false || scope.enablePan === false ) return; 1012 | 1013 | handleKeyDown( event ); 1014 | 1015 | } 1016 | 1017 | function onTouchStart( event ) { 1018 | 1019 | trackPointer( event ); 1020 | 1021 | switch ( pointers.length ) { 1022 | 1023 | case 1: 1024 | 1025 | switch ( scope.touches.ONE ) { 1026 | 1027 | case TOUCH.ROTATE: 1028 | 1029 | if ( scope.enableRotate === false ) return; 1030 | 1031 | handleTouchStartRotate(); 1032 | 1033 | state = STATE.TOUCH_ROTATE; 1034 | 1035 | break; 1036 | 1037 | case TOUCH.PAN: 1038 | 1039 | if ( scope.enablePan === false ) return; 1040 | 1041 | handleTouchStartPan(); 1042 | 1043 | state = STATE.TOUCH_PAN; 1044 | 1045 | break; 1046 | 1047 | default: 1048 | 1049 | state = STATE.NONE; 1050 | 1051 | } 1052 | 1053 | break; 1054 | 1055 | case 2: 1056 | 1057 | switch ( scope.touches.TWO ) { 1058 | 1059 | case TOUCH.DOLLY_PAN: 1060 | 1061 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 1062 | 1063 | handleTouchStartDollyPan(); 1064 | 1065 | state = STATE.TOUCH_DOLLY_PAN; 1066 | 1067 | break; 1068 | 1069 | case TOUCH.DOLLY_ROTATE: 1070 | 1071 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 1072 | 1073 | handleTouchStartDollyRotate(); 1074 | 1075 | state = STATE.TOUCH_DOLLY_ROTATE; 1076 | 1077 | break; 1078 | 1079 | default: 1080 | 1081 | state = STATE.NONE; 1082 | 1083 | } 1084 | 1085 | break; 1086 | 1087 | default: 1088 | 1089 | state = STATE.NONE; 1090 | 1091 | } 1092 | 1093 | if ( state !== STATE.NONE ) { 1094 | 1095 | scope.dispatchEvent( _startEvent ); 1096 | 1097 | } 1098 | 1099 | } 1100 | 1101 | function onTouchMove( event ) { 1102 | 1103 | trackPointer( event ); 1104 | 1105 | switch ( state ) { 1106 | 1107 | case STATE.TOUCH_ROTATE: 1108 | 1109 | if ( scope.enableRotate === false ) return; 1110 | 1111 | handleTouchMoveRotate( event ); 1112 | 1113 | scope.update(); 1114 | 1115 | break; 1116 | 1117 | case STATE.TOUCH_PAN: 1118 | 1119 | if ( scope.enablePan === false ) return; 1120 | 1121 | handleTouchMovePan( event ); 1122 | 1123 | scope.update(); 1124 | 1125 | break; 1126 | 1127 | case STATE.TOUCH_DOLLY_PAN: 1128 | 1129 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 1130 | 1131 | handleTouchMoveDollyPan( event ); 1132 | 1133 | scope.update(); 1134 | 1135 | break; 1136 | 1137 | case STATE.TOUCH_DOLLY_ROTATE: 1138 | 1139 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 1140 | 1141 | handleTouchMoveDollyRotate( event ); 1142 | 1143 | scope.update(); 1144 | 1145 | break; 1146 | 1147 | default: 1148 | 1149 | state = STATE.NONE; 1150 | 1151 | } 1152 | 1153 | } 1154 | 1155 | function onContextMenu( event ) { 1156 | 1157 | if ( scope.enabled === false ) return; 1158 | 1159 | event.preventDefault(); 1160 | 1161 | } 1162 | 1163 | function addPointer( event ) { 1164 | 1165 | pointers.push( event ); 1166 | 1167 | } 1168 | 1169 | function removePointer( event ) { 1170 | 1171 | delete pointerPositions[ event.pointerId ]; 1172 | 1173 | for ( let i = 0; i < pointers.length; i ++ ) { 1174 | 1175 | if ( pointers[ i ].pointerId == event.pointerId ) { 1176 | 1177 | pointers.splice( i, 1 ); 1178 | return; 1179 | 1180 | } 1181 | 1182 | } 1183 | 1184 | } 1185 | 1186 | function trackPointer( event ) { 1187 | 1188 | let position = pointerPositions[ event.pointerId ]; 1189 | 1190 | if ( position === undefined ) { 1191 | 1192 | position = new Vector2(); 1193 | pointerPositions[ event.pointerId ] = position; 1194 | 1195 | } 1196 | 1197 | position.set( event.pageX, event.pageY ); 1198 | 1199 | } 1200 | 1201 | function getSecondPointerPosition( event ) { 1202 | 1203 | const pointer = ( event.pointerId === pointers[ 0 ].pointerId ) ? pointers[ 1 ] : pointers[ 0 ]; 1204 | 1205 | return pointerPositions[ pointer.pointerId ]; 1206 | 1207 | } 1208 | 1209 | // 1210 | 1211 | scope.domElement.addEventListener( 'contextmenu', onContextMenu ); 1212 | 1213 | scope.domElement.addEventListener( 'pointerdown', onPointerDown ); 1214 | scope.domElement.addEventListener( 'pointercancel', onPointerCancel ); 1215 | scope.domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } ); 1216 | 1217 | // force an update at start 1218 | 1219 | this.update(); 1220 | 1221 | } 1222 | 1223 | } 1224 | 1225 | 1226 | // This set of controls performs orbiting, dollying (zooming), and panning. 1227 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 1228 | // This is very similar to OrbitControls, another set of touch behavior 1229 | // 1230 | // Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate 1231 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 1232 | // Pan - left mouse, or arrow keys / touch: one-finger move 1233 | 1234 | class MapControls extends OrbitControls { 1235 | 1236 | constructor( object, domElement ) { 1237 | 1238 | super( object, domElement ); 1239 | 1240 | this.screenSpacePanning = false; // pan orthogonal to world-space direction camera.up 1241 | 1242 | this.mouseButtons.LEFT = MOUSE.PAN; 1243 | this.mouseButtons.RIGHT = MOUSE.ROTATE; 1244 | 1245 | this.touches.ONE = TOUCH.PAN; 1246 | this.touches.TWO = TOUCH.DOLLY_ROTATE; 1247 | 1248 | } 1249 | 1250 | } 1251 | 1252 | export { OrbitControls, MapControls }; 1253 | -------------------------------------------------------------------------------- /examples/jsm/loaders/LDrawLoader.js: -------------------------------------------------------------------------------- 1 | import { 2 | BufferAttribute, 3 | BufferGeometry, 4 | Color, 5 | FileLoader, 6 | Group, 7 | LineBasicMaterial, 8 | LineSegments, 9 | Loader, 10 | Matrix4, 11 | Mesh, 12 | MeshStandardMaterial, 13 | ShaderMaterial, 14 | UniformsLib, 15 | UniformsUtils, 16 | Vector3, 17 | Ray 18 | } from '../../../build/three.module.js'; 19 | 20 | // Special surface finish tag types. 21 | // Note: "MATERIAL" tag (e.g. GLITTER, SPECKLE) is not implemented 22 | const FINISH_TYPE_DEFAULT = 0; 23 | const FINISH_TYPE_CHROME = 1; 24 | const FINISH_TYPE_PEARLESCENT = 2; 25 | const FINISH_TYPE_RUBBER = 3; 26 | const FINISH_TYPE_MATTE_METALLIC = 4; 27 | const FINISH_TYPE_METAL = 5; 28 | 29 | // State machine to search a subobject path. 30 | // The LDraw standard establishes these various possible subfolders. 31 | const FILE_LOCATION_AS_IS = 0; 32 | const FILE_LOCATION_TRY_PARTS = 1; 33 | const FILE_LOCATION_TRY_P = 2; 34 | const FILE_LOCATION_TRY_MODELS = 3; 35 | const FILE_LOCATION_TRY_RELATIVE = 4; 36 | const FILE_LOCATION_TRY_ABSOLUTE = 5; 37 | const FILE_LOCATION_NOT_FOUND = 6; 38 | 39 | const MAIN_COLOUR_CODE = '16'; 40 | const MAIN_EDGE_COLOUR_CODE = '24'; 41 | 42 | const _tempVec0 = new Vector3(); 43 | const _tempVec1 = new Vector3(); 44 | 45 | class LDrawConditionalLineMaterial extends ShaderMaterial { 46 | 47 | constructor( parameters ) { 48 | 49 | super( { 50 | 51 | uniforms: UniformsUtils.merge( [ 52 | UniformsLib.fog, 53 | { 54 | diffuse: { 55 | value: new Color() 56 | }, 57 | opacity: { 58 | value: 1.0 59 | } 60 | } 61 | ] ), 62 | 63 | vertexShader: /* glsl */` 64 | attribute vec3 control0; 65 | attribute vec3 control1; 66 | attribute vec3 direction; 67 | varying float discardFlag; 68 | 69 | #include 70 | #include 71 | #include 72 | #include 73 | #include 74 | void main() { 75 | #include 76 | 77 | vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 ); 78 | gl_Position = projectionMatrix * mvPosition; 79 | 80 | // Transform the line segment ends and control points into camera clip space 81 | vec4 c0 = projectionMatrix * modelViewMatrix * vec4( control0, 1.0 ); 82 | vec4 c1 = projectionMatrix * modelViewMatrix * vec4( control1, 1.0 ); 83 | vec4 p0 = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 84 | vec4 p1 = projectionMatrix * modelViewMatrix * vec4( position + direction, 1.0 ); 85 | 86 | c0.xy /= c0.w; 87 | c1.xy /= c1.w; 88 | p0.xy /= p0.w; 89 | p1.xy /= p1.w; 90 | 91 | // Get the direction of the segment and an orthogonal vector 92 | vec2 dir = p1.xy - p0.xy; 93 | vec2 norm = vec2( -dir.y, dir.x ); 94 | 95 | // Get control point directions from the line 96 | vec2 c0dir = c0.xy - p1.xy; 97 | vec2 c1dir = c1.xy - p1.xy; 98 | 99 | // If the vectors to the controls points are pointed in different directions away 100 | // from the line segment then the line should not be drawn. 101 | float d0 = dot( normalize( norm ), normalize( c0dir ) ); 102 | float d1 = dot( normalize( norm ), normalize( c1dir ) ); 103 | discardFlag = float( sign( d0 ) != sign( d1 ) ); 104 | 105 | #include 106 | #include 107 | #include 108 | } 109 | `, 110 | 111 | fragmentShader: /* glsl */` 112 | uniform vec3 diffuse; 113 | uniform float opacity; 114 | varying float discardFlag; 115 | 116 | #include 117 | #include 118 | #include 119 | #include 120 | #include 121 | void main() { 122 | 123 | if ( discardFlag > 0.5 ) discard; 124 | 125 | #include 126 | vec3 outgoingLight = vec3( 0.0 ); 127 | vec4 diffuseColor = vec4( diffuse, opacity ); 128 | #include 129 | #include 130 | outgoingLight = diffuseColor.rgb; // simple shader 131 | gl_FragColor = vec4( outgoingLight, diffuseColor.a ); 132 | #include 133 | #include 134 | #include 135 | #include 136 | } 137 | `, 138 | 139 | } ); 140 | 141 | Object.defineProperties( this, { 142 | 143 | opacity: { 144 | get: function () { 145 | 146 | return this.uniforms.opacity.value; 147 | 148 | }, 149 | 150 | set: function ( value ) { 151 | 152 | this.uniforms.opacity.value = value; 153 | 154 | } 155 | }, 156 | 157 | color: { 158 | get: function () { 159 | 160 | return this.uniforms.diffuse.value; 161 | 162 | } 163 | } 164 | 165 | } ); 166 | 167 | this.setValues( parameters ); 168 | this.isLDrawConditionalLineMaterial = true; 169 | 170 | } 171 | 172 | } 173 | 174 | class ConditionalLineSegments extends LineSegments { 175 | 176 | constructor( geometry, material ) { 177 | 178 | super( geometry, material ); 179 | this.isConditionalLine = true; 180 | 181 | } 182 | 183 | } 184 | 185 | function generateFaceNormals( faces ) { 186 | 187 | for ( let i = 0, l = faces.length; i < l; i ++ ) { 188 | 189 | const face = faces[ i ]; 190 | const vertices = face.vertices; 191 | const v0 = vertices[ 0 ]; 192 | const v1 = vertices[ 1 ]; 193 | const v2 = vertices[ 2 ]; 194 | 195 | _tempVec0.subVectors( v1, v0 ); 196 | _tempVec1.subVectors( v2, v1 ); 197 | face.faceNormal = new Vector3() 198 | .crossVectors( _tempVec0, _tempVec1 ) 199 | .normalize(); 200 | 201 | } 202 | 203 | } 204 | 205 | const _ray = new Ray(); 206 | function smoothNormals( faces, lineSegments, checkSubSegments = false ) { 207 | 208 | // NOTE: 1e2 is pretty coarse but was chosen to quantize the resulting value because 209 | // it allows edges to be smoothed as expected (see minifig arms). 210 | // -- 211 | // And the vector values are initialize multiplied by 1 + 1e-10 to account for floating 212 | // point errors on vertices along quantization boundaries. Ie after matrix multiplication 213 | // vertices that should be merged might be set to "1.7" and "1.6999..." meaning they won't 214 | // get merged. This added epsilon attempts to push these error values to the same quantized 215 | // value for the sake of hashing. See "AT-ST mini" dishes. See mrdoob/three#23169. 216 | 217 | const hashMultiplier = ( 1 + 1e-10 ) * 1e2; 218 | function hashVertex( v ) { 219 | 220 | const x = ~ ~ ( v.x * hashMultiplier ); 221 | const y = ~ ~ ( v.y * hashMultiplier ); 222 | const z = ~ ~ ( v.z * hashMultiplier ); 223 | 224 | return `${ x },${ y },${ z }`; 225 | 226 | } 227 | 228 | function hashEdge( v0, v1 ) { 229 | 230 | return `${ hashVertex( v0 ) }_${ hashVertex( v1 ) }`; 231 | 232 | } 233 | 234 | // converts the two vertices to a ray with a normalized direction and origin of 0, 0, 0 projected 235 | // onto the original line. 236 | function toNormalizedRay( v0, v1, targetRay ) { 237 | 238 | targetRay.direction.subVectors( v1, v0 ).normalize(); 239 | 240 | const scalar = v0.dot( targetRay.direction ); 241 | targetRay.origin.copy( v0 ).addScaledVector( targetRay.direction, - scalar ); 242 | 243 | return targetRay; 244 | 245 | } 246 | 247 | function hashRay( ray ) { 248 | 249 | return hashEdge( ray.origin, ray.direction ); 250 | 251 | } 252 | 253 | const hardEdges = new Set(); 254 | const hardEdgeRays = new Map(); 255 | const halfEdgeList = {}; 256 | const normals = []; 257 | 258 | // Save the list of hard edges by hash 259 | for ( let i = 0, l = lineSegments.length; i < l; i ++ ) { 260 | 261 | const ls = lineSegments[ i ]; 262 | const vertices = ls.vertices; 263 | const v0 = vertices[ 0 ]; 264 | const v1 = vertices[ 1 ]; 265 | hardEdges.add( hashEdge( v0, v1 ) ); 266 | hardEdges.add( hashEdge( v1, v0 ) ); 267 | 268 | // only generate the hard edge ray map if we're checking subsegments because it's more expensive to check 269 | // and requires more memory. 270 | if ( checkSubSegments ) { 271 | 272 | // add both ray directions to the map 273 | const ray = toNormalizedRay( v0, v1, new Ray() ); 274 | const rh1 = hashRay( ray ); 275 | if ( ! hardEdgeRays.has( rh1 ) ) { 276 | 277 | toNormalizedRay( v1, v0, ray ); 278 | const rh2 = hashRay( ray ); 279 | 280 | const info = { 281 | ray, 282 | distances: [], 283 | }; 284 | 285 | hardEdgeRays.set( rh1, info ); 286 | hardEdgeRays.set( rh2, info ); 287 | 288 | } 289 | 290 | // store both segments ends in min, max order in the distances array to check if a face edge is a 291 | // subsegment later. 292 | const info = hardEdgeRays.get( rh1 ); 293 | let d0 = info.ray.direction.dot( v0 ); 294 | let d1 = info.ray.direction.dot( v1 ); 295 | if ( d0 > d1 ) { 296 | 297 | [ d0, d1 ] = [ d1, d0 ]; 298 | 299 | } 300 | 301 | info.distances.push( d0, d1 ); 302 | 303 | } 304 | 305 | } 306 | 307 | // track the half edges associated with each triangle 308 | for ( let i = 0, l = faces.length; i < l; i ++ ) { 309 | 310 | const tri = faces[ i ]; 311 | const vertices = tri.vertices; 312 | const vertCount = vertices.length; 313 | for ( let i2 = 0; i2 < vertCount; i2 ++ ) { 314 | 315 | const index = i2; 316 | const next = ( i2 + 1 ) % vertCount; 317 | const v0 = vertices[ index ]; 318 | const v1 = vertices[ next ]; 319 | const hash = hashEdge( v0, v1 ); 320 | 321 | // don't add the triangle if the edge is supposed to be hard 322 | if ( hardEdges.has( hash ) ) { 323 | 324 | continue; 325 | 326 | } 327 | 328 | // if checking subsegments then check to see if this edge lies on a hard edge ray and whether its within any ray bounds 329 | if ( checkSubSegments ) { 330 | 331 | toNormalizedRay( v0, v1, _ray ); 332 | 333 | const rayHash = hashRay( _ray ); 334 | if ( hardEdgeRays.has( rayHash ) ) { 335 | 336 | const info = hardEdgeRays.get( rayHash ); 337 | const { ray, distances } = info; 338 | let d0 = ray.direction.dot( v0 ); 339 | let d1 = ray.direction.dot( v1 ); 340 | 341 | if ( d0 > d1 ) { 342 | 343 | [ d0, d1 ] = [ d1, d0 ]; 344 | 345 | } 346 | 347 | // return early if the face edge is found to be a subsegment of a line edge meaning the edge will have "hard" normals 348 | let found = false; 349 | for ( let i = 0, l = distances.length; i < l; i += 2 ) { 350 | 351 | if ( d0 >= distances[ i ] && d1 <= distances[ i + 1 ] ) { 352 | 353 | found = true; 354 | break; 355 | 356 | } 357 | 358 | } 359 | 360 | if ( found ) { 361 | 362 | continue; 363 | 364 | } 365 | 366 | } 367 | 368 | } 369 | 370 | const info = { 371 | index: index, 372 | tri: tri 373 | }; 374 | halfEdgeList[ hash ] = info; 375 | 376 | } 377 | 378 | } 379 | 380 | // Iterate until we've tried to connect all faces to share normals 381 | while ( true ) { 382 | 383 | // Stop if there are no more faces left 384 | let halfEdge = null; 385 | for ( const key in halfEdgeList ) { 386 | 387 | halfEdge = halfEdgeList[ key ]; 388 | break; 389 | 390 | } 391 | 392 | if ( halfEdge === null ) { 393 | 394 | break; 395 | 396 | } 397 | 398 | // Exhaustively find all connected faces 399 | const queue = [ halfEdge ]; 400 | while ( queue.length > 0 ) { 401 | 402 | // initialize all vertex normals in this triangle 403 | const tri = queue.pop().tri; 404 | const vertices = tri.vertices; 405 | const vertNormals = tri.normals; 406 | const faceNormal = tri.faceNormal; 407 | 408 | // Check if any edge is connected to another triangle edge 409 | const vertCount = vertices.length; 410 | for ( let i2 = 0; i2 < vertCount; i2 ++ ) { 411 | 412 | const index = i2; 413 | const next = ( i2 + 1 ) % vertCount; 414 | const v0 = vertices[ index ]; 415 | const v1 = vertices[ next ]; 416 | 417 | // delete this triangle from the list so it won't be found again 418 | const hash = hashEdge( v0, v1 ); 419 | delete halfEdgeList[ hash ]; 420 | 421 | const reverseHash = hashEdge( v1, v0 ); 422 | const otherInfo = halfEdgeList[ reverseHash ]; 423 | if ( otherInfo ) { 424 | 425 | const otherTri = otherInfo.tri; 426 | const otherIndex = otherInfo.index; 427 | const otherNormals = otherTri.normals; 428 | const otherVertCount = otherNormals.length; 429 | const otherFaceNormal = otherTri.faceNormal; 430 | 431 | // NOTE: If the angle between faces is > 67.5 degrees then assume it's 432 | // hard edge. There are some cases where the line segments do not line up exactly 433 | // with or span multiple triangle edges (see Lunar Vehicle wheels). 434 | if ( Math.abs( otherTri.faceNormal.dot( tri.faceNormal ) ) < 0.25 ) { 435 | 436 | continue; 437 | 438 | } 439 | 440 | // if this triangle has already been traversed then it won't be in 441 | // the halfEdgeList. If it has not then add it to the queue and delete 442 | // it so it won't be found again. 443 | if ( reverseHash in halfEdgeList ) { 444 | 445 | queue.push( otherInfo ); 446 | delete halfEdgeList[ reverseHash ]; 447 | 448 | } 449 | 450 | // share the first normal 451 | const otherNext = ( otherIndex + 1 ) % otherVertCount; 452 | if ( 453 | vertNormals[ index ] && otherNormals[ otherNext ] && 454 | vertNormals[ index ] !== otherNormals[ otherNext ] 455 | ) { 456 | 457 | otherNormals[ otherNext ].norm.add( vertNormals[ index ].norm ); 458 | vertNormals[ index ].norm = otherNormals[ otherNext ].norm; 459 | 460 | } 461 | 462 | let sharedNormal1 = vertNormals[ index ] || otherNormals[ otherNext ]; 463 | if ( sharedNormal1 === null ) { 464 | 465 | // it's possible to encounter an edge of a triangle that has already been traversed meaning 466 | // both edges already have different normals defined and shared. To work around this we create 467 | // a wrapper object so when those edges are merged the normals can be updated everywhere. 468 | sharedNormal1 = { norm: new Vector3() }; 469 | normals.push( sharedNormal1.norm ); 470 | 471 | } 472 | 473 | if ( vertNormals[ index ] === null ) { 474 | 475 | vertNormals[ index ] = sharedNormal1; 476 | sharedNormal1.norm.add( faceNormal ); 477 | 478 | } 479 | 480 | if ( otherNormals[ otherNext ] === null ) { 481 | 482 | otherNormals[ otherNext ] = sharedNormal1; 483 | sharedNormal1.norm.add( otherFaceNormal ); 484 | 485 | } 486 | 487 | // share the second normal 488 | if ( 489 | vertNormals[ next ] && otherNormals[ otherIndex ] && 490 | vertNormals[ next ] !== otherNormals[ otherIndex ] 491 | ) { 492 | 493 | otherNormals[ otherIndex ].norm.add( vertNormals[ next ].norm ); 494 | vertNormals[ next ].norm = otherNormals[ otherIndex ].norm; 495 | 496 | } 497 | 498 | let sharedNormal2 = vertNormals[ next ] || otherNormals[ otherIndex ]; 499 | if ( sharedNormal2 === null ) { 500 | 501 | sharedNormal2 = { norm: new Vector3() }; 502 | normals.push( sharedNormal2.norm ); 503 | 504 | } 505 | 506 | if ( vertNormals[ next ] === null ) { 507 | 508 | vertNormals[ next ] = sharedNormal2; 509 | sharedNormal2.norm.add( faceNormal ); 510 | 511 | } 512 | 513 | if ( otherNormals[ otherIndex ] === null ) { 514 | 515 | otherNormals[ otherIndex ] = sharedNormal2; 516 | sharedNormal2.norm.add( otherFaceNormal ); 517 | 518 | } 519 | 520 | } 521 | 522 | } 523 | 524 | } 525 | 526 | } 527 | 528 | // The normals of each face have been added up so now we average them by normalizing the vector. 529 | for ( let i = 0, l = normals.length; i < l; i ++ ) { 530 | 531 | normals[ i ].normalize(); 532 | 533 | } 534 | 535 | } 536 | 537 | function isPartType( type ) { 538 | 539 | return type === 'Part' || type === 'Unofficial_Part'; 540 | 541 | } 542 | 543 | function isPrimitiveType( type ) { 544 | 545 | return /primitive/i.test( type ) || type === 'Subpart'; 546 | 547 | } 548 | 549 | class LineParser { 550 | 551 | constructor( line, lineNumber ) { 552 | 553 | this.line = line; 554 | this.lineLength = line.length; 555 | this.currentCharIndex = 0; 556 | this.currentChar = ' '; 557 | this.lineNumber = lineNumber; 558 | 559 | } 560 | 561 | seekNonSpace() { 562 | 563 | while ( this.currentCharIndex < this.lineLength ) { 564 | 565 | this.currentChar = this.line.charAt( this.currentCharIndex ); 566 | 567 | if ( this.currentChar !== ' ' && this.currentChar !== '\t' ) { 568 | 569 | return; 570 | 571 | } 572 | 573 | this.currentCharIndex ++; 574 | 575 | } 576 | 577 | } 578 | 579 | getToken() { 580 | 581 | const pos0 = this.currentCharIndex ++; 582 | 583 | // Seek space 584 | while ( this.currentCharIndex < this.lineLength ) { 585 | 586 | this.currentChar = this.line.charAt( this.currentCharIndex ); 587 | 588 | if ( this.currentChar === ' ' || this.currentChar === '\t' ) { 589 | 590 | break; 591 | 592 | } 593 | 594 | this.currentCharIndex ++; 595 | 596 | } 597 | 598 | const pos1 = this.currentCharIndex; 599 | 600 | this.seekNonSpace(); 601 | 602 | return this.line.substring( pos0, pos1 ); 603 | 604 | } 605 | 606 | getVector() { 607 | 608 | return new Vector3( parseFloat( this.getToken() ), parseFloat( this.getToken() ), parseFloat( this.getToken() ) ); 609 | 610 | } 611 | 612 | getRemainingString() { 613 | 614 | return this.line.substring( this.currentCharIndex, this.lineLength ); 615 | 616 | } 617 | 618 | isAtTheEnd() { 619 | 620 | return this.currentCharIndex >= this.lineLength; 621 | 622 | } 623 | 624 | setToEnd() { 625 | 626 | this.currentCharIndex = this.lineLength; 627 | 628 | } 629 | 630 | getLineNumberString() { 631 | 632 | return this.lineNumber >= 0 ? ' at line ' + this.lineNumber : ''; 633 | 634 | } 635 | 636 | } 637 | 638 | // Fetches and parses an intermediate representation of LDraw parts files. 639 | class LDrawParsedCache { 640 | 641 | constructor( loader ) { 642 | 643 | this.loader = loader; 644 | this._cache = {}; 645 | 646 | } 647 | 648 | cloneResult( original ) { 649 | 650 | const result = {}; 651 | 652 | // vertices are transformed and normals computed before being converted to geometry 653 | // so these pieces must be cloned. 654 | result.faces = original.faces.map( face => { 655 | 656 | return { 657 | colorCode: face.colorCode, 658 | material: face.material, 659 | vertices: face.vertices.map( v => v.clone() ), 660 | normals: face.normals.map( () => null ), 661 | faceNormal: null 662 | }; 663 | 664 | } ); 665 | 666 | result.conditionalSegments = original.conditionalSegments.map( face => { 667 | 668 | return { 669 | colorCode: face.colorCode, 670 | material: face.material, 671 | vertices: face.vertices.map( v => v.clone() ), 672 | controlPoints: face.controlPoints.map( v => v.clone() ) 673 | }; 674 | 675 | } ); 676 | 677 | result.lineSegments = original.lineSegments.map( face => { 678 | 679 | return { 680 | colorCode: face.colorCode, 681 | material: face.material, 682 | vertices: face.vertices.map( v => v.clone() ) 683 | }; 684 | 685 | } ); 686 | 687 | // none if this is subsequently modified 688 | result.type = original.type; 689 | result.category = original.category; 690 | result.keywords = original.keywords; 691 | result.subobjects = original.subobjects; 692 | result.totalFaces = original.totalFaces; 693 | result.startingConstructionStep = original.startingConstructionStep; 694 | result.materials = original.materials; 695 | result.group = null; 696 | return result; 697 | 698 | } 699 | 700 | async fetchData( fileName ) { 701 | 702 | let triedLowerCase = false; 703 | let locationState = FILE_LOCATION_AS_IS; 704 | while ( locationState !== FILE_LOCATION_NOT_FOUND ) { 705 | 706 | let subobjectURL = fileName; 707 | switch ( locationState ) { 708 | 709 | case FILE_LOCATION_AS_IS: 710 | locationState = locationState + 1; 711 | break; 712 | 713 | case FILE_LOCATION_TRY_PARTS: 714 | subobjectURL = 'parts/' + subobjectURL; 715 | locationState = locationState + 1; 716 | break; 717 | 718 | case FILE_LOCATION_TRY_P: 719 | subobjectURL = 'p/' + subobjectURL; 720 | locationState = locationState + 1; 721 | break; 722 | 723 | case FILE_LOCATION_TRY_MODELS: 724 | subobjectURL = 'models/' + subobjectURL; 725 | locationState = locationState + 1; 726 | break; 727 | 728 | case FILE_LOCATION_TRY_RELATIVE: 729 | subobjectURL = fileName.substring( 0, fileName.lastIndexOf( '/' ) + 1 ) + subobjectURL; 730 | locationState = locationState + 1; 731 | break; 732 | 733 | case FILE_LOCATION_TRY_ABSOLUTE: 734 | 735 | if ( triedLowerCase ) { 736 | 737 | // Try absolute path 738 | locationState = FILE_LOCATION_NOT_FOUND; 739 | 740 | } else { 741 | 742 | // Next attempt is lower case 743 | fileName = fileName.toLowerCase(); 744 | subobjectURL = fileName; 745 | triedLowerCase = true; 746 | locationState = FILE_LOCATION_AS_IS; 747 | 748 | } 749 | 750 | break; 751 | 752 | } 753 | 754 | const loader = this.loader; 755 | const fileLoader = new FileLoader( loader.manager ); 756 | fileLoader.setPath( loader.partsLibraryPath ); 757 | fileLoader.setRequestHeader( loader.requestHeader ); 758 | fileLoader.setWithCredentials( loader.withCredentials ); 759 | 760 | try { 761 | 762 | const text = await fileLoader.loadAsync( subobjectURL ); 763 | return text; 764 | 765 | } catch { 766 | 767 | continue; 768 | 769 | } 770 | 771 | } 772 | 773 | throw new Error( 'LDrawLoader: Subobject "' + fileName + '" could not be loaded.' ); 774 | 775 | } 776 | 777 | parse( text, fileName = null ) { 778 | 779 | const loader = this.loader; 780 | 781 | // final results 782 | const faces = []; 783 | const lineSegments = []; 784 | const conditionalSegments = []; 785 | const subobjects = []; 786 | const materials = {}; 787 | 788 | const getLocalMaterial = colorCode => { 789 | 790 | return materials[ colorCode ] || null; 791 | 792 | }; 793 | 794 | let type = 'Model'; 795 | let category = null; 796 | let keywords = null; 797 | let totalFaces = 0; 798 | 799 | // split into lines 800 | if ( text.indexOf( '\r\n' ) !== - 1 ) { 801 | 802 | // This is faster than String.split with regex that splits on both 803 | text = text.replace( /\r\n/g, '\n' ); 804 | 805 | } 806 | 807 | const lines = text.split( '\n' ); 808 | const numLines = lines.length; 809 | 810 | let parsingEmbeddedFiles = false; 811 | let currentEmbeddedFileName = null; 812 | let currentEmbeddedText = null; 813 | 814 | let bfcCertified = false; 815 | let bfcCCW = true; 816 | let bfcInverted = false; 817 | let bfcCull = true; 818 | 819 | let startingConstructionStep = false; 820 | 821 | // Parse all line commands 822 | for ( let lineIndex = 0; lineIndex < numLines; lineIndex ++ ) { 823 | 824 | const line = lines[ lineIndex ]; 825 | 826 | if ( line.length === 0 ) continue; 827 | 828 | if ( parsingEmbeddedFiles ) { 829 | 830 | if ( line.startsWith( '0 FILE ' ) ) { 831 | 832 | // Save previous embedded file in the cache 833 | this.setData( currentEmbeddedFileName, currentEmbeddedText ); 834 | 835 | // New embedded text file 836 | currentEmbeddedFileName = line.substring( 7 ); 837 | currentEmbeddedText = ''; 838 | 839 | } else { 840 | 841 | currentEmbeddedText += line + '\n'; 842 | 843 | } 844 | 845 | continue; 846 | 847 | } 848 | 849 | const lp = new LineParser( line, lineIndex + 1 ); 850 | lp.seekNonSpace(); 851 | 852 | if ( lp.isAtTheEnd() ) { 853 | 854 | // Empty line 855 | continue; 856 | 857 | } 858 | 859 | // Parse the line type 860 | const lineType = lp.getToken(); 861 | 862 | let material; 863 | let colorCode; 864 | let segment; 865 | let ccw; 866 | let doubleSided; 867 | let v0, v1, v2, v3, c0, c1; 868 | 869 | switch ( lineType ) { 870 | 871 | // Line type 0: Comment or META 872 | case '0': 873 | 874 | // Parse meta directive 875 | const meta = lp.getToken(); 876 | 877 | if ( meta ) { 878 | 879 | switch ( meta ) { 880 | 881 | case '!LDRAW_ORG': 882 | 883 | type = lp.getToken(); 884 | break; 885 | 886 | case '!COLOUR': 887 | 888 | material = loader.parseColorMetaDirective( lp ); 889 | if ( material ) { 890 | 891 | materials[ material.userData.code ] = material; 892 | 893 | } else { 894 | 895 | console.warn( 'LDrawLoader: Error parsing material' + lp.getLineNumberString() ); 896 | 897 | } 898 | 899 | break; 900 | 901 | case '!CATEGORY': 902 | 903 | category = lp.getToken(); 904 | break; 905 | 906 | case '!KEYWORDS': 907 | 908 | const newKeywords = lp.getRemainingString().split( ',' ); 909 | if ( newKeywords.length > 0 ) { 910 | 911 | if ( ! keywords ) { 912 | 913 | keywords = []; 914 | 915 | } 916 | 917 | newKeywords.forEach( function ( keyword ) { 918 | 919 | keywords.push( keyword.trim() ); 920 | 921 | } ); 922 | 923 | } 924 | 925 | break; 926 | 927 | case 'FILE': 928 | 929 | if ( lineIndex > 0 ) { 930 | 931 | // Start embedded text files parsing 932 | parsingEmbeddedFiles = true; 933 | currentEmbeddedFileName = lp.getRemainingString(); 934 | currentEmbeddedText = ''; 935 | 936 | bfcCertified = false; 937 | bfcCCW = true; 938 | 939 | } 940 | 941 | break; 942 | 943 | case 'BFC': 944 | 945 | // Changes to the backface culling state 946 | while ( ! lp.isAtTheEnd() ) { 947 | 948 | const token = lp.getToken(); 949 | 950 | switch ( token ) { 951 | 952 | case 'CERTIFY': 953 | case 'NOCERTIFY': 954 | 955 | bfcCertified = token === 'CERTIFY'; 956 | bfcCCW = true; 957 | 958 | break; 959 | 960 | case 'CW': 961 | case 'CCW': 962 | 963 | bfcCCW = token === 'CCW'; 964 | 965 | break; 966 | 967 | case 'INVERTNEXT': 968 | 969 | bfcInverted = true; 970 | 971 | break; 972 | 973 | case 'CLIP': 974 | case 'NOCLIP': 975 | 976 | bfcCull = token === 'CLIP'; 977 | 978 | break; 979 | 980 | default: 981 | 982 | console.warn( 'THREE.LDrawLoader: BFC directive "' + token + '" is unknown.' ); 983 | 984 | break; 985 | 986 | } 987 | 988 | } 989 | 990 | break; 991 | 992 | case 'STEP': 993 | 994 | startingConstructionStep = true; 995 | 996 | break; 997 | 998 | default: 999 | // Other meta directives are not implemented 1000 | break; 1001 | 1002 | } 1003 | 1004 | } 1005 | 1006 | break; 1007 | 1008 | // Line type 1: Sub-object file 1009 | case '1': 1010 | 1011 | colorCode = lp.getToken(); 1012 | material = getLocalMaterial( colorCode ); 1013 | 1014 | const posX = parseFloat( lp.getToken() ); 1015 | const posY = parseFloat( lp.getToken() ); 1016 | const posZ = parseFloat( lp.getToken() ); 1017 | const m0 = parseFloat( lp.getToken() ); 1018 | const m1 = parseFloat( lp.getToken() ); 1019 | const m2 = parseFloat( lp.getToken() ); 1020 | const m3 = parseFloat( lp.getToken() ); 1021 | const m4 = parseFloat( lp.getToken() ); 1022 | const m5 = parseFloat( lp.getToken() ); 1023 | const m6 = parseFloat( lp.getToken() ); 1024 | const m7 = parseFloat( lp.getToken() ); 1025 | const m8 = parseFloat( lp.getToken() ); 1026 | 1027 | const matrix = new Matrix4().set( 1028 | m0, m1, m2, posX, 1029 | m3, m4, m5, posY, 1030 | m6, m7, m8, posZ, 1031 | 0, 0, 0, 1 1032 | ); 1033 | 1034 | let fileName = lp.getRemainingString().trim().replace( /\\/g, '/' ); 1035 | 1036 | if ( loader.fileMap[ fileName ] ) { 1037 | 1038 | // Found the subobject path in the preloaded file path map 1039 | fileName = loader.fileMap[ fileName ]; 1040 | 1041 | } else { 1042 | 1043 | // Standardized subfolders 1044 | if ( fileName.startsWith( 's/' ) ) { 1045 | 1046 | fileName = 'parts/' + fileName; 1047 | 1048 | } else if ( fileName.startsWith( '48/' ) ) { 1049 | 1050 | fileName = 'p/' + fileName; 1051 | 1052 | } 1053 | 1054 | } 1055 | 1056 | subobjects.push( { 1057 | material: material, 1058 | colorCode: colorCode, 1059 | matrix: matrix, 1060 | fileName: fileName, 1061 | inverted: bfcInverted, 1062 | startingConstructionStep: startingConstructionStep 1063 | } ); 1064 | 1065 | bfcInverted = false; 1066 | 1067 | break; 1068 | 1069 | // Line type 2: Line segment 1070 | case '2': 1071 | 1072 | colorCode = lp.getToken(); 1073 | material = getLocalMaterial( colorCode ); 1074 | v0 = lp.getVector(); 1075 | v1 = lp.getVector(); 1076 | 1077 | segment = { 1078 | material: material, 1079 | colorCode: colorCode, 1080 | vertices: [ v0, v1 ], 1081 | }; 1082 | 1083 | lineSegments.push( segment ); 1084 | 1085 | break; 1086 | 1087 | // Line type 5: Conditional Line segment 1088 | case '5': 1089 | 1090 | colorCode = lp.getToken(); 1091 | material = getLocalMaterial( colorCode ); 1092 | v0 = lp.getVector(); 1093 | v1 = lp.getVector(); 1094 | c0 = lp.getVector(); 1095 | c1 = lp.getVector(); 1096 | 1097 | segment = { 1098 | material: material, 1099 | colorCode: colorCode, 1100 | vertices: [ v0, v1 ], 1101 | controlPoints: [ c0, c1 ], 1102 | }; 1103 | 1104 | conditionalSegments.push( segment ); 1105 | 1106 | break; 1107 | 1108 | // Line type 3: Triangle 1109 | case '3': 1110 | 1111 | colorCode = lp.getToken(); 1112 | material = getLocalMaterial( colorCode ); 1113 | ccw = bfcCCW; 1114 | doubleSided = ! bfcCertified || ! bfcCull; 1115 | 1116 | if ( ccw === true ) { 1117 | 1118 | v0 = lp.getVector(); 1119 | v1 = lp.getVector(); 1120 | v2 = lp.getVector(); 1121 | 1122 | } else { 1123 | 1124 | v2 = lp.getVector(); 1125 | v1 = lp.getVector(); 1126 | v0 = lp.getVector(); 1127 | 1128 | } 1129 | 1130 | faces.push( { 1131 | material: material, 1132 | colorCode: colorCode, 1133 | faceNormal: null, 1134 | vertices: [ v0, v1, v2 ], 1135 | normals: [ null, null, null ], 1136 | } ); 1137 | totalFaces ++; 1138 | 1139 | if ( doubleSided === true ) { 1140 | 1141 | faces.push( { 1142 | material: material, 1143 | colorCode: colorCode, 1144 | faceNormal: null, 1145 | vertices: [ v2, v1, v0 ], 1146 | normals: [ null, null, null ], 1147 | } ); 1148 | totalFaces ++; 1149 | 1150 | } 1151 | 1152 | break; 1153 | 1154 | // Line type 4: Quadrilateral 1155 | case '4': 1156 | 1157 | colorCode = lp.getToken(); 1158 | material = getLocalMaterial( colorCode ); 1159 | ccw = bfcCCW; 1160 | doubleSided = ! bfcCertified || ! bfcCull; 1161 | 1162 | if ( ccw === true ) { 1163 | 1164 | v0 = lp.getVector(); 1165 | v1 = lp.getVector(); 1166 | v2 = lp.getVector(); 1167 | v3 = lp.getVector(); 1168 | 1169 | } else { 1170 | 1171 | v3 = lp.getVector(); 1172 | v2 = lp.getVector(); 1173 | v1 = lp.getVector(); 1174 | v0 = lp.getVector(); 1175 | 1176 | } 1177 | 1178 | // specifically place the triangle diagonal in the v0 and v1 slots so we can 1179 | // account for the doubling of vertices later when smoothing normals. 1180 | faces.push( { 1181 | material: material, 1182 | colorCode: colorCode, 1183 | faceNormal: null, 1184 | vertices: [ v0, v1, v2, v3 ], 1185 | normals: [ null, null, null, null ], 1186 | } ); 1187 | totalFaces += 2; 1188 | 1189 | if ( doubleSided === true ) { 1190 | 1191 | faces.push( { 1192 | material: material, 1193 | colorCode: colorCode, 1194 | faceNormal: null, 1195 | vertices: [ v3, v2, v1, v0 ], 1196 | normals: [ null, null, null, null ], 1197 | } ); 1198 | totalFaces += 2; 1199 | 1200 | } 1201 | 1202 | break; 1203 | 1204 | default: 1205 | throw new Error( 'LDrawLoader: Unknown line type "' + lineType + '"' + lp.getLineNumberString() + '.' ); 1206 | 1207 | } 1208 | 1209 | } 1210 | 1211 | if ( parsingEmbeddedFiles ) { 1212 | 1213 | this.setData( currentEmbeddedFileName, currentEmbeddedText ); 1214 | 1215 | } 1216 | 1217 | return { 1218 | faces, 1219 | conditionalSegments, 1220 | lineSegments, 1221 | type, 1222 | category, 1223 | keywords, 1224 | subobjects, 1225 | totalFaces, 1226 | startingConstructionStep, 1227 | materials, 1228 | fileName, 1229 | group: null 1230 | }; 1231 | 1232 | } 1233 | 1234 | // returns an (optionally cloned) instance of the data 1235 | getData( fileName, clone = true ) { 1236 | 1237 | const key = fileName.toLowerCase(); 1238 | const result = this._cache[ key ]; 1239 | if ( result === null || result instanceof Promise ) { 1240 | 1241 | return null; 1242 | 1243 | } 1244 | 1245 | if ( clone ) { 1246 | 1247 | return this.cloneResult( result ); 1248 | 1249 | } else { 1250 | 1251 | return result; 1252 | 1253 | } 1254 | 1255 | } 1256 | 1257 | // kicks off a fetch and parse of the requested data if it hasn't already been loaded. Returns when 1258 | // the data is ready to use and can be retrieved synchronously with "getData". 1259 | async ensureDataLoaded( fileName ) { 1260 | 1261 | const key = fileName.toLowerCase(); 1262 | if ( ! ( key in this._cache ) ) { 1263 | 1264 | // replace the promise with a copy of the parsed data for immediate processing 1265 | this._cache[ key ] = this.fetchData( fileName ).then( text => { 1266 | 1267 | const info = this.parse( text, fileName ); 1268 | this._cache[ key ] = info; 1269 | return info; 1270 | 1271 | } ); 1272 | 1273 | } 1274 | 1275 | await this._cache[ key ]; 1276 | 1277 | } 1278 | 1279 | // sets the data in the cache from parsed data 1280 | setData( fileName, text ) { 1281 | 1282 | const key = fileName.toLowerCase(); 1283 | this._cache[ key ] = this.parse( text, fileName ); 1284 | 1285 | } 1286 | 1287 | } 1288 | 1289 | // returns the material for an associated color code. If the color code is 16 for a face or 24 for 1290 | // an edge then the passthroughColorCode is used. 1291 | function getMaterialFromCode( colorCode, parentColorCode, materialHierarchy, forEdge ) { 1292 | 1293 | const isPassthrough = ! forEdge && colorCode === MAIN_COLOUR_CODE || forEdge && colorCode === MAIN_EDGE_COLOUR_CODE; 1294 | if ( isPassthrough ) { 1295 | 1296 | colorCode = parentColorCode; 1297 | 1298 | } 1299 | 1300 | return materialHierarchy[ colorCode ] || null; 1301 | 1302 | } 1303 | 1304 | // Applies the appropriate materials to a prebuilt hierarchy of geometry. Assumes that color codes are present 1305 | // in the material array if they need to be filled in. 1306 | function applyMaterialsToMesh( group, parentColorCode, materialHierarchy, finalMaterialPass = false ) { 1307 | 1308 | // find any missing materials as indicated by a color code string and replace it with a material from the current material lib 1309 | const parentIsPassthrough = parentColorCode === MAIN_COLOUR_CODE; 1310 | group.traverse( c => { 1311 | 1312 | if ( c.isMesh || c.isLineSegments ) { 1313 | 1314 | if ( Array.isArray( c.material ) ) { 1315 | 1316 | for ( let i = 0, l = c.material.length; i < l; i ++ ) { 1317 | 1318 | if ( ! c.material[ i ].isMaterial ) { 1319 | 1320 | c.material[ i ] = getMaterial( c, c.material[ i ] ); 1321 | 1322 | } 1323 | 1324 | } 1325 | 1326 | } else if ( ! c.material.isMaterial ) { 1327 | 1328 | c.material = getMaterial( c, c.material ); 1329 | 1330 | } 1331 | 1332 | } 1333 | 1334 | } ); 1335 | 1336 | 1337 | // Returns the appropriate material for the object (line or face) given color code. If the code is "pass through" 1338 | // (24 for lines, 16 for edges) then the pass through color code is used. If that is also pass through then it's 1339 | // simply returned for the subsequent material application. 1340 | function getMaterial( c, colorCode ) { 1341 | 1342 | // if our parent is a passthrough color code and we don't have the current material color available then 1343 | // return early. 1344 | if ( parentIsPassthrough && ! ( colorCode in materialHierarchy ) && ! finalMaterialPass ) { 1345 | 1346 | return colorCode; 1347 | 1348 | } 1349 | 1350 | const forEdge = c.isLineSegments || c.isConditionalLine; 1351 | const isPassthrough = ! forEdge && colorCode === MAIN_COLOUR_CODE || forEdge && colorCode === MAIN_EDGE_COLOUR_CODE; 1352 | if ( isPassthrough ) { 1353 | 1354 | colorCode = parentColorCode; 1355 | 1356 | } 1357 | 1358 | if ( ! ( colorCode in materialHierarchy ) ) { 1359 | 1360 | // throw an error if this is final opportunity to set the material 1361 | if ( finalMaterialPass ) { 1362 | 1363 | throw new Error( `LDrawLoader: Material properties for code ${ colorCode } not available.` ); 1364 | 1365 | } 1366 | 1367 | return colorCode; 1368 | 1369 | } 1370 | 1371 | let material = materialHierarchy[ colorCode ]; 1372 | if ( c.isLineSegments ) { 1373 | 1374 | material = material.userData.edgeMaterial; 1375 | 1376 | if ( c.isConditionalLine ) { 1377 | 1378 | material = material.userData.conditionalEdgeMaterial; 1379 | 1380 | } 1381 | 1382 | } 1383 | 1384 | return material; 1385 | 1386 | } 1387 | 1388 | } 1389 | 1390 | // Class used to parse and build LDraw parts as three.js objects and cache them if they're a "Part" type. 1391 | class LDrawPartsGeometryCache { 1392 | 1393 | constructor( loader ) { 1394 | 1395 | this.loader = loader; 1396 | this.parseCache = new LDrawParsedCache( loader ); 1397 | this._cache = {}; 1398 | 1399 | } 1400 | 1401 | // Convert the given file information into a mesh by processing subobjects. 1402 | async processIntoMesh( info ) { 1403 | 1404 | const parseCache = this.parseCache; 1405 | const faceMaterials = new Set(); 1406 | 1407 | // Processes the part subobject information to load child parts and merge geometry onto part 1408 | // piece object. 1409 | const processInfoSubobjects = async ( info, subobject = null ) => { 1410 | 1411 | const subobjects = info.subobjects; 1412 | const promises = []; 1413 | 1414 | // Trigger load of all subobjects. If a subobject isn't a primitive then load it as a separate 1415 | // group which lets instruction steps apply correctly. 1416 | for ( let i = 0, l = subobjects.length; i < l; i ++ ) { 1417 | 1418 | const subobject = subobjects[ i ]; 1419 | const promise = parseCache.ensureDataLoaded( subobject.fileName ).then( () => { 1420 | 1421 | const subobjectInfo = parseCache.getData( subobject.fileName, false ); 1422 | if ( ! isPrimitiveType( subobjectInfo.type ) ) { 1423 | 1424 | return this.loadModel( subobject.fileName ).catch( error => { 1425 | 1426 | console.warn( error ); 1427 | return null; 1428 | 1429 | } ); 1430 | 1431 | } 1432 | 1433 | return processInfoSubobjects( parseCache.getData( subobject.fileName ), subobject ); 1434 | 1435 | } ); 1436 | 1437 | promises.push( promise ); 1438 | 1439 | } 1440 | 1441 | const group = new Group(); 1442 | group.userData.category = info.category; 1443 | group.userData.keywords = info.keywords; 1444 | group.userData.type = info.type; 1445 | info.group = group; 1446 | 1447 | const subobjectInfos = await Promise.all( promises ); 1448 | for ( let i = 0, l = subobjectInfos.length; i < l; i ++ ) { 1449 | 1450 | const subobject = info.subobjects[ i ]; 1451 | const subobjectInfo = subobjectInfos[ i ]; 1452 | 1453 | if ( subobjectInfo === null ) { 1454 | 1455 | // the subobject failed to load 1456 | continue; 1457 | 1458 | } 1459 | 1460 | // if the subobject was loaded as a separate group then apply the parent scopes materials 1461 | if ( subobjectInfo.isGroup ) { 1462 | 1463 | const subobjectGroup = subobjectInfo; 1464 | subobject.matrix.decompose( subobjectGroup.position, subobjectGroup.quaternion, subobjectGroup.scale ); 1465 | subobjectGroup.userData.startingConstructionStep = subobject.startingConstructionStep; 1466 | subobjectGroup.name = subobject.fileName; 1467 | 1468 | applyMaterialsToMesh( subobjectGroup, subobject.colorCode, info.materials ); 1469 | 1470 | group.add( subobjectGroup ); 1471 | continue; 1472 | 1473 | } 1474 | 1475 | // add the subobject group if it has children in case it has both children and primitives 1476 | if ( subobjectInfo.group.children.length ) { 1477 | 1478 | group.add( subobjectInfo.group ); 1479 | 1480 | } 1481 | 1482 | // transform the primitives into the local space of the parent piece and append them to 1483 | // to the parent primitives list. 1484 | const parentLineSegments = info.lineSegments; 1485 | const parentConditionalSegments = info.conditionalSegments; 1486 | const parentFaces = info.faces; 1487 | 1488 | const lineSegments = subobjectInfo.lineSegments; 1489 | const conditionalSegments = subobjectInfo.conditionalSegments; 1490 | 1491 | const faces = subobjectInfo.faces; 1492 | const matrix = subobject.matrix; 1493 | const inverted = subobject.inverted; 1494 | const matrixScaleInverted = matrix.determinant() < 0; 1495 | const colorCode = subobject.colorCode; 1496 | 1497 | const lineColorCode = colorCode === MAIN_COLOUR_CODE ? MAIN_EDGE_COLOUR_CODE : colorCode; 1498 | for ( let i = 0, l = lineSegments.length; i < l; i ++ ) { 1499 | 1500 | const ls = lineSegments[ i ]; 1501 | const vertices = ls.vertices; 1502 | vertices[ 0 ].applyMatrix4( matrix ); 1503 | vertices[ 1 ].applyMatrix4( matrix ); 1504 | ls.colorCode = ls.colorCode === MAIN_EDGE_COLOUR_CODE ? lineColorCode : ls.colorCode; 1505 | ls.material = ls.material || getMaterialFromCode( ls.colorCode, ls.colorCode, info.materials, true ); 1506 | 1507 | parentLineSegments.push( ls ); 1508 | 1509 | } 1510 | 1511 | for ( let i = 0, l = conditionalSegments.length; i < l; i ++ ) { 1512 | 1513 | const os = conditionalSegments[ i ]; 1514 | const vertices = os.vertices; 1515 | const controlPoints = os.controlPoints; 1516 | vertices[ 0 ].applyMatrix4( matrix ); 1517 | vertices[ 1 ].applyMatrix4( matrix ); 1518 | controlPoints[ 0 ].applyMatrix4( matrix ); 1519 | controlPoints[ 1 ].applyMatrix4( matrix ); 1520 | os.colorCode = os.colorCode === MAIN_EDGE_COLOUR_CODE ? lineColorCode : os.colorCode; 1521 | os.material = os.material || getMaterialFromCode( os.colorCode, os.colorCode, info.materials, true ); 1522 | 1523 | parentConditionalSegments.push( os ); 1524 | 1525 | } 1526 | 1527 | for ( let i = 0, l = faces.length; i < l; i ++ ) { 1528 | 1529 | const tri = faces[ i ]; 1530 | const vertices = tri.vertices; 1531 | for ( let i = 0, l = vertices.length; i < l; i ++ ) { 1532 | 1533 | vertices[ i ].applyMatrix4( matrix ); 1534 | 1535 | } 1536 | 1537 | tri.colorCode = tri.colorCode === MAIN_COLOUR_CODE ? colorCode : tri.colorCode; 1538 | tri.material = tri.material || getMaterialFromCode( tri.colorCode, colorCode, info.materials, false ); 1539 | faceMaterials.add( tri.colorCode ); 1540 | 1541 | // If the scale of the object is negated then the triangle winding order 1542 | // needs to be flipped. 1543 | if ( matrixScaleInverted !== inverted ) { 1544 | 1545 | vertices.reverse(); 1546 | 1547 | } 1548 | 1549 | parentFaces.push( tri ); 1550 | 1551 | } 1552 | 1553 | info.totalFaces += subobjectInfo.totalFaces; 1554 | 1555 | } 1556 | 1557 | // Apply the parent subobjects pass through material code to this object. This is done several times due 1558 | // to material scoping. 1559 | if ( subobject ) { 1560 | 1561 | applyMaterialsToMesh( group, subobject.colorCode, info.materials ); 1562 | 1563 | } 1564 | 1565 | return info; 1566 | 1567 | }; 1568 | 1569 | // Track material use to see if we need to use the normal smooth slow path for hard edges. 1570 | for ( let i = 0, l = info.faces; i < l; i ++ ) { 1571 | 1572 | faceMaterials.add( info.faces[ i ].colorCode ); 1573 | 1574 | } 1575 | 1576 | await processInfoSubobjects( info ); 1577 | 1578 | if ( this.loader.smoothNormals ) { 1579 | 1580 | const checkSubSegments = faceMaterials.size > 1; 1581 | generateFaceNormals( info.faces ); 1582 | smoothNormals( info.faces, info.lineSegments, checkSubSegments ); 1583 | 1584 | } 1585 | 1586 | // Add the primitive objects and metadata. 1587 | const group = info.group; 1588 | if ( info.faces.length > 0 ) { 1589 | 1590 | group.add( createObject( info.faces, 3, false, info.totalFaces ) ); 1591 | 1592 | } 1593 | 1594 | if ( info.lineSegments.length > 0 ) { 1595 | 1596 | group.add( createObject( info.lineSegments, 2 ) ); 1597 | 1598 | } 1599 | 1600 | if ( info.conditionalSegments.length > 0 ) { 1601 | 1602 | group.add( createObject( info.conditionalSegments, 2, true ) ); 1603 | 1604 | } 1605 | 1606 | return group; 1607 | 1608 | } 1609 | 1610 | hasCachedModel( fileName ) { 1611 | 1612 | return fileName !== null && fileName.toLowerCase() in this._cache; 1613 | 1614 | } 1615 | 1616 | async getCachedModel( fileName ) { 1617 | 1618 | if ( fileName !== null && this.hasCachedModel( fileName ) ) { 1619 | 1620 | const key = fileName.toLowerCase(); 1621 | const group = await this._cache[ key ]; 1622 | return group.clone(); 1623 | 1624 | } else { 1625 | 1626 | return null; 1627 | 1628 | } 1629 | 1630 | } 1631 | 1632 | // Loads and parses the model with the given file name. Returns a cached copy if available. 1633 | async loadModel( fileName ) { 1634 | 1635 | const parseCache = this.parseCache; 1636 | const key = fileName.toLowerCase(); 1637 | if ( this.hasCachedModel( fileName ) ) { 1638 | 1639 | // Return cached model if available. 1640 | return this.getCachedModel( fileName ); 1641 | 1642 | } else { 1643 | 1644 | // Otherwise parse a new model. 1645 | // Ensure the file data is loaded and pre parsed. 1646 | await parseCache.ensureDataLoaded( fileName ); 1647 | 1648 | const info = parseCache.getData( fileName ); 1649 | const promise = this.processIntoMesh( info ); 1650 | 1651 | // Now that the file has loaded it's possible that another part parse has been waiting in parallel 1652 | // so check the cache again to see if it's been added since the last async operation so we don't 1653 | // do unnecessary work. 1654 | if ( this.hasCachedModel( fileName ) ) { 1655 | 1656 | return this.getCachedModel( fileName ); 1657 | 1658 | } 1659 | 1660 | // Cache object if it's a part so it can be reused later. 1661 | if ( isPartType( info.type ) ) { 1662 | 1663 | this._cache[ key ] = promise; 1664 | 1665 | } 1666 | 1667 | // return a copy 1668 | const group = await promise; 1669 | return group.clone(); 1670 | 1671 | } 1672 | 1673 | } 1674 | 1675 | // parses the given model text into a renderable object. Returns cached copy if available. 1676 | async parseModel( text ) { 1677 | 1678 | const parseCache = this.parseCache; 1679 | const info = parseCache.parse( text ); 1680 | if ( isPartType( info.type ) && this.hasCachedModel( info.fileName ) ) { 1681 | 1682 | return this.getCachedModel( info.fileName ); 1683 | 1684 | } 1685 | 1686 | return this.processIntoMesh( info ); 1687 | 1688 | } 1689 | 1690 | } 1691 | 1692 | function sortByMaterial( a, b ) { 1693 | 1694 | if ( a.colorCode === b.colorCode ) { 1695 | 1696 | return 0; 1697 | 1698 | } 1699 | 1700 | if ( a.colorCode < b.colorCode ) { 1701 | 1702 | return - 1; 1703 | 1704 | } 1705 | 1706 | return 1; 1707 | 1708 | } 1709 | 1710 | function createObject( elements, elementSize, isConditionalSegments = false, totalElements = null ) { 1711 | 1712 | // Creates a LineSegments (elementSize = 2) or a Mesh (elementSize = 3 ) 1713 | // With per face / segment material, implemented with mesh groups and materials array 1714 | 1715 | // Sort the faces or line segments by color code to make later the mesh groups 1716 | elements.sort( sortByMaterial ); 1717 | 1718 | if ( totalElements === null ) { 1719 | 1720 | totalElements = elements.length; 1721 | 1722 | } 1723 | 1724 | const positions = new Float32Array( elementSize * totalElements * 3 ); 1725 | const normals = elementSize === 3 ? new Float32Array( elementSize * totalElements * 3 ) : null; 1726 | const materials = []; 1727 | 1728 | const quadArray = new Array( 6 ); 1729 | const bufferGeometry = new BufferGeometry(); 1730 | let prevMaterial = undefined; 1731 | let index0 = 0; 1732 | let numGroupVerts = 0; 1733 | let offset = 0; 1734 | 1735 | for ( let iElem = 0, nElem = elements.length; iElem < nElem; iElem ++ ) { 1736 | 1737 | const elem = elements[ iElem ]; 1738 | let vertices = elem.vertices; 1739 | if ( vertices.length === 4 ) { 1740 | 1741 | quadArray[ 0 ] = vertices[ 0 ]; 1742 | quadArray[ 1 ] = vertices[ 1 ]; 1743 | quadArray[ 2 ] = vertices[ 2 ]; 1744 | quadArray[ 3 ] = vertices[ 0 ]; 1745 | quadArray[ 4 ] = vertices[ 2 ]; 1746 | quadArray[ 5 ] = vertices[ 3 ]; 1747 | vertices = quadArray; 1748 | 1749 | } 1750 | 1751 | for ( let j = 0, l = vertices.length; j < l; j ++ ) { 1752 | 1753 | const v = vertices[ j ]; 1754 | const index = offset + j * 3; 1755 | positions[ index + 0 ] = v.x; 1756 | positions[ index + 1 ] = v.y; 1757 | positions[ index + 2 ] = v.z; 1758 | 1759 | } 1760 | 1761 | // create the normals array if this is a set of faces 1762 | if ( elementSize === 3 ) { 1763 | 1764 | if ( ! elem.faceNormal ) { 1765 | 1766 | const v0 = vertices[ 0 ]; 1767 | const v1 = vertices[ 1 ]; 1768 | const v2 = vertices[ 2 ]; 1769 | _tempVec0.subVectors( v1, v0 ); 1770 | _tempVec1.subVectors( v2, v1 ); 1771 | elem.faceNormal = new Vector3() 1772 | .crossVectors( _tempVec0, _tempVec1 ) 1773 | .normalize(); 1774 | 1775 | } 1776 | 1777 | let elemNormals = elem.normals; 1778 | if ( elemNormals.length === 4 ) { 1779 | 1780 | quadArray[ 0 ] = elemNormals[ 0 ]; 1781 | quadArray[ 1 ] = elemNormals[ 1 ]; 1782 | quadArray[ 2 ] = elemNormals[ 2 ]; 1783 | quadArray[ 3 ] = elemNormals[ 0 ]; 1784 | quadArray[ 4 ] = elemNormals[ 2 ]; 1785 | quadArray[ 5 ] = elemNormals[ 3 ]; 1786 | elemNormals = quadArray; 1787 | 1788 | } 1789 | 1790 | for ( let j = 0, l = elemNormals.length; j < l; j ++ ) { 1791 | 1792 | // use face normal if a vertex normal is not provided 1793 | let n = elem.faceNormal; 1794 | if ( elemNormals[ j ] ) { 1795 | 1796 | n = elemNormals[ j ].norm; 1797 | 1798 | } 1799 | 1800 | const index = offset + j * 3; 1801 | normals[ index + 0 ] = n.x; 1802 | normals[ index + 1 ] = n.y; 1803 | normals[ index + 2 ] = n.z; 1804 | 1805 | } 1806 | 1807 | } 1808 | 1809 | if ( prevMaterial !== elem.colorCode ) { 1810 | 1811 | if ( prevMaterial !== null ) { 1812 | 1813 | bufferGeometry.addGroup( index0, numGroupVerts, materials.length - 1 ); 1814 | 1815 | } 1816 | 1817 | const material = elem.material; 1818 | if ( material !== null ) { 1819 | 1820 | if ( elementSize === 3 ) { 1821 | 1822 | materials.push( material ); 1823 | 1824 | } else if ( elementSize === 2 ) { 1825 | 1826 | if ( material !== null ) { 1827 | 1828 | if ( isConditionalSegments ) { 1829 | 1830 | materials.push( material.userData.edgeMaterial.userData.conditionalEdgeMaterial ); 1831 | 1832 | } else { 1833 | 1834 | materials.push( material.userData.edgeMaterial ); 1835 | 1836 | } 1837 | 1838 | } else { 1839 | 1840 | materials.push( null ); 1841 | 1842 | } 1843 | 1844 | } 1845 | 1846 | } else { 1847 | 1848 | // If a material has not been made available yet then keep the color code string in the material array 1849 | // to save the spot for the material once a parent scopes materials are being applied to the object. 1850 | materials.push( elem.colorCode ); 1851 | 1852 | } 1853 | 1854 | prevMaterial = elem.colorCode; 1855 | index0 = offset / 3; 1856 | numGroupVerts = vertices.length; 1857 | 1858 | } else { 1859 | 1860 | numGroupVerts += vertices.length; 1861 | 1862 | } 1863 | 1864 | offset += 3 * vertices.length; 1865 | 1866 | } 1867 | 1868 | if ( numGroupVerts > 0 ) { 1869 | 1870 | bufferGeometry.addGroup( index0, Infinity, materials.length - 1 ); 1871 | 1872 | } 1873 | 1874 | bufferGeometry.setAttribute( 'position', new BufferAttribute( positions, 3 ) ); 1875 | 1876 | if ( normals !== null ) { 1877 | 1878 | bufferGeometry.setAttribute( 'normal', new BufferAttribute( normals, 3 ) ); 1879 | 1880 | } 1881 | 1882 | let object3d = null; 1883 | 1884 | if ( elementSize === 2 ) { 1885 | 1886 | if ( isConditionalSegments ) { 1887 | 1888 | object3d = new ConditionalLineSegments( bufferGeometry, materials.length === 1 ? materials[ 0 ] : materials ); 1889 | 1890 | } else { 1891 | 1892 | object3d = new LineSegments( bufferGeometry, materials.length === 1 ? materials[ 0 ] : materials ); 1893 | 1894 | } 1895 | 1896 | } else if ( elementSize === 3 ) { 1897 | 1898 | object3d = new Mesh( bufferGeometry, materials.length === 1 ? materials[ 0 ] : materials ); 1899 | 1900 | } 1901 | 1902 | if ( isConditionalSegments ) { 1903 | 1904 | object3d.isConditionalLine = true; 1905 | 1906 | const controlArray0 = new Float32Array( elements.length * 3 * 2 ); 1907 | const controlArray1 = new Float32Array( elements.length * 3 * 2 ); 1908 | const directionArray = new Float32Array( elements.length * 3 * 2 ); 1909 | for ( let i = 0, l = elements.length; i < l; i ++ ) { 1910 | 1911 | const os = elements[ i ]; 1912 | const vertices = os.vertices; 1913 | const controlPoints = os.controlPoints; 1914 | const c0 = controlPoints[ 0 ]; 1915 | const c1 = controlPoints[ 1 ]; 1916 | const v0 = vertices[ 0 ]; 1917 | const v1 = vertices[ 1 ]; 1918 | const index = i * 3 * 2; 1919 | controlArray0[ index + 0 ] = c0.x; 1920 | controlArray0[ index + 1 ] = c0.y; 1921 | controlArray0[ index + 2 ] = c0.z; 1922 | controlArray0[ index + 3 ] = c0.x; 1923 | controlArray0[ index + 4 ] = c0.y; 1924 | controlArray0[ index + 5 ] = c0.z; 1925 | 1926 | controlArray1[ index + 0 ] = c1.x; 1927 | controlArray1[ index + 1 ] = c1.y; 1928 | controlArray1[ index + 2 ] = c1.z; 1929 | controlArray1[ index + 3 ] = c1.x; 1930 | controlArray1[ index + 4 ] = c1.y; 1931 | controlArray1[ index + 5 ] = c1.z; 1932 | 1933 | directionArray[ index + 0 ] = v1.x - v0.x; 1934 | directionArray[ index + 1 ] = v1.y - v0.y; 1935 | directionArray[ index + 2 ] = v1.z - v0.z; 1936 | directionArray[ index + 3 ] = v1.x - v0.x; 1937 | directionArray[ index + 4 ] = v1.y - v0.y; 1938 | directionArray[ index + 5 ] = v1.z - v0.z; 1939 | 1940 | } 1941 | 1942 | bufferGeometry.setAttribute( 'control0', new BufferAttribute( controlArray0, 3, false ) ); 1943 | bufferGeometry.setAttribute( 'control1', new BufferAttribute( controlArray1, 3, false ) ); 1944 | bufferGeometry.setAttribute( 'direction', new BufferAttribute( directionArray, 3, false ) ); 1945 | 1946 | } 1947 | 1948 | return object3d; 1949 | 1950 | } 1951 | 1952 | // 1953 | 1954 | class LDrawLoader extends Loader { 1955 | 1956 | constructor( manager ) { 1957 | 1958 | super( manager ); 1959 | 1960 | // Array of THREE.Material 1961 | this.materials = []; 1962 | this.materialLibrary = {}; 1963 | 1964 | // This also allows to handle the embedded text files ("0 FILE" lines) 1965 | this.partsCache = new LDrawPartsGeometryCache( this ); 1966 | 1967 | // This object is a map from file names to paths. It agilizes the paths search. If it is not set then files will be searched by trial and error. 1968 | this.fileMap = {}; 1969 | 1970 | // Initializes the materials library with default materials 1971 | this.setMaterials( [] ); 1972 | 1973 | // If this flag is set to true the vertex normals will be smoothed. 1974 | this.smoothNormals = true; 1975 | 1976 | // The path to load parts from the LDraw parts library from. 1977 | this.partsLibraryPath = ''; 1978 | 1979 | } 1980 | 1981 | setPartsLibraryPath( path ) { 1982 | 1983 | this.partsLibraryPath = path; 1984 | return this; 1985 | 1986 | } 1987 | 1988 | async preloadMaterials( url ) { 1989 | 1990 | const fileLoader = new FileLoader( this.manager ); 1991 | fileLoader.setPath( this.path ); 1992 | fileLoader.setRequestHeader( this.requestHeader ); 1993 | fileLoader.setWithCredentials( this.withCredentials ); 1994 | 1995 | const text = await fileLoader.loadAsync( url ); 1996 | const colorLineRegex = /^0 !COLOUR/; 1997 | const lines = text.split( /[\n\r]/g ); 1998 | const materials = []; 1999 | for ( let i = 0, l = lines.length; i < l; i ++ ) { 2000 | 2001 | const line = lines[ i ]; 2002 | if ( colorLineRegex.test( line ) ) { 2003 | 2004 | const directive = line.replace( colorLineRegex, '' ); 2005 | const material = this.parseColorMetaDirective( new LineParser( directive ) ); 2006 | materials.push( material ); 2007 | 2008 | } 2009 | 2010 | } 2011 | 2012 | this.setMaterials( materials ); 2013 | 2014 | } 2015 | 2016 | load( url, onLoad, onProgress, onError ) { 2017 | 2018 | const fileLoader = new FileLoader( this.manager ); 2019 | fileLoader.setPath( this.path ); 2020 | fileLoader.setRequestHeader( this.requestHeader ); 2021 | fileLoader.setWithCredentials( this.withCredentials ); 2022 | fileLoader.load( url, text => { 2023 | 2024 | this.partsCache 2025 | .parseModel( text, this.materialLibrary ) 2026 | .then( group => { 2027 | 2028 | applyMaterialsToMesh( group, MAIN_COLOUR_CODE, this.materialLibrary, true ); 2029 | this.computeConstructionSteps( group ); 2030 | onLoad( group ); 2031 | 2032 | } ) 2033 | .catch( onError ); 2034 | 2035 | }, onProgress, onError ); 2036 | 2037 | } 2038 | 2039 | parse( text, onLoad ) { 2040 | 2041 | this.partsCache 2042 | .parseModel( text, this.materialLibrary ) 2043 | .then( group => { 2044 | 2045 | this.computeConstructionSteps( group ); 2046 | onLoad( group ); 2047 | 2048 | } ); 2049 | 2050 | } 2051 | 2052 | setMaterials( materials ) { 2053 | 2054 | this.materialLibrary = {}; 2055 | this.materials = []; 2056 | for ( let i = 0, l = materials.length; i < l; i ++ ) { 2057 | 2058 | this.addMaterial( materials[ i ] ); 2059 | 2060 | } 2061 | 2062 | // Add default main triangle and line edge materials (used in pieces that can be colored with a main color) 2063 | this.addMaterial( this.parseColorMetaDirective( new LineParser( 'Main_Colour CODE 16 VALUE #FF8080 EDGE #333333' ) ) ); 2064 | this.addMaterial( this.parseColorMetaDirective( new LineParser( 'Edge_Colour CODE 24 VALUE #A0A0A0 EDGE #333333' ) ) ); 2065 | 2066 | return this; 2067 | 2068 | } 2069 | 2070 | setFileMap( fileMap ) { 2071 | 2072 | this.fileMap = fileMap; 2073 | 2074 | return this; 2075 | 2076 | } 2077 | 2078 | addMaterial( material ) { 2079 | 2080 | // Adds a material to the material library which is on top of the parse scopes stack. And also to the materials array 2081 | 2082 | const matLib = this.materialLibrary; 2083 | if ( ! matLib[ material.userData.code ] ) { 2084 | 2085 | this.materials.push( material ); 2086 | matLib[ material.userData.code ] = material; 2087 | 2088 | } 2089 | 2090 | return this; 2091 | 2092 | } 2093 | 2094 | getMaterial( colorCode ) { 2095 | 2096 | if ( colorCode.startsWith( '0x2' ) ) { 2097 | 2098 | // Special 'direct' material value (RGB color) 2099 | const color = colorCode.substring( 3 ); 2100 | 2101 | return this.parseColorMetaDirective( new LineParser( 'Direct_Color_' + color + ' CODE -1 VALUE #' + color + ' EDGE #' + color + '' ) ); 2102 | 2103 | } 2104 | 2105 | return this.materialLibrary[ colorCode ] || null; 2106 | 2107 | } 2108 | 2109 | getMainMaterial() { 2110 | 2111 | return this.getMaterial( MAIN_COLOUR_CODE ); 2112 | 2113 | } 2114 | 2115 | getMainEdgeMaterial() { 2116 | 2117 | return this.getMaterial( MAIN_EDGE_COLOUR_CODE ); 2118 | 2119 | } 2120 | 2121 | parseColorMetaDirective( lineParser ) { 2122 | 2123 | // Parses a color definition and returns a THREE.Material 2124 | 2125 | let code = null; 2126 | 2127 | // Triangle and line colors 2128 | let color = 0xFF00FF; 2129 | let edgeColor = 0xFF00FF; 2130 | 2131 | // Transparency 2132 | let alpha = 1; 2133 | let isTransparent = false; 2134 | // Self-illumination: 2135 | let luminance = 0; 2136 | 2137 | let finishType = FINISH_TYPE_DEFAULT; 2138 | 2139 | let edgeMaterial = null; 2140 | 2141 | const name = lineParser.getToken(); 2142 | if ( ! name ) { 2143 | 2144 | throw new Error( 'LDrawLoader: Material name was expected after "!COLOUR tag' + lineParser.getLineNumberString() + '.' ); 2145 | 2146 | } 2147 | 2148 | // Parse tag tokens and their parameters 2149 | let token = null; 2150 | while ( true ) { 2151 | 2152 | token = lineParser.getToken(); 2153 | 2154 | if ( ! token ) { 2155 | 2156 | break; 2157 | 2158 | } 2159 | 2160 | switch ( token.toUpperCase() ) { 2161 | 2162 | case 'CODE': 2163 | 2164 | code = lineParser.getToken(); 2165 | break; 2166 | 2167 | case 'VALUE': 2168 | 2169 | color = lineParser.getToken(); 2170 | if ( color.startsWith( '0x' ) ) { 2171 | 2172 | color = '#' + color.substring( 2 ); 2173 | 2174 | } else if ( ! color.startsWith( '#' ) ) { 2175 | 2176 | throw new Error( 'LDrawLoader: Invalid color while parsing material' + lineParser.getLineNumberString() + '.' ); 2177 | 2178 | } 2179 | 2180 | break; 2181 | 2182 | case 'EDGE': 2183 | 2184 | edgeColor = lineParser.getToken(); 2185 | if ( edgeColor.startsWith( '0x' ) ) { 2186 | 2187 | edgeColor = '#' + edgeColor.substring( 2 ); 2188 | 2189 | } else if ( ! edgeColor.startsWith( '#' ) ) { 2190 | 2191 | // Try to see if edge color is a color code 2192 | edgeMaterial = this.getMaterial( edgeColor ); 2193 | if ( ! edgeMaterial ) { 2194 | 2195 | throw new Error( 'LDrawLoader: Invalid edge color while parsing material' + lineParser.getLineNumberString() + '.' ); 2196 | 2197 | } 2198 | 2199 | // Get the edge material for this triangle material 2200 | edgeMaterial = edgeMaterial.userData.edgeMaterial; 2201 | 2202 | } 2203 | 2204 | break; 2205 | 2206 | case 'ALPHA': 2207 | 2208 | alpha = parseInt( lineParser.getToken() ); 2209 | 2210 | if ( isNaN( alpha ) ) { 2211 | 2212 | throw new Error( 'LDrawLoader: Invalid alpha value in material definition' + lineParser.getLineNumberString() + '.' ); 2213 | 2214 | } 2215 | 2216 | alpha = Math.max( 0, Math.min( 1, alpha / 255 ) ); 2217 | 2218 | if ( alpha < 1 ) { 2219 | 2220 | isTransparent = true; 2221 | 2222 | } 2223 | 2224 | break; 2225 | 2226 | case 'LUMINANCE': 2227 | 2228 | luminance = parseInt( lineParser.getToken() ); 2229 | 2230 | if ( isNaN( luminance ) ) { 2231 | 2232 | throw new Error( 'LDrawLoader: Invalid luminance value in material definition' + LineParser.getLineNumberString() + '.' ); 2233 | 2234 | } 2235 | 2236 | luminance = Math.max( 0, Math.min( 1, luminance / 255 ) ); 2237 | 2238 | break; 2239 | 2240 | case 'CHROME': 2241 | finishType = FINISH_TYPE_CHROME; 2242 | break; 2243 | 2244 | case 'PEARLESCENT': 2245 | finishType = FINISH_TYPE_PEARLESCENT; 2246 | break; 2247 | 2248 | case 'RUBBER': 2249 | finishType = FINISH_TYPE_RUBBER; 2250 | break; 2251 | 2252 | case 'MATTE_METALLIC': 2253 | finishType = FINISH_TYPE_MATTE_METALLIC; 2254 | break; 2255 | 2256 | case 'METAL': 2257 | finishType = FINISH_TYPE_METAL; 2258 | break; 2259 | 2260 | case 'MATERIAL': 2261 | // Not implemented 2262 | lineParser.setToEnd(); 2263 | break; 2264 | 2265 | default: 2266 | throw new Error( 'LDrawLoader: Unknown token "' + token + '" while parsing material' + lineParser.getLineNumberString() + '.' ); 2267 | 2268 | } 2269 | 2270 | } 2271 | 2272 | let material = null; 2273 | 2274 | switch ( finishType ) { 2275 | 2276 | case FINISH_TYPE_DEFAULT: 2277 | 2278 | material = new MeshStandardMaterial( { color: color, roughness: 0.3, metalness: 0 } ); 2279 | break; 2280 | 2281 | case FINISH_TYPE_PEARLESCENT: 2282 | 2283 | // Try to imitate pearlescency by making the surface glossy 2284 | material = new MeshStandardMaterial( { color: color, roughness: 0.3, metalness: 0.25 } ); 2285 | break; 2286 | 2287 | case FINISH_TYPE_CHROME: 2288 | 2289 | // Mirror finish surface 2290 | material = new MeshStandardMaterial( { color: color, roughness: 0, metalness: 1 } ); 2291 | break; 2292 | 2293 | case FINISH_TYPE_RUBBER: 2294 | 2295 | // Rubber finish 2296 | material = new MeshStandardMaterial( { color: color, roughness: 0.9, metalness: 0 } ); 2297 | break; 2298 | 2299 | case FINISH_TYPE_MATTE_METALLIC: 2300 | 2301 | // Brushed metal finish 2302 | material = new MeshStandardMaterial( { color: color, roughness: 0.8, metalness: 0.4 } ); 2303 | break; 2304 | 2305 | case FINISH_TYPE_METAL: 2306 | 2307 | // Average metal finish 2308 | material = new MeshStandardMaterial( { color: color, roughness: 0.2, metalness: 0.85 } ); 2309 | break; 2310 | 2311 | default: 2312 | // Should not happen 2313 | break; 2314 | 2315 | } 2316 | 2317 | material.transparent = isTransparent; 2318 | material.premultipliedAlpha = true; 2319 | material.opacity = alpha; 2320 | material.depthWrite = ! isTransparent; 2321 | 2322 | material.polygonOffset = true; 2323 | material.polygonOffsetFactor = 1; 2324 | 2325 | if ( luminance !== 0 ) { 2326 | 2327 | material.emissive.set( material.color ).multiplyScalar( luminance ); 2328 | 2329 | } 2330 | 2331 | if ( ! edgeMaterial ) { 2332 | 2333 | // This is the material used for edges 2334 | edgeMaterial = new LineBasicMaterial( { 2335 | color: edgeColor, 2336 | transparent: isTransparent, 2337 | opacity: alpha, 2338 | depthWrite: ! isTransparent 2339 | } ); 2340 | edgeMaterial.userData.code = code; 2341 | edgeMaterial.name = name + ' - Edge'; 2342 | 2343 | // This is the material used for conditional edges 2344 | edgeMaterial.userData.conditionalEdgeMaterial = new LDrawConditionalLineMaterial( { 2345 | 2346 | fog: true, 2347 | transparent: isTransparent, 2348 | depthWrite: ! isTransparent, 2349 | color: edgeColor, 2350 | opacity: alpha, 2351 | 2352 | } ); 2353 | 2354 | } 2355 | 2356 | material.userData.code = code; 2357 | material.name = name; 2358 | 2359 | material.userData.edgeMaterial = edgeMaterial; 2360 | 2361 | this.addMaterial( material ); 2362 | 2363 | return material; 2364 | 2365 | } 2366 | 2367 | computeConstructionSteps( model ) { 2368 | 2369 | // Sets userdata.constructionStep number in Group objects and userData.numConstructionSteps number in the root Group object. 2370 | 2371 | let stepNumber = 0; 2372 | 2373 | model.traverse( c => { 2374 | 2375 | if ( c.isGroup ) { 2376 | 2377 | if ( c.userData.startingConstructionStep ) { 2378 | 2379 | stepNumber ++; 2380 | 2381 | } 2382 | 2383 | c.userData.constructionStep = stepNumber; 2384 | 2385 | } 2386 | 2387 | } ); 2388 | 2389 | model.userData.numConstructionSteps = stepNumber + 1; 2390 | 2391 | } 2392 | 2393 | } 2394 | 2395 | export { LDrawLoader }; 2396 | --------------------------------------------------------------------------------