分享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.mzph.cn/news/796695.shtml

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

相关文章

Azkaban集群模式部署详细教程

序言 Azkaban是一个用于工作流程调度和任务调度的开源工具&#xff0c;它可以帮助用户轻松地管理和监控复杂的工作流程。Azkaban的架构设计旨在提供高度可扩展性和可靠性&#xff0c;同时保持易用性和灵活性。 Azkaban的架构可以分为三个主要组件:Executor、Web Server和db数据…

【力扣】94. 二叉树的中序遍历、144. 二叉树的前序遍历、145. 二叉树的后序遍历

先序遍历&#xff1a;根-左-右中序遍历&#xff1a;左-根-右后序遍历&#xff1a;左-右-根 94. 二叉树的中序遍历 题目描述 给定一个二叉树的根节点 root &#xff0c;返回 它的 中序 遍历 。 示例 1&#xff1a; 输入&#xff1a;root [1,null,2,3] 输出&#xff1a;[1,3…

Linux:五种IO模型的基本认识

文章目录 IO的本质五种IO模型异步和同步 阻塞IO非阻塞IO信号驱动IO IO的本质 在之前的内容中已经结束了对于网络和操作系统的学习&#xff0c;那么回过来再继续看IO&#xff0c;什么是IO呢&#xff1f; 对于网络的学习当中&#xff0c;实际上也是一种IO&#xff0c;数据从计算…

使用vite创建一个react18项目

一、vite是什么&#xff1f; vite 是一种新型前端构建工具&#xff0c;能够显著提升前端开发体验。它主要由两部分组成&#xff1a; 一个开发服务器&#xff0c;它基于原生 ES 模块提供了丰富的内建功能&#xff0c;如速度快到惊人的模块热更新&#xff08;HMR&#xff09;。 …

博客部署004-centos安装mysql及redis

1、如何查看当前centos版本&#xff1f; cat /etc/os-release 2、安装mysql 我的是centos8版本&#xff0c;使用dnf命令 2.1 CentOS 7/8: sudo yum install -y mysql-community-server 或者在CentOS 8上&#xff0c;使用DNF:&#x1f31f; sudo dnf install -y mysql-ser…

探秘MIMO技术:无线通信革命的多天线奇迹

单根发射天线和单根接收天线之间的信道容量受限于香农公式&#xff0c;要想在相同的频谱带宽下进一步提高信道容量&#xff0c;要采用多天线技术。 1. 什么是MIMO MIMO&#xff1a;Multiple-Input Multiple-Output&#xff0c;即多入多出系统。这里的入和出是相对于发射天线和…

力控机器人原理及力控制实现

力控机器人原理及力控制实现 力控机器人是一种能够感知力量并具有实时控制能力的机器人系统。它们可以在与人类进行精准协作和合作时&#xff0c;将力传感技术&#xff08;Force Sensing Technology&#xff09;和控制算法&#xff08;Control Algorithm&#xff09;结合起来&a…

Vue3_2024_7天【回顾上篇watch常见的后两种场景】

随笔&#xff1a;这年头工作不好找咯&#xff0c;大家有学历提升的赶快了&#xff0c;还有外出人多注意身体&#xff0c;没错我在深圳这边阳了&#xff0c;真的绝啊&#xff0c;最尴尬的还给朋友传染了&#xff01;&#xff01;&#xff01; 之前三种的监听情况&#xff0c;监听…

LabVIEW太赫兹波扫描成像系统

LabVIEW太赫兹波扫描成像系统 随着科技的不断发展&#xff0c;太赫兹波成像技术因其非电离性、高穿透性和高分辨率等特点&#xff0c;在生物医学、材料质量无损检测以及公共安全等领域得到了广泛的应用。然而&#xff0c;在实际操作中&#xff0c;封闭性较高的信号采集软件限制…

dm8数据迁移工具DTS

dm8数据迁移工具DTS DTS工具介绍 DM数据迁移工具提供了主流大型数据库迁移到DM、DM到DM、文件迁移到DM以及DM迁移到文件的功能。DM数据迁移工具采用向导方式引导用户通过简单的步骤完成需要的操作。 DM数据迁移工具支持&#xff1a; ◆ 主流大型数据库Oracle、SQLServer、MyS…

