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

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

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

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

描述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-150 // 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
还原模型到三维场景


参见:
3. 开发和学习环境,引入threejs | Three.js中文网
LDraw.org - LDraw.org Homepage
相关文章:
分享three.js实现乐高小汽车
前言 Web脚本语言JavaScript入门容易,但是想要熟练掌握却需要几年的学习与实践,还要在弱类型开发语言中习惯于使用模块来构建你的代码,就像小时候玩的乐高积木一样。 应用程序的模块化理念,通过将实现隐藏在一个简单的接口后面&a…...
gpt的构造和原理
gpt是序列预测模型。 问答是通过确定问答格式样本训练出来的!比如“Q:xxxx.A:xxx"本质还是根据前面的序列预测后面的序列。在自回归训练过程中,文本序列(可能包含问题和紧随其后的答案)被视为一个整体输入到模型…...
基于springboot实现教师人事档案管理系统项目【项目源码+论文说明】计算机毕业设计
基于springboot实现IT技术交流和分享平台系统演示 摘要 我国科学技术的不断发展,计算机的应用日渐成熟,其强大的功能给人们留下深刻的印象,它已经应用到了人类社会的各个层次的领域,发挥着重要的不可替换的作用。信息管理作为计算…...
K8S之Job和CronJob控制器
这里写目录标题 Job概念适用场景使用案例 CronJob概念适用场景使用案例 Job 概念 Job控制器用于管理Pod对象运行一次性任务,例如:对数据库备份,可以直接在k8s上启动一个mysqldump备份程序,也可以启动一个pod,这个pod…...
基于SSM的基于个人需求和地域特色的外卖推荐系统(有报告)。Javaee项目。ssm项目。
演示视频: 基于SSM的基于个人需求和地域特色的外卖推荐系统(有报告)。Javaee项目。ssm项目。 项目介绍: 采用M(model)V(view)C(controller)三层体系结构&…...
哈佛大学商业评论 --- 第三篇:真实世界中的增强现实
AR将全面融入公司发展战略! AR将成为人类和机器之间的新接口! AR将成为人类的关键技术之一! 请将此文转发给您的老板! --- 本文作者:Michael E.Porter和James E.Heppelmann 虽然物理世界是三维的,但大…...
华为ICT七力助推文化产业新质生产力发展
创新起主导作用的新质生产力由新劳动者、新劳动对象、新劳动工具、新基础设施等四大要素共同构成,符合新发展理念的先进生产力质态;具有高科技、高能效、高质量等三大突出特征。而通过壮大新产业、打造新模式、激发新动能,新质生产力能够摆脱…...
FastGpt流程
1.知识库 引入文本——>数据清洗 最好将pdf/ppt/xx转换成文本,在文本里面进行数据清洗(以防知识库删除后,数据清洗失效) 可以插图,将图片通过网页检查F12查看路径放进去 或者直接在csdn放,直接复制链接…...
怎么在UE游戏中加入原生振动效果
我是做振动触感的。人类的五感“视听嗅味触”,其中的“触”就是触觉,是指皮肤、毛发与物体接触时的感觉。触感可以带来更加逼真的沉浸式体验。但也许过于司空见惯,也是习以为常,很多人漠视了触感的价值。大家对触感的认知还远远不…...
【Hadoop技术框架-MapReduce和Yarn的详细描述和部署】
前言: 💞💞大家好,我是书生♡,今天的内容主要是Hadoop的后两个组件:MapReduce和yarn的相关内容。同时还有Hadoop的完整流程。希望对大家有所帮助。感谢大家关注点赞。 💞💞前路漫漫&…...
蓝桥杯刷题 前缀和与差分-[3507]异或和之和(C++)
题目描述 给定一个数组 Ai,分别求其每个子段的异或和,并求出它们的和。 或者说,对于每组满足 1≤L≤R≤n 的 L,R求出数组中第 L 至第 R 个元素的异或和。 然后输出每组 L,R 得到的结果加起来的值。 输入格式 输入…...
background背景图参数边渐变CSS中创建背景图像的渐变效果
效果:可以看到灰色边边很难受,希望和背景融为一体 原理: 可以使用线性渐变(linear-gradient)或径向渐变(radial-gradient)。以下是一个使用线性渐变作为背景图像 代码: background: linear-gradient(to top, rgba(255,255,255,0)…...
『大模型笔记』吴恩达:AI 智能体工作流引领人工智能新趋势
吴恩达:AI 智能体工作流引领人工智能新趋势 文章目录 一. 概述二. AI 智能体的设计模式2.1. 反思(Reflection)2.2. 使用工具(Tool use)2.3. 规划(Planning)2.4. 多智能体协作(Multi-agent collaboration)三. 最后总结四. 参考文献一. 概述 我期待与大家分享我在 AI 智能体方面…...
腾讯光子工作室群 一面 (30min)
问题: 你毕业是打算考研还是直接工作 深挖项目(介绍、剖析遇到问题如何解决): 你在进行攻击的时候会不会有穿模的情况,怎么解决 为什么会造成卡顿(多嘴说的) 说说行为树和状态机之间的差别 …...
Linux的信号栈的实现(1)
作者 pengdonglin137@163.com 环境 Linux 6.5 + ARM64 概述 在前一篇文章中介绍了Linux系统中的几种栈以及它们之间的切换,进程在用户态和内核态会使用不同的栈,在用户态的主线程和其他线程都有各自的栈,此外进程在执行信号处理程序时也需要栈,那么这个栈来自哪呢? …...
Python学习笔记——heapq
堆排序 思路 堆排序思路是: 将数组以二叉树的形式分析,令根节点索引值为0,索引值为index的节点,子节点索引值分别为index*21、index*22;对二叉树进行维护,使得每个非叶子节点的值,都大于或者…...
搜索与图论——拓扑排序
有向图的拓扑排序就是图的宽度优先遍历的一个应用 有向无环图一定存在拓扑序列(有向无环图又被称为拓扑图),有向有环图一定不存在拓扑序列。无向图没有拓扑序列。 拓扑序列:将一个图排成拓扑序后,所有的边都是从前指…...
linux CentOS7配置docker的yum源并安装
[TOC](这里写目录标题 配置yum源Docker的自动化安装一些其他启动相关的命令: 配置yum源 使用以下命令下载CentOS官方的yum源文件 wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo 清除yum缓存 yum clean all 更新yum缓存…...
vue结合Elempent-Plus/UI穿梭框更改宽度以及悬浮文本显示
由于分辨率不同会导致文本内容显示不全,如下所示: 因此需要 1、悬浮到对应行上出现悬浮信息 实现代码如下所示: 这里只演示Vue3版本代码,Vue2版本不再演示 区别就在插槽使用上Vue3使用:#default“”;Vu…...
汇川PLC学习Day4:电机参数和气缸控制参数
汇川PLC学习Day4:伺服电机参数和气缸控制参数 一、伺服电机参数二、气缸参数1. 输入IO映射(1)输入IO映射(2) 输入IO触摸屏标签显示映射 2. 输出IO映射(1)输出IO映射(2) …...
Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以?
Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以? 在 Golang 的面试中,map 类型的使用是一个常见的考点,其中对 key 类型的合法性 是一道常被提及的基础却很容易被忽视的问题。本文将带你深入理解 Golang 中…...
《Qt C++ 与 OpenCV:解锁视频播放程序设计的奥秘》
引言:探索视频播放程序设计之旅 在当今数字化时代,多媒体应用已渗透到我们生活的方方面面,从日常的视频娱乐到专业的视频监控、视频会议系统,视频播放程序作为多媒体应用的核心组成部分,扮演着至关重要的角色。无论是在个人电脑、移动设备还是智能电视等平台上,用户都期望…...
FFmpeg 低延迟同屏方案
引言 在实时互动需求激增的当下,无论是在线教育中的师生同屏演示、远程办公的屏幕共享协作,还是游戏直播的画面实时传输,低延迟同屏已成为保障用户体验的核心指标。FFmpeg 作为一款功能强大的多媒体框架,凭借其灵活的编解码、数据…...
论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(一)
宇树机器人多姿态起立控制强化学习框架论文解析 论文解读:交大&港大&上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(一) 论文解读:交大&港大&上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化…...
微信小程序云开发平台MySQL的连接方式
注:微信小程序云开发平台指的是腾讯云开发 先给结论:微信小程序云开发平台的MySQL,无法通过获取数据库连接信息的方式进行连接,连接只能通过云开发的SDK连接,具体要参考官方文档: 为什么? 因为…...
Map相关知识
数据结构 二叉树 二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子 节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只 有左子节点,有的节点只有…...
【碎碎念】宝可梦 Mesh GO : 基于MESH网络的口袋妖怪 宝可梦GO游戏自组网系统
目录 游戏说明《宝可梦 Mesh GO》 —— 局域宝可梦探索Pokmon GO 类游戏核心理念应用场景Mesh 特性 宝可梦玩法融合设计游戏构想要素1. 地图探索(基于物理空间 广播范围)2. 野生宝可梦生成与广播3. 对战系统4. 道具与通信5. 延伸玩法 安全性设计 技术选…...
Linux nano命令的基本使用
参考资料 GNU nanoを使いこなすnano基础 目录 一. 简介二. 文件打开2.1 普通方式打开文件2.2 只读方式打开文件 三. 文件查看3.1 打开文件时,显示行号3.2 翻页查看 四. 文件编辑4.1 Ctrl K 复制 和 Ctrl U 粘贴4.2 Alt/Esc U 撤回 五. 文件保存与退出5.1 Ctrl …...
【从零开始学习JVM | 第四篇】类加载器和双亲委派机制(高频面试题)
前言: 双亲委派机制对于面试这块来说非常重要,在实际开发中也是经常遇见需要打破双亲委派的需求,今天我们一起来探索一下什么是双亲委派机制,在此之前我们先介绍一下类的加载器。 目录 编辑 前言: 类加载器 1. …...
django blank 与 null的区别
1.blank blank控制表单验证时是否允许字段为空 2.null null控制数据库层面是否为空 但是,要注意以下几点: Django的表单验证与null无关:null参数控制的是数据库层面字段是否可以为NULL,而blank参数控制的是Django表单验证时字…...
