├── 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 |
5 |
--------------------------------------------------------------------------------
/files/ic_menu_black_24dp.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/files/ic_code_black_24dp.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/files/ic_close_black_24dp.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/files/ic_mode_edit_black_24dp.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/files/ic_search_black_24dp.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/files/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
22 |
23 |
24 |
25 |
670 |
671 |
672 |
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 |
--------------------------------------------------------------------------------