DC9 Debian和sql注入

信息收集 sudo arp-scan -l 列出局域网主机 arp-scan向局域网中所有可能的ip地址发出arp请求包&#xff0c;如果得到arp回应&#xff0c;就证明局域网中某台主机使用了该ip dc9的ip &#xff1a; 192.168.146.133 访问网页 cms为Debian 端口扫描 22端口是filtered 隐藏目…

详细分析Python爬虫中的xpath(附Demo)

目录 前言1. 基本知识2. 常用API3. 简易Demo 前言 关于爬虫的基本知识推荐阅读&#xff1a;Python爬虫从入门到应用&#xff08;超全讲解&#xff09; 该知识点需要提前安装相关依赖&#xff1a;pip install lxml 1. 基本知识 XPath&#xff08;XML Path Language&#xf…

GIt 删除某个特定commit

目的 多次commit&#xff0c;想删掉中间的一个/一些commit 操作方法 一句话说明&#xff1a;利用rebase命令的d表示移除commit的功能&#xff0c;来移除特定的commit # 压缩这3次commit,head~3表示从最近1次commit开始&#xff0c;前3个commit git rebase -i head~3rebase…

机器学习每周挑战——信用卡申请用户数据分析

数据集的截图 # 字段 说明 # Ind_ID 客户ID # Gender 性别信息 # Car_owner 是否有车 # Propert_owner 是否有房产 # Children 子女数量 # Annual_income 年收入 # Type_Income 收入类型 # Education 教育程度 # Marital_status 婚姻状况 # Housing_type 居住…

使用GPT需要注意的事项

GPT出来之后&#xff0c;基本就告别浏览器搜索问题答案了。将问题原封不动的copy给GPT基本可以得到解答。 但是这个也有弊端&#xff0c;那就是太依赖GPT了。 1&#xff0c;使用GPT需要更强的专业知识&#xff1a;除了能问对问题&#xff0c;还要具备识别GPT&q…

拦截器抛出异常无法被全局异常处理器捕获问题

文章目录 基本说明问题描述问题原因解决方法前端执行的所有请求都通过Controller&#xff0c;而不是直接访问html定义一个/error路径的方法 总结 基本说明 我的前后端项目是放在一起的&#xff0c;前后端都是由springMVC进行控制&#xff0c;但是现在我在拦截器的preHandle方法…

蓝桥杯单元测试专项练习Java版(单元测试4)(修正版)

关于简单循环覆盖法可以看看这里我的上一个文章http://t.csdnimg.cn/k92fn\ 题目4链接:单元测试专项练习&#xff08;JavaPython&#xff09; - 第四题单元测试题目&#xff08;Java&#xff09; - 蓝桥云课 (lanqiao.cn) 目录 题目描述 源代码功能 原题: Datas.java Good…

自动驾驶_交通标志识别:各目标检测算法评测

自动驾驶|交通标志识别&#xff1a;各目标检测算法评测 论文题目&#xff1a;Evaluation of Deep Neural Networks for traffic sign detection systems 开源代码&#xff1a;https://github.com/aarcosg/traffic-sign-detection 附赠自动驾驶学习资料和量产经验&#xff1a;…

计算机视觉——基于傅里叶幅度谱文档倾斜度检测与校正

概述 在计算机视觉领域&#xff0c;处理文档数据时&#xff0c;OCR算法的性能往往会受到文档的倾斜度影响。如果文档在输入到模型之前没有经过恰当的校正&#xff0c;模型就无法期待模型能够提供准确的预测结果&#xff0c;或者模型预测的精度会降低。例如&#xff0c;在信息提…

助力蓝桥杯单片机省一————模块之超声波

距离蓝桥杯单片机省赛还有7天 本次介绍的模块是超声波模块&#xff0c;将使用定时器1和PCA进行距离的测量。如果对PCA还未了解的&#xff0c;可以打开官方给的芯片数据手册&#xff0c;自行查看。 一、超声波测量原理 二、产生8个40KHz的超声波 void Init_wave() {unsigned …