522 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			522 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|  | import { | ||
|  | 	BufferGeometry, | ||
|  | 	Color, | ||
|  | 	FileLoader, | ||
|  | 	Float32BufferAttribute, | ||
|  | 	Group, | ||
|  | 	Loader, | ||
|  | 	Mesh, | ||
|  | 	MeshPhongMaterial | ||
|  | } from 'three'; | ||
|  | import * as fflate from '../libs/fflate.module.js'; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Description: Early release of an AMF Loader following the pattern of the | ||
|  |  * example loaders in the three.js project. | ||
|  |  * | ||
|  |  * Usage: | ||
|  |  *	const loader = new AMFLoader(); | ||
|  |  *	loader.load('/path/to/project.amf', function(objecttree) { | ||
|  |  *		scene.add(objecttree); | ||
|  |  *	}); | ||
|  |  * | ||
|  |  * Materials now supported, material colors supported | ||
|  |  * Zip support, requires fflate | ||
|  |  * No constellation support (yet)! | ||
|  |  * | ||
|  |  */ | ||
|  | 
 | ||
|  | class AMFLoader extends Loader { | ||
|  | 
 | ||
|  | 	constructor( manager ) { | ||
|  | 
 | ||
|  | 		super( manager ); | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	load( url, onLoad, onProgress, onError ) { | ||
|  | 
 | ||
|  | 		const scope = this; | ||
|  | 
 | ||
|  | 		const loader = new FileLoader( scope.manager ); | ||
|  | 		loader.setPath( scope.path ); | ||
|  | 		loader.setResponseType( 'arraybuffer' ); | ||
|  | 		loader.setRequestHeader( scope.requestHeader ); | ||
|  | 		loader.setWithCredentials( scope.withCredentials ); | ||
|  | 		loader.load( url, function ( text ) { | ||
|  | 
 | ||
|  | 			try { | ||
|  | 
 | ||
|  | 				onLoad( scope.parse( text ) ); | ||
|  | 
 | ||
|  | 			} catch ( e ) { | ||
|  | 
 | ||
|  | 				if ( onError ) { | ||
|  | 
 | ||
|  | 					onError( e ); | ||
|  | 
 | ||
|  | 				} else { | ||
|  | 
 | ||
|  | 					console.error( e ); | ||
|  | 
 | ||
|  | 				} | ||
|  | 
 | ||
|  | 				scope.manager.itemError( url ); | ||
|  | 
 | ||
|  | 			} | ||
|  | 
 | ||
|  | 		}, onProgress, onError ); | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	parse( data ) { | ||
|  | 
 | ||
|  | 		function loadDocument( data ) { | ||
|  | 
 | ||
|  | 			let view = new DataView( data ); | ||
|  | 			const magic = String.fromCharCode( view.getUint8( 0 ), view.getUint8( 1 ) ); | ||
|  | 
 | ||
|  | 			if ( magic === 'PK' ) { | ||
|  | 
 | ||
|  | 				let zip = null; | ||
|  | 				let file = null; | ||
|  | 
 | ||
|  | 				console.log( 'THREE.AMFLoader: Loading Zip' ); | ||
|  | 
 | ||
|  | 				try { | ||
|  | 
 | ||
|  | 					zip = fflate.unzipSync( new Uint8Array( data ) ); | ||
|  | 
 | ||
|  | 				} catch ( e ) { | ||
|  | 
 | ||
|  | 					if ( e instanceof ReferenceError ) { | ||
|  | 
 | ||
|  | 						console.log( 'THREE.AMFLoader: fflate missing and file is compressed.' ); | ||
|  | 						return null; | ||
|  | 
 | ||
|  | 					} | ||
|  | 
 | ||
|  | 				} | ||
|  | 
 | ||
|  | 				for ( file in zip ) { | ||
|  | 
 | ||
|  | 					if ( file.toLowerCase().slice( - 4 ) === '.amf' ) { | ||
|  | 
 | ||
|  | 						break; | ||
|  | 
 | ||
|  | 					} | ||
|  | 
 | ||
|  | 				} | ||
|  | 
 | ||
|  | 				console.log( 'THREE.AMFLoader: Trying to load file asset: ' + file ); | ||
|  | 				view = new DataView( zip[ file ].buffer ); | ||
|  | 
 | ||
|  | 			} | ||
|  | 
 | ||
|  | 			const fileText = new TextDecoder().decode( view ); | ||
|  | 			const xmlData = new DOMParser().parseFromString( fileText, 'application/xml' ); | ||
|  | 
 | ||
|  | 			if ( xmlData.documentElement.nodeName.toLowerCase() !== 'amf' ) { | ||
|  | 
 | ||
|  | 				console.log( 'THREE.AMFLoader: Error loading AMF - no AMF document found.' ); | ||
|  | 				return null; | ||
|  | 
 | ||
|  | 			} | ||
|  | 
 | ||
|  | 			return xmlData; | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		function loadDocumentScale( node ) { | ||
|  | 
 | ||
|  | 			let scale = 1.0; | ||
|  | 			let unit = 'millimeter'; | ||
|  | 
 | ||
|  | 			if ( node.documentElement.attributes.unit !== undefined ) { | ||
|  | 
 | ||
|  | 				unit = node.documentElement.attributes.unit.value.toLowerCase(); | ||
|  | 
 | ||
|  | 			} | ||
|  | 
 | ||
|  | 			const scaleUnits = { | ||
|  | 				millimeter: 1.0, | ||
|  | 				inch: 25.4, | ||
|  | 				feet: 304.8, | ||
|  | 				meter: 1000.0, | ||
|  | 				micron: 0.001 | ||
|  | 			}; | ||
|  | 
 | ||
|  | 			if ( scaleUnits[ unit ] !== undefined ) { | ||
|  | 
 | ||
|  | 				scale = scaleUnits[ unit ]; | ||
|  | 
 | ||
|  | 			} | ||
|  | 
 | ||
|  | 			console.log( 'THREE.AMFLoader: Unit scale: ' + scale ); | ||
|  | 			return scale; | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		function loadMaterials( node ) { | ||
|  | 
 | ||
|  | 			let matName = 'AMF Material'; | ||
|  | 			const matId = node.attributes.id.textContent; | ||
|  | 			let color = { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }; | ||
|  | 
 | ||
|  | 			let loadedMaterial = null; | ||
|  | 
 | ||
|  | 			for ( let i = 0; i < node.childNodes.length; i ++ ) { | ||
|  | 
 | ||
|  | 				const matChildEl = node.childNodes[ i ]; | ||
|  | 
 | ||
|  | 				if ( matChildEl.nodeName === 'metadata' && matChildEl.attributes.type !== undefined ) { | ||
|  | 
 | ||
|  | 					if ( matChildEl.attributes.type.value === 'name' ) { | ||
|  | 
 | ||
|  | 						matName = matChildEl.textContent; | ||
|  | 
 | ||
|  | 					} | ||
|  | 
 | ||
|  | 				} else if ( matChildEl.nodeName === 'color' ) { | ||
|  | 
 | ||
|  | 					color = loadColor( matChildEl ); | ||
|  | 
 | ||
|  | 				} | ||
|  | 
 | ||
|  | 			} | ||
|  | 
 | ||
|  | 			loadedMaterial = new MeshPhongMaterial( { | ||
|  | 				flatShading: true, | ||
|  | 				color: new Color( color.r, color.g, color.b ), | ||
|  | 				name: matName | ||
|  | 			} ); | ||
|  | 
 | ||
|  | 			if ( color.a !== 1.0 ) { | ||
|  | 
 | ||
|  | 				loadedMaterial.transparent = true; | ||
|  | 				loadedMaterial.opacity = color.a; | ||
|  | 
 | ||
|  | 			} | ||
|  | 
 | ||
|  | 			return { id: matId, material: loadedMaterial }; | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		function loadColor( node ) { | ||
|  | 
 | ||
|  | 			const color = { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }; | ||
|  | 
 | ||
|  | 			for ( let i = 0; i < node.childNodes.length; i ++ ) { | ||
|  | 
 | ||
|  | 				const matColor = node.childNodes[ i ]; | ||
|  | 
 | ||
|  | 				if ( matColor.nodeName === 'r' ) { | ||
|  | 
 | ||
|  | 					color.r = matColor.textContent; | ||
|  | 
 | ||
|  | 				} else if ( matColor.nodeName === 'g' ) { | ||
|  | 
 | ||
|  | 					color.g = matColor.textContent; | ||
|  | 
 | ||
|  | 				} else if ( matColor.nodeName === 'b' ) { | ||
|  | 
 | ||
|  | 					color.b = matColor.textContent; | ||
|  | 
 | ||
|  | 				} else if ( matColor.nodeName === 'a' ) { | ||
|  | 
 | ||
|  | 					color.a = matColor.textContent; | ||
|  | 
 | ||
|  | 				} | ||
|  | 
 | ||
|  | 			} | ||
|  | 
 | ||
|  | 			return color; | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		function loadMeshVolume( node ) { | ||
|  | 
 | ||
|  | 			const volume = { name: '', triangles: [], materialid: null }; | ||
|  | 
 | ||
|  | 			let currVolumeNode = node.firstElementChild; | ||
|  | 
 | ||
|  | 			if ( node.attributes.materialid !== undefined ) { | ||
|  | 
 | ||
|  | 				volume.materialId = node.attributes.materialid.nodeValue; | ||
|  | 
 | ||
|  | 			} | ||
|  | 
 | ||
|  | 			while ( currVolumeNode ) { | ||
|  | 
 | ||
|  | 				if ( currVolumeNode.nodeName === 'metadata' ) { | ||
|  | 
 | ||
|  | 					if ( currVolumeNode.attributes.type !== undefined ) { | ||
|  | 
 | ||
|  | 						if ( currVolumeNode.attributes.type.value === 'name' ) { | ||
|  | 
 | ||
|  | 							volume.name = currVolumeNode.textContent; | ||
|  | 
 | ||
|  | 						} | ||
|  | 
 | ||
|  | 					} | ||
|  | 
 | ||
|  | 				} else if ( currVolumeNode.nodeName === 'triangle' ) { | ||
|  | 
 | ||
|  | 					const v1 = currVolumeNode.getElementsByTagName( 'v1' )[ 0 ].textContent; | ||
|  | 					const v2 = currVolumeNode.getElementsByTagName( 'v2' )[ 0 ].textContent; | ||
|  | 					const v3 = currVolumeNode.getElementsByTagName( 'v3' )[ 0 ].textContent; | ||
|  | 
 | ||
|  | 					volume.triangles.push( v1, v2, v3 ); | ||
|  | 
 | ||
|  | 				} | ||
|  | 
 | ||
|  | 				currVolumeNode = currVolumeNode.nextElementSibling; | ||
|  | 
 | ||
|  | 			} | ||
|  | 
 | ||
|  | 			return volume; | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		function loadMeshVertices( node ) { | ||
|  | 
 | ||
|  | 			const vertArray = []; | ||
|  | 			const normalArray = []; | ||
|  | 			let currVerticesNode = node.firstElementChild; | ||
|  | 
 | ||
|  | 			while ( currVerticesNode ) { | ||
|  | 
 | ||
|  | 				if ( currVerticesNode.nodeName === 'vertex' ) { | ||
|  | 
 | ||
|  | 					let vNode = currVerticesNode.firstElementChild; | ||
|  | 
 | ||
|  | 					while ( vNode ) { | ||
|  | 
 | ||
|  | 						if ( vNode.nodeName === 'coordinates' ) { | ||
|  | 
 | ||
|  | 							const x = vNode.getElementsByTagName( 'x' )[ 0 ].textContent; | ||
|  | 							const y = vNode.getElementsByTagName( 'y' )[ 0 ].textContent; | ||
|  | 							const z = vNode.getElementsByTagName( 'z' )[ 0 ].textContent; | ||
|  | 
 | ||
|  | 							vertArray.push( x, y, z ); | ||
|  | 
 | ||
|  | 						} else if ( vNode.nodeName === 'normal' ) { | ||
|  | 
 | ||
|  | 							const nx = vNode.getElementsByTagName( 'nx' )[ 0 ].textContent; | ||
|  | 							const ny = vNode.getElementsByTagName( 'ny' )[ 0 ].textContent; | ||
|  | 							const nz = vNode.getElementsByTagName( 'nz' )[ 0 ].textContent; | ||
|  | 
 | ||
|  | 							normalArray.push( nx, ny, nz ); | ||
|  | 
 | ||
|  | 						} | ||
|  | 
 | ||
|  | 						vNode = vNode.nextElementSibling; | ||
|  | 
 | ||
|  | 					} | ||
|  | 
 | ||
|  | 				} | ||
|  | 
 | ||
|  | 				currVerticesNode = currVerticesNode.nextElementSibling; | ||
|  | 
 | ||
|  | 			} | ||
|  | 
 | ||
|  | 			return { 'vertices': vertArray, 'normals': normalArray }; | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		function loadObject( node ) { | ||
|  | 
 | ||
|  | 			const objId = node.attributes.id.textContent; | ||
|  | 			const loadedObject = { name: 'amfobject', meshes: [] }; | ||
|  | 			let currColor = null; | ||
|  | 			let currObjNode = node.firstElementChild; | ||
|  | 
 | ||
|  | 			while ( currObjNode ) { | ||
|  | 
 | ||
|  | 				if ( currObjNode.nodeName === 'metadata' ) { | ||
|  | 
 | ||
|  | 					if ( currObjNode.attributes.type !== undefined ) { | ||
|  | 
 | ||
|  | 						if ( currObjNode.attributes.type.value === 'name' ) { | ||
|  | 
 | ||
|  | 							loadedObject.name = currObjNode.textContent; | ||
|  | 
 | ||
|  | 						} | ||
|  | 
 | ||
|  | 					} | ||
|  | 
 | ||
|  | 				} else if ( currObjNode.nodeName === 'color' ) { | ||
|  | 
 | ||
|  | 					currColor = loadColor( currObjNode ); | ||
|  | 
 | ||
|  | 				} else if ( currObjNode.nodeName === 'mesh' ) { | ||
|  | 
 | ||
|  | 					let currMeshNode = currObjNode.firstElementChild; | ||
|  | 					const mesh = { vertices: [], normals: [], volumes: [], color: currColor }; | ||
|  | 
 | ||
|  | 					while ( currMeshNode ) { | ||
|  | 
 | ||
|  | 						if ( currMeshNode.nodeName === 'vertices' ) { | ||
|  | 
 | ||
|  | 							const loadedVertices = loadMeshVertices( currMeshNode ); | ||
|  | 
 | ||
|  | 							mesh.normals = mesh.normals.concat( loadedVertices.normals ); | ||
|  | 							mesh.vertices = mesh.vertices.concat( loadedVertices.vertices ); | ||
|  | 
 | ||
|  | 						} else if ( currMeshNode.nodeName === 'volume' ) { | ||
|  | 
 | ||
|  | 							mesh.volumes.push( loadMeshVolume( currMeshNode ) ); | ||
|  | 
 | ||
|  | 						} | ||
|  | 
 | ||
|  | 						currMeshNode = currMeshNode.nextElementSibling; | ||
|  | 
 | ||
|  | 					} | ||
|  | 
 | ||
|  | 					loadedObject.meshes.push( mesh ); | ||
|  | 
 | ||
|  | 				} | ||
|  | 
 | ||
|  | 				currObjNode = currObjNode.nextElementSibling; | ||
|  | 
 | ||
|  | 			} | ||
|  | 
 | ||
|  | 			return { 'id': objId, 'obj': loadedObject }; | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		const xmlData = loadDocument( data ); | ||
|  | 		let amfName = ''; | ||
|  | 		let amfAuthor = ''; | ||
|  | 		const amfScale = loadDocumentScale( xmlData ); | ||
|  | 		const amfMaterials = {}; | ||
|  | 		const amfObjects = {}; | ||
|  | 		const childNodes = xmlData.documentElement.childNodes; | ||
|  | 
 | ||
|  | 		let i, j; | ||
|  | 
 | ||
|  | 		for ( i = 0; i < childNodes.length; i ++ ) { | ||
|  | 
 | ||
|  | 			const child = childNodes[ i ]; | ||
|  | 
 | ||
|  | 			if ( child.nodeName === 'metadata' ) { | ||
|  | 
 | ||
|  | 				if ( child.attributes.type !== undefined ) { | ||
|  | 
 | ||
|  | 					if ( child.attributes.type.value === 'name' ) { | ||
|  | 
 | ||
|  | 						amfName = child.textContent; | ||
|  | 
 | ||
|  | 					} else if ( child.attributes.type.value === 'author' ) { | ||
|  | 
 | ||
|  | 						amfAuthor = child.textContent; | ||
|  | 
 | ||
|  | 					} | ||
|  | 
 | ||
|  | 				} | ||
|  | 
 | ||
|  | 			} else if ( child.nodeName === 'material' ) { | ||
|  | 
 | ||
|  | 				const loadedMaterial = loadMaterials( child ); | ||
|  | 
 | ||
|  | 				amfMaterials[ loadedMaterial.id ] = loadedMaterial.material; | ||
|  | 
 | ||
|  | 			} else if ( child.nodeName === 'object' ) { | ||
|  | 
 | ||
|  | 				const loadedObject = loadObject( child ); | ||
|  | 
 | ||
|  | 				amfObjects[ loadedObject.id ] = loadedObject.obj; | ||
|  | 
 | ||
|  | 			} | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		const sceneObject = new Group(); | ||
|  | 		const defaultMaterial = new MeshPhongMaterial( { | ||
|  | 			name: Loader.DEFAULT_MATERIAL_NAME, | ||
|  | 			color: 0xaaaaff, | ||
|  | 			flatShading: true | ||
|  | 		} ); | ||
|  | 
 | ||
|  | 		sceneObject.name = amfName; | ||
|  | 		sceneObject.userData.author = amfAuthor; | ||
|  | 		sceneObject.userData.loader = 'AMF'; | ||
|  | 
 | ||
|  | 		for ( const id in amfObjects ) { | ||
|  | 
 | ||
|  | 			const part = amfObjects[ id ]; | ||
|  | 			const meshes = part.meshes; | ||
|  | 			const newObject = new Group(); | ||
|  | 			newObject.name = part.name || ''; | ||
|  | 
 | ||
|  | 			for ( i = 0; i < meshes.length; i ++ ) { | ||
|  | 
 | ||
|  | 				let objDefaultMaterial = defaultMaterial; | ||
|  | 				const mesh = meshes[ i ]; | ||
|  | 				const vertices = new Float32BufferAttribute( mesh.vertices, 3 ); | ||
|  | 				let normals = null; | ||
|  | 
 | ||
|  | 				if ( mesh.normals.length ) { | ||
|  | 
 | ||
|  | 					normals = new Float32BufferAttribute( mesh.normals, 3 ); | ||
|  | 
 | ||
|  | 				} | ||
|  | 
 | ||
|  | 				if ( mesh.color ) { | ||
|  | 
 | ||
|  | 					const color = mesh.color; | ||
|  | 
 | ||
|  | 					objDefaultMaterial = defaultMaterial.clone(); | ||
|  | 					objDefaultMaterial.color = new Color( color.r, color.g, color.b ); | ||
|  | 
 | ||
|  | 					if ( color.a !== 1.0 ) { | ||
|  | 
 | ||
|  | 						objDefaultMaterial.transparent = true; | ||
|  | 						objDefaultMaterial.opacity = color.a; | ||
|  | 
 | ||
|  | 					} | ||
|  | 
 | ||
|  | 				} | ||
|  | 
 | ||
|  | 				const volumes = mesh.volumes; | ||
|  | 
 | ||
|  | 				for ( j = 0; j < volumes.length; j ++ ) { | ||
|  | 
 | ||
|  | 					const volume = volumes[ j ]; | ||
|  | 					const newGeometry = new BufferGeometry(); | ||
|  | 					let material = objDefaultMaterial; | ||
|  | 
 | ||
|  | 					newGeometry.setIndex( volume.triangles ); | ||
|  | 					newGeometry.setAttribute( 'position', vertices.clone() ); | ||
|  | 
 | ||
|  | 					if ( normals ) { | ||
|  | 
 | ||
|  | 						newGeometry.setAttribute( 'normal', normals.clone() ); | ||
|  | 
 | ||
|  | 					} | ||
|  | 
 | ||
|  | 					if ( amfMaterials[ volume.materialId ] !== undefined ) { | ||
|  | 
 | ||
|  | 						material = amfMaterials[ volume.materialId ]; | ||
|  | 
 | ||
|  | 					} | ||
|  | 
 | ||
|  | 					newGeometry.scale( amfScale, amfScale, amfScale ); | ||
|  | 					newObject.add( new Mesh( newGeometry, material.clone() ) ); | ||
|  | 
 | ||
|  | 				} | ||
|  | 
 | ||
|  | 			} | ||
|  | 
 | ||
|  | 			sceneObject.add( newObject ); | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		return sceneObject; | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | } | ||
|  | 
 | ||
|  | export { AMFLoader }; |