分享three.js实现乐高小汽车

前言

Web脚本语言JavaScript入门容易,但是想要熟练掌握却需要几年的学习与实践,还要在弱类型开发语言中习惯于使用模块来构建你的代码,就像小时候玩的乐高积木一样。

ed9a0578fc6644febe8166a6b832fcc6.png

应用程序的模块化理念,通过将实现隐藏在一个简单的接口后面,您可以使您的应用程序万无一失且易于使用。它只做它应该做的,没有别的

fff3592c52f648e58cab4a296b7d9cda.png

通过隐藏实现,我们对使用我们代码的人实施了良好的编码风格。您可以访问的实现越多,它就越有可能成为您以后必须处理的复杂的半生不熟的“修复”。

1df7f62e52f0443cac187724d293d517.png

创建3D场景时,唯一的限制是您的想象力 - 以及您的技术知识深度。

10089180ddae47bfbf1865f661568ff3.png

描述3D空间的坐标系和用于在坐标系内移动对象是难点加重点。场景图用于描述构成我们场景的对象层次结构的结构,向量用于描述3D空间中的位置(以及许多其他事物) ,还有不少于两种描述旋转的方式:欧拉角Euler angles和四元数quaternions

对 three.js 和乐高模型web化相关知识点进行实战。希望能与大家交流技术心得和经验,一起共同进步。涉及的知识点如下:

3D 场景初始化:场景、相机、渲染器

透视相机的位置调整

几何体:BoxGeometry、CylinderGeometry、LatheGeometry

材质:MeshLambertMaterial、MeshPhongMaterial、MeshBasicMaterial

光源:AmbientLight、SpotLightHelper、DirectionalLight

更新材质的纹理:TextureLoader

渲染 3D 文本:TextGeometry、FontLoader

实现物体阴影效果

3D 坐标的计算

物体交互的实现:Raycaster、坐标归一化

3D 资源的销毁释放

补间动画、动画编排

class 等

为了方便demo演示,采用传统的 HTML 单文件importmap、module方式来编写代码。

实践

容器

首先,准备一个空白容器,让它的尺寸与浏览器视窗大小相同,以充分利用屏幕空间。

<div id="scene-container"></div>

依赖

对于 JS 脚本,使用 导入映射 配置资源的 CDN 地址,这样就可以像使用 npm 包一样导入相关资源。

<script type="importmap">{"imports": {"three": "https://cdn.jsdelivr.net/npm/three@0.162.0/+esm","three/addons/": "https://cdn.jsdelivr.net/npm/three@0.162.0/examples/jsm/","lil-gui": "https://threejsfundamentals.org/3rdparty/dat.gui.module.js","@tweenjs/tween.js": "https://cdn.jsdelivr.net/npm/@tweenjs/tween.js@23.1.1/dist/tween.esm.js","canvas-confetti": "https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.2/+esm"}}</script>

接着就可以引入依赖。

<script type="module">import * as THREE from 'three';import * as TWEEN from '@tweenjs/tween.js';import confetti from 'canvas-confetti';import { GUI } from 'lil-gui';
</script>

设计变量、类、方法

定义相关变量

let container, progressBarDiv;
let camera, scene, renderer, controls, gui, guiData, anLoop;
let model;
const modelFileList = {'Car': './car.txt'}

设计乐高类

