@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
JavaScript
///////////////////////////////////////////////////////////////////////////////
// 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