UNPKG

@inweb/viewer-three

Version:

JavaScript library for rendering CAD and BIM files in a browser using Three.js

1,328 lines (1,319 loc) 48.2 kB
/////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2002-2025, Open Design Alliance (the "Alliance"). // All rights reserved. // // This software and its documentation and related materials are owned by // the Alliance. The software may only be incorporated into application // programs owned by members of the Alliance, subject to a signed // Membership Agreement and Supplemental Software License Agreement with the // Alliance. The structure and organization of this software are the valuable // trade secrets of the Alliance and its suppliers. The software is also // protected by copyright law and international treaty provisions. Application // programs incorporating this software must include the following statement // with their copyright notices: // // This application incorporates Open Design Alliance software pursuant to a // license agreement with Open Design Alliance. // Open Design Alliance Copyright (C) 2002-2025 by Open Design Alliance. // All rights reserved. // // By use of this software, its documentation or related materials, you // acknowledge and accept the above terms. /////////////////////////////////////////////////////////////////////////////// (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('@inweb/viewer-three'), require('three')) : typeof define === 'function' && define.amd ? define(['@inweb/viewer-three', 'three'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ODA.Three, global.THREE)); })(this, (function (viewerThree, three) { 'use strict'; class PCDLoader extends three.Loader { constructor( manager ) { super( manager ); this.littleEndian = true; } load( url, onLoad, onProgress, onError ) { const scope = this; const loader = new three.FileLoader( scope.manager ); loader.setPath( scope.path ); loader.setResponseType( 'arraybuffer' ); loader.setRequestHeader( scope.requestHeader ); loader.setWithCredentials( scope.withCredentials ); loader.load( url, function ( data ) { try { onLoad( scope.parse( data ) ); } catch ( e ) { if ( onError ) { onError( e ); } else { console.error( e ); } scope.manager.itemError( url ); } }, onProgress, onError ); } _getDataView( dataview, offset, type, size ) { switch ( type ) { case 'F': { if ( size === 8 ) { return dataview.getFloat64( offset, this.littleEndian ); } return dataview.getFloat32( offset, this.littleEndian ); } case 'I': { if ( size === 1 ) { return dataview.getInt8( offset ); } if ( size === 2 ) { return dataview.getInt16( offset, this.littleEndian ); } return dataview.getInt32( offset, this.littleEndian ); } case 'U': { if ( size === 1 ) { return dataview.getUint8( offset ); } if ( size === 2 ) { return dataview.getUint16( offset, this.littleEndian ); } return dataview.getUint32( offset, this.littleEndian ); } } } parse( data ) { function decompressLZF( inData, outLength ) { const inLength = inData.length; const outData = new Uint8Array( outLength ); let inPtr = 0; let outPtr = 0; let ctrl; let len; let ref; do { ctrl = inData[ inPtr ++ ]; if ( ctrl < ( 1 << 5 ) ) { ctrl ++; if ( outPtr + ctrl > outLength ) throw new Error( 'Output buffer is not large enough' ); if ( inPtr + ctrl > inLength ) throw new Error( 'Invalid compressed data' ); do { outData[ outPtr ++ ] = inData[ inPtr ++ ]; } while ( -- ctrl ); } else { len = ctrl >> 5; ref = outPtr - ( ( ctrl & 0x1f ) << 8 ) - 1; if ( inPtr >= inLength ) throw new Error( 'Invalid compressed data' ); if ( len === 7 ) { len += inData[ inPtr ++ ]; if ( inPtr >= inLength ) throw new Error( 'Invalid compressed data' ); } ref -= inData[ inPtr ++ ]; if ( outPtr + len + 2 > outLength ) throw new Error( 'Output buffer is not large enough' ); if ( ref < 0 ) throw new Error( 'Invalid compressed data' ); if ( ref >= outPtr ) throw new Error( 'Invalid compressed data' ); do { outData[ outPtr ++ ] = outData[ ref ++ ]; } while ( -- len + 2 ); } } while ( inPtr < inLength ); return outData; } function parseHeader( binaryData ) { const PCDheader = {}; const buffer = new Uint8Array( binaryData ); let data = '', line = '', i = 0, end = false; const max = buffer.length; while ( i < max && end === false ) { const char = String.fromCharCode( buffer[ i ++ ] ); if ( char === '\n' || char === '\r' ) { if ( line.trim().toLowerCase().startsWith( 'data' ) ) { end = true; } line = ''; } else { line += char; } data += char; } const result1 = data.search( /[\r\n]DATA\s(\S*)\s/i ); const result2 = /[\r\n]DATA\s(\S*)\s/i.exec( data.slice( result1 - 1 ) ); PCDheader.data = result2[ 1 ]; PCDheader.headerLen = result2[ 0 ].length + result1; PCDheader.str = data.slice( 0, PCDheader.headerLen ); PCDheader.str = PCDheader.str.replace( /#.*/gi, '' ); PCDheader.version = /^VERSION (.*)/im.exec( PCDheader.str ); PCDheader.fields = /^FIELDS (.*)/im.exec( PCDheader.str ); PCDheader.size = /^SIZE (.*)/im.exec( PCDheader.str ); PCDheader.type = /^TYPE (.*)/im.exec( PCDheader.str ); PCDheader.count = /^COUNT (.*)/im.exec( PCDheader.str ); PCDheader.width = /^WIDTH (.*)/im.exec( PCDheader.str ); PCDheader.height = /^HEIGHT (.*)/im.exec( PCDheader.str ); PCDheader.viewpoint = /^VIEWPOINT (.*)/im.exec( PCDheader.str ); PCDheader.points = /^POINTS (.*)/im.exec( PCDheader.str ); if ( PCDheader.version !== null ) PCDheader.version = parseFloat( PCDheader.version[ 1 ] ); PCDheader.fields = ( PCDheader.fields !== null ) ? PCDheader.fields[ 1 ].split( ' ' ) : []; if ( PCDheader.type !== null ) PCDheader.type = PCDheader.type[ 1 ].split( ' ' ); if ( PCDheader.width !== null ) PCDheader.width = parseInt( PCDheader.width[ 1 ] ); if ( PCDheader.height !== null ) PCDheader.height = parseInt( PCDheader.height[ 1 ] ); if ( PCDheader.viewpoint !== null ) PCDheader.viewpoint = PCDheader.viewpoint[ 1 ]; if ( PCDheader.points !== null ) PCDheader.points = parseInt( PCDheader.points[ 1 ], 10 ); if ( PCDheader.points === null ) PCDheader.points = PCDheader.width * PCDheader.height; if ( PCDheader.size !== null ) { PCDheader.size = PCDheader.size[ 1 ].split( ' ' ).map( function ( x ) { return parseInt( x, 10 ); } ); } if ( PCDheader.count !== null ) { PCDheader.count = PCDheader.count[ 1 ].split( ' ' ).map( function ( x ) { return parseInt( x, 10 ); } ); } else { PCDheader.count = []; for ( let i = 0, l = PCDheader.fields.length; i < l; i ++ ) { PCDheader.count.push( 1 ); } } PCDheader.offset = {}; let sizeSum = 0; for ( let i = 0, l = PCDheader.fields.length; i < l; i ++ ) { if ( PCDheader.data === 'ascii' ) { PCDheader.offset[ PCDheader.fields[ i ] ] = i; } else { PCDheader.offset[ PCDheader.fields[ i ] ] = sizeSum; sizeSum += PCDheader.size[ i ] * PCDheader.count[ i ]; } } PCDheader.rowSize = sizeSum; return PCDheader; } const PCDheader = parseHeader( data ); const position = []; const normal = []; const color = []; const intensity = []; const label = []; const c = new three.Color(); if ( PCDheader.data === 'ascii' ) { const offset = PCDheader.offset; const textData = new TextDecoder().decode( data ); const pcdData = textData.slice( PCDheader.headerLen ); const lines = pcdData.split( '\n' ); for ( let i = 0, l = lines.length; i < l; i ++ ) { if ( lines[ i ] === '' ) continue; const line = lines[ i ].split( ' ' ); if ( offset.x !== undefined ) { position.push( parseFloat( line[ offset.x ] ) ); position.push( parseFloat( line[ offset.y ] ) ); position.push( parseFloat( line[ offset.z ] ) ); } if ( offset.rgb !== undefined ) { const rgb_field_index = PCDheader.fields.findIndex( ( field ) => field === 'rgb' ); const rgb_type = PCDheader.type[ rgb_field_index ]; const float = parseFloat( line[ offset.rgb ] ); let rgb = float; if ( rgb_type === 'F' ) { const farr = new Float32Array( 1 ); farr[ 0 ] = float; rgb = new Int32Array( farr.buffer )[ 0 ]; } const r = ( ( rgb >> 16 ) & 0x0000ff ) / 255; const g = ( ( rgb >> 8 ) & 0x0000ff ) / 255; const b = ( ( rgb >> 0 ) & 0x0000ff ) / 255; c.setRGB( r, g, b, three.SRGBColorSpace ); color.push( c.r, c.g, c.b ); } if ( offset.normal_x !== undefined ) { normal.push( parseFloat( line[ offset.normal_x ] ) ); normal.push( parseFloat( line[ offset.normal_y ] ) ); normal.push( parseFloat( line[ offset.normal_z ] ) ); } if ( offset.intensity !== undefined ) { intensity.push( parseFloat( line[ offset.intensity ] ) ); } if ( offset.label !== undefined ) { label.push( parseInt( line[ offset.label ] ) ); } } } if ( PCDheader.data === 'binary_compressed' ) { const sizes = new Uint32Array( data.slice( PCDheader.headerLen, PCDheader.headerLen + 8 ) ); const compressedSize = sizes[ 0 ]; const decompressedSize = sizes[ 1 ]; const decompressed = decompressLZF( new Uint8Array( data, PCDheader.headerLen + 8, compressedSize ), decompressedSize ); const dataview = new DataView( decompressed.buffer ); const offset = PCDheader.offset; for ( let i = 0; i < PCDheader.points; i ++ ) { if ( offset.x !== undefined ) { const xIndex = PCDheader.fields.indexOf( 'x' ); const yIndex = PCDheader.fields.indexOf( 'y' ); const zIndex = PCDheader.fields.indexOf( 'z' ); position.push( this._getDataView( dataview, ( PCDheader.points * offset.x ) + PCDheader.size[ xIndex ] * i, PCDheader.type[ xIndex ], PCDheader.size[ xIndex ] ) ); position.push( this._getDataView( dataview, ( PCDheader.points * offset.y ) + PCDheader.size[ yIndex ] * i, PCDheader.type[ yIndex ], PCDheader.size[ yIndex ] ) ); position.push( this._getDataView( dataview, ( PCDheader.points * offset.z ) + PCDheader.size[ zIndex ] * i, PCDheader.type[ zIndex ], PCDheader.size[ zIndex ] ) ); } if ( offset.rgb !== undefined ) { const rgbIndex = PCDheader.fields.indexOf( 'rgb' ); const r = dataview.getUint8( ( PCDheader.points * offset.rgb ) + PCDheader.size[ rgbIndex ] * i + 2 ) / 255.0; const g = dataview.getUint8( ( PCDheader.points * offset.rgb ) + PCDheader.size[ rgbIndex ] * i + 1 ) / 255.0; const b = dataview.getUint8( ( PCDheader.points * offset.rgb ) + PCDheader.size[ rgbIndex ] * i + 0 ) / 255.0; c.setRGB( r, g, b, three.SRGBColorSpace ); color.push( c.r, c.g, c.b ); } if ( offset.normal_x !== undefined ) { const xIndex = PCDheader.fields.indexOf( 'normal_x' ); const yIndex = PCDheader.fields.indexOf( 'normal_y' ); const zIndex = PCDheader.fields.indexOf( 'normal_z' ); normal.push( this._getDataView( dataview, ( PCDheader.points * offset.normal_x ) + PCDheader.size[ xIndex ] * i, PCDheader.type[ xIndex ], PCDheader.size[ xIndex ] ) ); normal.push( this._getDataView( dataview, ( PCDheader.points * offset.normal_y ) + PCDheader.size[ yIndex ] * i, PCDheader.type[ yIndex ], PCDheader.size[ yIndex ] ) ); normal.push( this._getDataView( dataview, ( PCDheader.points * offset.normal_z ) + PCDheader.size[ zIndex ] * i, PCDheader.type[ zIndex ], PCDheader.size[ zIndex ] ) ); } if ( offset.intensity !== undefined ) { const intensityIndex = PCDheader.fields.indexOf( 'intensity' ); intensity.push( this._getDataView( dataview, ( PCDheader.points * offset.intensity ) + PCDheader.size[ intensityIndex ] * i, PCDheader.type[ intensityIndex ], PCDheader.size[ intensityIndex ] ) ); } if ( offset.label !== undefined ) { const labelIndex = PCDheader.fields.indexOf( 'label' ); label.push( dataview.getInt32( ( PCDheader.points * offset.label ) + PCDheader.size[ labelIndex ] * i, this.littleEndian ) ); } } } if ( PCDheader.data === 'binary' ) { const dataview = new DataView( data, PCDheader.headerLen ); const offset = PCDheader.offset; for ( let i = 0, row = 0; i < PCDheader.points; i ++, row += PCDheader.rowSize ) { if ( offset.x !== undefined ) { const xIndex = PCDheader.fields.indexOf( 'x' ); const yIndex = PCDheader.fields.indexOf( 'y' ); const zIndex = PCDheader.fields.indexOf( 'z' ); position.push( this._getDataView( dataview, row + offset.x, PCDheader.type[ xIndex ], PCDheader.size[ xIndex ] ) ); position.push( this._getDataView( dataview, row + offset.y, PCDheader.type[ yIndex ], PCDheader.size[ yIndex ] ) ); position.push( this._getDataView( dataview, row + offset.z, PCDheader.type[ zIndex ], PCDheader.size[ zIndex ] ) ); } if ( offset.rgb !== undefined ) { const r = dataview.getUint8( row + offset.rgb + 2 ) / 255.0; const g = dataview.getUint8( row + offset.rgb + 1 ) / 255.0; const b = dataview.getUint8( row + offset.rgb + 0 ) / 255.0; c.setRGB( r, g, b, three.SRGBColorSpace ); color.push( c.r, c.g, c.b ); } if ( offset.normal_x !== undefined ) { const xIndex = PCDheader.fields.indexOf( 'normal_x' ); const yIndex = PCDheader.fields.indexOf( 'normal_y' ); const zIndex = PCDheader.fields.indexOf( 'normal_z' ); normal.push( this._getDataView( dataview, row + offset.normal_x, PCDheader.type[ xIndex ], PCDheader.size[ xIndex ] ) ); normal.push( this._getDataView( dataview, row + offset.normal_y, PCDheader.type[ yIndex ], PCDheader.size[ yIndex ] ) ); normal.push( this._getDataView( dataview, row + offset.normal_z, PCDheader.type[ zIndex ], PCDheader.size[ zIndex ] ) ); } if ( offset.intensity !== undefined ) { const intensityIndex = PCDheader.fields.indexOf( 'intensity' ); intensity.push( this._getDataView( dataview, row + offset.intensity, PCDheader.type[ intensityIndex ], PCDheader.size[ intensityIndex ] ) ); } if ( offset.label !== undefined ) { label.push( dataview.getInt32( row + offset.label, this.littleEndian ) ); } } } const geometry = new three.BufferGeometry(); if ( position.length > 0 ) geometry.setAttribute( 'position', new three.Float32BufferAttribute( position, 3 ) ); if ( normal.length > 0 ) geometry.setAttribute( 'normal', new three.Float32BufferAttribute( normal, 3 ) ); if ( color.length > 0 ) geometry.setAttribute( 'color', new three.Float32BufferAttribute( color, 3 ) ); if ( intensity.length > 0 ) geometry.setAttribute( 'intensity', new three.Float32BufferAttribute( intensity, 1 ) ); if ( label.length > 0 ) geometry.setAttribute( 'label', new three.Int32BufferAttribute( label, 1 ) ); geometry.computeBoundingSphere(); const material = new three.PointsMaterial( { size: 0.005 } ); if ( color.length > 0 ) { material.vertexColors = true; } return new three.Points( geometry, material ); } } const THREE = { Box3: three.Box3, BufferAttribute: three.BufferAttribute, BufferGeometry: three.BufferGeometry, Color: three.Color, Euler: three.Euler, Float32BufferAttribute: three.Float32BufferAttribute, Group: three.Group, Line: three.Line, LineBasicMaterial: three.LineBasicMaterial, Matrix4: three.Matrix4, Mesh: three.Mesh, MeshLambertMaterial: three.MeshLambertMaterial, MeshStandardMaterial: three.MeshStandardMaterial, Points: three.Points, PointsMaterial: three.PointsMaterial, PerspectiveCamera: three.PerspectiveCamera, Scene: three.Scene, Vector3: three.Vector3, }; var StackedLayerProvider = class { providers; constructor(providers) { this.providers = providers; } async GetLayerByURI(uri) { let errorStack = []; for (let provider of this.providers) { let layer = await provider.GetLayerByURI(uri); if (!(layer instanceof Error)) { return layer; } else { errorStack.push(layer); } } return new Error(JSON.stringify(errorStack)); } }; var InMemoryLayerProvider = class { layers; constructor() { this.layers = new Map(); } GetLayerByURI(uri) { if (!this.layers.has(uri)) { return new Error(`File with uri "${uri}" not found`); } return Promise.resolve(this.layers.get(uri)); } add(file) { if (this.layers.has(file.header.id)) { throw new Error(`Inserting file with duplicate ID "${file.header.id}"`); } this.layers.set(file.header.id, file); return this; } AddAll(files) { files.forEach((f) => this.add(f)); return this; } }; function log(bla) { { console.log(`${JSON.stringify(arguments)}`); } } var FetchLayerProvider = class { layers; constructor() { this.layers = new Map(); } async FetchJson(url) { let result = await fetch(url); if (!result.ok) { return new Error(`Failed to fetch ${url}: ${result.status}`); } try { return await result.json(); } catch (e) { log(url); return new Error(`Failed to parse json at ${url}: ${e}`); } } async GetLayerByURI(uri) { if (!this.layers.has(uri)) { let fetched = await this.FetchJson(uri); if (fetched instanceof Error) { return new Error(`File with id "${uri}" not found`); } let file = fetched; this.layers.set(uri, file); return file; } return this.layers.get(uri); } }; function MMSet(map, key, value) { if (map.has(key)) { map.get(key)?.push(value); } else { map.set(key, [value]); } } var CycleError = class extends Error {}; function FindRootsOrCycles(nodes) { let dependencies = new Map(); let dependents = new Map(); nodes.forEach((node, path) => { Object.keys(node.inherits).forEach((inheritName) => { MMSet(dependencies, path, node.inherits[inheritName]); MMSet(dependents, node.inherits[inheritName], path); }); Object.keys(node.children).forEach((childName) => { MMSet(dependencies, path, node.children[childName]); MMSet(dependents, node.children[childName], path); }); }); let paths = [...nodes.keys()]; let perm = {}; let temp = {}; function visit(path) { if (perm[path]) return; if (temp[path]) throw new Error(`CYCLE!`); temp[path] = true; let deps = dependencies.get(path); if (deps) { deps.forEach((dep) => visit(dep)); } perm[path] = true; } let roots = new Set(); try { paths.forEach((path) => { if (!dependents.has(path) && path.indexOf("/") === -1) { roots.add(path); } visit(path); }); } catch (e) { return null; } return roots; } function GetHead(path) { return path.split("/")[0]; } function GetTail(path) { let parts = path.split("/"); parts.shift(); return parts.join("/"); } function MakePostCompositionNode(node) { return { node, children: new Map(), attributes: new Map(), }; } function GetChildNodeWithPath(node, path) { if (path === "") return node; let parts = path.split("/"); let child = node.children.get(parts[0]); if (child) { if (parts.length === 1) { return child; } return GetChildNodeWithPath(child, GetTail(path)); } else { return null; } } function FlattenPathToPreCompositionNode(path, inputNodes) { let compositionNode = { path, children: {}, inherits: {}, attributes: {}, }; inputNodes.forEach((node) => { Object.keys(node.children).forEach((childName) => { compositionNode.children[childName] = node.children[childName]; }); Object.keys(node.inherits).forEach((inheritName) => { let ih = node.inherits[inheritName]; if (ih === null) { delete compositionNode.inherits[inheritName]; } else { compositionNode.inherits[inheritName] = ih; } }); Object.keys(node.attributes).forEach((attrName) => { compositionNode.attributes[attrName] = node.attributes[attrName]; }); }); return compositionNode; } function FlattenCompositionInput(input) { let compositionNodes = new Map(); for (let [path, inputNodes] of input) { compositionNodes.set(path, FlattenPathToPreCompositionNode(path, inputNodes)); } return compositionNodes; } function ExpandFirstRootInInput(nodes) { let roots = FindRootsOrCycles(nodes); if (!roots) { throw new CycleError(); } return ComposeNodeFromPath([...roots.values()][0], nodes); } function CreateArtificialRoot(nodes) { let roots = FindRootsOrCycles(nodes); if (!roots) { throw new CycleError(); } let pseudoRoot = { node: "", attributes: new Map(), children: new Map(), }; roots.forEach((root) => { pseudoRoot.children.set(root, ComposeNodeFromPath(root, nodes)); }); return pseudoRoot; } function ComposeNodeFromPath(path, preCompositionNodes) { return ComposeNode(path, MakePostCompositionNode(path), preCompositionNodes); } function ComposeNode(path, postCompositionNode, preCompositionNodes) { let preCompositionNode = preCompositionNodes.get(path); if (preCompositionNode) { AddDataFromPreComposition(preCompositionNode, postCompositionNode, preCompositionNodes); } postCompositionNode.children.forEach((child, name) => { ComposeNode(`${path}/${name}`, child, preCompositionNodes); }); return postCompositionNode; } function AddDataFromPreComposition(input, node, nodes) { Object.values(input.inherits).forEach((inheritPath) => { let classNode = ComposeNodeFromPath(GetHead(inheritPath), nodes); let subnode = GetChildNodeWithPath(classNode, GetTail(inheritPath)); if (!subnode) throw new Error(`Unknown node ${inheritPath}`); subnode.children.forEach((child, childName) => { node.children.set(childName, child); }); for (let [attrID, attr] of subnode.attributes) { node.attributes.set(attrID, attr); } }); Object.entries(input.children).forEach(([childName, child]) => { if (child !== null) { let classNode = ComposeNodeFromPath(GetHead(child), nodes); let subnode = GetChildNodeWithPath(classNode, GetTail(child)); if (!subnode) throw new Error(`Unknown node ${child}`); node.children.set(childName, subnode); } else { node.children.delete(childName); } }); Object.entries(input.attributes).forEach(([attrID, attr]) => { node.attributes.set(attrID, attr); }); } var SchemaValidationError = class extends Error {}; function ValidateAttributeValue(desc, value, path, schemas) { if (desc.optional && value === void 0) { return; } if (desc.inherits) { desc.inherits.forEach((inheritedSchemaID) => { let inheritedSchema = schemas[inheritedSchemaID]; if (!inheritedSchema) { throw new SchemaValidationError(`Unknown inherited schema id "${desc.inherits}"`); } ValidateAttributeValue(inheritedSchema.value, value, path, schemas); }); } if (desc.dataType === "Boolean") { if (typeof value !== "boolean") { throw new SchemaValidationError(`Expected "${value}" to be of type boolean`); } } else if (desc.dataType === "String") { if (typeof value !== "string") { throw new SchemaValidationError(`Expected "${value}" to be of type string`); } } else if (desc.dataType === "DateTime") { if (typeof value !== "string") { throw new SchemaValidationError(`Expected "${value}" to be of type date`); } } else if (desc.dataType === "Enum") { if (typeof value !== "string") { throw new SchemaValidationError(`Expected "${value}" to be of type string`); } let found = desc.enumRestrictions.options.filter((option) => option === value).length === 1; if (!found) { throw new SchemaValidationError(`Expected "${value}" to be one of [${desc.enumRestrictions.options.join(",")}]`); } } else if (desc.dataType === "Integer") { if (typeof value !== "number") { throw new SchemaValidationError(`Expected "${value}" to be of type int`); } } else if (desc.dataType === "Real") { if (typeof value !== "number") { throw new SchemaValidationError(`Expected "${value}" to be of type real`); } } else if (desc.dataType === "Reference") { if (typeof value !== "string") { throw new SchemaValidationError(`Expected "${value}" to be of type string`); } } else if (desc.dataType === "Object") { if (typeof value !== "object") { throw new SchemaValidationError(`Expected "${value}" to be of type object`); } if (desc.objectRestrictions) { Object.keys(desc.objectRestrictions.values).forEach((key) => { let optional = desc.objectRestrictions.values[key].optional; let hasOwn = Object.hasOwn(value, key); if (optional && !hasOwn) return; if (!hasOwn) { throw new SchemaValidationError(`Expected "${value}" to have key ${key}`); } ValidateAttributeValue(desc.objectRestrictions.values[key], value[key], path + "." + key, schemas); }); } } else if (desc.dataType === "Array") { if (!Array.isArray(value)) { throw new SchemaValidationError(`Expected "${value}" to be of type array`); } value.forEach((entry) => { ValidateAttributeValue(desc.arrayRestrictions.value, entry, path + ".<array>.", schemas); }); } else { throw new SchemaValidationError(`Unexpected datatype ${desc.dataType}`); } } function Validate(schemas, inputNodes) { inputNodes.forEach((node) => { Object.keys(node.attributes) .filter((v) => !v.startsWith("__internal")) .forEach((schemaID) => { if (!schemas[schemaID]) { throw new SchemaValidationError(`Missing schema "${schemaID}" referenced by ["${node.path}"].attributes`); } let schema = schemas[schemaID]; let value = node.attributes[schemaID]; try { ValidateAttributeValue(schema.value, value, "", schemas); } catch (e) { if (e instanceof SchemaValidationError) { throw new SchemaValidationError( `Error validating ["${node.path}"].attributes["${schemaID}"]: ${e.message}` ); } else { throw e; } } }); }); } function ToInputNodes(data) { let inputNodes = new Map(); data.forEach((ifcxNode) => { let node = { path: ifcxNode.path, children: ifcxNode.children ? ifcxNode.children : {}, inherits: ifcxNode.inherits ? ifcxNode.inherits : {}, attributes: ifcxNode.attributes ? ifcxNode.attributes : {}, }; MMSet(inputNodes, node.path, node); }); return inputNodes; } function LoadIfcxFile(file, checkSchemas = true, createArtificialRoot = true) { let inputNodes = ToInputNodes(file.data); let compositionNodes = FlattenCompositionInput(inputNodes); try { if (checkSchemas) { Validate(file.schemas, compositionNodes); } } catch (e) { throw e; } if (createArtificialRoot) { return CreateArtificialRoot(compositionNodes); } else { return ExpandFirstRootInInput(compositionNodes); } } function Federate(files) { if (files.length === 0) { throw new Error(`Trying to federate empty set of files`); } let result = { header: files[0].header, schemas: {}, data: [], }; files.forEach((file) => { Object.keys(file.schemas).forEach((schemaID) => (result.schemas[schemaID] = file.schemas[schemaID])); }); files.forEach((file) => { file.data.forEach((node) => result.data.push(node)); }); return Prune(result); } function Collapse(nodes, deleteEmpty = false) { let result = { path: nodes[0].path, children: {}, inherits: {}, attributes: {}, }; nodes.forEach((node) => { Object.keys(node.children).forEach((name) => { result.children[name] = node.children[name]; }); Object.keys(node.inherits).forEach((name) => { result.inherits[name] = node.inherits[name]; }); Object.keys(node.attributes).forEach((name) => { result.attributes[name] = node.attributes[name]; }); }); if (deleteEmpty) { let empty = true; Object.keys(result.children).forEach((name) => { if (result.children[name] !== null) empty = false; }); Object.keys(result.inherits).forEach((name) => { if (result.inherits[name] !== null) empty = false; }); Object.keys(result.attributes).forEach((name) => { if (result.attributes[name] !== null) empty = false; }); if (empty) return null; } return result; } function Prune(file, deleteEmpty = false) { let result = { header: file.header, imports: [], schemas: file.schemas, data: [], }; let inputNodes = ToInputNodes(file.data); inputNodes.forEach((nodes) => { let collapsed = Collapse(nodes, deleteEmpty); if (collapsed) result.data.push({ path: collapsed.path, children: collapsed.children, inherits: collapsed.inherits, attributes: collapsed.attributes, }); }); return result; } var IfcxLayerStack = class { layers; tree; schemas; federated; constructor(layers) { this.layers = layers; this.Compose(); } GetLayerIds() { return this.layers.map((l) => l.header.id); } Compose() { this.federated = Federate(this.layers); this.schemas = this.federated.schemas; this.tree = LoadIfcxFile(this.federated); } GetFullTree() { this.Compose(); return this.tree; } GetFederatedLayer() { return this.federated; } GetSchemas() { return this.schemas; } }; var IfcxLayerStackBuilder = class { provider; mainLayerId = null; constructor(provider) { this.provider = provider; } FromId(id) { this.mainLayerId = id; return this; } async Build() { if (!this.mainLayerId) throw new Error(`no main layer ID specified`); let layers = await this.BuildLayerSet(this.mainLayerId); if (layers instanceof Error) { return layers; } try { return new IfcxLayerStack(layers); } catch (e) { return e; } } async SatisfyDependencies(activeLayer, placed, orderedLayers) { let pending = []; for (const impt of activeLayer.imports) { if (!placed.has(impt.uri)) { let layer = await this.provider.GetLayerByURI(impt.uri); if (layer instanceof Error) { return layer; } pending.push(layer); placed.set(impt.uri, true); } } let temp = []; for (const layer of pending) { temp.push(layer); let layers = await this.SatisfyDependencies(layer, placed, orderedLayers); if (layers instanceof Error) { return layers; } temp.push(...layers); } temp.forEach((t) => orderedLayers.push(t)); return temp; } async BuildLayerSet(activeLayerID) { let activeLayer = await this.provider.GetLayerByURI(activeLayerID); if (activeLayer instanceof Error) { return activeLayer; } let layerSet = [activeLayer]; let placed = new Map(); placed.set(activeLayer.header.id, true); let result = await this.SatisfyDependencies(activeLayer, placed, layerSet); if (result instanceof Error) { return result; } return layerSet; } }; function TreeNodeToComposedObject(path, node, schemas) { let co = { name: path, attributes: {}, children: [], }; node.children.forEach((childNode, childName) => { co.children?.push(TreeNodeToComposedObject(`${path}/${childName}`, childNode, schemas)); }); node.attributes.forEach((attr, attrName) => { if (attr && typeof attr === "object" && !Array.isArray(attr)) { Object.keys(attr).forEach((compname) => { co.attributes[`${attrName}::${compname}`] = attr[compname]; }); } else { let schema = schemas[attrName]; if (schema && schema.value.quantityKind) { let postfix = ""; let quantityKind = schema.value.quantityKind; if (quantityKind === "Length") { postfix = "m"; } else if (quantityKind === "Volume") { postfix = "m" + String.fromCodePoint(179); } co.attributes[attrName] = `${attr} ${postfix}`; } else { co.attributes[attrName] = attr; } } }); if (Object.keys(co.attributes).length === 0) delete co.attributes; return co; } async function compose3(files) { let userDefinedOrder = { header: { ...files[0].header }, imports: files.map((f) => { return { uri: f.header.id }; }), schemas: {}, data: [], }; userDefinedOrder.header.id = "USER_DEF"; let provider = new StackedLayerProvider([ new InMemoryLayerProvider().AddAll([userDefinedOrder, ...files]), new FetchLayerProvider(), ]); let layerStack = await new IfcxLayerStackBuilder(provider).FromId(userDefinedOrder.header.id).Build(); if (layerStack instanceof Error) { throw layerStack; } layerStack.GetFederatedLayer().data.forEach((n, i) => { n.attributes = n.attributes || {}; n.attributes[`__internal_${i}`] = n.path; }); return TreeNodeToComposedObject("", layerStack.GetFullTree(), layerStack.GetSchemas()); } var scene; var camera; var datas = []; var autoCamera = true; var objectMap = {}; var primMap = {}; var envMap; function init() { scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100); camera.up.set(0, 0, 1); camera.position.set(50, 50, 50); camera.lookAt(0, 0, 0); scene.add(camera); return scene; } function HasAttr(node, attrName) { if (!node || !node.attributes) return false; return !!node.attributes[attrName]; } function tryCreateMeshGltfMaterial(path) { for (let p of path) { if (!p.attributes) { continue; } const pbrMetallicRoughness = p.attributes["gltf::material::pbrMetallicRoughness"]; const normalTexture = p.attributes["gltf::material::normalTexture"]; const occlusionTexture = p.attributes["gltf::material::occlusionTexture"]; const emissiveTexture = p.attributes["gltf::material::emissiveTexture"]; const emissiveFactor = p.attributes["gltf::material::emissiveFactor"]; const alphaMode = p.attributes["gltf::material::alphaMode"]; const alphaCutoff = p.attributes["gltf::material::alphaCutoff"]; const doubleSided = p.attributes["gltf::material::doubleSided"]; if ( !pbrMetallicRoughness && !normalTexture && !occlusionTexture && !emissiveTexture && !emissiveFactor && !alphaMode && !alphaCutoff && !doubleSided ) { continue; } let material = new THREE.MeshStandardMaterial(); material.color = new THREE.Color(1, 1, 1); material.metalness = 1; material.roughness = 1; if (pbrMetallicRoughness) { let baseColorFactor = pbrMetallicRoughness["baseColorFactor"]; if (baseColorFactor) { material.color = new THREE.Color(baseColorFactor[0], baseColorFactor[1], baseColorFactor[2]); } let metallicFactor = pbrMetallicRoughness["metallicFactor"]; if (metallicFactor !== void 0) { material.metalness = metallicFactor; } let roughnessFactor = pbrMetallicRoughness["roughnessFactor"]; if (roughnessFactor !== void 0) { material.roughness = roughnessFactor; } } material.envMap = envMap; material.needsUpdate = true; material.envMapRotation = new THREE.Euler(0.5 * Math.PI, 0, 0); return material; } return void 0; } function createMaterialFromParent(path) { let material = { color: new THREE.Color(0.6, 0.6, 0.6), transparent: false, opacity: 1, }; for (let p of path) { const color = p.attributes ? p.attributes["bsi::ifc::presentation::diffuseColor"] : null; if (color) { material.color = new THREE.Color(...color); const opacity = p.attributes["bsi::ifc::presentation::opacity"]; if (opacity) { material.transparent = true; material.opacity = opacity; } break; } } return material; } function createCurveFromJson(path) { let points = new Float32Array(path[0].attributes["usd::usdgeom::basiscurves::points"].flat()); const geometry = new THREE.BufferGeometry(); geometry.setAttribute("position", new THREE.BufferAttribute(points, 3)); const material = createMaterialFromParent(path); let lineMaterial = new THREE.LineBasicMaterial({ ...material }); lineMaterial.color.multiplyScalar(0.8); return new THREE.Line(geometry, lineMaterial); } function createMeshFromJson(path) { let points = new Float32Array(path[0].attributes["usd::usdgeom::mesh::points"].flat()); let indices = new Uint16Array(path[0].attributes["usd::usdgeom::mesh::faceVertexIndices"]); const geometry = new THREE.BufferGeometry(); geometry.setAttribute("position", new THREE.BufferAttribute(points, 3)); geometry.setIndex(new THREE.BufferAttribute(indices, 1)); geometry.computeVertexNormals(); var meshMaterial; let gltfPbrMaterial = tryCreateMeshGltfMaterial(path); if (gltfPbrMaterial) { meshMaterial = gltfPbrMaterial; } else { const m = createMaterialFromParent(path); meshMaterial = new THREE.MeshLambertMaterial({ ...m }); } return new THREE.Mesh(geometry, meshMaterial); } function createPointsFromJsonPcdBase64(path) { const base64_string = path[0].attributes["pcd::base64"]; const decoded = atob(base64_string); const len = decoded.length; const bytes = new Uint8Array(len); for (let i = 0; i < len; i++) { bytes[i] = decoded.charCodeAt(i); } const loader = new PCDLoader(); const points = loader.parse(bytes.buffer); points.material.sizeAttenuation = false; points.material.size = 2; return points; } function createPoints(geometry, withColors) { const material = new THREE.PointsMaterial(); material.sizeAttenuation = false; material.fog = true; material.size = 5; material.color = new THREE.Color(withColors ? 16777215 : 0); if (withColors) { material.vertexColors = true; } return new THREE.Points(geometry, material); } function createPointsFromJsonArray(path) { const geometry = new THREE.BufferGeometry(); const positions = new Float32Array(path[0].attributes["points::array::positions"].flat()); geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3)); const colors = path[0].attributes["points::array::colors"]; if (colors) { const colors_ = new Float32Array(colors.flat()); geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors_, 3)); } return createPoints(geometry, colors); } function base64ToArrayBuffer(str) { let binary; try { binary = atob(str); } catch (e) { throw new Error("base64 encoded string is invalid"); } const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; ++i) { bytes[i] = binary.charCodeAt(i); } return bytes.buffer; } function createPointsFromJsonPositionBase64(path) { const geometry = new THREE.BufferGeometry(); const positions_base64 = path[0].attributes["points::base64::positions"]; const positions_bytes = base64ToArrayBuffer(positions_base64); if (!positions_bytes) { return null; } const positions = new Float32Array(positions_bytes); geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3)); const colors_base64 = path[0].attributes["points::base64::colors"]; if (colors_base64) { const colors_bytes = base64ToArrayBuffer(colors_base64); if (colors_bytes) { const colors = new Float32Array(colors_bytes); geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3)); } } return createPoints(geometry, colors_base64); } function traverseTree(path, parent, pathMapping) { const node = path[0]; let elem = new THREE.Group(); if (HasAttr(node, "usd::usdgeom::visibility::visibility")) { if (node.attributes["usd::usdgeom::visibility::visibility"] === "invisible") { return; } } else if (HasAttr(node, "usd::usdgeom::mesh::points")) { elem = createMeshFromJson(path); } else if (HasAttr(node, "usd::usdgeom::basiscurves::points")) { elem = createCurveFromJson(path); } else if (HasAttr(node, "pcd::base64")) { elem = createPointsFromJsonPcdBase64(path); } else if (HasAttr(node, "points::array::positions")) { elem = createPointsFromJsonArray(path); } else if (HasAttr(node, "points::base64::positions")) { elem = createPointsFromJsonPositionBase64(path); } objectMap[node.name] = elem; primMap[node.name] = node; elem.userData.path = node.name; for (let path2 of Object.entries(node.attributes || {}) .filter(([k, _]) => k.startsWith("__internal_")) .map(([_, v]) => v)) { (pathMapping[String(path2)] = pathMapping[String(path2)] || []).push(node.name); } parent.add(elem); if (path.length > 1) { elem.matrixAutoUpdate = false; let matrixNode = node.attributes && node.attributes["usd::xformop::transform"] ? node.attributes["usd::xformop::transform"].flat() : null; if (matrixNode) { let matrix = new THREE.Matrix4(); matrix.set(...matrixNode); matrix.transpose(); elem.matrix = matrix; } } (node.children || []).forEach((child) => traverseTree([child, ...path], elem || parent, pathMapping)); } async function composeAndRender() { if (scene) { scene.children = []; } objectMap = {}; primMap = {}; if (datas.length === 0) { return; } let tree = null; let dataArray = datas.map((arr) => arr[1]); tree = await compose3(dataArray); if (!tree) { console.error("No result from composition"); return; } if (!scene) { init(); } let pathMapping = {}; traverseTree([tree], scene, pathMapping); if (autoCamera) { const boundingBox = new THREE.Box3(); boundingBox.setFromObject(scene); if (!boundingBox.isEmpty()) { let avg = boundingBox.min.clone().add(boundingBox.max).multiplyScalar(0.5); let ext = boundingBox.max.clone().sub(boundingBox.min).length(); camera.position.copy(avg.clone().add(new THREE.Vector3(1, 1, 1).normalize().multiplyScalar(ext))); camera.far = ext * 3; camera.updateProjectionMatrix(); autoCamera = false; } } } async function parse(m, name) { datas.push([name, m]); await composeAndRender(); return scene; } function clear() { scene = undefined; datas.length = 0; autoCamera = true; } class IFCXLoader extends three.Loader { load(url, onLoad, onProgress, onError) { const manager = this.manager; manager.itemStart(url); const _onLoad = (scene) => { onLoad(scene); manager.itemEnd(url); }; const _onError = (e) => { if (onError) onError(e); else console.error(e); manager.itemError(url); manager.itemEnd(url); }; const loader = new three.FileLoader(this.manager); loader.setPath(this.path); loader.setResponseType("json"); loader.setRequestHeader(this.requestHeader); loader.setWithCredentials(this.withCredentials); loader.load(url, (json) => this.parse(json, _onLoad, _onError), onProgress, onError); } parse(json, onLoad, onError) { parse(json) .then((scene) => onLoad(scene)) .catch((err) => onError(err)) .finally(() => clear()); } } class IFCXFileLoader extends viewerThree.Loader { constructor(viewer) { super(); this.viewer = viewer; } isSupport(file, format) { return ((typeof file === "string" || file instanceof globalThis.File || file instanceof ArrayBuffer) && /(ifcx)$/i.test(format)); } async load(file, format, params = {}) { const manager = new viewerThree.GLTFLoadingManager(file, params); const loader = new IFCXLoader(manager); loader.setPath(manager.path); loader.setCrossOrigin(params.crossOrigin || loader.crossOrigin); loader.setWithCredentials(params.withCredentials || loader.withCredentials); const progress = (event) => { const { lengthComputable, loaded, total } = event; const progress = lengthComputable ? loaded / total : 1; this.viewer.emitEvent({ type: "geometryprogress", data: progress, file }); }; const scene = await loader.loadAsync(manager.fileURL, progress); if (!this.viewer.scene) return this; let handle = 1; scene.traverse((object) => { object.userData = { handle: handle + "", ...object.userData }; handle++; }); const modelImpl = new viewerThree.ModelImpl(scene); modelImpl.id = params.modelId || this.extractFileName(file); this.viewer.scene.add(scene); this.viewer.models.push(modelImpl); this.viewer.syncOptions(); this.viewer.syncOverlay(); this.viewer.emitEvent({ type: "databasechunk", data: scene, file }); this.viewer.update(true); return this; } } class IFCXCloudLoader extends viewerThree.Loader { constructor(viewer) { super(); this.viewer = viewer; } isSupport(file) { return (typeof file === "object" && typeof file.type === "string" && typeof file.download === "function" && /.ifcx$/i.test(file.type)); } async load(file) { const progress = (progress) => { this.viewer.emitEvent({ type: "geometryprogress", data: progress, file }); }; const arrayBuffer = await file.download(progress, this.abortController.signal); if (!this.viewer.scene) return this; const textDecoder = new TextDecoder(); const json = JSON.parse(textDecoder.decode(arrayBuffer)); const scene = await parse(json); clear(); let handle = 1; scene.traverse((object) => { object.userData = { handle: handle + "", ...object.userData }; handle++; }); const modelImpl = new viewerThree.ModelImpl(scene); modelImpl.id = file.id; this.viewer.scene.add(scene); this.viewer.models.push(modelImpl); this.viewer.syncOptions(); this.viewer.syncOverlay(); this.viewer.emitEvent({ type: "databasechunk", data: scene, file }); this.viewer.update(true); return this; } } viewerThree.loaders.registerLoader("ifcx-file", (viewer) => new IFCXFileLoader(viewer)); viewerThree.loaders.registerLoader("ifcx-cloud", (viewer) => new IFCXCloudLoader(viewer)); })); //# sourceMappingURL=IFCXLoader.js.map