class Ldraw {constructor(){// 首次使用构造器实例if (!(Ldraw.instance instanceof Ldraw)) {this.init();}return Ldraw.instance}init() {//container = document.createElement( 'div' );//document.body.appendChild( container );camera = new THREE.PerspectiveCamera( 45, container.clientWidth / container.clientHeight, 1, 10000 );camera.position.set( 150, 200, 250 );// rendererrenderer = new THREE.WebGLRenderer( { antialias: true } );//renderer.setSize( window.innerWidth, window.innerHeight );renderer.setSize(container.clientWidth, container.clientHeight);// eslint-disable-next-line no-undefrenderer.setPixelRatio(window.devicePixelRatio);renderer.toneMapping = THREE.ACESFilmicToneMapping;// canvas画布绝对定位//renderer.domElement.style.display = 'black';//renderer.domElement.style.position = 'absolute';//renderer.domElement.style.top = '0px';//renderer.domElement.style.left = '0px';//renderer.domElement.style.zIndex = -1;container.appendChild( renderer.domElement );// sceneconst pmremGenerator = new THREE.PMREMGenerator( renderer );scene = new THREE.Scene();scene.background = new THREE.Color( 0xdeebed );scene.environment = pmremGenerator.fromScene( new RoomEnvironment( renderer ) ).texture;controls = new OrbitControls( camera, renderer.domElement );controls.enableDamping = true;anLoop = new Loop(camera, scene, renderer);// guiguiData = {//modelFileName: modelFileList[ 'Car' ],displayLines: true,conditionalLines: true,smoothNormals: true,buildingStep: 0,noBuildingSteps: 'No steps.',flatColors: false,mergeModel: false};window.addEventListener( 'resize', this.onWindowResize );progressBarDiv = document.createElement( 'div' );progressBarDiv.innerText = 'Loading...';progressBarDiv.style.fontSize = '3em';progressBarDiv.style.color = '#888';progressBarDiv.style.display = 'block';progressBarDiv.style.position = 'absolute';progressBarDiv.style.top = '50%';progressBarDiv.style.width = '100%';progressBarDiv.style.textAlign = 'center';// load materials and then the modelthis.reloadObject( true );}updateObjectsVisibility() {model.traverse( c => {if ( c.isLineSegments ) {if ( c.isConditionalLine ) {c.visible = guiData.conditionalLines;} else {c.visible = guiData.displayLines;}} else if ( c.isGroup ) {// Hide objects with building step > gui settingc.visible = c.userData.buildingStep <= guiData.buildingStep;}} );}reloadObject( resetCamera ) {if ( model ) {scene.remove( model );}model = null;this.updateProgressBar( 0 );this.showProgressBar;// only smooth when not rendering with flat colors to improve processing timeconst lDrawLoader = new LDrawLoader();lDrawLoader.smoothNormals = guiData.smoothNormals && ! guiData.flatColors;lDrawLoader.load( './car.txt',  ( group2 )=> {//.setPath( ldrawPath )//.load( guiData.modelFileName,  ( group2 )=> {if ( model ) {scene.remove( model );}model = group2;// demonstrate how to use convert to flat colors to better mimic the lego instructions lookif ( guiData.flatColors ) {const convertMaterial = ( material )=> {const newMaterial = new THREE.MeshBasicMaterial();newMaterial.color.copy( material.color );newMaterial.polygonOffset = material.polygonOffset;newMaterial.polygonOffsetUnits = material.polygonOffsetUnits;newMaterial.polygonOffsetFactor = material.polygonOffsetFactor;newMaterial.opacity = material.opacity;newMaterial.transparent = material.transparent;newMaterial.depthWrite = material.depthWrite;newMaterial.toneMapping = false;return newMaterial;}model.traverse( c => {if ( c.isMesh ) {if ( Array.isArray( c.material ) ) {c.material = c.material.map( convertMaterial );} else {c.material = convertMaterial( c.material );}}} );}// Merge model geometries by materialif ( guiData.mergeModel ) model = LDrawUtils.mergeObject( model );// Convert from LDraw coordinates: rotate 180 degrees around OXmodel.rotation.x = Math.PI;scene.add( model );guiData.buildingStep = model.userData.numBuildingSteps - 1;this.updateObjectsVisibility;// Adjust camera and lightconst bbox = new THREE.Box3().setFromObject( model );const size = bbox.getSize( new THREE.Vector3() );const radius = Math.max( size.x, Math.max( size.y, size.z ) ) * 0.5;if ( resetCamera ) {controls.target0.copy( bbox.getCenter( new THREE.Vector3() ) );controls.position0.set( - 2.3, 1, 2 ).multiplyScalar( radius ).add( controls.target0 );controls.reset();}this.createGUI;this.hideProgressBar;}, this.onProgress, this.onError );//});}onWindowResize() {camera.aspect = window.innerWidth / window.innerHeight;camera.updateProjectionMatrix();renderer.setSize( window.innerWidth, window.innerHeight );}createGUI() {if ( gui ) {gui.destroy();}gui = new GUI();gui.add( guiData, 'modelFileName', modelFileList ).name( 'Model' ).onFinishChange( ()=> {this.reloadObject( true );} );gui.add( guiData, 'flatColors' ).name( 'Flat Colors' ).onChange( ()=> {this.reloadObject( false );} );gui.add( guiData, 'mergeModel' ).name( 'Merge model' ).onChange( ()=> {this.reloadObject( false );} );if ( model.userData.numBuildingSteps > 1 ) {gui.add( guiData, 'buildingStep', 0, model.userData.numBuildingSteps - 1 ).step( 1 ).name( 'Building step' ).onChange( this.updateObjectsVisibility );} else {gui.add( guiData, 'noBuildingSteps' ).name( 'Building step' ).onChange( this.updateObjectsVisibility );}const changeNormals = ()=> {this.reloadObject( false );} gui.add( guiData, 'smoothNormals' ).name( 'Smooth Normals' ).onChange( changeNormals );gui.add( guiData, 'displayLines' ).name( 'Display Lines' ).onChange( this.updateObjectsVisibility );gui.add( guiData, 'conditionalLines' ).name( 'Conditional Lines' ).onChange( this.updateObjectsVisibility );}animate() {requestAnimationFrame( this.animate );controls.update();this.render;}render() {renderer.render( scene, camera );}updateProgressBar( fraction ) {progressBarDiv.innerText = 'Loading... ' + Math.round( fraction * 100, 2 ) + '%';}onProgress( xhr ) {if ( xhr.lengthComputable ) {this.updateProgressBar( xhr.loaded / xhr.total );console.log( Math.round( xhr.loaded / xhr.total * 100, 2 ) + '% downloaded' );}}onError( error ) {const message = 'Error loading model';progressBarDiv.innerText = message;console.log( message );console.error( error );}showProgressBar() {document.body.appendChild( progressBarDiv );}hideProgressBar() {document.body.removeChild( progressBarDiv );}start() {anLoop.start();}stop() {anLoop.stop();}tick() {// Code to update animations will go hereanLoop.tick();}}//export { Ldraw }

创建一个场景(Scene)、一个透视相机(PerspectiveCamera)和一个 WebGL 渲染器(WebGLRenderer),并将渲染器添加到 DOM 中。同时,编写一个渲染函数,使用requestAnimationFrame 方法循环渲染场景。

import {EventDispatcher,MOUSE,Quaternion,Spherical,TOUCH,Plane,Ray,MathUtils,BackSide,BoxGeometry,Mesh,Scene,MeshBasicMaterial,MeshStandardMaterial,PointLight,BufferAttribute,BufferGeometry,FileLoader,Group,LineBasicMaterial,LineSegments,Loader,ShaderMaterial,SRGBColorSpace,UniformsLib,UniformsUtils,Clock,Color,Matrix3,Matrix4,PerspectiveCamera,Vector2,Vector3,Vector4,WebGLRenderTarget,HalfFloatType,Float32BufferAttribute,InstancedBufferAttribute,InterleavedBuffer,InterleavedBufferAttribute,TriangleFanDrawMode,TriangleStripDrawMode,TrianglesDrawMode,} from 'three';// OrbitControls performs orbiting, dollying (zooming), and panning.// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).////    Orbit - left mouse / touch: one-finger move//    Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish//    Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger moveconst _changeEvent = { type: 'change' };const _startEvent = { type: 'start' };const _endEvent = { type: 'end' };const _ray = new Ray();const _plane = new Plane();const TILT_LIMIT = Math.cos( 70 * MathUtils.DEG2RAD );class OrbitControls extends EventDispatcher {constructor( object, domElement ) {super();this.object = object;this.domElement = domElement;this.domElement.style.touchAction = 'none'; // disable touch scroll// Set to false to disable this controlthis.enabled = true;// "target" sets the location of focus, where the object orbits aroundthis.target = new Vector3();// Sets the 3D cursor (similar to Blender), from which the maxTargetRadius takes effectthis.cursor = new Vector3();// How far you can dolly in and out ( PerspectiveCamera only )this.minDistance = 0;this.maxDistance = Infinity;// How far you can zoom in and out ( OrthographicCamera only )this.minZoom = 0;this.maxZoom = Infinity;// Limit camera target within a spherical area around the cursorthis.minTargetRadius = 0;this.maxTargetRadius = Infinity;// How far you can orbit vertically, upper and lower limits.// Range is 0 to Math.PI radians.this.minPolarAngle = 0; // radiansthis.maxPolarAngle = Math.PI; // radians// How far you can orbit horizontally, upper and lower limits.// If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI )this.minAzimuthAngle = - Infinity; // radiansthis.maxAzimuthAngle = Infinity; // radians// Set to true to enable damping (inertia)// If damping is enabled, you must call controls.update() in your animation loopthis.enableDamping = false;this.dampingFactor = 0.05;// This option actually enables dollying in and out; left as "zoom" for backwards compatibility.// Set to false to disable zoomingthis.enableZoom = true;this.zoomSpeed = 1.0;// Set to false to disable rotatingthis.enableRotate = true;this.rotateSpeed = 1.0;// Set to false to disable panningthis.enablePan = true;this.panSpeed = 1.0;this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.upthis.keyPanSpeed = 7.0;	// pixels moved per arrow key pushthis.zoomToCursor = false;// Set to true to automatically rotate around the target// If auto-rotate is enabled, you must call controls.update() in your animation loopthis.autoRotate = false;this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60// The four arrow keysthis.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' };// Mouse buttonsthis.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN };// Touch fingersthis.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN };// for resetthis.target0 = this.target.clone();this.position0 = this.object.position.clone();this.zoom0 = this.object.zoom;// the target DOM element for key eventsthis._domElementKeyEvents = null;//// public methods//this.getPolarAngle = function () {return spherical.phi;};this.getAzimuthalAngle = function () {return spherical.theta;};this.getDistance = function () {return this.object.position.distanceTo( this.target );};this.listenToKeyEvents = function ( domElement ) {domElement.addEventListener( 'keydown', onKeyDown );this._domElementKeyEvents = domElement;};this.stopListenToKeyEvents = function () {this._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown );this._domElementKeyEvents = null;};this.saveState = function () {scope.target0.copy( scope.target );scope.position0.copy( scope.object.position );scope.zoom0 = scope.object.zoom;};this.reset = function () {scope.target.copy( scope.target0 );scope.object.position.copy( scope.position0 );scope.object.zoom = scope.zoom0;scope.object.updateProjectionMatrix();scope.dispatchEvent( _changeEvent );scope.update();state = STATE.NONE;};// this method is exposed, but perhaps it would be better if we can make it private...this.update = function () {const offset = new Vector3();// so camera.up is the orbit axisconst quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) );const quatInverse = quat.clone().invert();const lastPosition = new Vector3();const lastQuaternion = new Quaternion();const lastTargetPosition = new Vector3();const twoPI = 2 * Math.PI;return function update( deltaTime = null ) {const position = scope.object.position;offset.copy( position ).sub( scope.target );// rotate offset to "y-axis-is-up" spaceoffset.applyQuaternion( quat );// angle from z-axis around y-axisspherical.setFromVector3( offset );if ( scope.autoRotate && state === STATE.NONE ) {rotateLeft( getAutoRotationAngle( deltaTime ) );}if ( scope.enableDamping ) {spherical.theta += sphericalDelta.theta * scope.dampingFactor;spherical.phi += sphericalDelta.phi * scope.dampingFactor;} else {spherical.theta += sphericalDelta.theta;spherical.phi += sphericalDelta.phi;}// restrict theta to be between desired limitslet min = scope.minAzimuthAngle;let max = scope.maxAzimuthAngle;if ( isFinite( min ) && isFinite( max ) ) {if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI;if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI;if ( min <= max ) {spherical.theta = Math.max( min, Math.min( max, spherical.theta ) );} else {spherical.theta = ( spherical.theta > ( min + max ) / 2 ) ?Math.max( min, spherical.theta ) :Math.min( max, spherical.theta );}}// restrict phi to be between desired limitsspherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) );spherical.makeSafe();// move target to panned locationif ( scope.enableDamping === true ) {scope.target.addScaledVector( panOffset, scope.dampingFactor );} else {scope.target.add( panOffset );}// Limit the target distance from the cursor to create a sphere around the center of interestscope.target.sub( scope.cursor );scope.target.clampLength( scope.minTargetRadius, scope.maxTargetRadius );scope.target.add( scope.cursor );let zoomChanged = false;// adjust the camera position based on zoom only if we're not zooming to the cursor or if it's an ortho camera// we adjust zoom later in these casesif ( scope.zoomToCursor && performCursorZoom || scope.object.isOrthographicCamera ) {spherical.radius = clampDistance( spherical.radius );} else {const prevRadius = spherical.radius;spherical.radius = clampDistance( spherical.radius * scale );zoomChanged = prevRadius != spherical.radius;}offset.setFromSpherical( spherical );// rotate offset back to "camera-up-vector-is-up" spaceoffset.applyQuaternion( quatInverse );position.copy( scope.target ).add( offset );scope.object.lookAt( scope.target );if ( scope.enableDamping === true ) {sphericalDelta.theta *= ( 1 - scope.dampingFactor );sphericalDelta.phi *= ( 1 - scope.dampingFactor );panOffset.multiplyScalar( 1 - scope.dampingFactor );} else {sphericalDelta.set( 0, 0, 0 );panOffset.set( 0, 0, 0 );}// adjust camera positionif ( scope.zoomToCursor && performCursorZoom ) {let newRadius = null;if ( scope.object.isPerspectiveCamera ) {// move the camera down the pointer ray// this method avoids floating point errorconst prevRadius = offset.length();newRadius = clampDistance( prevRadius * scale );const radiusDelta = prevRadius - newRadius;scope.object.position.addScaledVector( dollyDirection, radiusDelta );scope.object.updateMatrixWorld();zoomChanged = !! radiusDelta;} else if ( scope.object.isOrthographicCamera ) {// adjust the ortho camera position based on zoom changesconst mouseBefore = new Vector3( mouse.x, mouse.y, 0 );mouseBefore.unproject( scope.object );const prevZoom = scope.object.zoom;scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / scale ) );scope.object.updateProjectionMatrix();zoomChanged = prevZoom !== scope.object.zoom;const mouseAfter = new Vector3( mouse.x, mouse.y, 0 );mouseAfter.unproject( scope.object );scope.object.position.sub( mouseAfter ).add( mouseBefore );scope.object.updateMatrixWorld();newRadius = offset.length();} else {console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled.' );scope.zoomToCursor = false;}// handle the placement of the targetif ( newRadius !== null ) {if ( this.screenSpacePanning ) {// position the orbit target in front of the new camera positionscope.target.set( 0, 0, - 1 ).transformDirection( scope.object.matrix ).multiplyScalar( newRadius ).add( scope.object.position );} else {// get the ray and translation plane to compute target_ray.origin.copy( scope.object.position );_ray.direction.set( 0, 0, - 1 ).transformDirection( scope.object.matrix );// if the camera is 20 degrees above the horizon then don't adjust the focus target to avoid// extremely large valuesif ( Math.abs( scope.object.up.dot( _ray.direction ) ) < TILT_LIMIT ) {object.lookAt( scope.target );} else {_plane.setFromNormalAndCoplanarPoint( scope.object.up, scope.target );_ray.intersectPlane( _plane, scope.target );}}}} else if ( scope.object.isOrthographicCamera ) {const prevZoom = scope.object.zoom;scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / scale ) );if ( prevZoom !== scope.object.zoom ) {scope.object.updateProjectionMatrix();zoomChanged = true;}}scale = 1;performCursorZoom = false;// update condition is:// min(camera displacement, camera rotation in radians)^2 > EPS// using small-angle approximation cos(x/2) = 1 - x^2 / 8if ( zoomChanged ||lastPosition.distanceToSquared( scope.object.position ) > EPS ||8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ||lastTargetPosition.distanceToSquared( scope.target ) > EPS ) {scope.dispatchEvent( _changeEvent );lastPosition.copy( scope.object.position );lastQuaternion.copy( scope.object.quaternion );lastTargetPosition.copy( scope.target );return true;}return false;};}();this.dispose = function () {scope.domElement.removeEventListener( 'contextmenu', onContextMenu );scope.domElement.removeEventListener( 'pointerdown', onPointerDown );scope.domElement.removeEventListener( 'pointercancel', onPointerUp );scope.domElement.removeEventListener( 'wheel', onMouseWheel );scope.domElement.removeEventListener( 'pointermove', onPointerMove );scope.domElement.removeEventListener( 'pointerup', onPointerUp );const document = scope.domElement.getRootNode(); // offscreen canvas compatibilitydocument.removeEventListener( 'keydown', interceptControlDown, { capture: true } );if ( scope._domElementKeyEvents !== null ) {scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown );scope._domElementKeyEvents = null;}//scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?};//// internals//const scope = this;const STATE = {NONE: - 1,ROTATE: 0,DOLLY: 1,PAN: 2,TOUCH_ROTATE: 3,TOUCH_PAN: 4,TOUCH_DOLLY_PAN: 5,TOUCH_DOLLY_ROTATE: 6};let state = STATE.NONE;const EPS = 0.000001;// current position in spherical coordinatesconst spherical = new Spherical();const sphericalDelta = new Spherical();let scale = 1;const panOffset = new Vector3();const rotateStart = new Vector2();const rotateEnd = new Vector2();const rotateDelta = new Vector2();const panStart = new Vector2();const panEnd = new Vector2();const panDelta = new Vector2();const dollyStart = new Vector2();const dollyEnd = new Vector2();const dollyDelta = new Vector2();const dollyDirection = new Vector3();const mouse = new Vector2();let performCursorZoom = false;const pointers = [];const pointerPositions = {};let controlActive = false;function getAutoRotationAngle( deltaTime ) {if ( deltaTime !== null ) {return ( 2 * Math.PI / 60 * scope.autoRotateSpeed ) * deltaTime;} else {return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;}}function getZoomScale( delta ) {const normalizedDelta = Math.abs( delta * 0.01 );return Math.pow( 0.95, scope.zoomSpeed * normalizedDelta );}function rotateLeft( angle ) {sphericalDelta.theta -= angle;}function rotateUp( angle ) {sphericalDelta.phi -= angle;}const panLeft = function () {const v = new Vector3();return function panLeft( distance, objectMatrix ) {v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrixv.multiplyScalar( - distance );panOffset.add( v );};}();const panUp = function () {const v = new Vector3();return function panUp( distance, objectMatrix ) {if ( scope.screenSpacePanning === true ) {v.setFromMatrixColumn( objectMatrix, 1 );} else {v.setFromMatrixColumn( objectMatrix, 0 );v.crossVectors( scope.object.up, v );}v.multiplyScalar( distance );panOffset.add( v );};}();// deltaX and deltaY are in pixels; right and down are positiveconst pan = function () {const offset = new Vector3();return function pan( deltaX, deltaY ) {const element = scope.domElement;if ( scope.object.isPerspectiveCamera ) {// perspectiveconst position = scope.object.position;offset.copy( position ).sub( scope.target );let targetDistance = offset.length();// half of the fov is center to top of screentargetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 );// we use only clientHeight here so aspect ratio does not distort speedpanLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix );panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix );} else if ( scope.object.isOrthographicCamera ) {// orthographicpanLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix );panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix );} else {// camera neither orthographic nor perspectiveconsole.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );scope.enablePan = false;}};}();function dollyOut( dollyScale ) {if ( scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera ) {scale /= dollyScale;} else {console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );scope.enableZoom = false;}}function dollyIn( dollyScale ) {if ( scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera ) {scale *= dollyScale;} else {console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );scope.enableZoom = false;}}function updateZoomParameters( x, y ) {if ( ! scope.zoomToCursor ) {return;}performCursorZoom = true;const rect = scope.domElement.getBoundingClientRect();const dx = x - rect.left;const dy = y - rect.top;const w = rect.width;const h = rect.height;mouse.x = ( dx / w ) * 2 - 1;mouse.y = - ( dy / h ) * 2 + 1;dollyDirection.set( mouse.x, mouse.y, 1 ).unproject( scope.object ).sub( scope.object.position ).normalize();}function clampDistance( dist ) {return Math.max( scope.minDistance, Math.min( scope.maxDistance, dist ) );}//// event callbacks - update the object state//function handleMouseDownRotate( event ) {rotateStart.set( event.clientX, event.clientY );}function handleMouseDownDolly( event ) {updateZoomParameters( event.clientX, event.clientX );dollyStart.set( event.clientX, event.clientY );}function handleMouseDownPan( event ) {panStart.set( event.clientX, event.clientY );}function handleMouseMoveRotate( event ) {rotateEnd.set( event.clientX, event.clientY );rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );const element = scope.domElement;rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, heightrotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );rotateStart.copy( rotateEnd );scope.update();}function handleMouseMoveDolly( event ) {dollyEnd.set( event.clientX, event.clientY );dollyDelta.subVectors( dollyEnd, dollyStart );if ( dollyDelta.y > 0 ) {dollyOut( getZoomScale( dollyDelta.y ) );} else if ( dollyDelta.y < 0 ) {dollyIn( getZoomScale( dollyDelta.y ) );}dollyStart.copy( dollyEnd );scope.update();}function handleMouseMovePan( event ) {panEnd.set( event.clientX, event.clientY );panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );pan( panDelta.x, panDelta.y );panStart.copy( panEnd );scope.update();}function handleMouseWheel( event ) {updateZoomParameters( event.clientX, event.clientY );if ( event.deltaY < 0 ) {dollyIn( getZoomScale( event.deltaY ) );} else if ( event.deltaY > 0 ) {dollyOut( getZoomScale( event.deltaY ) );}scope.update();}function handleKeyDown( event ) {let needsUpdate = false;switch ( event.code ) {case scope.keys.UP:if ( event.ctrlKey || event.metaKey || event.shiftKey ) {rotateUp( 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );} else {pan( 0, scope.keyPanSpeed );}needsUpdate = true;break;case scope.keys.BOTTOM:if ( event.ctrlKey || event.metaKey || event.shiftKey ) {rotateUp( - 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );} else {pan( 0, - scope.keyPanSpeed );}needsUpdate = true;break;case scope.keys.LEFT:if ( event.ctrlKey || event.metaKey || event.shiftKey ) {rotateLeft( 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );} else {pan( scope.keyPanSpeed, 0 );}needsUpdate = true;break;case scope.keys.RIGHT:if ( event.ctrlKey || event.metaKey || event.shiftKey ) {rotateLeft( - 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );} else {pan( - scope.keyPanSpeed, 0 );}needsUpdate = true;break;}if ( needsUpdate ) {// prevent the browser from scrolling on cursor keysevent.preventDefault();scope.update();}}function handleTouchStartRotate( event ) {if ( pointers.length === 1 ) {rotateStart.set( event.pageX, event.pageY );} else {const position = getSecondPointerPosition( event );const x = 0.5 * ( event.pageX + position.x );const y = 0.5 * ( event.pageY + position.y );rotateStart.set( x, y );}}function handleTouchStartPan( event ) {if ( pointers.length === 1 ) {panStart.set( event.pageX, event.pageY );} else {const position = getSecondPointerPosition( event );const x = 0.5 * ( event.pageX + position.x );const y = 0.5 * ( event.pageY + position.y );panStart.set( x, y );}}function handleTouchStartDolly( event ) {const position = getSecondPointerPosition( event );const dx = event.pageX - position.x;const dy = event.pageY - position.y;const distance = Math.sqrt( dx * dx + dy * dy );dollyStart.set( 0, distance );}function handleTouchStartDollyPan( event ) {if ( scope.enableZoom ) handleTouchStartDolly( event );if ( scope.enablePan ) handleTouchStartPan( event );}function handleTouchStartDollyRotate( event ) {if ( scope.enableZoom ) handleTouchStartDolly( event );if ( scope.enableRotate ) handleTouchStartRotate( event );}function handleTouchMoveRotate( event ) {if ( pointers.length == 1 ) {rotateEnd.set( event.pageX, event.pageY );} else {const position = getSecondPointerPosition( event );const x = 0.5 * ( event.pageX + position.x );const y = 0.5 * ( event.pageY + position.y );rotateEnd.set( x, y );}rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );const element = scope.domElement;rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, heightrotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );rotateStart.copy( rotateEnd );}function handleTouchMovePan( event ) {if ( pointers.length === 1 ) {panEnd.set( event.pageX, event.pageY );} else {const position = getSecondPointerPosition( event );const x = 0.5 * ( event.pageX + position.x );const y = 0.5 * ( event.pageY + position.y );panEnd.set( x, y );}panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );pan( panDelta.x, panDelta.y );panStart.copy( panEnd );}function handleTouchMoveDolly( event ) {const position = getSecondPointerPosition( event );const dx = event.pageX - position.x;const dy = event.pageY - position.y;const distance = Math.sqrt( dx * dx + dy * dy );dollyEnd.set( 0, distance );dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) );dollyOut( dollyDelta.y );dollyStart.copy( dollyEnd );const centerX = ( event.pageX + position.x ) * 0.5;const centerY = ( event.pageY + position.y ) * 0.5;updateZoomParameters( centerX, centerY );}function handleTouchMoveDollyPan( event ) {if ( scope.enableZoom ) handleTouchMoveDolly( event );if ( scope.enablePan ) handleTouchMovePan( event );}function handleTouchMoveDollyRotate( event ) {if ( scope.enableZoom ) handleTouchMoveDolly( event );if ( scope.enableRotate ) handleTouchMoveRotate( event );}//// event handlers - FSM: listen for events and reset state//function onPointerDown( event ) {if ( scope.enabled === false ) return;if ( pointers.length === 0 ) {scope.domElement.setPointerCapture( event.pointerId );scope.domElement.addEventListener( 'pointermove', onPointerMove );scope.domElement.addEventListener( 'pointerup', onPointerUp );}//if ( isTrackingPointer( event ) ) return;//addPointer( event );if ( event.pointerType === 'touch' ) {onTouchStart( event );} else {onMouseDown( event );}}function onPointerMove( event ) {if ( scope.enabled === false ) return;if ( event.pointerType === 'touch' ) {onTouchMove( event );} else {onMouseMove( event );}}function onPointerUp( event ) {removePointer( event );switch ( pointers.length ) {case 0:scope.domElement.releasePointerCapture( event.pointerId );scope.domElement.removeEventListener( 'pointermove', onPointerMove );scope.domElement.removeEventListener( 'pointerup', onPointerUp );scope.dispatchEvent( _endEvent );state = STATE.NONE;break;case 1:const pointerId = pointers[ 0 ];const position = pointerPositions[ pointerId ];// minimal placeholder event - allows state correction on pointer-uponTouchStart( { pointerId: pointerId, pageX: position.x, pageY: position.y } );break;}}function onMouseDown( event ) {let mouseAction;switch ( event.button ) {case 0:mouseAction = scope.mouseButtons.LEFT;break;case 1:mouseAction = scope.mouseButtons.MIDDLE;break;case 2:mouseAction = scope.mouseButtons.RIGHT;break;default:mouseAction = - 1;}switch ( mouseAction ) {case MOUSE.DOLLY:if ( scope.enableZoom === false ) return;handleMouseDownDolly( event );state = STATE.DOLLY;break;case MOUSE.ROTATE:if ( event.ctrlKey || event.metaKey || event.shiftKey ) {if ( scope.enablePan === false ) return;handleMouseDownPan( event );state = STATE.PAN;} else {if ( scope.enableRotate === false ) return;handleMouseDownRotate( event );state = STATE.ROTATE;}break;case MOUSE.PAN:if ( event.ctrlKey || event.metaKey || event.shiftKey ) {if ( scope.enableRotate === false ) return;handleMouseDownRotate( event );state = STATE.ROTATE;} else {if ( scope.enablePan === false ) return;handleMouseDownPan( event );state = STATE.PAN;}break;default:state = STATE.NONE;}if ( state !== STATE.NONE ) {scope.dispatchEvent( _startEvent );}}function onMouseMove( event ) {switch ( state ) {case STATE.ROTATE:if ( scope.enableRotate === false ) return;handleMouseMoveRotate( event );break;case STATE.DOLLY:if ( scope.enableZoom === false ) return;handleMouseMoveDolly( event );break;case STATE.PAN:if ( scope.enablePan === false ) return;handleMouseMovePan( event );break;}}function onMouseWheel( event ) {if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE ) return;event.preventDefault();scope.dispatchEvent( _startEvent );handleMouseWheel( customWheelEvent( event ) );scope.dispatchEvent( _endEvent );}function customWheelEvent( event ) {const mode = event.deltaMode;// minimal wheel event altered to meet delta-zoom demandconst newEvent = {clientX: event.clientX,clientY: event.clientY,deltaY: event.deltaY,};switch ( mode ) {case 1: // LINE_MODEnewEvent.deltaY *= 16;break;case 2: // PAGE_MODEnewEvent.deltaY *= 100;break;}// detect if event was triggered by pinchingif ( event.ctrlKey && ! controlActive ) {newEvent.deltaY *= 10;}return newEvent;}function interceptControlDown( event ) {if ( event.key === 'Control' ) {controlActive = true;const document = scope.domElement.getRootNode(); // offscreen canvas compatibilitydocument.addEventListener( 'keyup', interceptControlUp, { passive: true, capture: true } );}}function interceptControlUp( event ) {if ( event.key === 'Control' ) {controlActive = false;const document = scope.domElement.getRootNode(); // offscreen canvas compatibilitydocument.removeEventListener( 'keyup', interceptControlUp, { passive: true, capture: true } );}}function onKeyDown( event ) {if ( scope.enabled === false || scope.enablePan === false ) return;handleKeyDown( event );}function onTouchStart( event ) {trackPointer( event );switch ( pointers.length ) {case 1:switch ( scope.touches.ONE ) {case TOUCH.ROTATE:if ( scope.enableRotate === false ) return;handleTouchStartRotate( event );state = STATE.TOUCH_ROTATE;break;case TOUCH.PAN:if ( scope.enablePan === false ) return;handleTouchStartPan( event );state = STATE.TOUCH_PAN;break;default:state = STATE.NONE;}break;case 2:switch ( scope.touches.TWO ) {case TOUCH.DOLLY_PAN:if ( scope.enableZoom === false && scope.enablePan === false ) return;handleTouchStartDollyPan( event );state = STATE.TOUCH_DOLLY_PAN;break;case TOUCH.DOLLY_ROTATE:if ( scope.enableZoom === false && scope.enableRotate === false ) return;handleTouchStartDollyRotate( event );state = STATE.TOUCH_DOLLY_ROTATE;break;default:state = STATE.NONE;}break;default:state = STATE.NONE;}if ( state !== STATE.NONE ) {scope.dispatchEvent( _startEvent );}}function onTouchMove( event ) {trackPointer( event );switch ( state ) {case STATE.TOUCH_ROTATE:if ( scope.enableRotate === false ) return;handleTouchMoveRotate( event );scope.update();break;case STATE.TOUCH_PAN:if ( scope.enablePan === false ) return;handleTouchMovePan( event );scope.update();break;case STATE.TOUCH_DOLLY_PAN:if ( scope.enableZoom === false && scope.enablePan === false ) return;handleTouchMoveDollyPan( event );scope.update();break;case STATE.TOUCH_DOLLY_ROTATE:if ( scope.enableZoom === false && scope.enableRotate === false ) return;handleTouchMoveDollyRotate( event );scope.update();break;default:state = STATE.NONE;}}function onContextMenu( event ) {if ( scope.enabled === false ) return;event.preventDefault();}function addPointer( event ) {pointers.push( event.pointerId );}function removePointer( event ) {delete pointerPositions[ event.pointerId ];for ( let i = 0; i < pointers.length; i ++ ) {if ( pointers[ i ] == event.pointerId ) {pointers.splice( i, 1 );return;}}}function isTrackingPointer( event ) {for ( let i = 0; i < pointers.length; i ++ ) {if ( pointers[ i ] == event.pointerId ) return true;}return false;}function trackPointer( event ) {let position = pointerPositions[ event.pointerId ];if ( position === undefined ) {position = new Vector2();pointerPositions[ event.pointerId ] = position;}position.set( event.pageX, event.pageY );}function getSecondPointerPosition( event ) {const pointerId = ( event.pointerId === pointers[ 0 ] ) ? pointers[ 1 ] : pointers[ 0 ];return pointerPositions[ pointerId ];}//scope.domElement.addEventListener( 'contextmenu', onContextMenu );scope.domElement.addEventListener( 'pointerdown', onPointerDown );scope.domElement.addEventListener( 'pointercancel', onPointerUp );scope.domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } );const document = scope.domElement.getRootNode(); // offscreen canvas compatibilitydocument.addEventListener( 'keydown', interceptControlDown, { passive: true, capture: true } );// force an update at startthis.update();}}//export { OrbitControls };class RoomEnvironment extends Scene {constructor( renderer = null ) {super();const geometry = new BoxGeometry();geometry.deleteAttribute( 'uv' );const roomMaterial = new MeshStandardMaterial( { side: BackSide } );const boxMaterial = new MeshStandardMaterial();let intensity = 5;if ( renderer !== null && renderer._useLegacyLights === false ) intensity = 900;const mainLight = new PointLight( 0xffffff, intensity, 28, 2 );mainLight.position.set( 0.418, 16.199, 0.300 );this.add( mainLight );const room = new Mesh( geometry, roomMaterial );room.position.set( - 0.757, 13.219, 0.717 );room.scale.set( 31.713, 28.305, 28.591 );this.add( room );const box1 = new Mesh( geometry, boxMaterial );box1.position.set( - 10.906, 2.009, 1.846 );box1.rotation.set( 0, - 0.195, 0 );box1.scale.set( 2.328, 7.905, 4.651 );this.add( box1 );const box2 = new Mesh( geometry, boxMaterial );box2.position.set( - 5.607, - 0.754, - 0.758 );box2.rotation.set( 0, 0.994, 0 );box2.scale.set( 1.970, 1.534, 3.955 );this.add( box2 );const box3 = new Mesh( geometry, boxMaterial );box3.position.set( 6.167, 0.857, 7.803 );box3.rotation.set( 0, 0.561, 0 );box3.scale.set( 3.927, 6.285, 3.687 );this.add( box3 );const box4 = new Mesh( geometry, boxMaterial );box4.position.set( - 2.017, 0.018, 6.124 );box4.rotation.set( 0, 0.333, 0 );box4.scale.set( 2.002, 4.566, 2.064 );this.add( box4 );const box5 = new Mesh( geometry, boxMaterial );box5.position.set( 2.291, - 0.756, - 2.621 );box5.rotation.set( 0, - 0.286, 0 );box5.scale.set( 1.546, 1.552, 1.496 );this.add( box5 );const box6 = new Mesh( geometry, boxMaterial );box6.position.set( - 2.193, - 0.369, - 5.547 );box6.rotation.set( 0, 0.516, 0 );box6.scale.set( 3.875, 3.487, 2.986 );this.add( box6 );// -x rightconst light1 = new Mesh( geometry, createAreaLightMaterial( 50 ) );light1.position.set( - 16.116, 14.37, 8.208 );light1.scale.set( 0.1, 2.428, 2.739 );this.add( light1 );// -x leftconst light2 = new Mesh( geometry, createAreaLightMaterial( 50 ) );light2.position.set( - 16.109, 18.021, - 8.207 );light2.scale.set( 0.1, 2.425, 2.751 );this.add( light2 );// +xconst light3 = new Mesh( geometry, createAreaLightMaterial( 17 ) );light3.position.set( 14.904, 12.198, - 1.832 );light3.scale.set( 0.15, 4.265, 6.331 );this.add( light3 );// +zconst light4 = new Mesh( geometry, createAreaLightMaterial( 43 ) );light4.position.set( - 0.462, 8.89, 14.520 );light4.scale.set( 4.38, 5.441, 0.088 );this.add( light4 );// -zconst light5 = new Mesh( geometry, createAreaLightMaterial( 20 ) );light5.position.set( 3.235, 11.486, - 12.541 );light5.scale.set( 2.5, 2.0, 0.1 );this.add( light5 );// +yconst light6 = new Mesh( geometry, createAreaLightMaterial( 100 ) );light6.position.set( 0.0, 20.0, 0.0 );light6.scale.set( 1.0, 0.1, 1.0 );this.add( light6 );}dispose() {const resources = new Set();this.traverse( ( object ) => {if ( object.isMesh ) {resources.add( object.geometry );resources.add( object.material );}} );for ( const resource of resources ) {resource.dispose();}}}function createAreaLightMaterial( intensity ) {const material = new MeshBasicMaterial();material.color.setScalar( intensity );return material;}//export { RoomEnvironment };// Special surface finish tag types.// Note: "MATERIAL" tag (e.g. GLITTER, SPECKLE) is not implementedconst FINISH_TYPE_DEFAULT = 0;const FINISH_TYPE_CHROME = 1;const FINISH_TYPE_PEARLESCENT = 2;const FINISH_TYPE_RUBBER = 3;const FINISH_TYPE_MATTE_METALLIC = 4;const FINISH_TYPE_METAL = 5;// State machine to search a subobject path.// The LDraw standard establishes these various possible subfolders.const FILE_LOCATION_TRY_PARTS = 0;const FILE_LOCATION_TRY_P = 1;const FILE_LOCATION_TRY_MODELS = 2;const FILE_LOCATION_AS_IS = 3;const FILE_LOCATION_TRY_RELATIVE = 4;const FILE_LOCATION_TRY_ABSOLUTE = 5;const FILE_LOCATION_NOT_FOUND = 6;const MAIN_COLOUR_CODE = '16';const MAIN_EDGE_COLOUR_CODE = '24';const COLOR_SPACE_LDRAW = SRGBColorSpace;const _tempVec0 = new Vector3();const _tempVec1 = new Vector3();class LDrawConditionalLineMaterial extends ShaderMaterial {constructor( parameters ) {super( {uniforms: UniformsUtils.merge( [UniformsLib.fog,{diffuse: {value: new Color()},opacity: {value: 1.0}}] ),vertexShader: /* glsl */`attribute vec3 control0;attribute vec3 control1;attribute vec3 direction;varying float discardFlag;#include <common>#include <color_pars_vertex>#include <fog_pars_vertex>#include <logdepthbuf_pars_vertex>#include <clipping_planes_pars_vertex>void main() {#include <color_vertex>vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );gl_Position = projectionMatrix * mvPosition;// Transform the line segment ends and control points into camera clip spacevec4 c0 = projectionMatrix * modelViewMatrix * vec4( control0, 1.0 );vec4 c1 = projectionMatrix * modelViewMatrix * vec4( control1, 1.0 );vec4 p0 = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );vec4 p1 = projectionMatrix * modelViewMatrix * vec4( position + direction, 1.0 );c0.xy /= c0.w;c1.xy /= c1.w;p0.xy /= p0.w;p1.xy /= p1.w;// Get the direction of the segment and an orthogonal vectorvec2 dir = p1.xy - p0.xy;vec2 norm = vec2( -dir.y, dir.x );// Get control point directions from the linevec2 c0dir = c0.xy - p1.xy;vec2 c1dir = c1.xy - p1.xy;// If the vectors to the controls points are pointed in different directions away// from the line segment then the line should not be drawn.float d0 = dot( normalize( norm ), normalize( c0dir ) );float d1 = dot( normalize( norm ), normalize( c1dir ) );discardFlag = float( sign( d0 ) != sign( d1 ) );#include <logdepthbuf_vertex>#include <clipping_planes_vertex>#include <fog_vertex>}`,fragmentShader: /* glsl */`uniform vec3 diffuse;uniform float opacity;varying float discardFlag;#include <common>#include <color_pars_fragment>#include <fog_pars_fragment>#include <logdepthbuf_pars_fragment>#include <clipping_planes_pars_fragment>void main() {if ( discardFlag > 0.5 ) discard;#include <clipping_planes_fragment>vec3 outgoingLight = vec3( 0.0 );vec4 diffuseColor = vec4( diffuse, opacity );#include <logdepthbuf_fragment>#include <color_fragment>outgoingLight = diffuseColor.rgb; // simple shadergl_FragColor = vec4( outgoingLight, diffuseColor.a );#include <tonemapping_fragment>#include <colorspace_fragment>#include <fog_fragment>#include <premultiplied_alpha_fragment>}`,} );Object.defineProperties( this, {opacity: {get: function () {return this.uniforms.opacity.value;},set: function ( value ) {this.uniforms.opacity.value = value;}},color: {get: function () {return this.uniforms.diffuse.value;}}} );this.setValues( parameters );this.isLDrawConditionalLineMaterial = true;}}class ConditionalLineSegments extends LineSegments {constructor( geometry, material ) {super( geometry, material );this.isConditionalLine = true;}}function generateFaceNormals( faces ) {for ( let i = 0, l = faces.length; i < l; i ++ ) {const face = faces[ i ];const vertices = face.vertices;const v0 = vertices[ 0 ];const v1 = vertices[ 1 ];const v2 = vertices[ 2 ];_tempVec0.subVectors( v1, v0 );_tempVec1.subVectors( v2, v1 );face.faceNormal = new Vector3().crossVectors( _tempVec0, _tempVec1 ).normalize();}}//const _ray = new Ray();function smoothNormals( faces, lineSegments, checkSubSegments = false ) {// NOTE: 1e2 is pretty coarse but was chosen to quantize the resulting value because// it allows edges to be smoothed as expected (see minifig arms).// --// And the vector values are initialize multiplied by 1 + 1e-10 to account for floating// point errors on vertices along quantization boundaries. Ie after matrix multiplication// vertices that should be merged might be set to "1.7" and "1.6999..." meaning they won't// get merged. This added epsilon attempts to push these error values to the same quantized// value for the sake of hashing. See "AT-ST mini" dishes. See mrdoob/three#23169.const hashMultiplier = ( 1 + 1e-10 ) * 1e2;function hashVertex( v ) {const x = ~ ~ ( v.x * hashMultiplier );const y = ~ ~ ( v.y * hashMultiplier );const z = ~ ~ ( v.z * hashMultiplier );return `${ x },${ y },${ z }`;}function hashEdge( v0, v1 ) {return `${ hashVertex( v0 ) }_${ hashVertex( v1 ) }`;}// converts the two vertices to a ray with a normalized direction and origin of 0, 0, 0 projected// onto the original line.function toNormalizedRay( v0, v1, targetRay ) {targetRay.direction.subVectors( v1, v0 ).normalize();const scalar = v0.dot( targetRay.direction );targetRay.origin.copy( v0 ).addScaledVector( targetRay.direction, - scalar );return targetRay;}function hashRay( ray ) {return hashEdge( ray.origin, ray.direction );}const hardEdges = new Set();const hardEdgeRays = new Map();const halfEdgeList = {};const normals = [];// Save the list of hard edges by hashfor ( let i = 0, l = lineSegments.length; i < l; i ++ ) {const ls = lineSegments[ i ];const vertices = ls.vertices;const v0 = vertices[ 0 ];const v1 = vertices[ 1 ];hardEdges.add( hashEdge( v0, v1 ) );hardEdges.add( hashEdge( v1, v0 ) );// only generate the hard edge ray map if we're checking subsegments because it's more expensive to check// and requires more memory.if ( checkSubSegments ) {// add both ray directions to the mapconst ray = toNormalizedRay( v0, v1, new Ray() );const rh1 = hashRay( ray );if ( ! hardEdgeRays.has( rh1 ) ) {toNormalizedRay( v1, v0, ray );const rh2 = hashRay( ray );const info = {ray,distances: [],};hardEdgeRays.set( rh1, info );hardEdgeRays.set( rh2, info );}// store both segments ends in min, max order in the distances array to check if a face edge is a// subsegment later.const info = hardEdgeRays.get( rh1 );let d0 = info.ray.direction.dot( v0 );let d1 = info.ray.direction.dot( v1 );if ( d0 > d1 ) {[ d0, d1 ] = [ d1, d0 ];}info.distances.push( d0, d1 );}}// track the half edges associated with each trianglefor ( let i = 0, l = faces.length; i < l; i ++ ) {const tri = faces[ i ];const vertices = tri.vertices;const vertCount = vertices.length;for ( let i2 = 0; i2 < vertCount; i2 ++ ) {const index = i2;const next = ( i2 + 1 ) % vertCount;const v0 = vertices[ index ];const v1 = vertices[ next ];const hash = hashEdge( v0, v1 );// don't add the triangle if the edge is supposed to be hardif ( hardEdges.has( hash ) ) {continue;}// if checking subsegments then check to see if this edge lies on a hard edge ray and whether its within any ray boundsif ( checkSubSegments ) {toNormalizedRay( v0, v1, _ray );const rayHash = hashRay( _ray );if ( hardEdgeRays.has( rayHash ) ) {const info = hardEdgeRays.get( rayHash );const { ray, distances } = info;let d0 = ray.direction.dot( v0 );let d1 = ray.direction.dot( v1 );if ( d0 > d1 ) {[ d0, d1 ] = [ d1, d0 ];}// return early if the face edge is found to be a subsegment of a line edge meaning the edge will have "hard" normalslet found = false;for ( let i = 0, l = distances.length; i < l; i += 2 ) {if ( d0 >= distances[ i ] && d1 <= distances[ i + 1 ] ) {found = true;break;}}if ( found ) {continue;}}}const info = {index: index,tri: tri};halfEdgeList[ hash ] = info;}}// Iterate until we've tried to connect all faces to share normalswhile ( true ) {// Stop if there are no more faces leftlet halfEdge = null;for ( const key in halfEdgeList ) {halfEdge = halfEdgeList[ key ];break;}if ( halfEdge === null ) {break;}// Exhaustively find all connected facesconst queue = [ halfEdge ];while ( queue.length > 0 ) {// initialize all vertex normals in this triangleconst tri = queue.pop().tri;const vertices = tri.vertices;const vertNormals = tri.normals;const faceNormal = tri.faceNormal;// Check if any edge is connected to another triangle edgeconst vertCount = vertices.length;for ( let i2 = 0; i2 < vertCount; i2 ++ ) {const index = i2;const next = ( i2 + 1 ) % vertCount;const v0 = vertices[ index ];const v1 = vertices[ next ];// delete this triangle from the list so it won't be found againconst hash = hashEdge( v0, v1 );delete halfEdgeList[ hash ];const reverseHash = hashEdge( v1, v0 );const otherInfo = halfEdgeList[ reverseHash ];if ( otherInfo ) {const otherTri = otherInfo.tri;const otherIndex = otherInfo.index;const otherNormals = otherTri.normals;const otherVertCount = otherNormals.length;const otherFaceNormal = otherTri.faceNormal;// NOTE: If the angle between faces is > 67.5 degrees then assume it's// hard edge. There are some cases where the line segments do not line up exactly// with or span multiple triangle edges (see Lunar Vehicle wheels).if ( Math.abs( otherTri.faceNormal.dot( tri.faceNormal ) ) < 0.25 ) {continue;}// if this triangle has already been traversed then it won't be in// the halfEdgeList. If it has not then add it to the queue and delete// it so it won't be found again.if ( reverseHash in halfEdgeList ) {queue.push( otherInfo );delete halfEdgeList[ reverseHash ];}// share the first normalconst otherNext = ( otherIndex + 1 ) % otherVertCount;if (vertNormals[ index ] && otherNormals[ otherNext ] &&vertNormals[ index ] !== otherNormals[ otherNext ]) {otherNormals[ otherNext ].norm.add( vertNormals[ index ].norm );vertNormals[ index ].norm = otherNormals[ otherNext ].norm;}let sharedNormal1 = vertNormals[ index ] || otherNormals[ otherNext ];if ( sharedNormal1 === null ) {// it's possible to encounter an edge of a triangle that has already been traversed meaning// both edges already have different normals defined and shared. To work around this we create// a wrapper object so when those edges are merged the normals can be updated everywhere.sharedNormal1 = { norm: new Vector3() };normals.push( sharedNormal1.norm );}if ( vertNormals[ index ] === null ) {vertNormals[ index ] = sharedNormal1;sharedNormal1.norm.add( faceNormal );}if ( otherNormals[ otherNext ] === null ) {otherNormals[ otherNext ] = sharedNormal1;sharedNormal1.norm.add( otherFaceNormal );}// share the second normalif (vertNormals[ next ] && otherNormals[ otherIndex ] &&vertNormals[ next ] !== otherNormals[ otherIndex ]) {otherNormals[ otherIndex ].norm.add( vertNormals[ next ].norm );vertNormals[ next ].norm = otherNormals[ otherIndex ].norm;}let sharedNormal2 = vertNormals[ next ] || otherNormals[ otherIndex ];if ( sharedNormal2 === null ) {sharedNormal2 = { norm: new Vector3() };normals.push( sharedNormal2.norm );}if ( vertNormals[ next ] === null ) {vertNormals[ next ] = sharedNormal2;sharedNormal2.norm.add( faceNormal );}if ( otherNormals[ otherIndex ] === null ) {otherNormals[ otherIndex ] = sharedNormal2;sharedNormal2.norm.add( otherFaceNormal );}}}}}// The normals of each face have been added up so now we average them by normalizing the vector.for ( let i = 0, l = normals.length; i < l; i ++ ) {normals[ i ].normalize();}}function isPartType( type ) {return type === 'Part' || type === 'Unofficial_Part';}function isPrimitiveType( type ) {return /primitive/i.test( type ) || type === 'Subpart';}class LineParser {constructor( line, lineNumber ) {this.line = line;this.lineLength = line.length;this.currentCharIndex = 0;this.currentChar = ' ';this.lineNumber = lineNumber;}seekNonSpace() {while ( this.currentCharIndex < this.lineLength ) {this.currentChar = this.line.charAt( this.currentCharIndex );if ( this.currentChar !== ' ' && this.currentChar !== '\t' ) {return;}this.currentCharIndex ++;}}getToken() {const pos0 = this.currentCharIndex ++;// Seek spacewhile ( this.currentCharIndex < this.lineLength ) {this.currentChar = this.line.charAt( this.currentCharIndex );if ( this.currentChar === ' ' || this.currentChar === '\t' ) {break;}this.currentCharIndex ++;}const pos1 = this.currentCharIndex;this.seekNonSpace();return this.line.substring( pos0, pos1 );}getVector() {return new Vector3( parseFloat( this.getToken() ), parseFloat( this.getToken() ), parseFloat( this.getToken() ) );}getRemainingString() {return this.line.substring( this.currentCharIndex, this.lineLength );}isAtTheEnd() {return this.currentCharIndex >= this.lineLength;}setToEnd() {this.currentCharIndex = this.lineLength;}getLineNumberString() {return this.lineNumber >= 0 ? ' at line ' + this.lineNumber : '';}}// Fetches and parses an intermediate representation of LDraw parts files.class LDrawParsedCache {constructor( loader ) {this.loader = loader;this._cache = {};}cloneResult( original ) {const result = {};// vertices are transformed and normals computed before being converted to geometry// so these pieces must be cloned.result.faces = original.faces.map( face => {return {colorCode: face.colorCode,material: face.material,vertices: face.vertices.map( v => v.clone() ),normals: face.normals.map( () => null ),faceNormal: null};} );result.conditionalSegments = original.conditionalSegments.map( face => {return {colorCode: face.colorCode,material: face.material,vertices: face.vertices.map( v => v.clone() ),controlPoints: face.controlPoints.map( v => v.clone() )};} );result.lineSegments = original.lineSegments.map( face => {return {colorCode: face.colorCode,material: face.material,vertices: face.vertices.map( v => v.clone() )};} );// none if this is subsequently modifiedresult.type = original.type;result.category = original.category;result.keywords = original.keywords;result.author = original.author;result.subobjects = original.subobjects;result.fileName = original.fileName;result.totalFaces = original.totalFaces;result.startingBuildingStep = original.startingBuildingStep;result.materials = original.materials;result.group = null;return result;}async fetchData( fileName ) {let triedLowerCase = false;let locationState = FILE_LOCATION_TRY_PARTS;while ( locationState !== FILE_LOCATION_NOT_FOUND ) {let subobjectURL = fileName;switch ( locationState ) {case FILE_LOCATION_AS_IS:locationState = locationState + 1;break;case FILE_LOCATION_TRY_PARTS:subobjectURL = 'parts/' + subobjectURL;locationState = locationState + 1;break;case FILE_LOCATION_TRY_P:subobjectURL = 'p/' + subobjectURL;locationState = locationState + 1;break;case FILE_LOCATION_TRY_MODELS:subobjectURL = 'models/' + subobjectURL;locationState = locationState + 1;break;case FILE_LOCATION_TRY_RELATIVE:subobjectURL = fileName.substring( 0, fileName.lastIndexOf( '/' ) + 1 ) + subobjectURL;locationState = locationState + 1;break;case FILE_LOCATION_TRY_ABSOLUTE:if ( triedLowerCase ) {// Try absolute pathlocationState = FILE_LOCATION_NOT_FOUND;} else {// Next attempt is lower casefileName = fileName.toLowerCase();subobjectURL = fileName;triedLowerCase = true;locationState = FILE_LOCATION_TRY_PARTS;}break;}const loader = this.loader;const fileLoader = new FileLoader( loader.manager );fileLoader.setPath( loader.partsLibraryPath );fileLoader.setRequestHeader( loader.requestHeader );fileLoader.setWithCredentials( loader.withCredentials );try {const text = await fileLoader.loadAsync( subobjectURL );return text;} catch ( _ ) {continue;}}throw new Error( 'LDrawLoader: Subobject "' + fileName + '" could not be loaded.' );}parse( text, fileName = null ) {const loader = this.loader;// final resultsconst faces = [];const lineSegments = [];const conditionalSegments = [];const subobjects = [];const materials = {};const getLocalMaterial = colorCode => {return materials[ colorCode ] || null;};let type = 'Model';let category = null;let keywords = null;let author = null;let totalFaces = 0;// split into linesif ( text.indexOf( '\r\n' ) !== - 1 ) {// This is faster than String.split with regex that splits on bothtext = text.replace( /\r\n/g, '\n' );}const lines = text.split( '\n' );const numLines = lines.length;let parsingEmbeddedFiles = false;let currentEmbeddedFileName = null;let currentEmbeddedText = null;let bfcCertified = false;let bfcCCW = true;let bfcInverted = false;let bfcCull = true;let startingBuildingStep = false;try{// Parse all line commandsfor ( let lineIndex = 0; lineIndex < numLines; lineIndex ++ ) {const line = lines[ lineIndex ];if ( line.length === 0 ) continue;if ( parsingEmbeddedFiles ) {if ( line.startsWith( '0 FILE ' ) ) {// Save previous embedded file in the cachethis.setData( currentEmbeddedFileName, currentEmbeddedText );// New embedded text filecurrentEmbeddedFileName = line.substring( 7 );currentEmbeddedText = '';} else {currentEmbeddedText += line + '\n';}continue;}const lp = new LineParser( line, lineIndex + 1 );lp.seekNonSpace();if ( lp.isAtTheEnd() ) {// Empty linecontinue;}// Parse the line typeconst lineType = lp.getToken();let material;let colorCode;let segment;let ccw;let doubleSided;let v0, v1, v2, v3, c0, c1;switch ( lineType ) {// Line type 0: Comment or METAcase '0':// Parse meta directiveconst meta = lp.getToken();if ( meta ) {switch ( meta ) {case '!LDRAW_ORG':type = lp.getToken();break;case '!COLOUR':material = loader.parseColorMetaDirective( lp );if ( material ) {materials[ material.userData.code ] = material;}	else {console.warn( 'LDrawLoader: Error parsing material' + lp.getLineNumberString() );}break;case '!CATEGORY':category = lp.getToken();break;case '!KEYWORDS':const newKeywords = lp.getRemainingString().split( ',' );if ( newKeywords.length > 0 ) {if ( ! keywords ) {keywords = [];}newKeywords.forEach( function ( keyword ) {keywords.push( keyword.trim() );} );}break;case 'FILE':if ( lineIndex > 0 ) {// Start embedded text files parsingparsingEmbeddedFiles = true;currentEmbeddedFileName = lp.getRemainingString();currentEmbeddedText = '';bfcCertified = false;bfcCCW = true;}break;case 'BFC':// Changes to the backface culling statewhile ( ! lp.isAtTheEnd() ) {const token = lp.getToken();switch ( token ) {case 'CERTIFY':case 'NOCERTIFY':bfcCertified = token === 'CERTIFY';bfcCCW = true;break;case 'CW':case 'CCW':bfcCCW = token === 'CCW';break;case 'INVERTNEXT':bfcInverted = true;break;case 'CLIP':case 'NOCLIP':bfcCull = token === 'CLIP';break;default:console.warn( 'THREE.LDrawLoader: BFC directive "' + token + '" is unknown.' );break;}}break;case 'STEP':startingBuildingStep = true;break;case 'Author:':author = lp.getToken();break;default:// Other meta directives are not implementedbreak;}}break;// Line type 1: Sub-object filecase '1':colorCode = lp.getToken();material = getLocalMaterial( colorCode );const posX = parseFloat( lp.getToken() );const posY = parseFloat( lp.getToken() );const posZ = parseFloat( lp.getToken() );const m0 = parseFloat( lp.getToken() );const m1 = parseFloat( lp.getToken() );const m2 = parseFloat( lp.getToken() );const m3 = parseFloat( lp.getToken() );const m4 = parseFloat( lp.getToken() );const m5 = parseFloat( lp.getToken() );const m6 = parseFloat( lp.getToken() );const m7 = parseFloat( lp.getToken() );const m8 = parseFloat( lp.getToken() );const matrix = new Matrix4().set(m0, m1, m2, posX,m3, m4, m5, posY,m6, m7, m8, posZ,0, 0, 0, 1);let fileName = lp.getRemainingString().trim().replace( /\\/g, '/' );if ( loader.fileMap[ fileName ] ) {// Found the subobject path in the preloaded file path mapfileName = loader.fileMap[ fileName ];} else {// Standardized subfoldersif ( fileName.startsWith( 's/' ) ) {fileName = 'parts/' + fileName;} else if ( fileName.startsWith( '48/' ) ) {fileName = 'p/' + fileName;}}subobjects.push( {material: material,colorCode: colorCode,matrix: matrix,fileName: fileName,inverted: bfcInverted,startingBuildingStep: startingBuildingStep} );startingBuildingStep = false;bfcInverted = false;break;// Line type 2: Line segmentcase '2':colorCode = lp.getToken();material = getLocalMaterial( colorCode );v0 = lp.getVector();v1 = lp.getVector();segment = {material: material,colorCode: colorCode,vertices: [ v0, v1 ],};lineSegments.push( segment );break;// Line type 5: Conditional Line segmentcase '5':colorCode = lp.getToken();material = getLocalMaterial( colorCode );v0 = lp.getVector();v1 = lp.getVector();c0 = lp.getVector();c1 = lp.getVector();segment = {material: material,colorCode: colorCode,vertices: [ v0, v1 ],controlPoints: [ c0, c1 ],};conditionalSegments.push( segment );break;// Line type 3: Trianglecase '3':colorCode = lp.getToken();material = getLocalMaterial( colorCode );ccw = bfcCCW;doubleSided = ! bfcCertified || ! bfcCull;if ( ccw === true ) {v0 = lp.getVector();v1 = lp.getVector();v2 = lp.getVector();} else {v2 = lp.getVector();v1 = lp.getVector();v0 = lp.getVector();}faces.push( {material: material,colorCode: colorCode,faceNormal: null,vertices: [ v0, v1, v2 ],normals: [ null, null, null ],} );totalFaces ++;if ( doubleSided === true ) {faces.push( {material: material,colorCode: colorCode,faceNormal: null,vertices: [ v2, v1, v0 ],normals: [ null, null, null ],} );totalFaces ++;}break;// Line type 4: Quadrilateralcase '4':colorCode = lp.getToken();material = getLocalMaterial( colorCode );ccw = bfcCCW;doubleSided = ! bfcCertified || ! bfcCull;if ( ccw === true ) {v0 = lp.getVector();v1 = lp.getVector();v2 = lp.getVector();v3 = lp.getVector();} else {v3 = lp.getVector();v2 = lp.getVector();v1 = lp.getVector();v0 = lp.getVector();}// specifically place the triangle diagonal in the v0 and v1 slots so we can// account for the doubling of vertices later when smoothing normals.faces.push( {material: material,colorCode: colorCode,faceNormal: null,vertices: [ v0, v1, v2, v3 ],normals: [ null, null, null, null ],} );totalFaces += 2;if ( doubleSided === true ) {faces.push( {material: material,colorCode: colorCode,faceNormal: null,vertices: [ v3, v2, v1, v0 ],normals: [ null, null, null, null ],} );totalFaces += 2;}break;default:throw new Error( 'LDrawLoader: Unknown line type "' + lineType + '"' + lp.getLineNumberString() + '.' );}}}catch(error){console.error(error);}if ( parsingEmbeddedFiles ) {this.setData( currentEmbeddedFileName, currentEmbeddedText );}return {faces,conditionalSegments,lineSegments,type,category,keywords,author,subobjects,totalFaces,startingBuildingStep,materials,fileName,group: null};}// returns an (optionally cloned) instance of the datagetData( fileName, clone = true ) {const key = fileName.toLowerCase();const result = this._cache[ key ];if ( result === null || result instanceof Promise ) {return null;}if ( clone ) {return this.cloneResult( result );} else {return result;}}// kicks off a fetch and parse of the requested data if it hasn't already been loaded. Returns when// the data is ready to use and can be retrieved synchronously with "getData".async ensureDataLoaded( fileName ) {const key = fileName.toLowerCase();if ( ! ( key in this._cache ) ) {// replace the promise with a copy of the parsed data for immediate processingthis._cache[ key ] = this.fetchData( fileName ).then( text => {const info = this.parse( text, fileName );this._cache[ key ] = info;return info;} );}await this._cache[ key ];}// sets the data in the cache from parsed datasetData( fileName, text ) {const key = fileName.toLowerCase();this._cache[ key ] = this.parse( text, fileName );}}// returns the material for an associated color code. If the color code is 16 for a face or 24 for// an edge then the passthroughColorCode is used.function getMaterialFromCode( colorCode, parentColorCode, materialHierarchy, forEdge ) {const isPassthrough = ! forEdge && colorCode === MAIN_COLOUR_CODE || forEdge && colorCode === MAIN_EDGE_COLOUR_CODE;if ( isPassthrough ) {colorCode = parentColorCode;}return materialHierarchy[ colorCode ] || null;}// Class used to parse and build LDraw parts as three.js objects and cache them if they're a "Part" type.class LDrawPartsGeometryCache {constructor( loader ) {this.loader = loader;this.parseCache = new LDrawParsedCache( loader );this._cache = {};}// Convert the given file information into a mesh by processing subobjects.async processIntoMesh( info ) {const loader = this.loader;const parseCache = this.parseCache;const faceMaterials = new Set();// Processes the part subobject information to load child parts and merge geometry onto part// piece object.const processInfoSubobjects = async ( info, subobject = null ) => {const subobjects = info.subobjects;const promises = [];// Trigger load of all subobjects. If a subobject isn't a primitive then load it as a separate// group which lets instruction steps apply correctly.for ( let i = 0, l = subobjects.length; i < l; i ++ ) {const subobject = subobjects[ i ];const promise = parseCache.ensureDataLoaded( subobject.fileName ).then( () => {const subobjectInfo = parseCache.getData( subobject.fileName, false );if ( ! isPrimitiveType( subobjectInfo.type ) ) {return this.loadModel( subobject.fileName ).catch( error => {console.warn( error );return null;} );}return processInfoSubobjects( parseCache.getData( subobject.fileName ), subobject );} );promises.push( promise );}const group = new Group();group.userData.category = info.category;group.userData.keywords = info.keywords;group.userData.author = info.author;group.userData.type = info.type;group.userData.fileName = info.fileName;info.group = group;const subobjectInfos = await Promise.all( promises );for ( let i = 0, l = subobjectInfos.length; i < l; i ++ ) {const subobject = info.subobjects[ i ];const subobjectInfo = subobjectInfos[ i ];if ( subobjectInfo === null ) {// the subobject failed to loadcontinue;}// if the subobject was loaded as a separate group then apply the parent scopes materialsif ( subobjectInfo.isGroup ) {const subobjectGroup = subobjectInfo;subobject.matrix.decompose( subobjectGroup.position, subobjectGroup.quaternion, subobjectGroup.scale );subobjectGroup.userData.startingBuildingStep = subobject.startingBuildingStep;subobjectGroup.name = subobject.fileName;loader.applyMaterialsToMesh( subobjectGroup, subobject.colorCode, info.materials );subobjectGroup.userData.colorCode = subobject.colorCode;group.add( subobjectGroup );continue;}// add the subobject group if it has children in case it has both children and primitivesif ( subobjectInfo.group.children.length ) {group.add( subobjectInfo.group );}// transform the primitives into the local space of the parent piece and append them to// to the parent primitives list.const parentLineSegments = info.lineSegments;const parentConditionalSegments = info.conditionalSegments;const parentFaces = info.faces;const lineSegments = subobjectInfo.lineSegments;const conditionalSegments = subobjectInfo.conditionalSegments;const faces = subobjectInfo.faces;const matrix = subobject.matrix;const inverted = subobject.inverted;const matrixScaleInverted = matrix.determinant() < 0;const colorCode = subobject.colorCode;const lineColorCode = colorCode === MAIN_COLOUR_CODE ? MAIN_EDGE_COLOUR_CODE : colorCode;for ( let i = 0, l = lineSegments.length; i < l; i ++ ) {const ls = lineSegments[ i ];const vertices = ls.vertices;vertices[ 0 ].applyMatrix4( matrix );vertices[ 1 ].applyMatrix4( matrix );ls.colorCode = ls.colorCode === MAIN_EDGE_COLOUR_CODE ? lineColorCode : ls.colorCode;ls.material = ls.material || getMaterialFromCode( ls.colorCode, ls.colorCode, info.materials, true );parentLineSegments.push( ls );}for ( let i = 0, l = conditionalSegments.length; i < l; i ++ ) {const os = conditionalSegments[ i ];const vertices = os.vertices;const controlPoints = os.controlPoints;vertices[ 0 ].applyMatrix4( matrix );vertices[ 1 ].applyMatrix4( matrix );controlPoints[ 0 ].applyMatrix4( matrix );controlPoints[ 1 ].applyMatrix4( matrix );os.colorCode = os.colorCode === MAIN_EDGE_COLOUR_CODE ? lineColorCode : os.colorCode;os.material = os.material || getMaterialFromCode( os.colorCode, os.colorCode, info.materials, true );parentConditionalSegments.push( os );}for ( let i = 0, l = faces.length; i < l; i ++ ) {const tri = faces[ i ];const vertices = tri.vertices;for ( let i = 0, l = vertices.length; i < l; i ++ ) {vertices[ i ].applyMatrix4( matrix );}tri.colorCode = tri.colorCode === MAIN_COLOUR_CODE ? colorCode : tri.colorCode;tri.material = tri.material || getMaterialFromCode( tri.colorCode, colorCode, info.materials, false );faceMaterials.add( tri.colorCode );// If the scale of the object is negated then the triangle winding order// needs to be flipped.if ( matrixScaleInverted !== inverted ) {vertices.reverse();}parentFaces.push( tri );}info.totalFaces += subobjectInfo.totalFaces;}// Apply the parent subobjects pass through material code to this object. This is done several times due// to material scoping.if ( subobject ) {loader.applyMaterialsToMesh( group, subobject.colorCode, info.materials );group.userData.colorCode = subobject.colorCode;}return info;};// Track material use to see if we need to use the normal smooth slow path for hard edges.for ( let i = 0, l = info.faces; i < l; i ++ ) {faceMaterials.add( info.faces[ i ].colorCode );}await processInfoSubobjects( info );if ( loader.smoothNormals ) {const checkSubSegments = faceMaterials.size > 1;generateFaceNormals( info.faces );smoothNormals( info.faces, info.lineSegments, checkSubSegments );}// Add the primitive objects and metadata.const group = info.group;if ( info.faces.length > 0 ) {group.add( createObject( this.loader, info.faces, 3, false, info.totalFaces ) );}if ( info.lineSegments.length > 0 ) {group.add( createObject( this.loader, info.lineSegments, 2 ) );}if ( info.conditionalSegments.length > 0 ) {group.add( createObject( this.loader, info.conditionalSegments, 2, true ) );}return group;}hasCachedModel( fileName ) {return fileName !== null && fileName.toLowerCase() in this._cache;}async getCachedModel( fileName ) {if ( fileName !== null && this.hasCachedModel( fileName ) ) {const key = fileName.toLowerCase();const group = await this._cache[ key ];return group.clone();} else {return null;}}// Loads and parses the model with the given file name. Returns a cached copy if available.async loadModel( fileName ) {const parseCache = this.parseCache;const key = fileName.toLowerCase();if ( this.hasCachedModel( fileName ) ) {// Return cached model if available.return this.getCachedModel( fileName );} else {// Otherwise parse a new model.// Ensure the file data is loaded and pre parsed.await parseCache.ensureDataLoaded( fileName );const info = parseCache.getData( fileName );const promise = this.processIntoMesh( info );// Now that the file has loaded it's possible that another part parse has been waiting in parallel// so check the cache again to see if it's been added since the last async operation so we don't// do unnecessary work.if ( this.hasCachedModel( fileName ) ) {return this.getCachedModel( fileName );}// Cache object if it's a part so it can be reused later.if ( isPartType( info.type ) ) {this._cache[ key ] = promise;}// return a copyconst group = await promise;return group.clone();}}// parses the given model text into a renderable object. Returns cached copy if available.async parseModel( text ) {const parseCache = this.parseCache;const info = parseCache.parse( text );if ( isPartType( info.type ) && this.hasCachedModel( info.fileName ) ) {return this.getCachedModel( info.fileName );}return this.processIntoMesh( info );}}function sortByMaterial( a, b ) {if ( a.colorCode === b.colorCode ) {return 0;}if ( a.colorCode < b.colorCode ) {return - 1;}return 1;}function createObject( loader, elements, elementSize, isConditionalSegments = false, totalElements = null ) {// Creates a LineSegments (elementSize = 2) or a Mesh (elementSize = 3 )// With per face / segment material, implemented with mesh groups and materials array// Sort the faces or line segments by color code to make later the mesh groupselements.sort( sortByMaterial );if ( totalElements === null ) {totalElements = elements.length;}const positions = new Float32Array( elementSize * totalElements * 3 );const normals = elementSize === 3 ? new Float32Array( elementSize * totalElements * 3 ) : null;const materials = [];const quadArray = new Array( 6 );const bufferGeometry = new BufferGeometry();let prevMaterial = null;let index0 = 0;let numGroupVerts = 0;let offset = 0;for ( let iElem = 0, nElem = elements.length; iElem < nElem; iElem ++ ) {const elem = elements[ iElem ];let vertices = elem.vertices;if ( vertices.length === 4 ) {quadArray[ 0 ] = vertices[ 0 ];quadArray[ 1 ] = vertices[ 1 ];quadArray[ 2 ] = vertices[ 2 ];quadArray[ 3 ] = vertices[ 0 ];quadArray[ 4 ] = vertices[ 2 ];quadArray[ 5 ] = vertices[ 3 ];vertices = quadArray;}for ( let j = 0, l = vertices.length; j < l; j ++ ) {const v = vertices[ j ];const index = offset + j * 3;positions[ index + 0 ] = v.x;positions[ index + 1 ] = v.y;positions[ index + 2 ] = v.z;}// create the normals array if this is a set of facesif ( elementSize === 3 ) {if ( ! elem.faceNormal ) {const v0 = vertices[ 0 ];const v1 = vertices[ 1 ];const v2 = vertices[ 2 ];_tempVec0.subVectors( v1, v0 );_tempVec1.subVectors( v2, v1 );elem.faceNormal = new Vector3().crossVectors( _tempVec0, _tempVec1 ).normalize();}let elemNormals = elem.normals;if ( elemNormals.length === 4 ) {quadArray[ 0 ] = elemNormals[ 0 ];quadArray[ 1 ] = elemNormals[ 1 ];quadArray[ 2 ] = elemNormals[ 2 ];quadArray[ 3 ] = elemNormals[ 0 ];quadArray[ 4 ] = elemNormals[ 2 ];quadArray[ 5 ] = elemNormals[ 3 ];elemNormals = quadArray;}for ( let j = 0, l = elemNormals.length; j < l; j ++ ) {// use face normal if a vertex normal is not providedlet n = elem.faceNormal;if ( elemNormals[ j ] ) {n = elemNormals[ j ].norm;}const index = offset + j * 3;normals[ index + 0 ] = n.x;normals[ index + 1 ] = n.y;normals[ index + 2 ] = n.z;}}if ( prevMaterial !== elem.colorCode ) {if ( prevMaterial !== null ) {bufferGeometry.addGroup( index0, numGroupVerts, materials.length - 1 );}const material = elem.material;if ( material !== null ) {if ( elementSize === 3 ) {materials.push( material );} else if ( elementSize === 2 ) {if ( isConditionalSegments ) {const edgeMaterial = loader.edgeMaterialCache.get( material );materials.push( loader.conditionalEdgeMaterialCache.get( edgeMaterial ) );} else {materials.push( loader.edgeMaterialCache.get( material ) );}}} else {// If a material has not been made available yet then keep the color code string in the material array// to save the spot for the material once a parent scopes materials are being applied to the object.materials.push( elem.colorCode );}prevMaterial = elem.colorCode;index0 = offset / 3;numGroupVerts = vertices.length;} else {numGroupVerts += vertices.length;}offset += 3 * vertices.length;}if ( numGroupVerts > 0 ) {bufferGeometry.addGroup( index0, Infinity, materials.length - 1 );}bufferGeometry.setAttribute( 'position', new BufferAttribute( positions, 3 ) );if ( normals !== null ) {bufferGeometry.setAttribute( 'normal', new BufferAttribute( normals, 3 ) );}let object3d = null;if ( elementSize === 2 ) {if ( isConditionalSegments ) {object3d = new ConditionalLineSegments( bufferGeometry, materials.length === 1 ? materials[ 0 ] : materials );} else {object3d = new LineSegments( bufferGeometry, materials.length === 1 ? materials[ 0 ] : materials );}} else if ( elementSize === 3 ) {object3d = new Mesh( bufferGeometry, materials.length === 1 ? materials[ 0 ] : materials );}if ( isConditionalSegments ) {object3d.isConditionalLine = true;const controlArray0 = new Float32Array( elements.length * 3 * 2 );const controlArray1 = new Float32Array( elements.length * 3 * 2 );const directionArray = new Float32Array( elements.length * 3 * 2 );for ( let i = 0, l = elements.length; i < l; i ++ ) {const os = elements[ i ];const vertices = os.vertices;const controlPoints = os.controlPoints;const c0 = controlPoints[ 0 ];const c1 = controlPoints[ 1 ];const v0 = vertices[ 0 ];const v1 = vertices[ 1 ];const index = i * 3 * 2;controlArray0[ index + 0 ] = c0.x;controlArray0[ index + 1 ] = c0.y;controlArray0[ index + 2 ] = c0.z;controlArray0[ index + 3 ] = c0.x;controlArray0[ index + 4 ] = c0.y;controlArray0[ index + 5 ] = c0.z;controlArray1[ index + 0 ] = c1.x;controlArray1[ index + 1 ] = c1.y;controlArray1[ index + 2 ] = c1.z;controlArray1[ index + 3 ] = c1.x;controlArray1[ index + 4 ] = c1.y;controlArray1[ index + 5 ] = c1.z;directionArray[ index + 0 ] = v1.x - v0.x;directionArray[ index + 1 ] = v1.y - v0.y;directionArray[ index + 2 ] = v1.z - v0.z;directionArray[ index + 3 ] = v1.x - v0.x;directionArray[ index + 4 ] = v1.y - v0.y;directionArray[ index + 5 ] = v1.z - v0.z;}bufferGeometry.setAttribute( 'control0', new BufferAttribute( controlArray0, 3, false ) );bufferGeometry.setAttribute( 'control1', new BufferAttribute( controlArray1, 3, false ) );bufferGeometry.setAttribute( 'direction', new BufferAttribute( directionArray, 3, false ) );}return object3d;}//class LDrawLoader extends Loader {constructor( manager ) {super( manager );// Array of THREE.Materialthis.materials = [];this.materialLibrary = {};this.edgeMaterialCache = new WeakMap();this.conditionalEdgeMaterialCache = new WeakMap();// This also allows to handle the embedded text files ("0 FILE" lines)this.partsCache = new LDrawPartsGeometryCache( this );// 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.this.fileMap = {};// Initializes the materials library with default materialsthis.setMaterials( [] );// If this flag is set to true the vertex normals will be smoothed.this.smoothNormals = true;// The path to load parts from the LDraw parts library from.this.partsLibraryPath = '';// Material assigned to not available colors for meshes and edgesthis.missingColorMaterial = new MeshStandardMaterial( { name: Loader.DEFAULT_MATERIAL_NAME, color: 0xFF00FF, roughness: 0.3, metalness: 0 } );this.missingEdgeColorMaterial = new LineBasicMaterial( { name: Loader.DEFAULT_MATERIAL_NAME, color: 0xFF00FF } );this.missingConditionalEdgeColorMaterial = new LDrawConditionalLineMaterial( { name: Loader.DEFAULT_MATERIAL_NAME, fog: true, color: 0xFF00FF } );this.edgeMaterialCache.set( this.missingColorMaterial, this.missingEdgeColorMaterial );this.conditionalEdgeMaterialCache.set( this.missingEdgeColorMaterial, this.missingConditionalEdgeColorMaterial );}setPartsLibraryPath( path ) {this.partsLibraryPath = path;return this;}async preloadMaterials( url ) {const fileLoader = new FileLoader( this.manager );fileLoader.setPath( this.path );fileLoader.setRequestHeader( this.requestHeader );fileLoader.setWithCredentials( this.withCredentials );const text = await fileLoader.loadAsync( url );const colorLineRegex = /^0 !COLOUR/;const lines = text.split( /[\n\r]/g );const materials = [];for ( let i = 0, l = lines.length; i < l; i ++ ) {const line = lines[ i ];if ( colorLineRegex.test( line ) ) {const directive = line.replace( colorLineRegex, '' );const material = this.parseColorMetaDirective( new LineParser( directive ) );materials.push( material );}}this.setMaterials( materials );}load( url, onLoad, onProgress, onError ) {const fileLoader = new FileLoader( this.manager );fileLoader.setPath( this.path );fileLoader.setRequestHeader( this.requestHeader );fileLoader.setWithCredentials( this.withCredentials );fileLoader.load( url, text => {this.partsCache.parseModel( text, this.materialLibrary ).then( group => {this.applyMaterialsToMesh( group, MAIN_COLOUR_CODE, this.materialLibrary, true );this.computeBuildingSteps( group );group.userData.fileName = url;onLoad( group );} ).catch( onError );}, onProgress, onError );}parse( text, onLoad ) {this.partsCache.parseModel( text, this.materialLibrary ).then( group => {this.applyMaterialsToMesh( group, MAIN_COLOUR_CODE, this.materialLibrary, true );this.computeBuildingSteps( group );group.userData.fileName = '';onLoad( group );} );}setMaterials( materials ) {this.materialLibrary = {};this.materials = [];for ( let i = 0, l = materials.length; i < l; i ++ ) {this.addMaterial( materials[ i ] );}// Add default main triangle and line edge materials (used in pieces that can be colored with a main color)this.addMaterial( this.parseColorMetaDirective( new LineParser( 'Main_Colour CODE 16 VALUE #FF8080 EDGE #333333' ) ) );this.addMaterial( this.parseColorMetaDirective( new LineParser( 'Edge_Colour CODE 24 VALUE #A0A0A0 EDGE #333333' ) ) );return this;}setFileMap( fileMap ) {this.fileMap = fileMap;return this;}addMaterial( material ) {// Adds a material to the material library which is on top of the parse scopes stack. And also to the materials arrayconst matLib = this.materialLibrary;if ( ! matLib[ material.userData.code ] ) {this.materials.push( material );matLib[ material.userData.code ] = material;}return this;}getMaterial( colorCode ) {if ( colorCode.startsWith( '0x2' ) ) {// Special 'direct' material value (RGB color)const color = colorCode.substring( 3 );return this.parseColorMetaDirective( new LineParser( 'Direct_Color_' + color + ' CODE -1 VALUE #' + color + ' EDGE #' + color + '' ) );}return this.materialLibrary[ colorCode ] || null;}// Applies the appropriate materials to a prebuilt hierarchy of geometry. Assumes that color codes are present// in the material array if they need to be filled in.applyMaterialsToMesh( group, parentColorCode, materialHierarchy, finalMaterialPass = false ) {// find any missing materials as indicated by a color code string and replace it with a material from the current material libconst loader = this;const parentIsPassthrough = parentColorCode === MAIN_COLOUR_CODE;group.traverse( c => {if ( c.isMesh || c.isLineSegments ) {if ( Array.isArray( c.material ) ) {for ( let i = 0, l = c.material.length; i < l; i ++ ) {if ( ! c.material[ i ].isMaterial ) {c.material[ i ] = getMaterial( c, c.material[ i ] );}}} else if ( ! c.material.isMaterial ) {c.material = getMaterial( c, c.material );}}} );// Returns the appropriate material for the object (line or face) given color code. If the code is "pass through"// (24 for lines, 16 for edges) then the pass through color code is used. If that is also pass through then it's// simply returned for the subsequent material application.function getMaterial( c, colorCode ) {// if our parent is a passthrough color code and we don't have the current material color available then// return early.if ( parentIsPassthrough && ! ( colorCode in materialHierarchy ) && ! finalMaterialPass ) {return colorCode;}const forEdge = c.isLineSegments || c.isConditionalLine;const isPassthrough = ! forEdge && colorCode === MAIN_COLOUR_CODE || forEdge && colorCode === MAIN_EDGE_COLOUR_CODE;if ( isPassthrough ) {colorCode = parentColorCode;}let material = null;if ( colorCode in materialHierarchy ) {material = materialHierarchy[ colorCode ];} else if ( finalMaterialPass ) {// see if we can get the final material from from the "getMaterial" function which will attempt to// parse the "direct" colorsmaterial = loader.getMaterial( colorCode );if ( material === null ) {// otherwise throw a warning if this is final opportunity to set the materialconsole.warn( `LDrawLoader: Material properties for code ${ colorCode } not available.` );// And return the 'missing color' materialmaterial = loader.missingColorMaterial;}} else {return colorCode;}if ( c.isLineSegments ) {material = loader.edgeMaterialCache.get( material );if ( c.isConditionalLine ) {material = loader.conditionalEdgeMaterialCache.get( material );}}return material;}}getMainMaterial() {return this.getMaterial( MAIN_COLOUR_CODE );}getMainEdgeMaterial() {const mat = this.getMaterial( MAIN_EDGE_COLOUR_CODE );return mat ? this.edgeMaterialCache.get( mat ) : null;}parseColorMetaDirective( lineParser ) {// Parses a color definition and returns a THREE.Materiallet code = null;// Triangle and line colorslet fillColor = '#FF00FF';let edgeColor = '#FF00FF';// Transparencylet alpha = 1;let isTransparent = false;// Self-illumination:let luminance = 0;let finishType = FINISH_TYPE_DEFAULT;let edgeMaterial = null;const name = lineParser.getToken();if ( ! name ) {throw new Error( 'LDrawLoader: Material name was expected after "!COLOUR tag' + lineParser.getLineNumberString() + '.' );}// Parse tag tokens and their parameterslet token = null;while ( true ) {token = lineParser.getToken();if ( ! token ) {break;}if ( ! parseLuminance( token ) ) {switch ( token.toUpperCase() ) {case 'CODE':code = lineParser.getToken();break;case 'VALUE':fillColor = lineParser.getToken();if ( fillColor.startsWith( '0x' ) ) {fillColor = '#' + fillColor.substring( 2 );} else if ( ! fillColor.startsWith( '#' ) ) {throw new Error( 'LDrawLoader: Invalid color while parsing material' + lineParser.getLineNumberString() + '.' );}break;case 'EDGE':edgeColor = lineParser.getToken();if ( edgeColor.startsWith( '0x' ) ) {edgeColor = '#' + edgeColor.substring( 2 );} else if ( ! edgeColor.startsWith( '#' ) ) {// Try to see if edge color is a color codeedgeMaterial = this.getMaterial( edgeColor );if ( ! edgeMaterial ) {throw new Error( 'LDrawLoader: Invalid edge color while parsing material' + lineParser.getLineNumberString() + '.' );}// Get the edge material for this triangle materialedgeMaterial = this.edgeMaterialCache.get( edgeMaterial );}break;case 'ALPHA':alpha = parseInt( lineParser.getToken() );if ( isNaN( alpha ) ) {throw new Error( 'LDrawLoader: Invalid alpha value in material definition' + lineParser.getLineNumberString() + '.' );}alpha = Math.max( 0, Math.min( 1, alpha / 255 ) );if ( alpha < 1 ) {isTransparent = true;}break;case 'LUMINANCE':if ( ! parseLuminance( lineParser.getToken() ) ) {throw new Error( 'LDrawLoader: Invalid luminance value in material definition' + LineParser.getLineNumberString() + '.' );}break;case 'CHROME':finishType = FINISH_TYPE_CHROME;break;case 'PEARLESCENT':finishType = FINISH_TYPE_PEARLESCENT;break;case 'RUBBER':finishType = FINISH_TYPE_RUBBER;break;case 'MATTE_METALLIC':finishType = FINISH_TYPE_MATTE_METALLIC;break;case 'METAL':finishType = FINISH_TYPE_METAL;break;case 'MATERIAL':// Not implementedlineParser.setToEnd();break;default:throw new Error( 'LDrawLoader: Unknown token "' + token + '" while parsing material' + lineParser.getLineNumberString() + '.' );}}}let material = null;switch ( finishType ) {case FINISH_TYPE_DEFAULT:material = new MeshStandardMaterial( { roughness: 0.3, metalness: 0 } );break;case FINISH_TYPE_PEARLESCENT:// Try to imitate pearlescency by making the surface glossymaterial = new MeshStandardMaterial( { roughness: 0.3, metalness: 0.25 } );break;case FINISH_TYPE_CHROME:// Mirror finish surfacematerial = new MeshStandardMaterial( { roughness: 0, metalness: 1 } );break;case FINISH_TYPE_RUBBER:// Rubber finishmaterial = new MeshStandardMaterial( { roughness: 0.9, metalness: 0 } );break;case FINISH_TYPE_MATTE_METALLIC:// Brushed metal finishmaterial = new MeshStandardMaterial( { roughness: 0.8, metalness: 0.4 } );break;case FINISH_TYPE_METAL:// Average metal finishmaterial = new MeshStandardMaterial( { roughness: 0.2, metalness: 0.85 } );break;default:// Should not happenbreak;}material.color.setStyle( fillColor, COLOR_SPACE_LDRAW );material.transparent = isTransparent;material.premultipliedAlpha = true;material.opacity = alpha;material.depthWrite = ! isTransparent;material.polygonOffset = true;material.polygonOffsetFactor = 1;if ( luminance !== 0 ) {material.emissive.setStyle( fillColor, COLOR_SPACE_LDRAW ).multiplyScalar( luminance );}if ( ! edgeMaterial ) {// This is the material used for edgesedgeMaterial = new LineBasicMaterial( {color: new Color().setStyle( edgeColor, COLOR_SPACE_LDRAW ),transparent: isTransparent,opacity: alpha,depthWrite: ! isTransparent} );edgeMaterial.color;edgeMaterial.userData.code = code;edgeMaterial.name = name + ' - Edge';// This is the material used for conditional edgesconst conditionalEdgeMaterial = new LDrawConditionalLineMaterial( {fog: true,transparent: isTransparent,depthWrite: ! isTransparent,color: new Color().setStyle( edgeColor, COLOR_SPACE_LDRAW ),opacity: alpha,} );conditionalEdgeMaterial.userData.code = code;conditionalEdgeMaterial.name = name + ' - Conditional Edge';this.conditionalEdgeMaterialCache.set( edgeMaterial, conditionalEdgeMaterial );}material.userData.code = code;material.name = name;this.edgeMaterialCache.set( material, edgeMaterial );this.addMaterial( material );return material;function parseLuminance( token ) {// Returns successlet lum;if ( token.startsWith( 'LUMINANCE' ) ) {lum = parseInt( token.substring( 9 ) );} else {lum = parseInt( token );}if ( isNaN( lum ) ) {return false;}luminance = Math.max( 0, Math.min( 1, lum / 255 ) );return true;}}computeBuildingSteps( model ) {// Sets userdata.buildingStep number in Group objects and userData.numBuildingSteps number in the root Group object.let stepNumber = 0;model.traverse( c => {if ( c.isGroup ) {if ( c.userData.startingBuildingStep ) {stepNumber ++;}c.userData.buildingStep = stepNumber;}} );model.userData.numBuildingSteps = stepNumber + 1;}}//export { LDrawLoader };class Reflector extends Mesh {constructor( geometry, options = {} ) {super( geometry );this.isReflector = true;this.type = 'Reflector';this.camera = new PerspectiveCamera();const scope = this;const color = ( options.color !== undefined ) ? new Color( options.color ) : new Color( 0x7F7F7F );const textureWidth = options.textureWidth || 512;const textureHeight = options.textureHeight || 512;const clipBias = options.clipBias || 0;const shader = options.shader || Reflector.ReflectorShader;const multisample = ( options.multisample !== undefined ) ? options.multisample : 4;//const reflectorPlane = new Plane();const normal = new Vector3();const reflectorWorldPosition = new Vector3();const cameraWorldPosition = new Vector3();const rotationMatrix = new Matrix4();const lookAtPosition = new Vector3( 0, 0, - 1 );const clipPlane = new Vector4();const view = new Vector3();const target = new Vector3();const q = new Vector4();const textureMatrix = new Matrix4();const virtualCamera = this.camera;const renderTarget = new WebGLRenderTarget( textureWidth, textureHeight, { samples: multisample, type: HalfFloatType } );const material = new ShaderMaterial( {name: ( shader.name !== undefined ) ? shader.name : 'unspecified',uniforms: UniformsUtils.clone( shader.uniforms ),fragmentShader: shader.fragmentShader,vertexShader: shader.vertexShader} );material.uniforms[ 'tDiffuse' ].value = renderTarget.texture;material.uniforms[ 'color' ].value = color;material.uniforms[ 'textureMatrix' ].value = textureMatrix;this.material = material;this.onBeforeRender = function ( renderer, scene, camera ) {reflectorWorldPosition.setFromMatrixPosition( scope.matrixWorld );cameraWorldPosition.setFromMatrixPosition( camera.matrixWorld );rotationMatrix.extractRotation( scope.matrixWorld );normal.set( 0, 0, 1 );normal.applyMatrix4( rotationMatrix );view.subVectors( reflectorWorldPosition, cameraWorldPosition );// Avoid rendering when reflector is facing awayif ( view.dot( normal ) > 0 ) return;view.reflect( normal ).negate();view.add( reflectorWorldPosition );rotationMatrix.extractRotation( camera.matrixWorld );lookAtPosition.set( 0, 0, - 1 );lookAtPosition.applyMatrix4( rotationMatrix );lookAtPosition.add( cameraWorldPosition );target.subVectors( reflectorWorldPosition, lookAtPosition );target.reflect( normal ).negate();target.add( reflectorWorldPosition );virtualCamera.position.copy( view );virtualCamera.up.set( 0, 1, 0 );virtualCamera.up.applyMatrix4( rotationMatrix );virtualCamera.up.reflect( normal );virtualCamera.lookAt( target );virtualCamera.far = camera.far; // Used in WebGLBackgroundvirtualCamera.updateMatrixWorld();virtualCamera.projectionMatrix.copy( camera.projectionMatrix );// Update the texture matrixtextureMatrix.set(0.5, 0.0, 0.0, 0.5,0.0, 0.5, 0.0, 0.5,0.0, 0.0, 0.5, 0.5,0.0, 0.0, 0.0, 1.0);textureMatrix.multiply( virtualCamera.projectionMatrix );textureMatrix.multiply( virtualCamera.matrixWorldInverse );textureMatrix.multiply( scope.matrixWorld );// Now update projection matrix with new clip plane, implementing code from: http://www.terathon.com/code/oblique.html// Paper explaining this technique: http://www.terathon.com/lengyel/Lengyel-Oblique.pdfreflectorPlane.setFromNormalAndCoplanarPoint( normal, reflectorWorldPosition );reflectorPlane.applyMatrix4( virtualCamera.matrixWorldInverse );clipPlane.set( reflectorPlane.normal.x, reflectorPlane.normal.y, reflectorPlane.normal.z, reflectorPlane.constant );const projectionMatrix = virtualCamera.projectionMatrix;q.x = ( Math.sign( clipPlane.x ) + projectionMatrix.elements[ 8 ] ) / projectionMatrix.elements[ 0 ];q.y = ( Math.sign( clipPlane.y ) + projectionMatrix.elements[ 9 ] ) / projectionMatrix.elements[ 5 ];q.z = - 1.0;q.w = ( 1.0 + projectionMatrix.elements[ 10 ] ) / projectionMatrix.elements[ 14 ];// Calculate the scaled plane vectorclipPlane.multiplyScalar( 2.0 / clipPlane.dot( q ) );// Replacing the third row of the projection matrixprojectionMatrix.elements[ 2 ] = clipPlane.x;projectionMatrix.elements[ 6 ] = clipPlane.y;projectionMatrix.elements[ 10 ] = clipPlane.z + 1.0 - clipBias;projectionMatrix.elements[ 14 ] = clipPlane.w;// Renderscope.visible = false;const currentRenderTarget = renderer.getRenderTarget();const currentXrEnabled = renderer.xr.enabled;const currentShadowAutoUpdate = renderer.shadowMap.autoUpdate;renderer.xr.enabled = false; // Avoid camera modificationrenderer.shadowMap.autoUpdate = false; // Avoid re-computing shadowsrenderer.setRenderTarget( renderTarget );renderer.state.buffers.depth.setMask( true ); // make sure the depth buffer is writable so it can be properly cleared, see #18897if ( renderer.autoClear === false ) renderer.clear();renderer.render( scene, virtualCamera );renderer.xr.enabled = currentXrEnabled;renderer.shadowMap.autoUpdate = currentShadowAutoUpdate;renderer.setRenderTarget( currentRenderTarget );// Restore viewportconst viewport = camera.viewport;if ( viewport !== undefined ) {renderer.state.viewport( viewport );}scope.visible = true;};this.getRenderTarget = function () {return renderTarget;};this.dispose = function () {renderTarget.dispose();scope.material.dispose();};}}Reflector.ReflectorShader = {name: 'ReflectorShader',uniforms: {'color': {value: null},'tDiffuse': {value: null},'textureMatrix': {value: null}},vertexShader: /* glsl */`uniform mat4 textureMatrix;varying vec4 vUv;#include <common>#include <logdepthbuf_pars_vertex>void main() {vUv = textureMatrix * vec4( position, 1.0 );gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );#include <logdepthbuf_vertex>}`,fragmentShader: /* glsl */`uniform vec3 color;uniform sampler2D tDiffuse;varying vec4 vUv;#include <logdepthbuf_pars_fragment>float blendOverlay( float base, float blend ) {return( base < 0.5 ? ( 2.0 * base * blend ) : ( 1.0 - 2.0 * ( 1.0 - base ) * ( 1.0 - blend ) ) );}vec3 blendOverlay( vec3 base, vec3 blend ) {return vec3( blendOverlay( base.r, blend.r ), blendOverlay( base.g, blend.g ), blendOverlay( base.b, blend.b ) );}void main() {#include <logdepthbuf_fragment>vec4 base = texture2DProj( tDiffuse, vUv );gl_FragColor = vec4( blendOverlay( base.rgb, color ), 1.0 );#include <tonemapping_fragment>#include <colorspace_fragment>}`};//export { Reflector };function computeMikkTSpaceTangents( geometry, MikkTSpace, negateSign = true ) {if ( ! MikkTSpace || ! MikkTSpace.isReady ) {throw new Error( 'BufferGeometryUtils: Initialized MikkTSpace library required.' );}if ( ! geometry.hasAttribute( 'position' ) || ! geometry.hasAttribute( 'normal' ) || ! geometry.hasAttribute( 'uv' ) ) {throw new Error( 'BufferGeometryUtils: Tangents require "position", "normal", and "uv" attributes.' );}function getAttributeArray( attribute ) {if ( attribute.normalized || attribute.isInterleavedBufferAttribute ) {const dstArray = new Float32Array( attribute.count * attribute.itemSize );for ( let i = 0, j = 0; i < attribute.count; i ++ ) {dstArray[ j ++ ] = attribute.getX( i );dstArray[ j ++ ] = attribute.getY( i );if ( attribute.itemSize > 2 ) {dstArray[ j ++ ] = attribute.getZ( i );}}return dstArray;}if ( attribute.array instanceof Float32Array ) {return attribute.array;}return new Float32Array( attribute.array );}// MikkTSpace algorithm requires non-indexed input.const _geometry = geometry.index ? geometry.toNonIndexed() : geometry;// Compute vertex tangents.const tangents = MikkTSpace.generateTangents(getAttributeArray( _geometry.attributes.position ),getAttributeArray( _geometry.attributes.normal ),getAttributeArray( _geometry.attributes.uv ));// Texture coordinate convention of glTF differs from the apparent// default of the MikkTSpace library; .w component must be flipped.if ( negateSign ) {for ( let i = 3; i < tangents.length; i += 4 ) {tangents[ i ] *= - 1;}}//_geometry.setAttribute( 'tangent', new BufferAttribute( tangents, 4 ) );if ( geometry !== _geometry ) {geometry.copy( _geometry );}return geometry;}/*** @param  {Array<BufferGeometry>} geometries* @param  {Boolean} useGroups* @return {BufferGeometry}*/function mergeGeometries( geometries, useGroups = false ) {const isIndexed = geometries[ 0 ].index !== null;const attributesUsed = new Set( Object.keys( geometries[ 0 ].attributes ) );const morphAttributesUsed = new Set( Object.keys( geometries[ 0 ].morphAttributes ) );const attributes = {};const morphAttributes = {};const morphTargetsRelative = geometries[ 0 ].morphTargetsRelative;const mergedGeometry = new BufferGeometry();let offset = 0;for ( let i = 0; i < geometries.length; ++ i ) {const geometry = geometries[ i ];let attributesCount = 0;// ensure that all geometries are indexed, or noneif ( isIndexed !== ( geometry.index !== null ) ) {console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. All geometries must have compatible attributes; make sure index attribute exists among all geometries, or in none of them.' );return null;}// gather attributes, exit early if they're differentfor ( const name in geometry.attributes ) {if ( ! attributesUsed.has( name ) ) {console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. All geometries must have compatible attributes; make sure "' + name + '" attribute exists among all geometries, or in none of them.' );return null;}if ( attributes[ name ] === undefined ) attributes[ name ] = [];attributes[ name ].push( geometry.attributes[ name ] );attributesCount ++;}// ensure geometries have the same number of attributesif ( attributesCount !== attributesUsed.size ) {console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. Make sure all geometries have the same number of attributes.' );return null;}// gather morph attributes, exit early if they're differentif ( morphTargetsRelative !== geometry.morphTargetsRelative ) {console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. .morphTargetsRelative must be consistent throughout all geometries.' );return null;}for ( const name in geometry.morphAttributes ) {if ( ! morphAttributesUsed.has( name ) ) {console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '.  .morphAttributes must be consistent throughout all geometries.' );return null;}if ( morphAttributes[ name ] === undefined ) morphAttributes[ name ] = [];morphAttributes[ name ].push( geometry.morphAttributes[ name ] );}if ( useGroups ) {let count;if ( isIndexed ) {count = geometry.index.count;} else if ( geometry.attributes.position !== undefined ) {count = geometry.attributes.position.count;} else {console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. The geometry must have either an index or a position attribute' );return null;}mergedGeometry.addGroup( offset, count, i );offset += count;}}// merge indicesif ( isIndexed ) {let indexOffset = 0;const mergedIndex = [];for ( let i = 0; i < geometries.length; ++ i ) {const index = geometries[ i ].index;for ( let j = 0; j < index.count; ++ j ) {mergedIndex.push( index.getX( j ) + indexOffset );}indexOffset += geometries[ i ].attributes.position.count;}mergedGeometry.setIndex( mergedIndex );}// merge attributesfor ( const name in attributes ) {const mergedAttribute = mergeAttributes( attributes[ name ] );if ( ! mergedAttribute ) {console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed while trying to merge the ' + name + ' attribute.' );return null;}mergedGeometry.setAttribute( name, mergedAttribute );}// merge morph attributesfor ( const name in morphAttributes ) {const numMorphTargets = morphAttributes[ name ][ 0 ].length;if ( numMorphTargets === 0 ) break;mergedGeometry.morphAttributes = mergedGeometry.morphAttributes || {};mergedGeometry.morphAttributes[ name ] = [];for ( let i = 0; i < numMorphTargets; ++ i ) {const morphAttributesToMerge = [];for ( let j = 0; j < morphAttributes[ name ].length; ++ j ) {morphAttributesToMerge.push( morphAttributes[ name ][ j ][ i ] );}const mergedMorphAttribute = mergeAttributes( morphAttributesToMerge );if ( ! mergedMorphAttribute ) {console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed while trying to merge the ' + name + ' morphAttribute.' );return null;}mergedGeometry.morphAttributes[ name ].push( mergedMorphAttribute );}}return mergedGeometry;}/*** @param {Array<BufferAttribute>} attributes* @return {BufferAttribute}*/function mergeAttributes( attributes ) {let TypedArray;let itemSize;let normalized;let gpuType = - 1;let arrayLength = 0;for ( let i = 0; i < attributes.length; ++ i ) {const attribute = attributes[ i ];if ( TypedArray === undefined ) TypedArray = attribute.array.constructor;if ( TypedArray !== attribute.array.constructor ) {console.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.array must be of consistent array types across matching attributes.' );return null;}if ( itemSize === undefined ) itemSize = attribute.itemSize;if ( itemSize !== attribute.itemSize ) {console.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.itemSize must be consistent across matching attributes.' );return null;}if ( normalized === undefined ) normalized = attribute.normalized;if ( normalized !== attribute.normalized ) {console.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.normalized must be consistent across matching attributes.' );return null;}if ( gpuType === - 1 ) gpuType = attribute.gpuType;if ( gpuType !== attribute.gpuType ) {console.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.gpuType must be consistent across matching attributes.' );return null;}arrayLength += attribute.count * itemSize;}const array = new TypedArray( arrayLength );const result = new BufferAttribute( array, itemSize, normalized );let offset = 0;for ( let i = 0; i < attributes.length; ++ i ) {const attribute = attributes[ i ];if ( attribute.isInterleavedBufferAttribute ) {const tupleOffset = offset / itemSize;for ( let j = 0, l = attribute.count; j < l; j ++ ) {for ( let c = 0; c < itemSize; c ++ ) {const value = attribute.getComponent( j, c );result.setComponent( j + tupleOffset, c, value );}}} else {array.set( attribute.array, offset );}offset += attribute.count * itemSize;}if ( gpuType !== undefined ) {result.gpuType = gpuType;}return result;}/*** @param {BufferAttribute}* @return {BufferAttribute}*/export function deepCloneAttribute( attribute ) {if ( attribute.isInstancedInterleavedBufferAttribute || attribute.isInterleavedBufferAttribute ) {return deinterleaveAttribute( attribute );}if ( attribute.isInstancedBufferAttribute ) {return new InstancedBufferAttribute().copy( attribute );}return new BufferAttribute().copy( attribute );}/*** @param {Array<BufferAttribute>} attributes* @return {Array<InterleavedBufferAttribute>}*/function interleaveAttributes( attributes ) {// Interleaves the provided attributes into an InterleavedBuffer and returns// a set of InterleavedBufferAttributes for each attributelet TypedArray;let arrayLength = 0;let stride = 0;// calculate the length and type of the interleavedBufferfor ( let i = 0, l = attributes.length; i < l; ++ i ) {const attribute = attributes[ i ];if ( TypedArray === undefined ) TypedArray = attribute.array.constructor;if ( TypedArray !== attribute.array.constructor ) {console.error( 'AttributeBuffers of different types cannot be interleaved' );return null;}arrayLength += attribute.array.length;stride += attribute.itemSize;}// Create the set of buffer attributesconst interleavedBuffer = new InterleavedBuffer( new TypedArray( arrayLength ), stride );let offset = 0;const res = [];const getters = [ 'getX', 'getY', 'getZ', 'getW' ];const setters = [ 'setX', 'setY', 'setZ', 'setW' ];for ( let j = 0, l = attributes.length; j < l; j ++ ) {const attribute = attributes[ j ];const itemSize = attribute.itemSize;const count = attribute.count;const iba = new InterleavedBufferAttribute( interleavedBuffer, itemSize, offset, attribute.normalized );res.push( iba );offset += itemSize;// Move the data for each attribute into the new interleavedBuffer// at the appropriate offsetfor ( let c = 0; c < count; c ++ ) {for ( let k = 0; k < itemSize; k ++ ) {iba[ setters[ k ] ]( c, attribute[ getters[ k ] ]( c ) );}}}return res;}// returns a new, non-interleaved version of the provided attributeexport function deinterleaveAttribute( attribute ) {const cons = attribute.data.array.constructor;const count = attribute.count;const itemSize = attribute.itemSize;const normalized = attribute.normalized;const array = new cons( count * itemSize );let newAttribute;if ( attribute.isInstancedInterleavedBufferAttribute ) {newAttribute = new InstancedBufferAttribute( array, itemSize, normalized, attribute.meshPerAttribute );} else {newAttribute = new BufferAttribute( array, itemSize, normalized );}for ( let i = 0; i < count; i ++ ) {newAttribute.setX( i, attribute.getX( i ) );if ( itemSize >= 2 ) {newAttribute.setY( i, attribute.getY( i ) );}if ( itemSize >= 3 ) {newAttribute.setZ( i, attribute.getZ( i ) );}if ( itemSize >= 4 ) {newAttribute.setW( i, attribute.getW( i ) );}}return newAttribute;}// deinterleaves all attributes on the geometryexport function deinterleaveGeometry( geometry ) {const attributes = geometry.attributes;const morphTargets = geometry.morphTargets;const attrMap = new Map();for ( const key in attributes ) {const attr = attributes[ key ];if ( attr.isInterleavedBufferAttribute ) {if ( ! attrMap.has( attr ) ) {attrMap.set( attr, deinterleaveAttribute( attr ) );}attributes[ key ] = attrMap.get( attr );}}for ( const key in morphTargets ) {const attr = morphTargets[ key ];if ( attr.isInterleavedBufferAttribute ) {if ( ! attrMap.has( attr ) ) {attrMap.set( attr, deinterleaveAttribute( attr ) );}morphTargets[ key ] = attrMap.get( attr );}}}/*** @param {BufferGeometry} geometry* @return {number}*/function estimateBytesUsed( geometry ) {// Return the estimated memory used by this geometry in bytes// Calculate using itemSize, count, and BYTES_PER_ELEMENT to account// for InterleavedBufferAttributes.let mem = 0;for ( const name in geometry.attributes ) {const attr = geometry.getAttribute( name );mem += attr.count * attr.itemSize * attr.array.BYTES_PER_ELEMENT;}const indices = geometry.getIndex();mem += indices ? indices.count * indices.itemSize * indices.array.BYTES_PER_ELEMENT : 0;return mem;}/*** @param {BufferGeometry} geometry* @param {number} tolerance* @return {BufferGeometry}*/function mergeVertices( geometry, tolerance = 1e-4 ) {tolerance = Math.max( tolerance, Number.EPSILON );// Generate an index buffer if the geometry doesn't have one, or optimize it// if it's already available.const hashToIndex = {};const indices = geometry.getIndex();const positions = geometry.getAttribute( 'position' );const vertexCount = indices ? indices.count : positions.count;// next value for triangle indiceslet nextIndex = 0;// attributes and new attribute arraysconst attributeNames = Object.keys( geometry.attributes );const tmpAttributes = {};const tmpMorphAttributes = {};const newIndices = [];const getters = [ 'getX', 'getY', 'getZ', 'getW' ];const setters = [ 'setX', 'setY', 'setZ', 'setW' ];// Initialize the arrays, allocating space conservatively. Extra// space will be trimmed in the last step.for ( let i = 0, l = attributeNames.length; i < l; i ++ ) {const name = attributeNames[ i ];const attr = geometry.attributes[ name ];tmpAttributes[ name ] = new BufferAttribute(new attr.array.constructor( attr.count * attr.itemSize ),attr.itemSize,attr.normalized);const morphAttr = geometry.morphAttributes[ name ];if ( morphAttr ) {tmpMorphAttributes[ name ] = new BufferAttribute(new morphAttr.array.constructor( morphAttr.count * morphAttr.itemSize ),morphAttr.itemSize,morphAttr.normalized);}}// convert the error tolerance to an amount of decimal places to truncate toconst halfTolerance = tolerance * 0.5;const exponent = Math.log10( 1 / tolerance );const hashMultiplier = Math.pow( 10, exponent );const hashAdditive = halfTolerance * hashMultiplier;for ( let i = 0; i < vertexCount; i ++ ) {const index = indices ? indices.getX( i ) : i;// Generate a hash for the vertex attributes at the current index 'i'let hash = '';for ( let j = 0, l = attributeNames.length; j < l; j ++ ) {const name = attributeNames[ j ];const attribute = geometry.getAttribute( name );const itemSize = attribute.itemSize;for ( let k = 0; k < itemSize; k ++ ) {// double tilde truncates the decimal valuehash += `${ ~ ~ ( attribute[ getters[ k ] ]( index ) * hashMultiplier + hashAdditive ) },`;}}// Add another reference to the vertex if it's already// used by another indexif ( hash in hashToIndex ) {newIndices.push( hashToIndex[ hash ] );} else {// copy data to the new index in the temporary attributesfor ( let j = 0, l = attributeNames.length; j < l; j ++ ) {const name = attributeNames[ j ];const attribute = geometry.getAttribute( name );const morphAttr = geometry.morphAttributes[ name ];const itemSize = attribute.itemSize;const newarray = tmpAttributes[ name ];const newMorphArrays = tmpMorphAttributes[ name ];for ( let k = 0; k < itemSize; k ++ ) {const getterFunc = getters[ k ];const setterFunc = setters[ k ];newarray[ setterFunc ]( nextIndex, attribute[ getterFunc ]( index ) );if ( morphAttr ) {for ( let m = 0, ml = morphAttr.length; m < ml; m ++ ) {newMorphArrays[ m ][ setterFunc ]( nextIndex, morphAttr[ m ][ getterFunc ]( index ) );}}}}hashToIndex[ hash ] = nextIndex;newIndices.push( nextIndex );nextIndex ++;}}// generate result BufferGeometryconst result = geometry.clone();for ( const name in geometry.attributes ) {const tmpAttribute = tmpAttributes[ name ];result.setAttribute( name, new BufferAttribute(tmpAttribute.array.slice( 0, nextIndex * tmpAttribute.itemSize ),tmpAttribute.itemSize,tmpAttribute.normalized,) );if ( ! ( name in tmpMorphAttributes ) ) continue;for ( let j = 0; j < tmpMorphAttributes[ name ].length; j ++ ) {const tmpMorphAttribute = tmpMorphAttributes[ name ][ j ];result.morphAttributes[ name ][ j ] = new BufferAttribute(tmpMorphAttribute.array.slice( 0, nextIndex * tmpMorphAttribute.itemSize ),tmpMorphAttribute.itemSize,tmpMorphAttribute.normalized,);}}// indicesresult.setIndex( newIndices );return result;}/*** @param {BufferGeometry} geometry* @param {number} drawMode* @return {BufferGeometry}*/function toTrianglesDrawMode( geometry, drawMode ) {if ( drawMode === TrianglesDrawMode ) {console.warn( 'THREE.BufferGeometryUtils.toTrianglesDrawMode(): Geometry already defined as triangles.' );return geometry;}if ( drawMode === TriangleFanDrawMode || drawMode === TriangleStripDrawMode ) {let index = geometry.getIndex();// generate index if not presentif ( index === null ) {const indices = [];const position = geometry.getAttribute( 'position' );if ( position !== undefined ) {for ( let i = 0; i < position.count; i ++ ) {indices.push( i );}geometry.setIndex( indices );index = geometry.getIndex();} else {console.error( 'THREE.BufferGeometryUtils.toTrianglesDrawMode(): Undefined position attribute. Processing not possible.' );return geometry;}}//const numberOfTriangles = index.count - 2;const newIndices = [];if ( drawMode === TriangleFanDrawMode ) {// gl.TRIANGLE_FANfor ( let i = 1; i <= numberOfTriangles; i ++ ) {newIndices.push( index.getX( 0 ) );newIndices.push( index.getX( i ) );newIndices.push( index.getX( i + 1 ) );}} else {// gl.TRIANGLE_STRIPfor ( let i = 0; i < numberOfTriangles; i ++ ) {if ( i % 2 === 0 ) {newIndices.push( index.getX( i ) );newIndices.push( index.getX( i + 1 ) );newIndices.push( index.getX( i + 2 ) );} else {newIndices.push( index.getX( i + 2 ) );newIndices.push( index.getX( i + 1 ) );newIndices.push( index.getX( i ) );}}}if ( ( newIndices.length / 3 ) !== numberOfTriangles ) {console.error( 'THREE.BufferGeometryUtils.toTrianglesDrawMode(): Unable to generate correct amount of triangles.' );}// build final geometryconst newGeometry = geometry.clone();newGeometry.setIndex( newIndices );newGeometry.clearGroups();return newGeometry;} else {console.error( 'THREE.BufferGeometryUtils.toTrianglesDrawMode(): Unknown draw mode:', drawMode );return geometry;}}/*** Calculates the morphed attributes of a morphed/skinned BufferGeometry.* Helpful for Raytracing or Decals.* @param {Mesh | Line | Points} object An instance of Mesh, Line or Points.* @return {Object} An Object with original position/normal attributes and morphed ones.*/function computeMorphedAttributes( object ) {const _vA = new Vector3();const _vB = new Vector3();const _vC = new Vector3();const _tempA = new Vector3();const _tempB = new Vector3();const _tempC = new Vector3();const _morphA = new Vector3();const _morphB = new Vector3();const _morphC = new Vector3();function _calculateMorphedAttributeData(object,attribute,morphAttribute,morphTargetsRelative,a,b,c,modifiedAttributeArray) {_vA.fromBufferAttribute( attribute, a );_vB.fromBufferAttribute( attribute, b );_vC.fromBufferAttribute( attribute, c );const morphInfluences = object.morphTargetInfluences;if ( morphAttribute && morphInfluences ) {_morphA.set( 0, 0, 0 );_morphB.set( 0, 0, 0 );_morphC.set( 0, 0, 0 );for ( let i = 0, il = morphAttribute.length; i < il; i ++ ) {const influence = morphInfluences[ i ];const morph = morphAttribute[ i ];if ( influence === 0 ) continue;_tempA.fromBufferAttribute( morph, a );_tempB.fromBufferAttribute( morph, b );_tempC.fromBufferAttribute( morph, c );if ( morphTargetsRelative ) {_morphA.addScaledVector( _tempA, influence );_morphB.addScaledVector( _tempB, influence );_morphC.addScaledVector( _tempC, influence );} else {_morphA.addScaledVector( _tempA.sub( _vA ), influence );_morphB.addScaledVector( _tempB.sub( _vB ), influence );_morphC.addScaledVector( _tempC.sub( _vC ), influence );}}_vA.add( _morphA );_vB.add( _morphB );_vC.add( _morphC );}if ( object.isSkinnedMesh ) {object.applyBoneTransform( a, _vA );object.applyBoneTransform( b, _vB );object.applyBoneTransform( c, _vC );}modifiedAttributeArray[ a * 3 + 0 ] = _vA.x;modifiedAttributeArray[ a * 3 + 1 ] = _vA.y;modifiedAttributeArray[ a * 3 + 2 ] = _vA.z;modifiedAttributeArray[ b * 3 + 0 ] = _vB.x;modifiedAttributeArray[ b * 3 + 1 ] = _vB.y;modifiedAttributeArray[ b * 3 + 2 ] = _vB.z;modifiedAttributeArray[ c * 3 + 0 ] = _vC.x;modifiedAttributeArray[ c * 3 + 1 ] = _vC.y;modifiedAttributeArray[ c * 3 + 2 ] = _vC.z;}const geometry = object.geometry;const material = object.material;let a, b, c;const index = geometry.index;const positionAttribute = geometry.attributes.position;const morphPosition = geometry.morphAttributes.position;const morphTargetsRelative = geometry.morphTargetsRelative;const normalAttribute = geometry.attributes.normal;const morphNormal = geometry.morphAttributes.position;const groups = geometry.groups;const drawRange = geometry.drawRange;let i, j, il, jl;let group;let start, end;const modifiedPosition = new Float32Array( positionAttribute.count * positionAttribute.itemSize );const modifiedNormal = new Float32Array( normalAttribute.count * normalAttribute.itemSize );if ( index !== null ) {// indexed buffer geometryif ( Array.isArray( material ) ) {for ( i = 0, il = groups.length; i < il; i ++ ) {group = groups[ i ];start = Math.max( group.start, drawRange.start );end = Math.min( ( group.start + group.count ), ( drawRange.start + drawRange.count ) );for ( j = start, jl = end; j < jl; j += 3 ) {a = index.getX( j );b = index.getX( j + 1 );c = index.getX( j + 2 );_calculateMorphedAttributeData(object,positionAttribute,morphPosition,morphTargetsRelative,a, b, c,modifiedPosition);_calculateMorphedAttributeData(object,normalAttribute,morphNormal,morphTargetsRelative,a, b, c,modifiedNormal);}}} else {start = Math.max( 0, drawRange.start );end = Math.min( index.count, ( drawRange.start + drawRange.count ) );for ( i = start, il = end; i < il; i += 3 ) {a = index.getX( i );b = index.getX( i + 1 );c = index.getX( i + 2 );_calculateMorphedAttributeData(object,positionAttribute,morphPosition,morphTargetsRelative,a, b, c,modifiedPosition);_calculateMorphedAttributeData(object,normalAttribute,morphNormal,morphTargetsRelative,a, b, c,modifiedNormal);}}} else {// non-indexed buffer geometryif ( Array.isArray( material ) ) {for ( i = 0, il = groups.length; i < il; i ++ ) {group = groups[ i ];start = Math.max( group.start, drawRange.start );end = Math.min( ( group.start + group.count ), ( drawRange.start + drawRange.count ) );for ( j = start, jl = end; j < jl; j += 3 ) {a = j;b = j + 1;c = j + 2;_calculateMorphedAttributeData(object,positionAttribute,morphPosition,morphTargetsRelative,a, b, c,modifiedPosition);_calculateMorphedAttributeData(object,normalAttribute,morphNormal,morphTargetsRelative,a, b, c,modifiedNormal);}}} else {start = Math.max( 0, drawRange.start );end = Math.min( positionAttribute.count, ( drawRange.start + drawRange.count ) );for ( i = start, il = end; i < il; i += 3 ) {a = i;b = i + 1;c = i + 2;_calculateMorphedAttributeData(object,positionAttribute,morphPosition,morphTargetsRelative,a, b, c,modifiedPosition);_calculateMorphedAttributeData(object,normalAttribute,morphNormal,morphTargetsRelative,a, b, c,modifiedNormal);}}}const morphedPositionAttribute = new Float32BufferAttribute( modifiedPosition, 3 );const morphedNormalAttribute = new Float32BufferAttribute( modifiedNormal, 3 );return {positionAttribute: positionAttribute,normalAttribute: normalAttribute,morphedPositionAttribute: morphedPositionAttribute,morphedNormalAttribute: morphedNormalAttribute};}function mergeGroups( geometry ) {if ( geometry.groups.length === 0 ) {console.warn( 'THREE.BufferGeometryUtils.mergeGroups(): No groups are defined. Nothing to merge.' );return geometry;}let groups = geometry.groups;// sort groups by material indexgroups = groups.sort( ( a, b ) => {if ( a.materialIndex !== b.materialIndex ) return a.materialIndex - b.materialIndex;return a.start - b.start;} );// create index for non-indexed geometriesif ( geometry.getIndex() === null ) {const positionAttribute = geometry.getAttribute( 'position' );const indices = [];for ( let i = 0; i < positionAttribute.count; i += 3 ) {indices.push( i, i + 1, i + 2 );}geometry.setIndex( indices );}// sort indexconst index = geometry.getIndex();const newIndices = [];for ( let i = 0; i < groups.length; i ++ ) {const group = groups[ i ];const groupStart = group.start;const groupLength = groupStart + group.count;for ( let j = groupStart; j < groupLength; j ++ ) {newIndices.push( index.getX( j ) );}}geometry.dispose(); // Required to force buffer recreationgeometry.setIndex( newIndices );// update groups indiceslet start = 0;for ( let i = 0; i < groups.length; i ++ ) {const group = groups[ i ];group.start = start;start += group.count;}// merge groupslet currentGroup = groups[ 0 ];geometry.groups = [ currentGroup ];for ( let i = 1; i < groups.length; i ++ ) {const group = groups[ i ];if ( currentGroup.materialIndex === group.materialIndex ) {currentGroup.count += group.count;} else {currentGroup = group;geometry.groups.push( currentGroup );}}return geometry;}/*** Modifies the supplied geometry if it is non-indexed, otherwise creates a new,* non-indexed geometry. Returns the geometry with smooth normals everywhere except* faces that meet at an angle greater than the crease angle.** @param {BufferGeometry} geometry* @param {number} [creaseAngle]* @return {BufferGeometry}*/function toCreasedNormals( geometry, creaseAngle = Math.PI / 3 /* 60 degrees */ ) {const creaseDot = Math.cos( creaseAngle );const hashMultiplier = ( 1 + 1e-10 ) * 1e2;// reusable vectorsconst verts = [ new Vector3(), new Vector3(), new Vector3() ];const tempVec1 = new Vector3();const tempVec2 = new Vector3();const tempNorm = new Vector3();const tempNorm2 = new Vector3();// hashes a vectorfunction hashVertex( v ) {const x = ~ ~ ( v.x * hashMultiplier );const y = ~ ~ ( v.y * hashMultiplier );const z = ~ ~ ( v.z * hashMultiplier );return `${x},${y},${z}`;}// BufferGeometry.toNonIndexed() warns if the geometry is non-indexed// and returns the original geometryconst resultGeometry = geometry.index ? geometry.toNonIndexed() : geometry;const posAttr = resultGeometry.attributes.position;const vertexMap = {};// find all the normals shared by commonly located verticesfor ( let i = 0, l = posAttr.count / 3; i < l; i ++ ) {const i3 = 3 * i;const a = verts[ 0 ].fromBufferAttribute( posAttr, i3 + 0 );const b = verts[ 1 ].fromBufferAttribute( posAttr, i3 + 1 );const c = verts[ 2 ].fromBufferAttribute( posAttr, i3 + 2 );tempVec1.subVectors( c, b );tempVec2.subVectors( a, b );// add the normal to the map for all verticesconst normal = new Vector3().crossVectors( tempVec1, tempVec2 ).normalize();for ( let n = 0; n < 3; n ++ ) {const vert = verts[ n ];const hash = hashVertex( vert );if ( ! ( hash in vertexMap ) ) {vertexMap[ hash ] = [];}vertexMap[ hash ].push( normal );}}// average normals from all vertices that share a common location if they are within the// provided crease thresholdconst normalArray = new Float32Array( posAttr.count * 3 );const normAttr = new BufferAttribute( normalArray, 3, false );for ( let i = 0, l = posAttr.count / 3; i < l; i ++ ) {// get the face normal for this vertexconst i3 = 3 * i;const a = verts[ 0 ].fromBufferAttribute( posAttr, i3 + 0 );const b = verts[ 1 ].fromBufferAttribute( posAttr, i3 + 1 );const c = verts[ 2 ].fromBufferAttribute( posAttr, i3 + 2 );tempVec1.subVectors( c, b );tempVec2.subVectors( a, b );tempNorm.crossVectors( tempVec1, tempVec2 ).normalize();// average all normals that meet the threshold and set the normal valuefor ( let n = 0; n < 3; n ++ ) {const vert = verts[ n ];const hash = hashVertex( vert );const otherNormals = vertexMap[ hash ];tempNorm2.set( 0, 0, 0 );for ( let k = 0, lk = otherNormals.length; k < lk; k ++ ) {const otherNorm = otherNormals[ k ];if ( tempNorm.dot( otherNorm ) > creaseDot ) {tempNorm2.add( otherNorm );}}tempNorm2.normalize();normAttr.setXYZ( i3 + n, tempNorm2.x, tempNorm2.y, tempNorm2.z );}}resultGeometry.setAttribute( 'normal', normAttr );return resultGeometry;}/*export {computeMikkTSpaceTangents,mergeGeometries,mergeAttributes,interleaveAttributes,estimateBytesUsed,mergeVertices,toTrianglesDrawMode,computeMorphedAttributes,mergeGroups,toCreasedNormals};*///import { mergeGeometries } from './BufferGeometryUtils.js';class LDrawUtils {static mergeObject( object ) {// Merges geometries in object by materials and returns new object. Use on not indexed geometries.// The object buffers reference the old object ones.// Special treatment is done to the conditional lines generated by LDrawLoader.function extractGroup( geometry, group, elementSize, isConditionalLine ) {// Extracts a group from a geometry as a new geometry (with attribute buffers referencing original buffers)const newGeometry = new BufferGeometry();const originalPositions = geometry.getAttribute( 'position' ).array;const originalNormals = elementSize === 3 ? geometry.getAttribute( 'normal' ).array : null;const numVertsGroup = Math.min( group.count, Math.floor( originalPositions.length / 3 ) - group.start );const vertStart = group.start * 3;const vertEnd = ( group.start + numVertsGroup ) * 3;const positions = originalPositions.subarray( vertStart, vertEnd );const normals = originalNormals !== null ? originalNormals.subarray( vertStart, vertEnd ) : null;newGeometry.setAttribute( 'position', new BufferAttribute( positions, 3 ) );if ( normals !== null ) newGeometry.setAttribute( 'normal', new BufferAttribute( normals, 3 ) );if ( isConditionalLine ) {const controlArray0 = geometry.getAttribute( 'control0' ).array.subarray( vertStart, vertEnd );const controlArray1 = geometry.getAttribute( 'control1' ).array.subarray( vertStart, vertEnd );const directionArray = geometry.getAttribute( 'direction' ).array.subarray( vertStart, vertEnd );newGeometry.setAttribute( 'control0', new BufferAttribute( controlArray0, 3, false ) );newGeometry.setAttribute( 'control1', new BufferAttribute( controlArray1, 3, false ) );newGeometry.setAttribute( 'direction', new BufferAttribute( directionArray, 3, false ) );}return newGeometry;}function addGeometry( mat, geometry, geometries ) {const geoms = geometries[ mat.uuid ];if ( ! geoms ) {geometries[ mat.uuid ] = {mat: mat,arr: [ geometry ]};} else {geoms.arr.push( geometry );}}function permuteAttribute( attribute, elemSize ) {// Permutes first two vertices of each attribute elementif ( ! attribute ) return;const verts = attribute.array;const numVerts = Math.floor( verts.length / 3 );let offset = 0;for ( let i = 0; i < numVerts; i ++ ) {const x = verts[ offset ];const y = verts[ offset + 1 ];const z = verts[ offset + 2 ];verts[ offset ] = verts[ offset + 3 ];verts[ offset + 1 ] = verts[ offset + 4 ];verts[ offset + 2 ] = verts[ offset + 5 ];verts[ offset + 3 ] = x;verts[ offset + 4 ] = y;verts[ offset + 5 ] = z;offset += elemSize * 3;}}// Traverse the object hierarchy collecting geometries and transforming them to world spaceconst meshGeometries = {};const linesGeometries = {};const condLinesGeometries = {};object.updateMatrixWorld( true );const normalMatrix = new Matrix3();object.traverse( c => {if ( c.isMesh | c.isLineSegments ) {const elemSize = c.isMesh ? 3 : 2;const geometry = c.geometry.clone();const matrixIsInverted = c.matrixWorld.determinant() < 0;if ( matrixIsInverted ) {permuteAttribute( geometry.attributes.position, elemSize );permuteAttribute( geometry.attributes.normal, elemSize );}geometry.applyMatrix4( c.matrixWorld );if ( c.isConditionalLine ) {geometry.attributes.control0.applyMatrix4( c.matrixWorld );geometry.attributes.control1.applyMatrix4( c.matrixWorld );normalMatrix.getNormalMatrix( c.matrixWorld );geometry.attributes.direction.applyNormalMatrix( normalMatrix );}const geometries = c.isMesh ? meshGeometries : ( c.isConditionalLine ? condLinesGeometries : linesGeometries );if ( Array.isArray( c.material ) ) {for ( const groupIndex in geometry.groups ) {const group = geometry.groups[ groupIndex ];const mat = c.material[ group.materialIndex ];const newGeometry = extractGroup( geometry, group, elemSize, c.isConditionalLine );addGeometry( mat, newGeometry, geometries );}} else {addGeometry( c.material, geometry, geometries );}}} );// Create object with merged geometriesconst mergedObject = new Group();const meshMaterialsIds = Object.keys( meshGeometries );for ( const meshMaterialsId of meshMaterialsIds ) {const meshGeometry = meshGeometries[ meshMaterialsId ];const mergedGeometry = mergeGeometries( meshGeometry.arr );mergedObject.add( new Mesh( mergedGeometry, meshGeometry.mat ) );}const linesMaterialsIds = Object.keys( linesGeometries );for ( const linesMaterialsId of linesMaterialsIds ) {const lineGeometry = linesGeometries[ linesMaterialsId ];const mergedGeometry = mergeGeometries( lineGeometry.arr );mergedObject.add( new LineSegments( mergedGeometry, lineGeometry.mat ) );}const condLinesMaterialsIds = Object.keys( condLinesGeometries );for ( const condLinesMaterialsId of condLinesMaterialsIds ) {const condLineGeometry = condLinesGeometries[ condLinesMaterialsId ];const mergedGeometry = mergeGeometries( condLineGeometry.arr );const condLines = new LineSegments( mergedGeometry, condLineGeometry.mat );condLines.isConditionalLine = true;mergedObject.add( condLines );}mergedObject.userData.constructionStep = 0;mergedObject.userData.numConstructionSteps = 1;return mergedObject;}}//export { LDrawUtils };const clock = new Clock();class Loop {constructor(camera, scene, renderer) {this.camera = camera;this.scene = scene;this.renderer = renderer;// somewhere in the Loop class:this.updatables = []}start() {this.renderer.setAnimationLoop(() => {// tell every animated object to tick forward one frame// this.tick();// render a framethis.renderer.render(this.scene, this.camera);});}stop() {this.renderer.setAnimationLoop(null);}tick(){// only call the getDelta function once per frame!const delta = clock.getDelta();// console.log(//   `The last frame rendered in ${delta * 1000} milliseconds`,// );// eslint-disable-next-line @typescript-eslint/strict-boolean-expressionsif(this.updatables.length){for (const object of this.updatables) {if(typeof object.tick == 'function'){object.tick(delta);}}}}}//export { Loop };initViewer = ()=>{container = document.querySelector('#scene-container');let ldraw = new Ldraw();ldraw.start();}

执行代码

现在我们已经成功添加了很多功能和复杂的交互逻辑,将不同的细节进行分层管理。后续可采用 MVC 模式重构代码,将代码分为三个层级:模型层、视图层和控制层。模型层负责数据的管理,视图层负责展示数据和渲染 UI,控制层则负责协调模型层和视图层之间的交互,同时处理一些业务逻辑。重构后代码层级会更清晰,方便拓展其功能。

最后,将脚本执行到dom即可看到模型。

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="theme-color" content="#000000" /><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="renderer" content="webkit"><meta name="force-rendering" content="webkit"><meta name="google-site-verification" content="FTeR0c8arOPKh8c5DYh_9uu98_zJbaWw53J-Sch9MTg"><meta data-rh="true" name="keywords" content="three.js实现乐高小轿车"><meta data-rh="true" name="description" content="three.js实现乐高小轿车"><meta data-rh="true" property="og:title" content="three.js实现乐高小轿车"><link rel="icon" href="./favicon.ico"><title>three.js实现乐高小轿车</title><style>body {padding: 0;margin: 0;font: normal 14px/1.42857 Tahoma;}#scene-container {height: 100vh;}</style>
</head>
<body onload="initViewer()"><div id="scene-container"></div><script>let initViewer = null</script>
</body>
</html>

模型描述文本

0 LDraw.org Configuration File
0 Name: LDConfig.ldr
0 Author: LDraw.org
0 !LDRAW_ORG Configuration UPDATE 2017-12-15

0 // LDraw Solid Colours
0                              // LEGOID  26 - Black
0 !COLOUR Black                                                 CODE   0   VALUE #05131D   EDGE #595959
0                              // LEGOID  23 - Bright Blue
0 !COLOUR Blue                                                  CODE   1   VALUE #0055BF   EDGE #333333
0                              // LEGOID  28 - Dark Green
0 !COLOUR Green                                                 CODE   2   VALUE #257A3E   EDGE #333333
0                              // LEGOID 107 - Bright Bluish Green
0 !COLOUR Dark_Turquoise                                        CODE   3   VALUE #00838F   EDGE #333333
0                              // LEGOID  21 - Bright Red
0 !COLOUR Red                                                   CODE   4   VALUE #C91A09   EDGE #333333
0                              // LEGOID 221 - Bright Purple
0 !COLOUR Dark_Pink                                             CODE   5   VALUE #C870A0   EDGE #333333
0                              // LEGOID 217 - Brown
0 !COLOUR Brown                                                 CODE   6   VALUE #583927   EDGE #1E1E1E
0                              // LEGOID   2 - Grey
0 !COLOUR Light_Grey                                            CODE   7   VALUE #9BA19D   EDGE #333333
0                              // LEGOID  27 - Dark Grey
0 !COLOUR Dark_Grey                                             CODE   8   VALUE #6D6E5C   EDGE #333333
0                              // LEGOID  45 - Light Blue
0 !COLOUR Light_Blue                                            CODE   9   VALUE #B4D2E3   EDGE #333333
0                              // LEGOID  37 - Bright Green
0 !COLOUR Bright_Green                                          CODE  10   VALUE #4B9F4A   EDGE #333333
0                              // LEGOID 116 - Medium Bluish Green
0 !COLOUR Light_Turquoise                                       CODE  11   VALUE #55A5AF   EDGE #333333
0                              // LEGOID   4 - Brick Red
0 !COLOUR Salmon                                                CODE  12   VALUE #F2705E   EDGE #333333
0                              // LEGOID   9 - Light Reddish Violet
0 !COLOUR Pink                                                  CODE  13   VALUE #FC97AC   EDGE #333333
0                              // LEGOID  24 - Bright Yellow
0 !COLOUR Yellow                                                CODE  14   VALUE #F2CD37   EDGE #333333

还原模型到三维场景

ca61c57de04246e98fe78af4562ac3e0.png

参见:

3. 开发和学习环境,引入threejs | Three.js中文网

LDraw.org - LDraw.org Homepage

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/299887.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JavaWeb--JavaScript Part 01

1. JavaScript概述 JavaScript&#xff08;简称JS&#xff09;是一种轻量级的、解释执行的客户端脚本语言&#xff0c;主要用于增强网页的交互性和动态性。它起源于Netscape的LiveScript&#xff0c;并在1995年发布时更名为JavaScript。尽管名称中包含"Java"&#xf…

STM32F407 FSMC并口读取AD7606

先贴一下最终效果图.这个是AD7606并口读取数据一个周期后的数据结果. 原始波形用示波器看是很平滑的. AD7606不知为何就会出现干扰, 我猜测可能是数字信号干扰导致的. 因为干扰的波形很有规律. 这种现象基本上可以排除是程序问题. 应该是干扰或者数字信号干扰,或者是数字和模拟…

软考113-上午题-【计算机网络】-IPv6、无线网络、Windows命令

一、IPv6 IPv6 具有长达 128 位的地址空间&#xff0c;可以彻底解决 IPv4 地址不足的问题。由于 IPv4 地址是32 位二进制&#xff0c;所能表示的IP 地址个数为 2^32 4 294 967 29640 亿&#xff0c;因而在因特网上约有 40亿个P 地址。 由 32 位的IPv4 升级至 128 位的IPv6&am…

对代理模式的理解

目录 一、前言二、案例1 代码2 自定义代理类【静态代理】2.1 一个接口多个实现&#xff0c;到底注入哪个依赖呢&#xff1f;2.1.1 Primary注解2.1.2 Resource注解&#xff08;指定name属性&#xff09;2.1.3 Qualifier注解 2.2 面向接口编程2.3 如果没接口咋办呢&#xff1f;2.…

Ceph学习 - 1.存储知识

文章目录 1.存储基础1.1 基础知识1.1.1 存储基础1.1.2 存储使用 1.2 文件系统1.2.1 简介1.2.2 数据存储1.2.3 存储应用的基本方式1.2.4 文件存储 1.3 小结 1.存储基础 学习目标&#xff1a;这一节&#xff0c;我们从基础知识、文件系统、小节三个方面来学习。 1.1 基础知识 1.…

一种有效的隐私保护联邦学习方法-文章翻译

一种有效的隐私保护联邦学习方法 摘要 联邦学习已成为协作学习和隐私保护学习的一种很有前途的方法。联合学习过程中的参和者通过交换模型参数而不是实际的训练数据(他们可能希望保持私有)来合作训练模型。然而,参数交互和生成的模型仍然可能会披露有关所用训练数据的信息…

2024HW-->Wireshark攻击流量分析

在HW中&#xff0c;最离不开的&#xff0c;肯定是看监控了&#xff0c;那么就要去了解一些wireshark的基础用法以及攻击的流量&#xff01;&#xff01;&#xff01;&#xff01; 1.Wireshark的基本用法 比如人家面试官给你一段流量包&#xff0c;你要会用 1.分组详情 对于我…

Mac 配置 Aria2

文章目录 1. Aria2 安装1.1 安装 brew1.2 安装 Aria2 2. 配置 Aria22.1 创建配置文件 aria2.conf 和空对话文件 aria2.session2.2 编辑配置文件 aria2.conf 3. 开机启动设置3.1 创建用户启动文件3.2 管理自启动项 4. 配置 BT tracker 自动更新4.1 XIU2/TrackersListCollection …

总结:微信小程序中跨组件的通信、状态管理的方案

在微信小程序中实现跨组件通信和状态管理,有以下几种主要方案: 事件机制 通过事件机制可以实现父子组件、兄弟组件的通信。 示例: 父组件向子组件传递数据: 父组件: <child binddata"handleChildData" /> 子组件: Component({..., methods: { handleChildData(…

[leetcode] 25. K 个一组翻转链表

给你链表的头节点 head &#xff0c;每 k 个节点一组进行翻转&#xff0c;请你返回修改后的链表。 k 是一个正整数&#xff0c;它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍&#xff0c;那么请将最后剩余的节点保持原有顺序。 你不能只是单纯的改变节点内部的值…

【智能排班系统】雪花算法生成分布式ID

文章目录 雪花算法介绍起源与命名基本原理与结构优势与特点应用场景 代码实现代码结构自定义机器标识RandomWorkIdChooseLocalRedisWorkIdChooselua脚本 实体类SnowflakeIdInfoWorkCenterInfo 雪花算法类配置类雪花算法工具类 说明 雪花算法介绍 在复杂而庞大的分布式系统中&a…

【Frida】【Android】 工具篇:ProxyPin抓包详解

&#x1f6eb; 系列文章导航 【Frida】【Android】01_手把手教你环境搭建 https://blog.csdn.net/kinghzking/article/details/136986950【Frida】【Android】02_JAVA层HOOK https://blog.csdn.net/kinghzking/article/details/137008446【Frida】【Android】03_RPC https://bl…

【漏洞复现】某科技X2Modbus网关多个漏洞

漏洞描述 最近某科技X2Modbus网关出了一个GetUser的信息泄露的漏洞,但是经过审计发现该系统80%以上的接口均是未授权的,没有添加相应的鉴权机制,以下列举多个未授权接口以及获取相关敏感信息的接口。 免责声明 技术文章仅供参考,任何个人和组织使用网络应当遵守宪法法律…

解决element-plus table组件 fixed=“right“(left)浮动后横向滚动文字穿透的问题

BUG 版本&#xff1a;element-plus 2.6.1 浏览器&#xff1a;360极速浏览器22.1 (Chromium内核) 组件&#xff1a;el-table组件 问题&#xff1a;在头部/尾部浮动加上斑马条纹后&#xff0c;横向滚动存在文字穿透的问题。具体如图&#xff1a; 白色背景行的文字&#xff0c…

《Ubuntu20.04环境下的ROS进阶学习6》

一、手持激光雷达建图 在上次的学习中我们已经使用hector_Mapping在仿真环境下建图了&#xff0c;那么本节我们将拿出真实雷达做一次室内的建图。我们使用的是思岚的S2L激光雷达。 二、下载思岚的应用手册 首先我们根据自己的激光雷达类型去到思岚官网下载相应的ROS包&#xff…

成都源聚达:抖音小店押金收费标准

在数字浪潮中&#xff0c;抖音小店如星辰般熠熠生辉&#xff0c;吸引了无数商家和创业者。然而&#xff0c;想要在这片星海中畅游&#xff0c;首先得了解其押金的收费标准。正如古人言&#xff1a;“无规矩不成方圆”&#xff0c;明确规则是成功的关键一步。 抖音小店的押金制度…

CSS-属性

&#x1f4da;详见 W3scholl&#xff0c;本篇只做快速思维索引。 CSS 背景 用于定义元素的背景效果。 background-colorbackground-imagebackground-positionbackground-repeatbackground-attachment background-color background-color 属性指定元素的背景色。 h1 {back…

mysql的安装和部署

##官网下载mysql 我下载的是一个mysql-5.7.38-linux-glibc2.12-x86_64.tar.gz 可以通过xshell 或者xftp传送 xshell则是先下载一个lrzsz 执行以下的命令 yum install lrzsz -y #安装好我下面有个一键安装的脚本 #!/bin/bash#解决软件的依赖关系 yum install cmake ncurses…

Redis 客户端

Redis 客户端 客户端-服务器结构 Redis 同 Mysql 一样&#xff0c;也是一个客户端-服务器结构的程序&#xff0c;结构如下图&#xff1a; 注&#xff1a;Redis 客户端和服务器可以在同一个主机上&#xff0c;也可以在不同主机上 Redis 客户端的多种形态 自带的命令行客户端&…

通过 Cookie、Redis共享Session 和 Spring 拦截器技术,实现对用户登录状态的持有和清理(三)

本篇内容对应 “2.4 生成验证码” 小节 和 “4.7 优化登陆模块”小节 视频链接 1 Kaptcha介绍 Kaotcga是一个生成验证码的工具。 你的网站验证码是什么&#xff1f; 在我们这个牛客论坛项目&#xff0c;验证码分为两部分 给用户看的是图片&#xff0c;用户根据图片上显示的…