diff --git a/package.json b/package.json index 7fb6bc3..59d4588 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "uuid": "^9.0.1" }, "peerDependencies": { - "bim-fragment": "1.4.1", + "bim-fragment": "1.4.2", "three": "^0.160.1", "web-ifc": "^0.0.51" }, @@ -47,7 +47,7 @@ "@types/uuid": "9.0.8", "@typescript-eslint/eslint-plugin": "^4.27.0", "@typescript-eslint/parser": "^4.27.0", - "bim-fragment": "1.4.1", + "bim-fragment": "1.4.2", "cpy-cli": "^3.1.1", "eslint": "^7.28.0", "eslint-config-airbnb-base": "^14.2.1", diff --git a/resources/openbim-clay.js b/resources/openbim-clay.js index f5f66b1..30dc1fb 100644 --- a/resources/openbim-clay.js +++ b/resources/openbim-clay.js @@ -1,5 +1,5 @@ import * as THREE from 'three'; -import { Raycaster as Raycaster$1, Vector3, Quaternion, Euler, Matrix4, Object3D, MeshBasicMaterial, LineBasicMaterial, CylinderGeometry, BoxGeometry, BufferGeometry, Float32BufferAttribute, Mesh, OctahedronGeometry, Line, SphereGeometry, TorusGeometry, PlaneGeometry, DoubleSide } from 'three'; +import { Raycaster as Raycaster$1, Vector3, Quaternion, Euler, Matrix4, Object3D, MeshBasicMaterial, LineBasicMaterial, CylinderGeometry, BoxGeometry, BufferGeometry, Float32BufferAttribute, Mesh, OctahedronGeometry, Line, SphereGeometry, TorusGeometry, PlaneGeometry, DoubleSide, BufferAttribute, Vector2, Plane, Line3, Triangle, Sphere, Box3, BackSide, FrontSide, Ray } from 'three'; class BufferManager { /** The current size of the buffers. */ @@ -288,10 +288,10 @@ class Raycaster { set trackMouse(active) { this._trackMouse = active; if (active) { - window.addEventListener("mousemove", this.getMousePosition); + simpleWindow.addEventListener("mousemove", this.getMousePosition); } else { - window.removeEventListener("mousemove", this.getMousePosition); + simpleWindow.removeEventListener("mousemove", this.getMousePosition); } } constructor() { @@ -77110,7 +77110,7 @@ function stringToBytes(str) { } const DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; -const URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8'; +const URL$1 = '6ba7b811-9dad-11d1-80b4-00c04fd430c8'; function v35(name, version, hashfunc) { function generateUUID(value, namespace, buf, offset) { var _namespace; @@ -77157,7 +77157,7 @@ function v35(name, version, hashfunc) { generateUUID.DNS = DNS; - generateUUID.URL = URL; + generateUUID.URL = URL$1; return generateUUID; } @@ -77503,52 +77503,6165 @@ function sha1(bytes) { v35('v5', 0x50, sha1); -class ClayObject { - constructor(model) { - this.model = model; +class FragmentMesh extends THREE.InstancedMesh { + constructor(geometry, material, count, fragment) { + super(geometry, material, count); + if (!Array.isArray(material)) { + material = [material]; + } + this.material = material; + if (!geometry.index) { + throw new Error("The geometry for fragments must be indexed!"); + } + this.geometry = geometry; + this.fragment = fragment; + const size = geometry.index.count; + if (!geometry.groups.length) { + geometry.groups.push({ + start: 0, + count: size, + materialIndex: 0, + }); + } + } + exportData() { + const position = this.geometry.attributes.position.array; + const normal = this.geometry.attributes.normal.array; + const index = Array.from(this.geometry.index.array); + const groups = []; + for (const group of this.geometry.groups) { + const index = group.materialIndex || 0; + const { start, count } = group; + groups.push(start, count, index); + } + const materials = []; + if (Array.isArray(this.material)) { + for (const material of this.material) { + const opacity = material.opacity; + const transparent = material.transparent ? 1 : 0; + const color = new THREE.Color(material.color).toArray(); + materials.push(opacity, transparent, ...color); + } + } + const matrices = Array.from(this.instanceMatrix.array); + let colors; + if (this.instanceColor !== null) { + colors = Array.from(this.instanceColor.array); + } + else { + colors = []; + } + return { + position, + normal, + index, + groups, + materials, + matrices, + colors, + }; + } +} + +// Split strategy constants +const CENTER = 0; +const AVERAGE = 1; +const SAH = 2; +const CONTAINED = 2; + +// SAH cost constants +// TODO: hone these costs more. The relative difference between them should be the +// difference in measured time to perform a triangle intersection vs traversing +// bounds. +const TRIANGLE_INTERSECT_COST = 1.25; +const TRAVERSAL_COST = 1; + + +// Build constants +const BYTES_PER_NODE = 6 * 4 + 4 + 4; +const IS_LEAFNODE_FLAG = 0xFFFF; + +// EPSILON for computing floating point error during build +// https://en.wikipedia.org/wiki/Machine_epsilon#Values_for_standard_hardware_floating_point_arithmetics +const FLOAT32_EPSILON = Math.pow( 2, - 24 ); + +const SKIP_GENERATION = Symbol( 'SKIP_GENERATION' ); + +function getVertexCount( geo ) { + + return geo.index ? geo.index.count : geo.attributes.position.count; + +} + +function getTriCount( geo ) { + + return getVertexCount( geo ) / 3; + +} + +function getIndexArray( vertexCount, BufferConstructor = ArrayBuffer ) { + + if ( vertexCount > 65535 ) { + + return new Uint32Array( new BufferConstructor( 4 * vertexCount ) ); + + } else { + + return new Uint16Array( new BufferConstructor( 2 * vertexCount ) ); + + } + +} + +// ensures that an index is present on the geometry +function ensureIndex( geo, options ) { + + if ( ! geo.index ) { + + const vertexCount = geo.attributes.position.count; + const BufferConstructor = options.useSharedArrayBuffer ? SharedArrayBuffer : ArrayBuffer; + const index = getIndexArray( vertexCount, BufferConstructor ); + geo.setIndex( new BufferAttribute( index, 1 ) ); + + for ( let i = 0; i < vertexCount; i ++ ) { + + index[ i ] = i; + + } + + } + +} + +// Computes the set of { offset, count } ranges which need independent BVH roots. Each +// region in the geometry index that belongs to a different set of material groups requires +// a separate BVH root, so that triangles indices belonging to one group never get swapped +// with triangle indices belongs to another group. For example, if the groups were like this: +// +// [-------------------------------------------------------------] +// |__________________| +// g0 = [0, 20] |______________________||_____________________| +// g1 = [16, 40] g2 = [41, 60] +// +// we would need four BVH roots: [0, 15], [16, 20], [21, 40], [41, 60]. +function getFullGeometryRange( geo ) { + + const triCount = getTriCount( geo ); + const drawRange = geo.drawRange; + const start = drawRange.start / 3; + const end = ( drawRange.start + drawRange.count ) / 3; + + const offset = Math.max( 0, start ); + const count = Math.min( triCount, end ) - offset; + return [ { + offset: Math.floor( offset ), + count: Math.floor( count ), + } ]; + +} + +function getRootIndexRanges( geo ) { + + if ( ! geo.groups || ! geo.groups.length ) { + + return getFullGeometryRange( geo ); + + } + + const ranges = []; + const rangeBoundaries = new Set(); + + const drawRange = geo.drawRange; + const drawRangeStart = drawRange.start / 3; + const drawRangeEnd = ( drawRange.start + drawRange.count ) / 3; + for ( const group of geo.groups ) { + + const groupStart = group.start / 3; + const groupEnd = ( group.start + group.count ) / 3; + rangeBoundaries.add( Math.max( drawRangeStart, groupStart ) ); + rangeBoundaries.add( Math.min( drawRangeEnd, groupEnd ) ); + + } + + + // note that if you don't pass in a comparator, it sorts them lexicographically as strings :-( + const sortedBoundaries = Array.from( rangeBoundaries.values() ).sort( ( a, b ) => a - b ); + for ( let i = 0; i < sortedBoundaries.length - 1; i ++ ) { + + const start = sortedBoundaries[ i ]; + const end = sortedBoundaries[ i + 1 ]; + + ranges.push( { + offset: Math.floor( start ), + count: Math.floor( end - start ), + } ); + + } + + return ranges; + +} + +function hasGroupGaps( geometry ) { + + if ( geometry.groups.length === 0 ) { + + return false; + + } + + const vertexCount = getTriCount( geometry ); + const groups = getRootIndexRanges( geometry ) + .sort( ( a, b ) => a.offset - b.offset ); + + const finalGroup = groups[ groups.length - 1 ]; + finalGroup.count = Math.min( vertexCount - finalGroup.offset, finalGroup.count ); + + let total = 0; + groups.forEach( ( { count } ) => total += count ); + return vertexCount !== total; + +} + +function arrayToBox( nodeIndex32, array, target ) { + + target.min.x = array[ nodeIndex32 ]; + target.min.y = array[ nodeIndex32 + 1 ]; + target.min.z = array[ nodeIndex32 + 2 ]; + + target.max.x = array[ nodeIndex32 + 3 ]; + target.max.y = array[ nodeIndex32 + 4 ]; + target.max.z = array[ nodeIndex32 + 5 ]; + + return target; + +} + +function makeEmptyBounds( target ) { + + target[ 0 ] = target[ 1 ] = target[ 2 ] = Infinity; + target[ 3 ] = target[ 4 ] = target[ 5 ] = - Infinity; + +} + +function getLongestEdgeIndex( bounds ) { + + let splitDimIdx = - 1; + let splitDist = - Infinity; + + for ( let i = 0; i < 3; i ++ ) { + + const dist = bounds[ i + 3 ] - bounds[ i ]; + if ( dist > splitDist ) { + + splitDist = dist; + splitDimIdx = i; + + } + + } + + return splitDimIdx; + +} + +// copies bounds a into bounds b +function copyBounds( source, target ) { + + target.set( source ); + +} + +// sets bounds target to the union of bounds a and b +function unionBounds( a, b, target ) { + + let aVal, bVal; + for ( let d = 0; d < 3; d ++ ) { + + const d3 = d + 3; + + // set the minimum values + aVal = a[ d ]; + bVal = b[ d ]; + target[ d ] = aVal < bVal ? aVal : bVal; + + // set the max values + aVal = a[ d3 ]; + bVal = b[ d3 ]; + target[ d3 ] = aVal > bVal ? aVal : bVal; + + } + +} + +// expands the given bounds by the provided triangle bounds +function expandByTriangleBounds( startIndex, triangleBounds, bounds ) { + + for ( let d = 0; d < 3; d ++ ) { + + const tCenter = triangleBounds[ startIndex + 2 * d ]; + const tHalf = triangleBounds[ startIndex + 2 * d + 1 ]; + + const tMin = tCenter - tHalf; + const tMax = tCenter + tHalf; + + if ( tMin < bounds[ d ] ) { + + bounds[ d ] = tMin; + + } + + if ( tMax > bounds[ d + 3 ] ) { + + bounds[ d + 3 ] = tMax; + + } + + } + +} + +// compute bounds surface area +function computeSurfaceArea( bounds ) { + + const d0 = bounds[ 3 ] - bounds[ 0 ]; + const d1 = bounds[ 4 ] - bounds[ 1 ]; + const d2 = bounds[ 5 ] - bounds[ 2 ]; + + return 2 * ( d0 * d1 + d1 * d2 + d2 * d0 ); + +} + +// computes the union of the bounds of all of the given triangles and puts the resulting box in target. If +// centroidTarget is provided then a bounding box is computed for the centroids of the triangles, as well. +// These are computed together to avoid redundant accesses to bounds array. +function getBounds( triangleBounds, offset, count, target, centroidTarget = null ) { + + let minx = Infinity; + let miny = Infinity; + let minz = Infinity; + let maxx = - Infinity; + let maxy = - Infinity; + let maxz = - Infinity; + + let cminx = Infinity; + let cminy = Infinity; + let cminz = Infinity; + let cmaxx = - Infinity; + let cmaxy = - Infinity; + let cmaxz = - Infinity; + + const includeCentroid = centroidTarget !== null; + for ( let i = offset * 6, end = ( offset + count ) * 6; i < end; i += 6 ) { + + const cx = triangleBounds[ i + 0 ]; + const hx = triangleBounds[ i + 1 ]; + const lx = cx - hx; + const rx = cx + hx; + if ( lx < minx ) minx = lx; + if ( rx > maxx ) maxx = rx; + if ( includeCentroid && cx < cminx ) cminx = cx; + if ( includeCentroid && cx > cmaxx ) cmaxx = cx; + + const cy = triangleBounds[ i + 2 ]; + const hy = triangleBounds[ i + 3 ]; + const ly = cy - hy; + const ry = cy + hy; + if ( ly < miny ) miny = ly; + if ( ry > maxy ) maxy = ry; + if ( includeCentroid && cy < cminy ) cminy = cy; + if ( includeCentroid && cy > cmaxy ) cmaxy = cy; + + const cz = triangleBounds[ i + 4 ]; + const hz = triangleBounds[ i + 5 ]; + const lz = cz - hz; + const rz = cz + hz; + if ( lz < minz ) minz = lz; + if ( rz > maxz ) maxz = rz; + if ( includeCentroid && cz < cminz ) cminz = cz; + if ( includeCentroid && cz > cmaxz ) cmaxz = cz; + + } + + target[ 0 ] = minx; + target[ 1 ] = miny; + target[ 2 ] = minz; + + target[ 3 ] = maxx; + target[ 4 ] = maxy; + target[ 5 ] = maxz; + + if ( includeCentroid ) { + + centroidTarget[ 0 ] = cminx; + centroidTarget[ 1 ] = cminy; + centroidTarget[ 2 ] = cminz; + + centroidTarget[ 3 ] = cmaxx; + centroidTarget[ 4 ] = cmaxy; + centroidTarget[ 5 ] = cmaxz; + + } + +} + +// A stand alone function for retrieving the centroid bounds. +function getCentroidBounds( triangleBounds, offset, count, centroidTarget ) { + + let cminx = Infinity; + let cminy = Infinity; + let cminz = Infinity; + let cmaxx = - Infinity; + let cmaxy = - Infinity; + let cmaxz = - Infinity; + + for ( let i = offset * 6, end = ( offset + count ) * 6; i < end; i += 6 ) { + + const cx = triangleBounds[ i + 0 ]; + if ( cx < cminx ) cminx = cx; + if ( cx > cmaxx ) cmaxx = cx; + + const cy = triangleBounds[ i + 2 ]; + if ( cy < cminy ) cminy = cy; + if ( cy > cmaxy ) cmaxy = cy; + + const cz = triangleBounds[ i + 4 ]; + if ( cz < cminz ) cminz = cz; + if ( cz > cmaxz ) cmaxz = cz; + + } + + centroidTarget[ 0 ] = cminx; + centroidTarget[ 1 ] = cminy; + centroidTarget[ 2 ] = cminz; + + centroidTarget[ 3 ] = cmaxx; + centroidTarget[ 4 ] = cmaxy; + centroidTarget[ 5 ] = cmaxz; + +} + + +// precomputes the bounding box for each triangle; required for quickly calculating tree splits. +// result is an array of size tris.length * 6 where triangle i maps to a +// [x_center, x_delta, y_center, y_delta, z_center, z_delta] tuple starting at index i * 6, +// representing the center and half-extent in each dimension of triangle i +function computeTriangleBounds( geo, fullBounds ) { + + // clear the bounds to empty + makeEmptyBounds( fullBounds ); + + const posAttr = geo.attributes.position; + const index = geo.index ? geo.index.array : null; + const triCount = getTriCount( geo ); + const triangleBounds = new Float32Array( triCount * 6 ); + const normalized = posAttr.normalized; + + // used for non-normalized positions + const posArr = posAttr.array; + + // support for an interleaved position buffer + const bufferOffset = posAttr.offset || 0; + let stride = 3; + if ( posAttr.isInterleavedBufferAttribute ) { + + stride = posAttr.data.stride; + + } + + // used for normalized positions + const getters = [ 'getX', 'getY', 'getZ' ]; + + for ( let tri = 0; tri < triCount; tri ++ ) { + + const tri3 = tri * 3; + const tri6 = tri * 6; + + let ai = tri3 + 0; + let bi = tri3 + 1; + let ci = tri3 + 2; + + if ( index ) { + + ai = index[ ai ]; + bi = index[ bi ]; + ci = index[ ci ]; + + } + + // we add the stride and offset here since we access the array directly + // below for the sake of performance + if ( ! normalized ) { + + ai = ai * stride + bufferOffset; + bi = bi * stride + bufferOffset; + ci = ci * stride + bufferOffset; + + } + + for ( let el = 0; el < 3; el ++ ) { + + let a, b, c; + + if ( normalized ) { + + a = posAttr[ getters[ el ] ]( ai ); + b = posAttr[ getters[ el ] ]( bi ); + c = posAttr[ getters[ el ] ]( ci ); + + } else { + + a = posArr[ ai + el ]; + b = posArr[ bi + el ]; + c = posArr[ ci + el ]; + + } + + let min = a; + if ( b < min ) min = b; + if ( c < min ) min = c; + + let max = a; + if ( b > max ) max = b; + if ( c > max ) max = c; + + // Increase the bounds size by float32 epsilon to avoid precision errors when + // converting to 32 bit float. Scale the epsilon by the size of the numbers being + // worked with. + const halfExtents = ( max - min ) / 2; + const el2 = el * 2; + triangleBounds[ tri6 + el2 + 0 ] = min + halfExtents; + triangleBounds[ tri6 + el2 + 1 ] = halfExtents + ( Math.abs( min ) + halfExtents ) * FLOAT32_EPSILON; + + if ( min < fullBounds[ el ] ) fullBounds[ el ] = min; + if ( max > fullBounds[ el + 3 ] ) fullBounds[ el + 3 ] = max; + + } + + } + + return triangleBounds; + +} + +const BIN_COUNT = 32; +const binsSort = ( a, b ) => a.candidate - b.candidate; +const sahBins = new Array( BIN_COUNT ).fill().map( () => { + + return { + + count: 0, + bounds: new Float32Array( 6 ), + rightCacheBounds: new Float32Array( 6 ), + leftCacheBounds: new Float32Array( 6 ), + candidate: 0, + + }; + +} ); +const leftBounds = new Float32Array( 6 ); + +function getOptimalSplit( nodeBoundingData, centroidBoundingData, triangleBounds, offset, count, strategy ) { + + let axis = - 1; + let pos = 0; + + // Center + if ( strategy === CENTER ) { + + axis = getLongestEdgeIndex( centroidBoundingData ); + if ( axis !== - 1 ) { + + pos = ( centroidBoundingData[ axis ] + centroidBoundingData[ axis + 3 ] ) / 2; + + } + + } else if ( strategy === AVERAGE ) { + + axis = getLongestEdgeIndex( nodeBoundingData ); + if ( axis !== - 1 ) { + + pos = getAverage( triangleBounds, offset, count, axis ); + + } + + } else if ( strategy === SAH ) { + + const rootSurfaceArea = computeSurfaceArea( nodeBoundingData ); + let bestCost = TRIANGLE_INTERSECT_COST * count; + + // iterate over all axes + const cStart = offset * 6; + const cEnd = ( offset + count ) * 6; + for ( let a = 0; a < 3; a ++ ) { + + const axisLeft = centroidBoundingData[ a ]; + const axisRight = centroidBoundingData[ a + 3 ]; + const axisLength = axisRight - axisLeft; + const binWidth = axisLength / BIN_COUNT; + + // If we have fewer triangles than we're planning to split then just check all + // the triangle positions because it will be faster. + if ( count < BIN_COUNT / 4 ) { + + // initialize the bin candidates + const truncatedBins = [ ...sahBins ]; + truncatedBins.length = count; + + // set the candidates + let b = 0; + for ( let c = cStart; c < cEnd; c += 6, b ++ ) { + + const bin = truncatedBins[ b ]; + bin.candidate = triangleBounds[ c + 2 * a ]; + bin.count = 0; + + const { + bounds, + leftCacheBounds, + rightCacheBounds, + } = bin; + for ( let d = 0; d < 3; d ++ ) { + + rightCacheBounds[ d ] = Infinity; + rightCacheBounds[ d + 3 ] = - Infinity; + + leftCacheBounds[ d ] = Infinity; + leftCacheBounds[ d + 3 ] = - Infinity; + + bounds[ d ] = Infinity; + bounds[ d + 3 ] = - Infinity; + + } + + expandByTriangleBounds( c, triangleBounds, bounds ); + + } + + truncatedBins.sort( binsSort ); + + // remove redundant splits + let splitCount = count; + for ( let bi = 0; bi < splitCount; bi ++ ) { + + const bin = truncatedBins[ bi ]; + while ( bi + 1 < splitCount && truncatedBins[ bi + 1 ].candidate === bin.candidate ) { + + truncatedBins.splice( bi + 1, 1 ); + splitCount --; + + } + + } + + // find the appropriate bin for each triangle and expand the bounds. + for ( let c = cStart; c < cEnd; c += 6 ) { + + const center = triangleBounds[ c + 2 * a ]; + for ( let bi = 0; bi < splitCount; bi ++ ) { + + const bin = truncatedBins[ bi ]; + if ( center >= bin.candidate ) { + + expandByTriangleBounds( c, triangleBounds, bin.rightCacheBounds ); + + } else { + + expandByTriangleBounds( c, triangleBounds, bin.leftCacheBounds ); + bin.count ++; + + } + + } + + } + + // expand all the bounds + for ( let bi = 0; bi < splitCount; bi ++ ) { + + const bin = truncatedBins[ bi ]; + const leftCount = bin.count; + const rightCount = count - bin.count; + + // check the cost of this split + const leftBounds = bin.leftCacheBounds; + const rightBounds = bin.rightCacheBounds; + + let leftProb = 0; + if ( leftCount !== 0 ) { + + leftProb = computeSurfaceArea( leftBounds ) / rootSurfaceArea; + + } + + let rightProb = 0; + if ( rightCount !== 0 ) { + + rightProb = computeSurfaceArea( rightBounds ) / rootSurfaceArea; + + } + + const cost = TRAVERSAL_COST + TRIANGLE_INTERSECT_COST * ( + leftProb * leftCount + rightProb * rightCount + ); + + if ( cost < bestCost ) { + + axis = a; + bestCost = cost; + pos = bin.candidate; + + } + + } + + } else { + + // reset the bins + for ( let i = 0; i < BIN_COUNT; i ++ ) { + + const bin = sahBins[ i ]; + bin.count = 0; + bin.candidate = axisLeft + binWidth + i * binWidth; + + const bounds = bin.bounds; + for ( let d = 0; d < 3; d ++ ) { + + bounds[ d ] = Infinity; + bounds[ d + 3 ] = - Infinity; + + } + + } + + // iterate over all center positions + for ( let c = cStart; c < cEnd; c += 6 ) { + + const triCenter = triangleBounds[ c + 2 * a ]; + const relativeCenter = triCenter - axisLeft; + + // in the partition function if the centroid lies on the split plane then it is + // considered to be on the right side of the split + let binIndex = ~ ~ ( relativeCenter / binWidth ); + if ( binIndex >= BIN_COUNT ) binIndex = BIN_COUNT - 1; + + const bin = sahBins[ binIndex ]; + bin.count ++; + + expandByTriangleBounds( c, triangleBounds, bin.bounds ); + + } + + // cache the unioned bounds from right to left so we don't have to regenerate them each time + const lastBin = sahBins[ BIN_COUNT - 1 ]; + copyBounds( lastBin.bounds, lastBin.rightCacheBounds ); + for ( let i = BIN_COUNT - 2; i >= 0; i -- ) { + + const bin = sahBins[ i ]; + const nextBin = sahBins[ i + 1 ]; + unionBounds( bin.bounds, nextBin.rightCacheBounds, bin.rightCacheBounds ); + + } + + let leftCount = 0; + for ( let i = 0; i < BIN_COUNT - 1; i ++ ) { + + const bin = sahBins[ i ]; + const binCount = bin.count; + const bounds = bin.bounds; + + const nextBin = sahBins[ i + 1 ]; + const rightBounds = nextBin.rightCacheBounds; + + // don't do anything with the bounds if the new bounds have no triangles + if ( binCount !== 0 ) { + + if ( leftCount === 0 ) { + + copyBounds( bounds, leftBounds ); + + } else { + + unionBounds( bounds, leftBounds, leftBounds ); + + } + + } + + leftCount += binCount; + + // check the cost of this split + let leftProb = 0; + let rightProb = 0; + + if ( leftCount !== 0 ) { + + leftProb = computeSurfaceArea( leftBounds ) / rootSurfaceArea; + + } + + const rightCount = count - leftCount; + if ( rightCount !== 0 ) { + + rightProb = computeSurfaceArea( rightBounds ) / rootSurfaceArea; + + } + + const cost = TRAVERSAL_COST + TRIANGLE_INTERSECT_COST * ( + leftProb * leftCount + rightProb * rightCount + ); + + if ( cost < bestCost ) { + + axis = a; + bestCost = cost; + pos = bin.candidate; + + } + + } + + } + + } + + } else { + + console.warn( `MeshBVH: Invalid build strategy value ${ strategy } used.` ); + + } + + return { axis, pos }; + +} + +// returns the average coordinate on the specified axis of the all the provided triangles +function getAverage( triangleBounds, offset, count, axis ) { + + let avg = 0; + for ( let i = offset, end = offset + count; i < end; i ++ ) { + + avg += triangleBounds[ i * 6 + axis * 2 ]; + + } + + return avg / count; + +} + +class MeshBVHNode { + + constructor() { + + // internal nodes have boundingData, left, right, and splitAxis + // leaf nodes have offset and count (referring to primitives in the mesh geometry) + + } + +} + +/********************************************************/ +/* This file is generated from "sortUtils.template.js". */ +/********************************************************/ +// reorders `tris` such that for `count` elements after `offset`, elements on the left side of the split +// will be on the left and elements on the right side of the split will be on the right. returns the index +// of the first element on the right side, or offset + count if there are no elements on the right side. +function partition( indirectBuffer, index, triangleBounds, offset, count, split ) { + + let left = offset; + let right = offset + count - 1; + const pos = split.pos; + const axisOffset = split.axis * 2; + + // hoare partitioning, see e.g. https://en.wikipedia.org/wiki/Quicksort#Hoare_partition_scheme + while ( true ) { + + while ( left <= right && triangleBounds[ left * 6 + axisOffset ] < pos ) { + + left ++; + + } + + // if a triangle center lies on the partition plane it is considered to be on the right side + while ( left <= right && triangleBounds[ right * 6 + axisOffset ] >= pos ) { + + right --; + + } + + if ( left < right ) { + + // we need to swap all of the information associated with the triangles at index + // left and right; that's the verts in the geometry index, the bounds, + // and perhaps the SAH planes + + for ( let i = 0; i < 3; i ++ ) { + + let t0 = index[ left * 3 + i ]; + index[ left * 3 + i ] = index[ right * 3 + i ]; + index[ right * 3 + i ] = t0; + + } + + + // swap bounds + for ( let i = 0; i < 6; i ++ ) { + + let tb = triangleBounds[ left * 6 + i ]; + triangleBounds[ left * 6 + i ] = triangleBounds[ right * 6 + i ]; + triangleBounds[ right * 6 + i ] = tb; + + } + + left ++; + right --; + + } else { + + return left; + + } + + } + +} + +/********************************************************/ +/* This file is generated from "sortUtils.template.js". */ +/********************************************************/ +// reorders `tris` such that for `count` elements after `offset`, elements on the left side of the split +// will be on the left and elements on the right side of the split will be on the right. returns the index +// of the first element on the right side, or offset + count if there are no elements on the right side. +function partition_indirect( indirectBuffer, index, triangleBounds, offset, count, split ) { + + let left = offset; + let right = offset + count - 1; + const pos = split.pos; + const axisOffset = split.axis * 2; + + // hoare partitioning, see e.g. https://en.wikipedia.org/wiki/Quicksort#Hoare_partition_scheme + while ( true ) { + + while ( left <= right && triangleBounds[ left * 6 + axisOffset ] < pos ) { + + left ++; + + } + + // if a triangle center lies on the partition plane it is considered to be on the right side + while ( left <= right && triangleBounds[ right * 6 + axisOffset ] >= pos ) { + + right --; + + } + + if ( left < right ) { + + // we need to swap all of the information associated with the triangles at index + // left and right; that's the verts in the geometry index, the bounds, + // and perhaps the SAH planes + let t = indirectBuffer[ left ]; + indirectBuffer[ left ] = indirectBuffer[ right ]; + indirectBuffer[ right ] = t; + + + // swap bounds + for ( let i = 0; i < 6; i ++ ) { + + let tb = triangleBounds[ left * 6 + i ]; + triangleBounds[ left * 6 + i ] = triangleBounds[ right * 6 + i ]; + triangleBounds[ right * 6 + i ] = tb; + + } + + left ++; + right --; + + } else { + + return left; + + } + + } + +} + +function generateIndirectBuffer( geometry, useSharedArrayBuffer ) { + + const triCount = ( geometry.index ? geometry.index.count : geometry.attributes.position.count ) / 3; + const useUint32 = triCount > 2 ** 16; + const byteCount = useUint32 ? 4 : 2; + + const buffer = useSharedArrayBuffer ? new SharedArrayBuffer( triCount * byteCount ) : new ArrayBuffer( triCount * byteCount ); + const indirectBuffer = useUint32 ? new Uint32Array( buffer ) : new Uint16Array( buffer ); + for ( let i = 0, l = indirectBuffer.length; i < l; i ++ ) { + + indirectBuffer[ i ] = i; + + } + + return indirectBuffer; + +} + +function buildTree( bvh, options ) { + + // Compute the full bounds of the geometry at the same time as triangle bounds because + // we'll need it for the root bounds in the case with no groups and it should be fast here. + // We can't use the geometry bounding box if it's available because it may be out of date. + const geometry = bvh.geometry; + const indexArray = geometry.index ? geometry.index.array : null; + const maxDepth = options.maxDepth; + const verbose = options.verbose; + const maxLeafTris = options.maxLeafTris; + const strategy = options.strategy; + const onProgress = options.onProgress; + const totalTriangles = getTriCount( geometry ); + const indirectBuffer = bvh._indirectBuffer; + let reachedMaxDepth = false; + + const fullBounds = new Float32Array( 6 ); + const cacheCentroidBoundingData = new Float32Array( 6 ); + const triangleBounds = computeTriangleBounds( geometry, fullBounds ); + const partionFunc = options.indirect ? partition_indirect : partition; + + const roots = []; + const ranges = options.indirect ? getFullGeometryRange( geometry ) : getRootIndexRanges( geometry ); + + if ( ranges.length === 1 ) { + + const range = ranges[ 0 ]; + const root = new MeshBVHNode(); + root.boundingData = fullBounds; + getCentroidBounds( triangleBounds, range.offset, range.count, cacheCentroidBoundingData ); + + splitNode( root, range.offset, range.count, cacheCentroidBoundingData ); + roots.push( root ); + + } else { + + for ( let range of ranges ) { + + const root = new MeshBVHNode(); + root.boundingData = new Float32Array( 6 ); + getBounds( triangleBounds, range.offset, range.count, root.boundingData, cacheCentroidBoundingData ); + + splitNode( root, range.offset, range.count, cacheCentroidBoundingData ); + roots.push( root ); + + } + + } + + return roots; + + function triggerProgress( trianglesProcessed ) { + + if ( onProgress ) { + + onProgress( trianglesProcessed / totalTriangles ); + + } + + } + + // either recursively splits the given node, creating left and right subtrees for it, or makes it a leaf node, + // recording the offset and count of its triangles and writing them into the reordered geometry index. + function splitNode( node, offset, count, centroidBoundingData = null, depth = 0 ) { + + if ( ! reachedMaxDepth && depth >= maxDepth ) { + + reachedMaxDepth = true; + if ( verbose ) { + + console.warn( `MeshBVH: Max depth of ${ maxDepth } reached when generating BVH. Consider increasing maxDepth.` ); + console.warn( geometry ); + + } + + } + + // early out if we've met our capacity + if ( count <= maxLeafTris || depth >= maxDepth ) { + + triggerProgress( offset + count ); + node.offset = offset; + node.count = count; + return node; + + } + + // Find where to split the volume + const split = getOptimalSplit( node.boundingData, centroidBoundingData, triangleBounds, offset, count, strategy ); + if ( split.axis === - 1 ) { + + triggerProgress( offset + count ); + node.offset = offset; + node.count = count; + return node; + + } + + const splitOffset = partionFunc( indirectBuffer, indexArray, triangleBounds, offset, count, split ); + + // create the two new child nodes + if ( splitOffset === offset || splitOffset === offset + count ) { + + triggerProgress( offset + count ); + node.offset = offset; + node.count = count; + + } else { + + node.splitAxis = split.axis; + + // create the left child and compute its bounding box + const left = new MeshBVHNode(); + const lstart = offset; + const lcount = splitOffset - offset; + node.left = left; + left.boundingData = new Float32Array( 6 ); + + getBounds( triangleBounds, lstart, lcount, left.boundingData, cacheCentroidBoundingData ); + splitNode( left, lstart, lcount, cacheCentroidBoundingData, depth + 1 ); + + // repeat for right + const right = new MeshBVHNode(); + const rstart = splitOffset; + const rcount = count - lcount; + node.right = right; + right.boundingData = new Float32Array( 6 ); + + getBounds( triangleBounds, rstart, rcount, right.boundingData, cacheCentroidBoundingData ); + splitNode( right, rstart, rcount, cacheCentroidBoundingData, depth + 1 ); + + } + + return node; + + } + +} + +function buildPackedTree( bvh, options ) { + + const geometry = bvh.geometry; + if ( options.indirect ) { + + bvh._indirectBuffer = generateIndirectBuffer( geometry, options.useSharedArrayBuffer ); + + if ( hasGroupGaps( geometry ) && ! options.verbose ) { + + console.warn( + 'MeshBVH: Provided geometry contains groups that do not fully span the vertex contents while using the "indirect" option. ' + + 'BVH may incorrectly report intersections on unrendered portions of the geometry.' + ); + + } + + } + + if ( ! bvh._indirectBuffer ) { + + ensureIndex( geometry, options ); + + } + + // boundingData : 6 float32 + // right / offset : 1 uint32 + // splitAxis / isLeaf + count : 1 uint32 / 2 uint16 + const roots = buildTree( bvh, options ); + + let float32Array; + let uint32Array; + let uint16Array; + const packedRoots = []; + const BufferConstructor = options.useSharedArrayBuffer ? SharedArrayBuffer : ArrayBuffer; + for ( let i = 0; i < roots.length; i ++ ) { + + const root = roots[ i ]; + let nodeCount = countNodes( root ); + + const buffer = new BufferConstructor( BYTES_PER_NODE * nodeCount ); + float32Array = new Float32Array( buffer ); + uint32Array = new Uint32Array( buffer ); + uint16Array = new Uint16Array( buffer ); + populateBuffer( 0, root ); + packedRoots.push( buffer ); + + } + + bvh._roots = packedRoots; + return; + + function countNodes( node ) { + + if ( node.count ) { + + return 1; + + } else { + + return 1 + countNodes( node.left ) + countNodes( node.right ); + + } + + } + + function populateBuffer( byteOffset, node ) { + + const stride4Offset = byteOffset / 4; + const stride2Offset = byteOffset / 2; + const isLeaf = ! ! node.count; + const boundingData = node.boundingData; + for ( let i = 0; i < 6; i ++ ) { + + float32Array[ stride4Offset + i ] = boundingData[ i ]; + + } + + if ( isLeaf ) { + + const offset = node.offset; + const count = node.count; + uint32Array[ stride4Offset + 6 ] = offset; + uint16Array[ stride2Offset + 14 ] = count; + uint16Array[ stride2Offset + 15 ] = IS_LEAFNODE_FLAG; + return byteOffset + BYTES_PER_NODE; + + } else { + + const left = node.left; + const right = node.right; + const splitAxis = node.splitAxis; + + let nextUnusedPointer; + nextUnusedPointer = populateBuffer( byteOffset + BYTES_PER_NODE, left ); + + if ( ( nextUnusedPointer / 4 ) > Math.pow( 2, 32 ) ) { + + throw new Error( 'MeshBVH: Cannot store child pointer greater than 32 bits.' ); + + } + + uint32Array[ stride4Offset + 6 ] = nextUnusedPointer / 4; + nextUnusedPointer = populateBuffer( nextUnusedPointer, right ); + + uint32Array[ stride4Offset + 7 ] = splitAxis; + return nextUnusedPointer; + + } + + } + +} + +class SeparatingAxisBounds { + + constructor() { + + this.min = Infinity; + this.max = - Infinity; + + } + + setFromPointsField( points, field ) { + + let min = Infinity; + let max = - Infinity; + for ( let i = 0, l = points.length; i < l; i ++ ) { + + const p = points[ i ]; + const val = p[ field ]; + min = val < min ? val : min; + max = val > max ? val : max; + + } + + this.min = min; + this.max = max; + + } + + setFromPoints( axis, points ) { + + let min = Infinity; + let max = - Infinity; + for ( let i = 0, l = points.length; i < l; i ++ ) { + + const p = points[ i ]; + const val = axis.dot( p ); + min = val < min ? val : min; + max = val > max ? val : max; + + } + + this.min = min; + this.max = max; + + } + + isSeparated( other ) { + + return this.min > other.max || other.min > this.max; + + } + +} + +SeparatingAxisBounds.prototype.setFromBox = ( function () { + + const p = new Vector3(); + return function setFromBox( axis, box ) { + + const boxMin = box.min; + const boxMax = box.max; + let min = Infinity; + let max = - Infinity; + for ( let x = 0; x <= 1; x ++ ) { + + for ( let y = 0; y <= 1; y ++ ) { + + for ( let z = 0; z <= 1; z ++ ) { + + p.x = boxMin.x * x + boxMax.x * ( 1 - x ); + p.y = boxMin.y * y + boxMax.y * ( 1 - y ); + p.z = boxMin.z * z + boxMax.z * ( 1 - z ); + + const val = axis.dot( p ); + min = Math.min( val, min ); + max = Math.max( val, max ); + + } + + } + + } + + this.min = min; + this.max = max; + + }; + +} )(); + +const closestPointLineToLine = ( function () { + + // https://github.com/juj/MathGeoLib/blob/master/src/Geometry/Line.cpp#L56 + const dir1 = new Vector3(); + const dir2 = new Vector3(); + const v02 = new Vector3(); + return function closestPointLineToLine( l1, l2, result ) { + + const v0 = l1.start; + const v10 = dir1; + const v2 = l2.start; + const v32 = dir2; + + v02.subVectors( v0, v2 ); + dir1.subVectors( l1.end, l1.start ); + dir2.subVectors( l2.end, l2.start ); + + // float d0232 = v02.Dot(v32); + const d0232 = v02.dot( v32 ); + + // float d3210 = v32.Dot(v10); + const d3210 = v32.dot( v10 ); + + // float d3232 = v32.Dot(v32); + const d3232 = v32.dot( v32 ); + + // float d0210 = v02.Dot(v10); + const d0210 = v02.dot( v10 ); + + // float d1010 = v10.Dot(v10); + const d1010 = v10.dot( v10 ); + + // float denom = d1010*d3232 - d3210*d3210; + const denom = d1010 * d3232 - d3210 * d3210; + + let d, d2; + if ( denom !== 0 ) { + + d = ( d0232 * d3210 - d0210 * d3232 ) / denom; + + } else { + + d = 0; + + } + + d2 = ( d0232 + d * d3210 ) / d3232; + + result.x = d; + result.y = d2; + + }; + +} )(); + +const closestPointsSegmentToSegment = ( function () { + + // https://github.com/juj/MathGeoLib/blob/master/src/Geometry/LineSegment.cpp#L187 + const paramResult = new Vector2(); + const temp1 = new Vector3(); + const temp2 = new Vector3(); + return function closestPointsSegmentToSegment( l1, l2, target1, target2 ) { + + closestPointLineToLine( l1, l2, paramResult ); + + let d = paramResult.x; + let d2 = paramResult.y; + if ( d >= 0 && d <= 1 && d2 >= 0 && d2 <= 1 ) { + + l1.at( d, target1 ); + l2.at( d2, target2 ); + + return; + + } else if ( d >= 0 && d <= 1 ) { + + // Only d2 is out of bounds. + if ( d2 < 0 ) { + + l2.at( 0, target2 ); + + } else { + + l2.at( 1, target2 ); + + } + + l1.closestPointToPoint( target2, true, target1 ); + return; + + } else if ( d2 >= 0 && d2 <= 1 ) { + + // Only d is out of bounds. + if ( d < 0 ) { + + l1.at( 0, target1 ); + + } else { + + l1.at( 1, target1 ); + + } + + l2.closestPointToPoint( target1, true, target2 ); + return; + + } else { + + // Both u and u2 are out of bounds. + let p; + if ( d < 0 ) { + + p = l1.start; + + } else { + + p = l1.end; + + } + + let p2; + if ( d2 < 0 ) { + + p2 = l2.start; + + } else { + + p2 = l2.end; + + } + + const closestPoint = temp1; + const closestPoint2 = temp2; + l1.closestPointToPoint( p2, true, temp1 ); + l2.closestPointToPoint( p, true, temp2 ); + + if ( closestPoint.distanceToSquared( p2 ) <= closestPoint2.distanceToSquared( p ) ) { + + target1.copy( closestPoint ); + target2.copy( p2 ); + return; + + } else { + + target1.copy( p ); + target2.copy( closestPoint2 ); + return; + + } + + } + + }; + +} )(); + + +const sphereIntersectTriangle = ( function () { + + // https://stackoverflow.com/questions/34043955/detect-collision-between-sphere-and-triangle-in-three-js + const closestPointTemp = new Vector3(); + const projectedPointTemp = new Vector3(); + const planeTemp = new Plane(); + const lineTemp = new Line3(); + return function sphereIntersectTriangle( sphere, triangle ) { + + const { radius, center } = sphere; + const { a, b, c } = triangle; + + // phase 1 + lineTemp.start = a; + lineTemp.end = b; + const closestPoint1 = lineTemp.closestPointToPoint( center, true, closestPointTemp ); + if ( closestPoint1.distanceTo( center ) <= radius ) return true; + + lineTemp.start = a; + lineTemp.end = c; + const closestPoint2 = lineTemp.closestPointToPoint( center, true, closestPointTemp ); + if ( closestPoint2.distanceTo( center ) <= radius ) return true; + + lineTemp.start = b; + lineTemp.end = c; + const closestPoint3 = lineTemp.closestPointToPoint( center, true, closestPointTemp ); + if ( closestPoint3.distanceTo( center ) <= radius ) return true; + + // phase 2 + const plane = triangle.getPlane( planeTemp ); + const dp = Math.abs( plane.distanceToPoint( center ) ); + if ( dp <= radius ) { + + const pp = plane.projectPoint( center, projectedPointTemp ); + const cp = triangle.containsPoint( pp ); + if ( cp ) return true; + + } + + return false; + + }; + +} )(); + +const ZERO_EPSILON = 1e-15; +function isNearZero( value ) { + + return Math.abs( value ) < ZERO_EPSILON; + +} + +class ExtendedTriangle extends Triangle { + + constructor( ...args ) { + + super( ...args ); + + this.isExtendedTriangle = true; + this.satAxes = new Array( 4 ).fill().map( () => new Vector3() ); + this.satBounds = new Array( 4 ).fill().map( () => new SeparatingAxisBounds() ); + this.points = [ this.a, this.b, this.c ]; + this.sphere = new Sphere(); + this.plane = new Plane(); + this.needsUpdate = true; + + } + + intersectsSphere( sphere ) { + + return sphereIntersectTriangle( sphere, this ); + + } + + update() { + + const a = this.a; + const b = this.b; + const c = this.c; + const points = this.points; + + const satAxes = this.satAxes; + const satBounds = this.satBounds; + + const axis0 = satAxes[ 0 ]; + const sab0 = satBounds[ 0 ]; + this.getNormal( axis0 ); + sab0.setFromPoints( axis0, points ); + + const axis1 = satAxes[ 1 ]; + const sab1 = satBounds[ 1 ]; + axis1.subVectors( a, b ); + sab1.setFromPoints( axis1, points ); + + const axis2 = satAxes[ 2 ]; + const sab2 = satBounds[ 2 ]; + axis2.subVectors( b, c ); + sab2.setFromPoints( axis2, points ); + + const axis3 = satAxes[ 3 ]; + const sab3 = satBounds[ 3 ]; + axis3.subVectors( c, a ); + sab3.setFromPoints( axis3, points ); + + this.sphere.setFromPoints( this.points ); + this.plane.setFromNormalAndCoplanarPoint( axis0, a ); + this.needsUpdate = false; + + } + +} + +ExtendedTriangle.prototype.closestPointToSegment = ( function () { + + const point1 = new Vector3(); + const point2 = new Vector3(); + const edge = new Line3(); + + return function distanceToSegment( segment, target1 = null, target2 = null ) { + + const { start, end } = segment; + const points = this.points; + let distSq; + let closestDistanceSq = Infinity; + + // check the triangle edges + for ( let i = 0; i < 3; i ++ ) { + + const nexti = ( i + 1 ) % 3; + edge.start.copy( points[ i ] ); + edge.end.copy( points[ nexti ] ); + + closestPointsSegmentToSegment( edge, segment, point1, point2 ); + + distSq = point1.distanceToSquared( point2 ); + if ( distSq < closestDistanceSq ) { + + closestDistanceSq = distSq; + if ( target1 ) target1.copy( point1 ); + if ( target2 ) target2.copy( point2 ); + + } + + } + + // check end points + this.closestPointToPoint( start, point1 ); + distSq = start.distanceToSquared( point1 ); + if ( distSq < closestDistanceSq ) { + + closestDistanceSq = distSq; + if ( target1 ) target1.copy( point1 ); + if ( target2 ) target2.copy( start ); + + } + + this.closestPointToPoint( end, point1 ); + distSq = end.distanceToSquared( point1 ); + if ( distSq < closestDistanceSq ) { + + closestDistanceSq = distSq; + if ( target1 ) target1.copy( point1 ); + if ( target2 ) target2.copy( end ); + + } + + return Math.sqrt( closestDistanceSq ); + + }; + +} )(); + +ExtendedTriangle.prototype.intersectsTriangle = ( function () { + + const saTri2 = new ExtendedTriangle(); + const arr1 = new Array( 3 ); + const arr2 = new Array( 3 ); + const cachedSatBounds = new SeparatingAxisBounds(); + const cachedSatBounds2 = new SeparatingAxisBounds(); + const cachedAxis = new Vector3(); + const dir = new Vector3(); + const dir1 = new Vector3(); + const dir2 = new Vector3(); + const tempDir = new Vector3(); + const edge = new Line3(); + const edge1 = new Line3(); + const edge2 = new Line3(); + const tempPoint = new Vector3(); + + function triIntersectPlane( tri, plane, targetEdge ) { + + // find the edge that intersects the other triangle plane + const points = tri.points; + let count = 0; + let startPointIntersection = - 1; + for ( let i = 0; i < 3; i ++ ) { + + const { start, end } = edge; + start.copy( points[ i ] ); + end.copy( points[ ( i + 1 ) % 3 ] ); + edge.delta( dir ); + + const startIntersects = isNearZero( plane.distanceToPoint( start ) ); + if ( isNearZero( plane.normal.dot( dir ) ) && startIntersects ) { + + // if the edge lies on the plane then take the line + targetEdge.copy( edge ); + count = 2; + break; + + } + + // check if the start point is near the plane because "intersectLine" is not robust to that case + const doesIntersect = plane.intersectLine( edge, tempPoint ); + if ( ! doesIntersect && startIntersects ) { + + tempPoint.copy( start ); + + } + + // ignore the end point + if ( ( doesIntersect || startIntersects ) && ! isNearZero( tempPoint.distanceTo( end ) ) ) { + + if ( count <= 1 ) { + + // assign to the start or end point and save which index was snapped to + // the start point if necessary + const point = count === 1 ? targetEdge.start : targetEdge.end; + point.copy( tempPoint ); + if ( startIntersects ) { + + startPointIntersection = count; + + } + + } else if ( count >= 2 ) { + + // if we're here that means that there must have been one point that had + // snapped to the start point so replace it here + const point = startPointIntersection === 1 ? targetEdge.start : targetEdge.end; + point.copy( tempPoint ); + count = 2; + break; + + } + + count ++; + if ( count === 2 && startPointIntersection === - 1 ) { + + break; + + } + + } + + } + + return count; + + } + + // TODO: If the triangles are coplanar and intersecting the target is nonsensical. It should at least + // be a line contained by both triangles if not a different special case somehow represented in the return result. + return function intersectsTriangle( other, target = null, suppressLog = false ) { + + if ( this.needsUpdate ) { + + this.update(); + + } + + if ( ! other.isExtendedTriangle ) { + + saTri2.copy( other ); + saTri2.update(); + other = saTri2; + + } else if ( other.needsUpdate ) { + + other.update(); + + } + + const plane1 = this.plane; + const plane2 = other.plane; + + if ( Math.abs( plane1.normal.dot( plane2.normal ) ) > 1.0 - 1e-10 ) { + + // perform separating axis intersection test only for coplanar triangles + const satBounds1 = this.satBounds; + const satAxes1 = this.satAxes; + arr2[ 0 ] = other.a; + arr2[ 1 ] = other.b; + arr2[ 2 ] = other.c; + for ( let i = 0; i < 4; i ++ ) { + + const sb = satBounds1[ i ]; + const sa = satAxes1[ i ]; + cachedSatBounds.setFromPoints( sa, arr2 ); + if ( sb.isSeparated( cachedSatBounds ) ) return false; + + } + + const satBounds2 = other.satBounds; + const satAxes2 = other.satAxes; + arr1[ 0 ] = this.a; + arr1[ 1 ] = this.b; + arr1[ 2 ] = this.c; + for ( let i = 0; i < 4; i ++ ) { + + const sb = satBounds2[ i ]; + const sa = satAxes2[ i ]; + cachedSatBounds.setFromPoints( sa, arr1 ); + if ( sb.isSeparated( cachedSatBounds ) ) return false; + + } + + // check crossed axes + for ( let i = 0; i < 4; i ++ ) { + + const sa1 = satAxes1[ i ]; + for ( let i2 = 0; i2 < 4; i2 ++ ) { + + const sa2 = satAxes2[ i2 ]; + cachedAxis.crossVectors( sa1, sa2 ); + cachedSatBounds.setFromPoints( cachedAxis, arr1 ); + cachedSatBounds2.setFromPoints( cachedAxis, arr2 ); + if ( cachedSatBounds.isSeparated( cachedSatBounds2 ) ) return false; + + } + + } + + if ( target ) { + + // TODO find two points that intersect on the edges and make that the result + if ( ! suppressLog ) { + + console.warn( 'ExtendedTriangle.intersectsTriangle: Triangles are coplanar which does not support an output edge. Setting edge to 0, 0, 0.' ); + + } + + target.start.set( 0, 0, 0 ); + target.end.set( 0, 0, 0 ); + + } + + return true; + + } else { + + // find the edge that intersects the other triangle plane + const count1 = triIntersectPlane( this, plane2, edge1 ); + if ( count1 === 1 && other.containsPoint( edge1.end ) ) { + + if ( target ) { + + target.start.copy( edge1.end ); + target.end.copy( edge1.end ); + + } + + return true; + + } else if ( count1 !== 2 ) { + + return false; + + } + + // find the other triangles edge that intersects this plane + const count2 = triIntersectPlane( other, plane1, edge2 ); + if ( count2 === 1 && this.containsPoint( edge2.end ) ) { + + if ( target ) { + + target.start.copy( edge2.end ); + target.end.copy( edge2.end ); + + } + + return true; + + } else if ( count2 !== 2 ) { + + return false; + + } + + // find swap the second edge so both lines are running the same direction + edge1.delta( dir1 ); + edge2.delta( dir2 ); + + if ( dir1.dot( dir2 ) < 0 ) { + + let tmp = edge2.start; + edge2.start = edge2.end; + edge2.end = tmp; + + } + + // check if the edges are overlapping + const s1 = edge1.start.dot( dir1 ); + const e1 = edge1.end.dot( dir1 ); + const s2 = edge2.start.dot( dir1 ); + const e2 = edge2.end.dot( dir1 ); + const separated1 = e1 < s2; + const separated2 = s1 < e2; + + if ( s1 !== e2 && s2 !== e1 && separated1 === separated2 ) { + + return false; + + } + + // assign the target output + if ( target ) { + + tempDir.subVectors( edge1.start, edge2.start ); + if ( tempDir.dot( dir1 ) > 0 ) { + + target.start.copy( edge1.start ); + + } else { + + target.start.copy( edge2.start ); + + } + + tempDir.subVectors( edge1.end, edge2.end ); + if ( tempDir.dot( dir1 ) < 0 ) { + + target.end.copy( edge1.end ); + + } else { + + target.end.copy( edge2.end ); + + } + + } + + return true; + + } + + }; + +} )(); + + +ExtendedTriangle.prototype.distanceToPoint = ( function () { + + const target = new Vector3(); + return function distanceToPoint( point ) { + + this.closestPointToPoint( point, target ); + return point.distanceTo( target ); + + }; + +} )(); + + +ExtendedTriangle.prototype.distanceToTriangle = ( function () { + + const point = new Vector3(); + const point2 = new Vector3(); + const cornerFields = [ 'a', 'b', 'c' ]; + const line1 = new Line3(); + const line2 = new Line3(); + + return function distanceToTriangle( other, target1 = null, target2 = null ) { + + const lineTarget = target1 || target2 ? line1 : null; + if ( this.intersectsTriangle( other, lineTarget ) ) { + + if ( target1 || target2 ) { + + if ( target1 ) lineTarget.getCenter( target1 ); + if ( target2 ) lineTarget.getCenter( target2 ); + + } + + return 0; + + } + + let closestDistanceSq = Infinity; + + // check all point distances + for ( let i = 0; i < 3; i ++ ) { + + let dist; + const field = cornerFields[ i ]; + const otherVec = other[ field ]; + this.closestPointToPoint( otherVec, point ); + + dist = otherVec.distanceToSquared( point ); + + if ( dist < closestDistanceSq ) { + + closestDistanceSq = dist; + if ( target1 ) target1.copy( point ); + if ( target2 ) target2.copy( otherVec ); + + } + + + const thisVec = this[ field ]; + other.closestPointToPoint( thisVec, point ); + + dist = thisVec.distanceToSquared( point ); + + if ( dist < closestDistanceSq ) { + + closestDistanceSq = dist; + if ( target1 ) target1.copy( thisVec ); + if ( target2 ) target2.copy( point ); + + } + + } + + for ( let i = 0; i < 3; i ++ ) { + + const f11 = cornerFields[ i ]; + const f12 = cornerFields[ ( i + 1 ) % 3 ]; + line1.set( this[ f11 ], this[ f12 ] ); + for ( let i2 = 0; i2 < 3; i2 ++ ) { + + const f21 = cornerFields[ i2 ]; + const f22 = cornerFields[ ( i2 + 1 ) % 3 ]; + line2.set( other[ f21 ], other[ f22 ] ); + + closestPointsSegmentToSegment( line1, line2, point, point2 ); + + const dist = point.distanceToSquared( point2 ); + if ( dist < closestDistanceSq ) { + + closestDistanceSq = dist; + if ( target1 ) target1.copy( point ); + if ( target2 ) target2.copy( point2 ); + + } + + } + + } + + return Math.sqrt( closestDistanceSq ); + + }; + +} )(); + +class OrientedBox { + + constructor( min, max, matrix ) { + + this.isOrientedBox = true; + this.min = new Vector3(); + this.max = new Vector3(); + this.matrix = new Matrix4(); + this.invMatrix = new Matrix4(); + this.points = new Array( 8 ).fill().map( () => new Vector3() ); + this.satAxes = new Array( 3 ).fill().map( () => new Vector3() ); + this.satBounds = new Array( 3 ).fill().map( () => new SeparatingAxisBounds() ); + this.alignedSatBounds = new Array( 3 ).fill().map( () => new SeparatingAxisBounds() ); + this.needsUpdate = false; + + if ( min ) this.min.copy( min ); + if ( max ) this.max.copy( max ); + if ( matrix ) this.matrix.copy( matrix ); + + } + + set( min, max, matrix ) { + + this.min.copy( min ); + this.max.copy( max ); + this.matrix.copy( matrix ); + this.needsUpdate = true; + + } + + copy( other ) { + + this.min.copy( other.min ); + this.max.copy( other.max ); + this.matrix.copy( other.matrix ); + this.needsUpdate = true; + + } + +} + +OrientedBox.prototype.update = ( function () { + + return function update() { + + const matrix = this.matrix; + const min = this.min; + const max = this.max; + + const points = this.points; + for ( let x = 0; x <= 1; x ++ ) { + + for ( let y = 0; y <= 1; y ++ ) { + + for ( let z = 0; z <= 1; z ++ ) { + + const i = ( ( 1 << 0 ) * x ) | ( ( 1 << 1 ) * y ) | ( ( 1 << 2 ) * z ); + const v = points[ i ]; + v.x = x ? max.x : min.x; + v.y = y ? max.y : min.y; + v.z = z ? max.z : min.z; + + v.applyMatrix4( matrix ); + + } + + } + + } + + const satBounds = this.satBounds; + const satAxes = this.satAxes; + const minVec = points[ 0 ]; + for ( let i = 0; i < 3; i ++ ) { + + const axis = satAxes[ i ]; + const sb = satBounds[ i ]; + const index = 1 << i; + const pi = points[ index ]; + + axis.subVectors( minVec, pi ); + sb.setFromPoints( axis, points ); + + } + + const alignedSatBounds = this.alignedSatBounds; + alignedSatBounds[ 0 ].setFromPointsField( points, 'x' ); + alignedSatBounds[ 1 ].setFromPointsField( points, 'y' ); + alignedSatBounds[ 2 ].setFromPointsField( points, 'z' ); + + this.invMatrix.copy( this.matrix ).invert(); + this.needsUpdate = false; + + }; + +} )(); + +OrientedBox.prototype.intersectsBox = ( function () { + + const aabbBounds = new SeparatingAxisBounds(); + return function intersectsBox( box ) { + + // TODO: should this be doing SAT against the AABB? + if ( this.needsUpdate ) { + + this.update(); + + } + + const min = box.min; + const max = box.max; + const satBounds = this.satBounds; + const satAxes = this.satAxes; + const alignedSatBounds = this.alignedSatBounds; + + aabbBounds.min = min.x; + aabbBounds.max = max.x; + if ( alignedSatBounds[ 0 ].isSeparated( aabbBounds ) ) return false; + + aabbBounds.min = min.y; + aabbBounds.max = max.y; + if ( alignedSatBounds[ 1 ].isSeparated( aabbBounds ) ) return false; + + aabbBounds.min = min.z; + aabbBounds.max = max.z; + if ( alignedSatBounds[ 2 ].isSeparated( aabbBounds ) ) return false; + + for ( let i = 0; i < 3; i ++ ) { + + const axis = satAxes[ i ]; + const sb = satBounds[ i ]; + aabbBounds.setFromBox( axis, box ); + if ( sb.isSeparated( aabbBounds ) ) return false; + + } + + return true; + + }; + +} )(); + +OrientedBox.prototype.intersectsTriangle = ( function () { + + const saTri = new ExtendedTriangle(); + const pointsArr = new Array( 3 ); + const cachedSatBounds = new SeparatingAxisBounds(); + const cachedSatBounds2 = new SeparatingAxisBounds(); + const cachedAxis = new Vector3(); + return function intersectsTriangle( triangle ) { + + if ( this.needsUpdate ) { + + this.update(); + + } + + if ( ! triangle.isExtendedTriangle ) { + + saTri.copy( triangle ); + saTri.update(); + triangle = saTri; + + } else if ( triangle.needsUpdate ) { + + triangle.update(); + + } + + const satBounds = this.satBounds; + const satAxes = this.satAxes; + + pointsArr[ 0 ] = triangle.a; + pointsArr[ 1 ] = triangle.b; + pointsArr[ 2 ] = triangle.c; + + for ( let i = 0; i < 3; i ++ ) { + + const sb = satBounds[ i ]; + const sa = satAxes[ i ]; + cachedSatBounds.setFromPoints( sa, pointsArr ); + if ( sb.isSeparated( cachedSatBounds ) ) return false; + + } + + const triSatBounds = triangle.satBounds; + const triSatAxes = triangle.satAxes; + const points = this.points; + for ( let i = 0; i < 3; i ++ ) { + + const sb = triSatBounds[ i ]; + const sa = triSatAxes[ i ]; + cachedSatBounds.setFromPoints( sa, points ); + if ( sb.isSeparated( cachedSatBounds ) ) return false; + + } + + // check crossed axes + for ( let i = 0; i < 3; i ++ ) { + + const sa1 = satAxes[ i ]; + for ( let i2 = 0; i2 < 4; i2 ++ ) { + + const sa2 = triSatAxes[ i2 ]; + cachedAxis.crossVectors( sa1, sa2 ); + cachedSatBounds.setFromPoints( cachedAxis, pointsArr ); + cachedSatBounds2.setFromPoints( cachedAxis, points ); + if ( cachedSatBounds.isSeparated( cachedSatBounds2 ) ) return false; + + } + + } + + return true; + + }; + +} )(); + +OrientedBox.prototype.closestPointToPoint = ( function () { + + return function closestPointToPoint( point, target1 ) { + + if ( this.needsUpdate ) { + + this.update(); + + } + + target1 + .copy( point ) + .applyMatrix4( this.invMatrix ) + .clamp( this.min, this.max ) + .applyMatrix4( this.matrix ); + + return target1; + + }; + +} )(); + +OrientedBox.prototype.distanceToPoint = ( function () { + + const target = new Vector3(); + return function distanceToPoint( point ) { + + this.closestPointToPoint( point, target ); + return point.distanceTo( target ); + + }; + +} )(); + +OrientedBox.prototype.distanceToBox = ( function () { + + const xyzFields = [ 'x', 'y', 'z' ]; + const segments1 = new Array( 12 ).fill().map( () => new Line3() ); + const segments2 = new Array( 12 ).fill().map( () => new Line3() ); + + const point1 = new Vector3(); + const point2 = new Vector3(); + + // early out if we find a value below threshold + return function distanceToBox( box, threshold = 0, target1 = null, target2 = null ) { + + if ( this.needsUpdate ) { + + this.update(); + + } + + if ( this.intersectsBox( box ) ) { + + if ( target1 || target2 ) { + + box.getCenter( point2 ); + this.closestPointToPoint( point2, point1 ); + box.closestPointToPoint( point1, point2 ); + + if ( target1 ) target1.copy( point1 ); + if ( target2 ) target2.copy( point2 ); + + } + + return 0; + + } + + const threshold2 = threshold * threshold; + const min = box.min; + const max = box.max; + const points = this.points; + + + // iterate over every edge and compare distances + let closestDistanceSq = Infinity; + + // check over all these points + for ( let i = 0; i < 8; i ++ ) { + + const p = points[ i ]; + point2.copy( p ).clamp( min, max ); + + const dist = p.distanceToSquared( point2 ); + if ( dist < closestDistanceSq ) { + + closestDistanceSq = dist; + if ( target1 ) target1.copy( p ); + if ( target2 ) target2.copy( point2 ); + + if ( dist < threshold2 ) return Math.sqrt( dist ); + + } + + } + + // generate and check all line segment distances + let count = 0; + for ( let i = 0; i < 3; i ++ ) { + + for ( let i1 = 0; i1 <= 1; i1 ++ ) { + + for ( let i2 = 0; i2 <= 1; i2 ++ ) { + + const nextIndex = ( i + 1 ) % 3; + const nextIndex2 = ( i + 2 ) % 3; + + // get obb line segments + const index = i1 << nextIndex | i2 << nextIndex2; + const index2 = 1 << i | i1 << nextIndex | i2 << nextIndex2; + const p1 = points[ index ]; + const p2 = points[ index2 ]; + const line1 = segments1[ count ]; + line1.set( p1, p2 ); + + + // get aabb line segments + const f1 = xyzFields[ i ]; + const f2 = xyzFields[ nextIndex ]; + const f3 = xyzFields[ nextIndex2 ]; + const line2 = segments2[ count ]; + const start = line2.start; + const end = line2.end; + + start[ f1 ] = min[ f1 ]; + start[ f2 ] = i1 ? min[ f2 ] : max[ f2 ]; + start[ f3 ] = i2 ? min[ f3 ] : max[ f2 ]; + + end[ f1 ] = max[ f1 ]; + end[ f2 ] = i1 ? min[ f2 ] : max[ f2 ]; + end[ f3 ] = i2 ? min[ f3 ] : max[ f2 ]; + + count ++; + + } + + } + + } + + // check all the other boxes point + for ( let x = 0; x <= 1; x ++ ) { + + for ( let y = 0; y <= 1; y ++ ) { + + for ( let z = 0; z <= 1; z ++ ) { + + point2.x = x ? max.x : min.x; + point2.y = y ? max.y : min.y; + point2.z = z ? max.z : min.z; + + this.closestPointToPoint( point2, point1 ); + const dist = point2.distanceToSquared( point1 ); + if ( dist < closestDistanceSq ) { + + closestDistanceSq = dist; + if ( target1 ) target1.copy( point1 ); + if ( target2 ) target2.copy( point2 ); + + if ( dist < threshold2 ) return Math.sqrt( dist ); + + } + + } + + } + + } + + for ( let i = 0; i < 12; i ++ ) { + + const l1 = segments1[ i ]; + for ( let i2 = 0; i2 < 12; i2 ++ ) { + + const l2 = segments2[ i2 ]; + closestPointsSegmentToSegment( l1, l2, point1, point2 ); + const dist = point1.distanceToSquared( point2 ); + if ( dist < closestDistanceSq ) { + + closestDistanceSq = dist; + if ( target1 ) target1.copy( point1 ); + if ( target2 ) target2.copy( point2 ); + + if ( dist < threshold2 ) return Math.sqrt( dist ); + + } + + } + + } + + return Math.sqrt( closestDistanceSq ); + + }; + +} )(); + +class PrimitivePool { + + constructor( getNewPrimitive ) { + + this._getNewPrimitive = getNewPrimitive; + this._primitives = []; + + } + + getPrimitive() { + + const primitives = this._primitives; + if ( primitives.length === 0 ) { + + return this._getNewPrimitive(); + + } else { + + return primitives.pop(); + + } + + } + + releasePrimitive( primitive ) { + + this._primitives.push( primitive ); + + } + +} + +class ExtendedTrianglePoolBase extends PrimitivePool { + + constructor() { + + super( () => new ExtendedTriangle() ); + + } + +} + +const ExtendedTrianglePool = /* @__PURE__ */ new ExtendedTrianglePoolBase(); + +function IS_LEAF( n16, uint16Array ) { + + return uint16Array[ n16 + 15 ] === 0xFFFF; + +} + +function OFFSET( n32, uint32Array ) { + + return uint32Array[ n32 + 6 ]; + +} + +function COUNT( n16, uint16Array ) { + + return uint16Array[ n16 + 14 ]; + +} + +function LEFT_NODE( n32 ) { + + return n32 + 8; + +} + +function RIGHT_NODE( n32, uint32Array ) { + + return uint32Array[ n32 + 6 ]; + +} + +function SPLIT_AXIS( n32, uint32Array ) { + + return uint32Array[ n32 + 7 ]; + +} + +function BOUNDING_DATA_INDEX( n32 ) { + + return n32; + +} + +class _BufferStack { + + constructor() { + + this.float32Array = null; + this.uint16Array = null; + this.uint32Array = null; + + const stack = []; + let prevBuffer = null; + this.setBuffer = buffer => { + + if ( prevBuffer ) { + + stack.push( prevBuffer ); + + } + + prevBuffer = buffer; + this.float32Array = new Float32Array( buffer ); + this.uint16Array = new Uint16Array( buffer ); + this.uint32Array = new Uint32Array( buffer ); + + }; + + this.clearBuffer = () => { + + prevBuffer = null; + this.float32Array = null; + this.uint16Array = null; + this.uint32Array = null; + + if ( stack.length !== 0 ) { + + this.setBuffer( stack.pop() ); + + } + + }; + + } + +} + +const BufferStack = new _BufferStack(); + +let _box1, _box2; +const boxStack = []; +const boxPool = /* @__PURE__ */ new PrimitivePool( () => new Box3() ); + +function shapecast( bvh, root, intersectsBounds, intersectsRange, boundsTraverseOrder, byteOffset ) { + + // setup + _box1 = boxPool.getPrimitive(); + _box2 = boxPool.getPrimitive(); + boxStack.push( _box1, _box2 ); + BufferStack.setBuffer( bvh._roots[ root ] ); + + const result = shapecastTraverse( 0, bvh.geometry, intersectsBounds, intersectsRange, boundsTraverseOrder, byteOffset ); + + // cleanup + BufferStack.clearBuffer(); + boxPool.releasePrimitive( _box1 ); + boxPool.releasePrimitive( _box2 ); + boxStack.pop(); + boxStack.pop(); + + const length = boxStack.length; + if ( length > 0 ) { + + _box2 = boxStack[ length - 1 ]; + _box1 = boxStack[ length - 2 ]; + + } + + return result; + +} + +function shapecastTraverse( + nodeIndex32, + geometry, + intersectsBoundsFunc, + intersectsRangeFunc, + nodeScoreFunc = null, + nodeIndexByteOffset = 0, // offset for unique node identifier + depth = 0 +) { + + const { float32Array, uint16Array, uint32Array } = BufferStack; + let nodeIndex16 = nodeIndex32 * 2; + + const isLeaf = IS_LEAF( nodeIndex16, uint16Array ); + if ( isLeaf ) { + + const offset = OFFSET( nodeIndex32, uint32Array ); + const count = COUNT( nodeIndex16, uint16Array ); + arrayToBox( BOUNDING_DATA_INDEX( nodeIndex32 ), float32Array, _box1 ); + return intersectsRangeFunc( offset, count, false, depth, nodeIndexByteOffset + nodeIndex32, _box1 ); + + } else { + + const left = LEFT_NODE( nodeIndex32 ); + const right = RIGHT_NODE( nodeIndex32, uint32Array ); + let c1 = left; + let c2 = right; + + let score1, score2; + let box1, box2; + if ( nodeScoreFunc ) { + + box1 = _box1; + box2 = _box2; + + // bounding data is not offset + arrayToBox( BOUNDING_DATA_INDEX( c1 ), float32Array, box1 ); + arrayToBox( BOUNDING_DATA_INDEX( c2 ), float32Array, box2 ); + + score1 = nodeScoreFunc( box1 ); + score2 = nodeScoreFunc( box2 ); + + if ( score2 < score1 ) { + + c1 = right; + c2 = left; + + const temp = score1; + score1 = score2; + score2 = temp; + + box1 = box2; + // box2 is always set before use below + + } + + } + + // Check box 1 intersection + if ( ! box1 ) { + + box1 = _box1; + arrayToBox( BOUNDING_DATA_INDEX( c1 ), float32Array, box1 ); + + } + + const isC1Leaf = IS_LEAF( c1 * 2, uint16Array ); + const c1Intersection = intersectsBoundsFunc( box1, isC1Leaf, score1, depth + 1, nodeIndexByteOffset + c1 ); + + let c1StopTraversal; + if ( c1Intersection === CONTAINED ) { + + const offset = getLeftOffset( c1 ); + const end = getRightEndOffset( c1 ); + const count = end - offset; + + c1StopTraversal = intersectsRangeFunc( offset, count, true, depth + 1, nodeIndexByteOffset + c1, box1 ); + + } else { + + c1StopTraversal = + c1Intersection && + shapecastTraverse( + c1, + geometry, + intersectsBoundsFunc, + intersectsRangeFunc, + nodeScoreFunc, + nodeIndexByteOffset, + depth + 1 + ); + + } + + if ( c1StopTraversal ) return true; + + // Check box 2 intersection + // cached box2 will have been overwritten by previous traversal + box2 = _box2; + arrayToBox( BOUNDING_DATA_INDEX( c2 ), float32Array, box2 ); + + const isC2Leaf = IS_LEAF( c2 * 2, uint16Array ); + const c2Intersection = intersectsBoundsFunc( box2, isC2Leaf, score2, depth + 1, nodeIndexByteOffset + c2 ); + + let c2StopTraversal; + if ( c2Intersection === CONTAINED ) { + + const offset = getLeftOffset( c2 ); + const end = getRightEndOffset( c2 ); + const count = end - offset; + + c2StopTraversal = intersectsRangeFunc( offset, count, true, depth + 1, nodeIndexByteOffset + c2, box2 ); + + } else { + + c2StopTraversal = + c2Intersection && + shapecastTraverse( + c2, + geometry, + intersectsBoundsFunc, + intersectsRangeFunc, + nodeScoreFunc, + nodeIndexByteOffset, + depth + 1 + ); + + } + + if ( c2StopTraversal ) return true; + + return false; + + // Define these inside the function so it has access to the local variables needed + // when converting to the buffer equivalents + function getLeftOffset( nodeIndex32 ) { + + const { uint16Array, uint32Array } = BufferStack; + let nodeIndex16 = nodeIndex32 * 2; + + // traverse until we find a leaf + while ( ! IS_LEAF( nodeIndex16, uint16Array ) ) { + + nodeIndex32 = LEFT_NODE( nodeIndex32 ); + nodeIndex16 = nodeIndex32 * 2; + + } + + return OFFSET( nodeIndex32, uint32Array ); + + } + + function getRightEndOffset( nodeIndex32 ) { + + const { uint16Array, uint32Array } = BufferStack; + let nodeIndex16 = nodeIndex32 * 2; + + // traverse until we find a leaf + while ( ! IS_LEAF( nodeIndex16, uint16Array ) ) { + + // adjust offset to point to the right node + nodeIndex32 = RIGHT_NODE( nodeIndex32, uint32Array ); + nodeIndex16 = nodeIndex32 * 2; + + } + + // return the end offset of the triangle range + return OFFSET( nodeIndex32, uint32Array ) + COUNT( nodeIndex16, uint16Array ); + + } + + } + +} + +const temp = /* @__PURE__ */ new Vector3(); +const temp1$2 = /* @__PURE__ */ new Vector3(); + +function closestPointToPoint( + bvh, + point, + target = { }, + minThreshold = 0, + maxThreshold = Infinity, +) { + + // early out if under minThreshold + // skip checking if over maxThreshold + // set minThreshold = maxThreshold to quickly check if a point is within a threshold + // returns Infinity if no value found + const minThresholdSq = minThreshold * minThreshold; + const maxThresholdSq = maxThreshold * maxThreshold; + let closestDistanceSq = Infinity; + let closestDistanceTriIndex = null; + bvh.shapecast( + + { + + boundsTraverseOrder: box => { + + temp.copy( point ).clamp( box.min, box.max ); + return temp.distanceToSquared( point ); + + }, + + intersectsBounds: ( box, isLeaf, score ) => { + + return score < closestDistanceSq && score < maxThresholdSq; + + }, + + intersectsTriangle: ( tri, triIndex ) => { + + tri.closestPointToPoint( point, temp ); + const distSq = point.distanceToSquared( temp ); + if ( distSq < closestDistanceSq ) { + + temp1$2.copy( temp ); + closestDistanceSq = distSq; + closestDistanceTriIndex = triIndex; + + } + + if ( distSq < minThresholdSq ) { + + return true; + + } else { + + return false; + + } + + }, + + } + + ); + + if ( closestDistanceSq === Infinity ) return null; + + const closestDistance = Math.sqrt( closestDistanceSq ); + + if ( ! target.point ) target.point = temp1$2.clone(); + else target.point.copy( temp1$2 ); + target.distance = closestDistance, + target.faceIndex = closestDistanceTriIndex; + + return target; + +} + +// Ripped and modified From THREE.js Mesh raycast +// https://github.com/mrdoob/three.js/blob/0aa87c999fe61e216c1133fba7a95772b503eddf/src/objects/Mesh.js#L115 +const _vA = /* @__PURE__ */ new Vector3(); +const _vB = /* @__PURE__ */ new Vector3(); +const _vC = /* @__PURE__ */ new Vector3(); + +const _uvA = /* @__PURE__ */ new Vector2(); +const _uvB = /* @__PURE__ */ new Vector2(); +const _uvC = /* @__PURE__ */ new Vector2(); + +const _normalA = /* @__PURE__ */ new Vector3(); +const _normalB = /* @__PURE__ */ new Vector3(); +const _normalC = /* @__PURE__ */ new Vector3(); + +const _intersectionPoint = /* @__PURE__ */ new Vector3(); +function checkIntersection( ray, pA, pB, pC, point, side ) { + + let intersect; + if ( side === BackSide ) { + + intersect = ray.intersectTriangle( pC, pB, pA, true, point ); + + } else { + + intersect = ray.intersectTriangle( pA, pB, pC, side !== DoubleSide, point ); + + } + + if ( intersect === null ) return null; + + const distance = ray.origin.distanceTo( point ); + + return { + + distance: distance, + point: point.clone(), + + }; + +} + +function checkBufferGeometryIntersection( ray, position, normal, uv, uv1, a, b, c, side ) { + + _vA.fromBufferAttribute( position, a ); + _vB.fromBufferAttribute( position, b ); + _vC.fromBufferAttribute( position, c ); + + const intersection = checkIntersection( ray, _vA, _vB, _vC, _intersectionPoint, side ); + + if ( intersection ) { + + if ( uv ) { + + _uvA.fromBufferAttribute( uv, a ); + _uvB.fromBufferAttribute( uv, b ); + _uvC.fromBufferAttribute( uv, c ); + + intersection.uv = Triangle.getInterpolation( _intersectionPoint, _vA, _vB, _vC, _uvA, _uvB, _uvC, new Vector2() ); + + } + + if ( uv1 ) { + + _uvA.fromBufferAttribute( uv1, a ); + _uvB.fromBufferAttribute( uv1, b ); + _uvC.fromBufferAttribute( uv1, c ); + + intersection.uv1 = Triangle.getInterpolation( _intersectionPoint, _vA, _vB, _vC, _uvA, _uvB, _uvC, new Vector2() ); + + } + + if ( normal ) { + + _normalA.fromBufferAttribute( normal, a ); + _normalB.fromBufferAttribute( normal, b ); + _normalC.fromBufferAttribute( normal, c ); + + intersection.normal = Triangle.getInterpolation( _intersectionPoint, _vA, _vB, _vC, _normalA, _normalB, _normalC, new Vector3() ); + if ( intersection.normal.dot( ray.direction ) > 0 ) { + + intersection.normal.multiplyScalar( - 1 ); + + } + + } + + const face = { + a: a, + b: b, + c: c, + normal: new Vector3(), + materialIndex: 0 + }; + + Triangle.getNormal( _vA, _vB, _vC, face.normal ); + + intersection.face = face; + intersection.faceIndex = a; + + } + + return intersection; + +} + +// https://github.com/mrdoob/three.js/blob/0aa87c999fe61e216c1133fba7a95772b503eddf/src/objects/Mesh.js#L258 +function intersectTri( geo, side, ray, tri, intersections ) { + + const triOffset = tri * 3; + let a = triOffset + 0; + let b = triOffset + 1; + let c = triOffset + 2; + + const index = geo.index; + if ( geo.index ) { + + a = index.getX( a ); + b = index.getX( b ); + c = index.getX( c ); + + } + + const { position, normal, uv, uv1 } = geo.attributes; + const intersection = checkBufferGeometryIntersection( ray, position, normal, uv, uv1, a, b, c, side ); + + if ( intersection ) { + + intersection.faceIndex = tri; + if ( intersections ) intersections.push( intersection ); + return intersection; + + } + + return null; + +} + +// sets the vertices of triangle `tri` with the 3 vertices after i +function setTriangle( tri, i, index, pos ) { + + const ta = tri.a; + const tb = tri.b; + const tc = tri.c; + + let i0 = i; + let i1 = i + 1; + let i2 = i + 2; + if ( index ) { + + i0 = index.getX( i0 ); + i1 = index.getX( i1 ); + i2 = index.getX( i2 ); + + } + + ta.x = pos.getX( i0 ); + ta.y = pos.getY( i0 ); + ta.z = pos.getZ( i0 ); + + tb.x = pos.getX( i1 ); + tb.y = pos.getY( i1 ); + tb.z = pos.getZ( i1 ); + + tc.x = pos.getX( i2 ); + tc.y = pos.getY( i2 ); + tc.z = pos.getZ( i2 ); + +} + +/*************************************************************/ +/* This file is generated from "iterationUtils.template.js". */ +/*************************************************************/ +/* eslint-disable indent */ + +function intersectTris( bvh, side, ray, offset, count, intersections ) { + + const { geometry, _indirectBuffer } = bvh; + for ( let i = offset, end = offset + count; i < end; i ++ ) { + + + intersectTri( geometry, side, ray, i, intersections ); + + + } + +} + +function intersectClosestTri( bvh, side, ray, offset, count ) { + + const { geometry, _indirectBuffer } = bvh; + let dist = Infinity; + let res = null; + for ( let i = offset, end = offset + count; i < end; i ++ ) { + + let intersection; + + intersection = intersectTri( geometry, side, ray, i ); + + + if ( intersection && intersection.distance < dist ) { + + res = intersection; + dist = intersection.distance; + + } + + } + + return res; + +} + +function iterateOverTriangles( + offset, + count, + bvh, + intersectsTriangleFunc, + contained, + depth, + triangle +) { + + const { geometry } = bvh; + const { index } = geometry; + const pos = geometry.attributes.position; + for ( let i = offset, l = count + offset; i < l; i ++ ) { + + let tri; + + tri = i; + + setTriangle( triangle, tri * 3, index, pos ); + triangle.needsUpdate = true; + + if ( intersectsTriangleFunc( triangle, tri, contained, depth ) ) { + + return true; + + } + + } + + return false; + +} + +/****************************************************/ +/* This file is generated from "refit.template.js". */ +/****************************************************/ + +function refit( bvh, nodeIndices = null ) { + + if ( nodeIndices && Array.isArray( nodeIndices ) ) { + + nodeIndices = new Set( nodeIndices ); + + } + + const geometry = bvh.geometry; + const indexArr = geometry.index ? geometry.index.array : null; + const posAttr = geometry.attributes.position; + + let buffer, uint32Array, uint16Array, float32Array; + let byteOffset = 0; + const roots = bvh._roots; + for ( let i = 0, l = roots.length; i < l; i ++ ) { + + buffer = roots[ i ]; + uint32Array = new Uint32Array( buffer ); + uint16Array = new Uint16Array( buffer ); + float32Array = new Float32Array( buffer ); + + _traverse( 0, byteOffset ); + byteOffset += buffer.byteLength; + + } + + function _traverse( node32Index, byteOffset, force = false ) { + + const node16Index = node32Index * 2; + const isLeaf = uint16Array[ node16Index + 15 ] === IS_LEAFNODE_FLAG; + if ( isLeaf ) { + + const offset = uint32Array[ node32Index + 6 ]; + const count = uint16Array[ node16Index + 14 ]; + + let minx = Infinity; + let miny = Infinity; + let minz = Infinity; + let maxx = - Infinity; + let maxy = - Infinity; + let maxz = - Infinity; + + + for ( let i = 3 * offset, l = 3 * ( offset + count ); i < l; i ++ ) { + + let index = indexArr[ i ]; + const x = posAttr.getX( index ); + const y = posAttr.getY( index ); + const z = posAttr.getZ( index ); + + if ( x < minx ) minx = x; + if ( x > maxx ) maxx = x; + + if ( y < miny ) miny = y; + if ( y > maxy ) maxy = y; + + if ( z < minz ) minz = z; + if ( z > maxz ) maxz = z; + + } + + + if ( + float32Array[ node32Index + 0 ] !== minx || + float32Array[ node32Index + 1 ] !== miny || + float32Array[ node32Index + 2 ] !== minz || + + float32Array[ node32Index + 3 ] !== maxx || + float32Array[ node32Index + 4 ] !== maxy || + float32Array[ node32Index + 5 ] !== maxz + ) { + + float32Array[ node32Index + 0 ] = minx; + float32Array[ node32Index + 1 ] = miny; + float32Array[ node32Index + 2 ] = minz; + + float32Array[ node32Index + 3 ] = maxx; + float32Array[ node32Index + 4 ] = maxy; + float32Array[ node32Index + 5 ] = maxz; + + return true; + + } else { + + return false; + + } + + } else { + + const left = node32Index + 8; + const right = uint32Array[ node32Index + 6 ]; + + // the identifying node indices provided by the shapecast function include offsets of all + // root buffers to guarantee they're unique between roots so offset left and right indices here. + const offsetLeft = left + byteOffset; + const offsetRight = right + byteOffset; + let forceChildren = force; + let includesLeft = false; + let includesRight = false; + + if ( nodeIndices ) { + + // if we see that neither the left or right child are included in the set that need to be updated + // then we assume that all children need to be updated. + if ( ! forceChildren ) { + + includesLeft = nodeIndices.has( offsetLeft ); + includesRight = nodeIndices.has( offsetRight ); + forceChildren = ! includesLeft && ! includesRight; + + } + + } else { + + includesLeft = true; + includesRight = true; + + } + + const traverseLeft = forceChildren || includesLeft; + const traverseRight = forceChildren || includesRight; + + let leftChange = false; + if ( traverseLeft ) { + + leftChange = _traverse( left, byteOffset, forceChildren ); + + } + + let rightChange = false; + if ( traverseRight ) { + + rightChange = _traverse( right, byteOffset, forceChildren ); + + } + + const didChange = leftChange || rightChange; + if ( didChange ) { + + for ( let i = 0; i < 3; i ++ ) { + + const lefti = left + i; + const righti = right + i; + const minLeftValue = float32Array[ lefti ]; + const maxLeftValue = float32Array[ lefti + 3 ]; + const minRightValue = float32Array[ righti ]; + const maxRightValue = float32Array[ righti + 3 ]; + + float32Array[ node32Index + i ] = minLeftValue < minRightValue ? minLeftValue : minRightValue; + float32Array[ node32Index + i + 3 ] = maxLeftValue > maxRightValue ? maxLeftValue : maxRightValue; + + } + + } + + return didChange; + + } + + } + +} + +const _boundingBox = /* @__PURE__ */ new Box3(); +function intersectRay( nodeIndex32, array, ray, target ) { + + arrayToBox( nodeIndex32, array, _boundingBox ); + return ray.intersectBox( _boundingBox, target ); + +} + +/*************************************************************/ +/* This file is generated from "iterationUtils.template.js". */ +/*************************************************************/ +/* eslint-disable indent */ + +function intersectTris_indirect( bvh, side, ray, offset, count, intersections ) { + + const { geometry, _indirectBuffer } = bvh; + for ( let i = offset, end = offset + count; i < end; i ++ ) { + + let vi = _indirectBuffer ? _indirectBuffer[ i ] : i; + intersectTri( geometry, side, ray, vi, intersections ); + + + } + +} + +function intersectClosestTri_indirect( bvh, side, ray, offset, count ) { + + const { geometry, _indirectBuffer } = bvh; + let dist = Infinity; + let res = null; + for ( let i = offset, end = offset + count; i < end; i ++ ) { + + let intersection; + intersection = intersectTri( geometry, side, ray, _indirectBuffer ? _indirectBuffer[ i ] : i ); + + + if ( intersection && intersection.distance < dist ) { + + res = intersection; + dist = intersection.distance; + + } + + } + + return res; + +} + +function iterateOverTriangles_indirect( + offset, + count, + bvh, + intersectsTriangleFunc, + contained, + depth, + triangle +) { + + const { geometry } = bvh; + const { index } = geometry; + const pos = geometry.attributes.position; + for ( let i = offset, l = count + offset; i < l; i ++ ) { + + let tri; + tri = bvh.resolveTriangleIndex( i ); + + setTriangle( triangle, tri * 3, index, pos ); + triangle.needsUpdate = true; + + if ( intersectsTriangleFunc( triangle, tri, contained, depth ) ) { + + return true; + + } + + } + + return false; + +} + +/******************************************************/ +/* This file is generated from "raycast.template.js". */ +/******************************************************/ + +const _boxIntersection$3 = /* @__PURE__ */ new Vector3(); +function raycast( bvh, root, side, ray, intersects ) { + + BufferStack.setBuffer( bvh._roots[ root ] ); + _raycast$1( 0, bvh, side, ray, intersects ); + BufferStack.clearBuffer(); + +} + +function _raycast$1( nodeIndex32, bvh, side, ray, intersects ) { + + const { float32Array, uint16Array, uint32Array } = BufferStack; + const nodeIndex16 = nodeIndex32 * 2; + const isLeaf = IS_LEAF( nodeIndex16, uint16Array ); + if ( isLeaf ) { + + const offset = OFFSET( nodeIndex32, uint32Array ); + const count = COUNT( nodeIndex16, uint16Array ); + + + intersectTris( bvh, side, ray, offset, count, intersects ); + + + } else { + + const leftIndex = LEFT_NODE( nodeIndex32 ); + if ( intersectRay( leftIndex, float32Array, ray, _boxIntersection$3 ) ) { + + _raycast$1( leftIndex, bvh, side, ray, intersects ); + + } + + const rightIndex = RIGHT_NODE( nodeIndex32, uint32Array ); + if ( intersectRay( rightIndex, float32Array, ray, _boxIntersection$3 ) ) { + + _raycast$1( rightIndex, bvh, side, ray, intersects ); + + } + + } + +} + +/***********************************************************/ +/* This file is generated from "raycastFirst.template.js". */ +/***********************************************************/ +const _boxIntersection$2 = /* @__PURE__ */ new Vector3(); +const _xyzFields$1 = [ 'x', 'y', 'z' ]; +function raycastFirst( bvh, root, side, ray ) { + + BufferStack.setBuffer( bvh._roots[ root ] ); + const result = _raycastFirst$1( 0, bvh, side, ray ); + BufferStack.clearBuffer(); + + return result; + +} + +function _raycastFirst$1( nodeIndex32, bvh, side, ray ) { + + const { float32Array, uint16Array, uint32Array } = BufferStack; + let nodeIndex16 = nodeIndex32 * 2; + + const isLeaf = IS_LEAF( nodeIndex16, uint16Array ); + if ( isLeaf ) { + + const offset = OFFSET( nodeIndex32, uint32Array ); + const count = COUNT( nodeIndex16, uint16Array ); + + + return intersectClosestTri( bvh, side, ray, offset, count ); + + + } else { + + // consider the position of the split plane with respect to the oncoming ray; whichever direction + // the ray is coming from, look for an intersection among that side of the tree first + const splitAxis = SPLIT_AXIS( nodeIndex32, uint32Array ); + const xyzAxis = _xyzFields$1[ splitAxis ]; + const rayDir = ray.direction[ xyzAxis ]; + const leftToRight = rayDir >= 0; + + // c1 is the child to check first + let c1, c2; + if ( leftToRight ) { + + c1 = LEFT_NODE( nodeIndex32 ); + c2 = RIGHT_NODE( nodeIndex32, uint32Array ); + + } else { + + c1 = RIGHT_NODE( nodeIndex32, uint32Array ); + c2 = LEFT_NODE( nodeIndex32 ); + + } + + const c1Intersection = intersectRay( c1, float32Array, ray, _boxIntersection$2 ); + const c1Result = c1Intersection ? _raycastFirst$1( c1, bvh, side, ray ) : null; + + // if we got an intersection in the first node and it's closer than the second node's bounding + // box, we don't need to consider the second node because it couldn't possibly be a better result + if ( c1Result ) { + + // check if the point is within the second bounds + // "point" is in the local frame of the bvh + const point = c1Result.point[ xyzAxis ]; + const isOutside = leftToRight ? + point <= float32Array[ c2 + splitAxis ] : // min bounding data + point >= float32Array[ c2 + splitAxis + 3 ]; // max bounding data + + if ( isOutside ) { + + return c1Result; + + } + + } + + // either there was no intersection in the first node, or there could still be a closer + // intersection in the second, so check the second node and then take the better of the two + const c2Intersection = intersectRay( c2, float32Array, ray, _boxIntersection$2 ); + const c2Result = c2Intersection ? _raycastFirst$1( c2, bvh, side, ray ) : null; + + if ( c1Result && c2Result ) { + + return c1Result.distance <= c2Result.distance ? c1Result : c2Result; + + } else { + + return c1Result || c2Result || null; + + } + + } + +} + +/*****************************************************************/ +/* This file is generated from "intersectsGeometry.template.js". */ +/*****************************************************************/ +/* eslint-disable indent */ + +const boundingBox$1 = /* @__PURE__ */ new Box3(); +const triangle$1 = /* @__PURE__ */ new ExtendedTriangle(); +const triangle2$1 = /* @__PURE__ */ new ExtendedTriangle(); +const invertedMat$1 = /* @__PURE__ */ new Matrix4(); + +const obb$4 = /* @__PURE__ */ new OrientedBox(); +const obb2$3 = /* @__PURE__ */ new OrientedBox(); + +function intersectsGeometry( bvh, root, otherGeometry, geometryToBvh ) { + + BufferStack.setBuffer( bvh._roots[ root ] ); + const result = _intersectsGeometry$1( 0, bvh, otherGeometry, geometryToBvh ); + BufferStack.clearBuffer(); + + return result; + +} + +function _intersectsGeometry$1( nodeIndex32, bvh, otherGeometry, geometryToBvh, cachedObb = null ) { + + const { float32Array, uint16Array, uint32Array } = BufferStack; + let nodeIndex16 = nodeIndex32 * 2; + + if ( cachedObb === null ) { + + if ( ! otherGeometry.boundingBox ) { + + otherGeometry.computeBoundingBox(); + + } + + obb$4.set( otherGeometry.boundingBox.min, otherGeometry.boundingBox.max, geometryToBvh ); + cachedObb = obb$4; + + } + + const isLeaf = IS_LEAF( nodeIndex16, uint16Array ); + if ( isLeaf ) { + + const thisGeometry = bvh.geometry; + const thisIndex = thisGeometry.index; + const thisPos = thisGeometry.attributes.position; + + const index = otherGeometry.index; + const pos = otherGeometry.attributes.position; + + const offset = OFFSET( nodeIndex32, uint32Array ); + const count = COUNT( nodeIndex16, uint16Array ); + + // get the inverse of the geometry matrix so we can transform our triangles into the + // geometry space we're trying to test. We assume there are fewer triangles being checked + // here. + invertedMat$1.copy( geometryToBvh ).invert(); + + if ( otherGeometry.boundsTree ) { + + // if there's a bounds tree + arrayToBox( BOUNDING_DATA_INDEX( nodeIndex32 ), float32Array, obb2$3 ); + obb2$3.matrix.copy( invertedMat$1 ); + obb2$3.needsUpdate = true; + + // TODO: use a triangle iteration function here + const res = otherGeometry.boundsTree.shapecast( { + + intersectsBounds: box => obb2$3.intersectsBox( box ), + + intersectsTriangle: tri => { + + tri.a.applyMatrix4( geometryToBvh ); + tri.b.applyMatrix4( geometryToBvh ); + tri.c.applyMatrix4( geometryToBvh ); + tri.needsUpdate = true; + + + for ( let i = offset * 3, l = ( count + offset ) * 3; i < l; i += 3 ) { + + // this triangle needs to be transformed into the current BVH coordinate frame + setTriangle( triangle2$1, i, thisIndex, thisPos ); + triangle2$1.needsUpdate = true; + if ( tri.intersectsTriangle( triangle2$1 ) ) { + + return true; + + } + + } + + + return false; + + } + + } ); + + return res; + + } else { + + // if we're just dealing with raw geometry + + for ( let i = offset * 3, l = ( count + offset ) * 3; i < l; i += 3 ) { + + // this triangle needs to be transformed into the current BVH coordinate frame + setTriangle( triangle$1, i, thisIndex, thisPos ); + + + triangle$1.a.applyMatrix4( invertedMat$1 ); + triangle$1.b.applyMatrix4( invertedMat$1 ); + triangle$1.c.applyMatrix4( invertedMat$1 ); + triangle$1.needsUpdate = true; + + for ( let i2 = 0, l2 = index.count; i2 < l2; i2 += 3 ) { + + setTriangle( triangle2$1, i2, index, pos ); + triangle2$1.needsUpdate = true; + + if ( triangle$1.intersectsTriangle( triangle2$1 ) ) { + + return true; + + } + + } + + + } + + + } + + } else { + + const left = nodeIndex32 + 8; + const right = uint32Array[ nodeIndex32 + 6 ]; + + arrayToBox( BOUNDING_DATA_INDEX( left ), float32Array, boundingBox$1 ); + const leftIntersection = + cachedObb.intersectsBox( boundingBox$1 ) && + _intersectsGeometry$1( left, bvh, otherGeometry, geometryToBvh, cachedObb ); + + if ( leftIntersection ) return true; + + arrayToBox( BOUNDING_DATA_INDEX( right ), float32Array, boundingBox$1 ); + const rightIntersection = + cachedObb.intersectsBox( boundingBox$1 ) && + _intersectsGeometry$1( right, bvh, otherGeometry, geometryToBvh, cachedObb ); + + if ( rightIntersection ) return true; + + return false; + + } + +} + +/*********************************************************************/ +/* This file is generated from "closestPointToGeometry.template.js". */ +/*********************************************************************/ + +const tempMatrix$1 = /* @__PURE__ */ new Matrix4(); +const obb$3 = /* @__PURE__ */ new OrientedBox(); +const obb2$2 = /* @__PURE__ */ new OrientedBox(); +const temp1$1 = /* @__PURE__ */ new Vector3(); +const temp2$1 = /* @__PURE__ */ new Vector3(); +const temp3$1 = /* @__PURE__ */ new Vector3(); +const temp4$1 = /* @__PURE__ */ new Vector3(); + +function closestPointToGeometry( + bvh, + otherGeometry, + geometryToBvh, + target1 = { }, + target2 = { }, + minThreshold = 0, + maxThreshold = Infinity, +) { + + if ( ! otherGeometry.boundingBox ) { + + otherGeometry.computeBoundingBox(); + + } + + obb$3.set( otherGeometry.boundingBox.min, otherGeometry.boundingBox.max, geometryToBvh ); + obb$3.needsUpdate = true; + + const geometry = bvh.geometry; + const pos = geometry.attributes.position; + const index = geometry.index; + const otherPos = otherGeometry.attributes.position; + const otherIndex = otherGeometry.index; + const triangle = ExtendedTrianglePool.getPrimitive(); + const triangle2 = ExtendedTrianglePool.getPrimitive(); + + let tempTarget1 = temp1$1; + let tempTargetDest1 = temp2$1; + let tempTarget2 = null; + let tempTargetDest2 = null; + + if ( target2 ) { + + tempTarget2 = temp3$1; + tempTargetDest2 = temp4$1; + + } + + let closestDistance = Infinity; + let closestDistanceTriIndex = null; + let closestDistanceOtherTriIndex = null; + tempMatrix$1.copy( geometryToBvh ).invert(); + obb2$2.matrix.copy( tempMatrix$1 ); + bvh.shapecast( + { + + boundsTraverseOrder: box => { + + return obb$3.distanceToBox( box ); + + }, + + intersectsBounds: ( box, isLeaf, score ) => { + + if ( score < closestDistance && score < maxThreshold ) { + + // if we know the triangles of this bounds will be intersected next then + // save the bounds to use during triangle checks. + if ( isLeaf ) { + + obb2$2.min.copy( box.min ); + obb2$2.max.copy( box.max ); + obb2$2.needsUpdate = true; + + } + + return true; + + } + + return false; + + }, + + intersectsRange: ( offset, count ) => { + + if ( otherGeometry.boundsTree ) { + + // if the other geometry has a bvh then use the accelerated path where we use shapecast to find + // the closest bounds in the other geometry to check. + const otherBvh = otherGeometry.boundsTree; + return otherBvh.shapecast( { + boundsTraverseOrder: box => { + + return obb2$2.distanceToBox( box ); + + }, + + intersectsBounds: ( box, isLeaf, score ) => { + + return score < closestDistance && score < maxThreshold; + + }, + + intersectsRange: ( otherOffset, otherCount ) => { + + for ( let i2 = otherOffset, l2 = otherOffset + otherCount; i2 < l2; i2 ++ ) { + + + setTriangle( triangle2, 3 * i2, otherIndex, otherPos ); + + triangle2.a.applyMatrix4( geometryToBvh ); + triangle2.b.applyMatrix4( geometryToBvh ); + triangle2.c.applyMatrix4( geometryToBvh ); + triangle2.needsUpdate = true; + + for ( let i = offset, l = offset + count; i < l; i ++ ) { + + + setTriangle( triangle, 3 * i, index, pos ); + + triangle.needsUpdate = true; + + const dist = triangle.distanceToTriangle( triangle2, tempTarget1, tempTarget2 ); + if ( dist < closestDistance ) { + + tempTargetDest1.copy( tempTarget1 ); + + if ( tempTargetDest2 ) { + + tempTargetDest2.copy( tempTarget2 ); + + } + + closestDistance = dist; + closestDistanceTriIndex = i; + closestDistanceOtherTriIndex = i2; + + } + + // stop traversal if we find a point that's under the given threshold + if ( dist < minThreshold ) { + + return true; + + } + + } + + } + + }, + } ); + + } else { + + // If no bounds tree then we'll just check every triangle. + const triCount = getTriCount( otherGeometry ); + for ( let i2 = 0, l2 = triCount; i2 < l2; i2 ++ ) { + + setTriangle( triangle2, 3 * i2, otherIndex, otherPos ); + triangle2.a.applyMatrix4( geometryToBvh ); + triangle2.b.applyMatrix4( geometryToBvh ); + triangle2.c.applyMatrix4( geometryToBvh ); + triangle2.needsUpdate = true; + + for ( let i = offset, l = offset + count; i < l; i ++ ) { + + + setTriangle( triangle, 3 * i, index, pos ); + + triangle.needsUpdate = true; + + const dist = triangle.distanceToTriangle( triangle2, tempTarget1, tempTarget2 ); + if ( dist < closestDistance ) { + + tempTargetDest1.copy( tempTarget1 ); + + if ( tempTargetDest2 ) { + + tempTargetDest2.copy( tempTarget2 ); + + } + + closestDistance = dist; + closestDistanceTriIndex = i; + closestDistanceOtherTriIndex = i2; + + } + + // stop traversal if we find a point that's under the given threshold + if ( dist < minThreshold ) { + + return true; + + } + + } + + } + + } + + }, + + } + + ); + + ExtendedTrianglePool.releasePrimitive( triangle ); + ExtendedTrianglePool.releasePrimitive( triangle2 ); + + if ( closestDistance === Infinity ) { + + return null; + + } + + if ( ! target1.point ) { + + target1.point = tempTargetDest1.clone(); + + } else { + + target1.point.copy( tempTargetDest1 ); + + } + + target1.distance = closestDistance, + target1.faceIndex = closestDistanceTriIndex; + + if ( target2 ) { + + if ( ! target2.point ) target2.point = tempTargetDest2.clone(); + else target2.point.copy( tempTargetDest2 ); + target2.point.applyMatrix4( tempMatrix$1 ); + tempTargetDest1.applyMatrix4( tempMatrix$1 ); + target2.distance = tempTargetDest1.sub( target2.point ).length(); + target2.faceIndex = closestDistanceOtherTriIndex; + + } + + return target1; + +} + +/****************************************************/ +/* This file is generated from "refit.template.js". */ +/****************************************************/ + +function refit_indirect( bvh, nodeIndices = null ) { + + if ( nodeIndices && Array.isArray( nodeIndices ) ) { + + nodeIndices = new Set( nodeIndices ); + + } + + const geometry = bvh.geometry; + const indexArr = geometry.index ? geometry.index.array : null; + const posAttr = geometry.attributes.position; + + let buffer, uint32Array, uint16Array, float32Array; + let byteOffset = 0; + const roots = bvh._roots; + for ( let i = 0, l = roots.length; i < l; i ++ ) { + + buffer = roots[ i ]; + uint32Array = new Uint32Array( buffer ); + uint16Array = new Uint16Array( buffer ); + float32Array = new Float32Array( buffer ); + + _traverse( 0, byteOffset ); + byteOffset += buffer.byteLength; + + } + + function _traverse( node32Index, byteOffset, force = false ) { + + const node16Index = node32Index * 2; + const isLeaf = uint16Array[ node16Index + 15 ] === IS_LEAFNODE_FLAG; + if ( isLeaf ) { + + const offset = uint32Array[ node32Index + 6 ]; + const count = uint16Array[ node16Index + 14 ]; + + let minx = Infinity; + let miny = Infinity; + let minz = Infinity; + let maxx = - Infinity; + let maxy = - Infinity; + let maxz = - Infinity; + + for ( let i = offset, l = offset + count; i < l; i ++ ) { + + const t = 3 * bvh.resolveTriangleIndex( i ); + for ( let j = 0; j < 3; j ++ ) { + + let index = t + j; + index = indexArr ? indexArr[ index ] : index; + + const x = posAttr.getX( index ); + const y = posAttr.getY( index ); + const z = posAttr.getZ( index ); + + if ( x < minx ) minx = x; + if ( x > maxx ) maxx = x; + + if ( y < miny ) miny = y; + if ( y > maxy ) maxy = y; + + if ( z < minz ) minz = z; + if ( z > maxz ) maxz = z; + + + } + + } + + + if ( + float32Array[ node32Index + 0 ] !== minx || + float32Array[ node32Index + 1 ] !== miny || + float32Array[ node32Index + 2 ] !== minz || + + float32Array[ node32Index + 3 ] !== maxx || + float32Array[ node32Index + 4 ] !== maxy || + float32Array[ node32Index + 5 ] !== maxz + ) { + + float32Array[ node32Index + 0 ] = minx; + float32Array[ node32Index + 1 ] = miny; + float32Array[ node32Index + 2 ] = minz; + + float32Array[ node32Index + 3 ] = maxx; + float32Array[ node32Index + 4 ] = maxy; + float32Array[ node32Index + 5 ] = maxz; + + return true; + + } else { + + return false; + + } + + } else { + + const left = node32Index + 8; + const right = uint32Array[ node32Index + 6 ]; + + // the identifying node indices provided by the shapecast function include offsets of all + // root buffers to guarantee they're unique between roots so offset left and right indices here. + const offsetLeft = left + byteOffset; + const offsetRight = right + byteOffset; + let forceChildren = force; + let includesLeft = false; + let includesRight = false; + + if ( nodeIndices ) { + + // if we see that neither the left or right child are included in the set that need to be updated + // then we assume that all children need to be updated. + if ( ! forceChildren ) { + + includesLeft = nodeIndices.has( offsetLeft ); + includesRight = nodeIndices.has( offsetRight ); + forceChildren = ! includesLeft && ! includesRight; + + } + + } else { + + includesLeft = true; + includesRight = true; + + } + + const traverseLeft = forceChildren || includesLeft; + const traverseRight = forceChildren || includesRight; + + let leftChange = false; + if ( traverseLeft ) { + + leftChange = _traverse( left, byteOffset, forceChildren ); + + } + + let rightChange = false; + if ( traverseRight ) { + + rightChange = _traverse( right, byteOffset, forceChildren ); + + } + + const didChange = leftChange || rightChange; + if ( didChange ) { + + for ( let i = 0; i < 3; i ++ ) { + + const lefti = left + i; + const righti = right + i; + const minLeftValue = float32Array[ lefti ]; + const maxLeftValue = float32Array[ lefti + 3 ]; + const minRightValue = float32Array[ righti ]; + const maxRightValue = float32Array[ righti + 3 ]; + + float32Array[ node32Index + i ] = minLeftValue < minRightValue ? minLeftValue : minRightValue; + float32Array[ node32Index + i + 3 ] = maxLeftValue > maxRightValue ? maxLeftValue : maxRightValue; + + } + + } + + return didChange; + + } + + } + +} + +/******************************************************/ +/* This file is generated from "raycast.template.js". */ +/******************************************************/ + +const _boxIntersection$1 = /* @__PURE__ */ new Vector3(); +function raycast_indirect( bvh, root, side, ray, intersects ) { + + BufferStack.setBuffer( bvh._roots[ root ] ); + _raycast( 0, bvh, side, ray, intersects ); + BufferStack.clearBuffer(); + +} + +function _raycast( nodeIndex32, bvh, side, ray, intersects ) { + + const { float32Array, uint16Array, uint32Array } = BufferStack; + const nodeIndex16 = nodeIndex32 * 2; + const isLeaf = IS_LEAF( nodeIndex16, uint16Array ); + if ( isLeaf ) { + + const offset = OFFSET( nodeIndex32, uint32Array ); + const count = COUNT( nodeIndex16, uint16Array ); + + intersectTris_indirect( bvh, side, ray, offset, count, intersects ); + + + } else { + + const leftIndex = LEFT_NODE( nodeIndex32 ); + if ( intersectRay( leftIndex, float32Array, ray, _boxIntersection$1 ) ) { + + _raycast( leftIndex, bvh, side, ray, intersects ); + + } + + const rightIndex = RIGHT_NODE( nodeIndex32, uint32Array ); + if ( intersectRay( rightIndex, float32Array, ray, _boxIntersection$1 ) ) { + + _raycast( rightIndex, bvh, side, ray, intersects ); + + } + + } + +} + +/***********************************************************/ +/* This file is generated from "raycastFirst.template.js". */ +/***********************************************************/ +const _boxIntersection = /* @__PURE__ */ new Vector3(); +const _xyzFields = [ 'x', 'y', 'z' ]; +function raycastFirst_indirect( bvh, root, side, ray ) { + + BufferStack.setBuffer( bvh._roots[ root ] ); + const result = _raycastFirst( 0, bvh, side, ray ); + BufferStack.clearBuffer(); + + return result; + +} + +function _raycastFirst( nodeIndex32, bvh, side, ray ) { + + const { float32Array, uint16Array, uint32Array } = BufferStack; + let nodeIndex16 = nodeIndex32 * 2; + + const isLeaf = IS_LEAF( nodeIndex16, uint16Array ); + if ( isLeaf ) { + + const offset = OFFSET( nodeIndex32, uint32Array ); + const count = COUNT( nodeIndex16, uint16Array ); + + return intersectClosestTri_indirect( bvh, side, ray, offset, count ); + + + } else { + + // consider the position of the split plane with respect to the oncoming ray; whichever direction + // the ray is coming from, look for an intersection among that side of the tree first + const splitAxis = SPLIT_AXIS( nodeIndex32, uint32Array ); + const xyzAxis = _xyzFields[ splitAxis ]; + const rayDir = ray.direction[ xyzAxis ]; + const leftToRight = rayDir >= 0; + + // c1 is the child to check first + let c1, c2; + if ( leftToRight ) { + + c1 = LEFT_NODE( nodeIndex32 ); + c2 = RIGHT_NODE( nodeIndex32, uint32Array ); + + } else { + + c1 = RIGHT_NODE( nodeIndex32, uint32Array ); + c2 = LEFT_NODE( nodeIndex32 ); + + } + + const c1Intersection = intersectRay( c1, float32Array, ray, _boxIntersection ); + const c1Result = c1Intersection ? _raycastFirst( c1, bvh, side, ray ) : null; + + // if we got an intersection in the first node and it's closer than the second node's bounding + // box, we don't need to consider the second node because it couldn't possibly be a better result + if ( c1Result ) { + + // check if the point is within the second bounds + // "point" is in the local frame of the bvh + const point = c1Result.point[ xyzAxis ]; + const isOutside = leftToRight ? + point <= float32Array[ c2 + splitAxis ] : // min bounding data + point >= float32Array[ c2 + splitAxis + 3 ]; // max bounding data + + if ( isOutside ) { + + return c1Result; + + } + + } + + // either there was no intersection in the first node, or there could still be a closer + // intersection in the second, so check the second node and then take the better of the two + const c2Intersection = intersectRay( c2, float32Array, ray, _boxIntersection ); + const c2Result = c2Intersection ? _raycastFirst( c2, bvh, side, ray ) : null; + + if ( c1Result && c2Result ) { + + return c1Result.distance <= c2Result.distance ? c1Result : c2Result; + + } else { + + return c1Result || c2Result || null; + + } + + } + +} + +/*****************************************************************/ +/* This file is generated from "intersectsGeometry.template.js". */ +/*****************************************************************/ +/* eslint-disable indent */ + +const boundingBox = /* @__PURE__ */ new Box3(); +const triangle = /* @__PURE__ */ new ExtendedTriangle(); +const triangle2 = /* @__PURE__ */ new ExtendedTriangle(); +const invertedMat = /* @__PURE__ */ new Matrix4(); + +const obb$2 = /* @__PURE__ */ new OrientedBox(); +const obb2$1 = /* @__PURE__ */ new OrientedBox(); + +function intersectsGeometry_indirect( bvh, root, otherGeometry, geometryToBvh ) { + + BufferStack.setBuffer( bvh._roots[ root ] ); + const result = _intersectsGeometry( 0, bvh, otherGeometry, geometryToBvh ); + BufferStack.clearBuffer(); + + return result; + +} + +function _intersectsGeometry( nodeIndex32, bvh, otherGeometry, geometryToBvh, cachedObb = null ) { + + const { float32Array, uint16Array, uint32Array } = BufferStack; + let nodeIndex16 = nodeIndex32 * 2; + + if ( cachedObb === null ) { + + if ( ! otherGeometry.boundingBox ) { + + otherGeometry.computeBoundingBox(); + + } + + obb$2.set( otherGeometry.boundingBox.min, otherGeometry.boundingBox.max, geometryToBvh ); + cachedObb = obb$2; + + } + + const isLeaf = IS_LEAF( nodeIndex16, uint16Array ); + if ( isLeaf ) { + + const thisGeometry = bvh.geometry; + const thisIndex = thisGeometry.index; + const thisPos = thisGeometry.attributes.position; + + const index = otherGeometry.index; + const pos = otherGeometry.attributes.position; + + const offset = OFFSET( nodeIndex32, uint32Array ); + const count = COUNT( nodeIndex16, uint16Array ); + + // get the inverse of the geometry matrix so we can transform our triangles into the + // geometry space we're trying to test. We assume there are fewer triangles being checked + // here. + invertedMat.copy( geometryToBvh ).invert(); + + if ( otherGeometry.boundsTree ) { + + // if there's a bounds tree + arrayToBox( BOUNDING_DATA_INDEX( nodeIndex32 ), float32Array, obb2$1 ); + obb2$1.matrix.copy( invertedMat ); + obb2$1.needsUpdate = true; + + // TODO: use a triangle iteration function here + const res = otherGeometry.boundsTree.shapecast( { + + intersectsBounds: box => obb2$1.intersectsBox( box ), + + intersectsTriangle: tri => { + + tri.a.applyMatrix4( geometryToBvh ); + tri.b.applyMatrix4( geometryToBvh ); + tri.c.applyMatrix4( geometryToBvh ); + tri.needsUpdate = true; + + for ( let i = offset, l = count + offset; i < l; i ++ ) { + + // this triangle needs to be transformed into the current BVH coordinate frame + setTriangle( triangle2, 3 * bvh.resolveTriangleIndex( i ), thisIndex, thisPos ); + triangle2.needsUpdate = true; + if ( tri.intersectsTriangle( triangle2 ) ) { + + return true; + + } + + } + + + return false; + + } + + } ); + + return res; + + } else { + + // if we're just dealing with raw geometry + for ( let i = offset, l = count + offset; i < l; i ++ ) { + + // this triangle needs to be transformed into the current BVH coordinate frame + const ti = bvh.resolveTriangleIndex( i ); + setTriangle( triangle, 3 * ti, thisIndex, thisPos ); + + + triangle.a.applyMatrix4( invertedMat ); + triangle.b.applyMatrix4( invertedMat ); + triangle.c.applyMatrix4( invertedMat ); + triangle.needsUpdate = true; + + for ( let i2 = 0, l2 = index.count; i2 < l2; i2 += 3 ) { + + setTriangle( triangle2, i2, index, pos ); + triangle2.needsUpdate = true; + + if ( triangle.intersectsTriangle( triangle2 ) ) { + + return true; + + } + + } + + } + + + } + + } else { + + const left = nodeIndex32 + 8; + const right = uint32Array[ nodeIndex32 + 6 ]; + + arrayToBox( BOUNDING_DATA_INDEX( left ), float32Array, boundingBox ); + const leftIntersection = + cachedObb.intersectsBox( boundingBox ) && + _intersectsGeometry( left, bvh, otherGeometry, geometryToBvh, cachedObb ); + + if ( leftIntersection ) return true; + + arrayToBox( BOUNDING_DATA_INDEX( right ), float32Array, boundingBox ); + const rightIntersection = + cachedObb.intersectsBox( boundingBox ) && + _intersectsGeometry( right, bvh, otherGeometry, geometryToBvh, cachedObb ); + + if ( rightIntersection ) return true; + + return false; + + } + +} + +/*********************************************************************/ +/* This file is generated from "closestPointToGeometry.template.js". */ +/*********************************************************************/ + +const tempMatrix = /* @__PURE__ */ new Matrix4(); +const obb$1 = /* @__PURE__ */ new OrientedBox(); +const obb2 = /* @__PURE__ */ new OrientedBox(); +const temp1 = /* @__PURE__ */ new Vector3(); +const temp2 = /* @__PURE__ */ new Vector3(); +const temp3 = /* @__PURE__ */ new Vector3(); +const temp4 = /* @__PURE__ */ new Vector3(); + +function closestPointToGeometry_indirect( + bvh, + otherGeometry, + geometryToBvh, + target1 = { }, + target2 = { }, + minThreshold = 0, + maxThreshold = Infinity, +) { + + if ( ! otherGeometry.boundingBox ) { + + otherGeometry.computeBoundingBox(); + + } + + obb$1.set( otherGeometry.boundingBox.min, otherGeometry.boundingBox.max, geometryToBvh ); + obb$1.needsUpdate = true; + + const geometry = bvh.geometry; + const pos = geometry.attributes.position; + const index = geometry.index; + const otherPos = otherGeometry.attributes.position; + const otherIndex = otherGeometry.index; + const triangle = ExtendedTrianglePool.getPrimitive(); + const triangle2 = ExtendedTrianglePool.getPrimitive(); + + let tempTarget1 = temp1; + let tempTargetDest1 = temp2; + let tempTarget2 = null; + let tempTargetDest2 = null; + + if ( target2 ) { + + tempTarget2 = temp3; + tempTargetDest2 = temp4; + + } + + let closestDistance = Infinity; + let closestDistanceTriIndex = null; + let closestDistanceOtherTriIndex = null; + tempMatrix.copy( geometryToBvh ).invert(); + obb2.matrix.copy( tempMatrix ); + bvh.shapecast( + { + + boundsTraverseOrder: box => { + + return obb$1.distanceToBox( box ); + + }, + + intersectsBounds: ( box, isLeaf, score ) => { + + if ( score < closestDistance && score < maxThreshold ) { + + // if we know the triangles of this bounds will be intersected next then + // save the bounds to use during triangle checks. + if ( isLeaf ) { + + obb2.min.copy( box.min ); + obb2.max.copy( box.max ); + obb2.needsUpdate = true; + + } + + return true; + + } + + return false; + + }, + + intersectsRange: ( offset, count ) => { + + if ( otherGeometry.boundsTree ) { + + // if the other geometry has a bvh then use the accelerated path where we use shapecast to find + // the closest bounds in the other geometry to check. + const otherBvh = otherGeometry.boundsTree; + return otherBvh.shapecast( { + boundsTraverseOrder: box => { + + return obb2.distanceToBox( box ); + + }, + + intersectsBounds: ( box, isLeaf, score ) => { + + return score < closestDistance && score < maxThreshold; + + }, + + intersectsRange: ( otherOffset, otherCount ) => { + + for ( let i2 = otherOffset, l2 = otherOffset + otherCount; i2 < l2; i2 ++ ) { + + const ti2 = otherBvh.resolveTriangleIndex( i2 ); + setTriangle( triangle2, 3 * ti2, otherIndex, otherPos ); + + triangle2.a.applyMatrix4( geometryToBvh ); + triangle2.b.applyMatrix4( geometryToBvh ); + triangle2.c.applyMatrix4( geometryToBvh ); + triangle2.needsUpdate = true; + + for ( let i = offset, l = offset + count; i < l; i ++ ) { + + const ti = bvh.resolveTriangleIndex( i ); + setTriangle( triangle, 3 * ti, index, pos ); + + triangle.needsUpdate = true; + + const dist = triangle.distanceToTriangle( triangle2, tempTarget1, tempTarget2 ); + if ( dist < closestDistance ) { + + tempTargetDest1.copy( tempTarget1 ); + + if ( tempTargetDest2 ) { + + tempTargetDest2.copy( tempTarget2 ); + + } + + closestDistance = dist; + closestDistanceTriIndex = i; + closestDistanceOtherTriIndex = i2; + + } + + // stop traversal if we find a point that's under the given threshold + if ( dist < minThreshold ) { + + return true; + + } + + } + + } + + }, + } ); + + } else { + + // If no bounds tree then we'll just check every triangle. + const triCount = getTriCount( otherGeometry ); + for ( let i2 = 0, l2 = triCount; i2 < l2; i2 ++ ) { + + setTriangle( triangle2, 3 * i2, otherIndex, otherPos ); + triangle2.a.applyMatrix4( geometryToBvh ); + triangle2.b.applyMatrix4( geometryToBvh ); + triangle2.c.applyMatrix4( geometryToBvh ); + triangle2.needsUpdate = true; + + for ( let i = offset, l = offset + count; i < l; i ++ ) { + + const ti = bvh.resolveTriangleIndex( i ); + setTriangle( triangle, 3 * ti, index, pos ); + + triangle.needsUpdate = true; + + const dist = triangle.distanceToTriangle( triangle2, tempTarget1, tempTarget2 ); + if ( dist < closestDistance ) { + + tempTargetDest1.copy( tempTarget1 ); + + if ( tempTargetDest2 ) { + + tempTargetDest2.copy( tempTarget2 ); + + } + + closestDistance = dist; + closestDistanceTriIndex = i; + closestDistanceOtherTriIndex = i2; + + } + + // stop traversal if we find a point that's under the given threshold + if ( dist < minThreshold ) { + + return true; + + } + + } + + } + + } + + }, + + } + + ); + + ExtendedTrianglePool.releasePrimitive( triangle ); + ExtendedTrianglePool.releasePrimitive( triangle2 ); + + if ( closestDistance === Infinity ) { + + return null; + + } + + if ( ! target1.point ) { + + target1.point = tempTargetDest1.clone(); + + } else { + + target1.point.copy( tempTargetDest1 ); + + } + + target1.distance = closestDistance, + target1.faceIndex = closestDistanceTriIndex; + + if ( target2 ) { + + if ( ! target2.point ) target2.point = tempTargetDest2.clone(); + else target2.point.copy( tempTargetDest2 ); + target2.point.applyMatrix4( tempMatrix ); + tempTargetDest1.applyMatrix4( tempMatrix ); + target2.distance = tempTargetDest1.sub( target2.point ).length(); + target2.faceIndex = closestDistanceOtherTriIndex; + + } + + return target1; + +} + +function isSharedArrayBufferSupported() { + + return typeof SharedArrayBuffer !== 'undefined'; + +} + +const _bufferStack1 = new BufferStack.constructor(); +const _bufferStack2 = new BufferStack.constructor(); +const _boxPool = new PrimitivePool( () => new Box3() ); +const _leftBox1 = new Box3(); +const _rightBox1 = new Box3(); + +const _leftBox2 = new Box3(); +const _rightBox2 = new Box3(); + +let _active = false; + +function bvhcast( bvh, otherBvh, matrixToLocal, intersectsRanges ) { + + if ( _active ) { + + throw new Error( 'MeshBVH: Recursive calls to bvhcast not supported.' ); + + } + + _active = true; + + const roots = bvh._roots; + const otherRoots = otherBvh._roots; + let result; + let offset1 = 0; + let offset2 = 0; + const invMat = new Matrix4().copy( matrixToLocal ).invert(); + + // iterate over the first set of roots + for ( let i = 0, il = roots.length; i < il; i ++ ) { + + _bufferStack1.setBuffer( roots[ i ] ); + offset2 = 0; + + // prep the initial root box + const localBox = _boxPool.getPrimitive(); + arrayToBox( BOUNDING_DATA_INDEX( 0 ), _bufferStack1.float32Array, localBox ); + localBox.applyMatrix4( invMat ); + + // iterate over the second set of roots + for ( let j = 0, jl = otherRoots.length; j < jl; j ++ ) { + + _bufferStack2.setBuffer( otherRoots[ i ] ); + + result = _traverse( + 0, 0, matrixToLocal, invMat, intersectsRanges, + offset1, offset2, 0, 0, + localBox, + ); + + _bufferStack2.clearBuffer(); + offset2 += otherRoots[ j ].length; + + if ( result ) { + + break; + + } + + } + + // release stack info + _boxPool.releasePrimitive( localBox ); + _bufferStack1.clearBuffer(); + offset1 += roots[ i ].length; + + if ( result ) { + + break; + + } + + } + + _active = false; + return result; + +} + +function _traverse( + node1Index32, + node2Index32, + matrix2to1, + matrix1to2, + intersectsRangesFunc, + + // offsets for ids + node1IndexByteOffset = 0, + node2IndexByteOffset = 0, + + // tree depth + depth1 = 0, + depth2 = 0, + + currBox = null, + reversed = false, + +) { + + // get the buffer stacks associated with the current indices + let bufferStack1, bufferStack2; + if ( reversed ) { + + bufferStack1 = _bufferStack2; + bufferStack2 = _bufferStack1; + + } else { + + bufferStack1 = _bufferStack1; + bufferStack2 = _bufferStack2; + + } + + // get the local instances of the typed buffers + const + float32Array1 = bufferStack1.float32Array, + uint32Array1 = bufferStack1.uint32Array, + uint16Array1 = bufferStack1.uint16Array, + float32Array2 = bufferStack2.float32Array, + uint32Array2 = bufferStack2.uint32Array, + uint16Array2 = bufferStack2.uint16Array; + + const node1Index16 = node1Index32 * 2; + const node2Index16 = node2Index32 * 2; + const isLeaf1 = IS_LEAF( node1Index16, uint16Array1 ); + const isLeaf2 = IS_LEAF( node2Index16, uint16Array2 ); + let result = false; + if ( isLeaf2 && isLeaf1 ) { + + // if both bounds are leaf nodes then fire the callback if the boxes intersect + if ( reversed ) { + + result = intersectsRangesFunc( + OFFSET( node2Index32, uint32Array2 ), COUNT( node2Index32 * 2, uint16Array2 ), + OFFSET( node1Index32, uint32Array1 ), COUNT( node1Index32 * 2, uint16Array1 ), + depth2, node2IndexByteOffset + node2Index32, + depth1, node1IndexByteOffset + node1Index32, + ); + + } else { + + result = intersectsRangesFunc( + OFFSET( node1Index32, uint32Array1 ), COUNT( node1Index32 * 2, uint16Array1 ), + OFFSET( node2Index32, uint32Array2 ), COUNT( node2Index32 * 2, uint16Array2 ), + depth1, node1IndexByteOffset + node1Index32, + depth2, node2IndexByteOffset + node2Index32, + ); + + } + + } else if ( isLeaf2 ) { + + // SWAP + // If we've traversed to the leaf node on the other bvh then we need to swap over + // to traverse down the first one + + // get the new box to use + const newBox = _boxPool.getPrimitive(); + arrayToBox( BOUNDING_DATA_INDEX( node2Index32 ), float32Array2, newBox ); + newBox.applyMatrix4( matrix2to1 ); + + // get the child bounds to check before traversal + const cl1 = LEFT_NODE( node1Index32 ); + const cr1 = RIGHT_NODE( node1Index32, uint32Array1 ); + arrayToBox( BOUNDING_DATA_INDEX( cl1 ), float32Array1, _leftBox1 ); + arrayToBox( BOUNDING_DATA_INDEX( cr1 ), float32Array1, _rightBox1 ); + + // precompute the intersections otherwise the global boxes will be modified during traversal + const intersectCl1 = newBox.intersectsBox( _leftBox1 ); + const intersectCr1 = newBox.intersectsBox( _rightBox1 ); + result = ( + intersectCl1 && _traverse( + node2Index32, cl1, matrix1to2, matrix2to1, intersectsRangesFunc, + node2IndexByteOffset, node1IndexByteOffset, depth2, depth1 + 1, + newBox, ! reversed, + ) + ) || ( + intersectCr1 && _traverse( + node2Index32, cr1, matrix1to2, matrix2to1, intersectsRangesFunc, + node2IndexByteOffset, node1IndexByteOffset, depth2, depth1 + 1, + newBox, ! reversed, + ) + ); + + _boxPool.releasePrimitive( newBox ); + + } else { + + // if neither are leaves then we should swap if one of the children does not + // intersect with the current bounds + + // get the child bounds to check + const cl2 = LEFT_NODE( node2Index32 ); + const cr2 = RIGHT_NODE( node2Index32, uint32Array2 ); + arrayToBox( BOUNDING_DATA_INDEX( cl2 ), float32Array2, _leftBox2 ); + arrayToBox( BOUNDING_DATA_INDEX( cr2 ), float32Array2, _rightBox2 ); + + const leftIntersects = currBox.intersectsBox( _leftBox2 ); + const rightIntersects = currBox.intersectsBox( _rightBox2 ); + if ( leftIntersects && rightIntersects ) { + + // continue to traverse both children if they both intersect + result = _traverse( + node1Index32, cl2, matrix2to1, matrix1to2, intersectsRangesFunc, + node1IndexByteOffset, node2IndexByteOffset, depth1, depth2 + 1, + currBox, reversed, + ) || _traverse( + node1Index32, cr2, matrix2to1, matrix1to2, intersectsRangesFunc, + node1IndexByteOffset, node2IndexByteOffset, depth1, depth2 + 1, + currBox, reversed, + ); + + } else if ( leftIntersects ) { + + if ( isLeaf1 ) { + + // if the current box is a leaf then just continue + result = _traverse( + node1Index32, cl2, matrix2to1, matrix1to2, intersectsRangesFunc, + node1IndexByteOffset, node2IndexByteOffset, depth1, depth2 + 1, + currBox, reversed, + ); + + } else { + + // SWAP + // if only one box intersects then we have to swap to the other bvh to continue + const newBox = _boxPool.getPrimitive(); + newBox.copy( _leftBox2 ).applyMatrix4( matrix2to1 ); + + const cl1 = LEFT_NODE( node1Index32 ); + const cr1 = RIGHT_NODE( node1Index32, uint32Array1 ); + arrayToBox( BOUNDING_DATA_INDEX( cl1 ), float32Array1, _leftBox1 ); + arrayToBox( BOUNDING_DATA_INDEX( cr1 ), float32Array1, _rightBox1 ); + + // precompute the intersections otherwise the global boxes will be modified during traversal + const intersectCl1 = newBox.intersectsBox( _leftBox1 ); + const intersectCr1 = newBox.intersectsBox( _rightBox1 ); + result = ( + intersectCl1 && _traverse( + cl2, cl1, matrix1to2, matrix2to1, intersectsRangesFunc, + node2IndexByteOffset, node1IndexByteOffset, depth2, depth1 + 1, + newBox, ! reversed, + ) + ) || ( + intersectCr1 && _traverse( + cl2, cr1, matrix1to2, matrix2to1, intersectsRangesFunc, + node2IndexByteOffset, node1IndexByteOffset, depth2, depth1 + 1, + newBox, ! reversed, + ) + ); + + _boxPool.releasePrimitive( newBox ); + + } + + } else if ( rightIntersects ) { + + if ( isLeaf1 ) { + + // if the current box is a leaf then just continue + result = _traverse( + node1Index32, cr2, matrix2to1, matrix1to2, intersectsRangesFunc, + node1IndexByteOffset, node2IndexByteOffset, depth1, depth2 + 1, + currBox, reversed, + ); + + } else { + + // SWAP + // if only one box intersects then we have to swap to the other bvh to continue + const newBox = _boxPool.getPrimitive(); + newBox.copy( _rightBox2 ).applyMatrix4( matrix2to1 ); + + const cl1 = LEFT_NODE( node1Index32 ); + const cr1 = RIGHT_NODE( node1Index32, uint32Array1 ); + arrayToBox( BOUNDING_DATA_INDEX( cl1 ), float32Array1, _leftBox1 ); + arrayToBox( BOUNDING_DATA_INDEX( cr1 ), float32Array1, _rightBox1 ); + + // precompute the intersections otherwise the global boxes will be modified during traversal + const intersectCl1 = newBox.intersectsBox( _leftBox1 ); + const intersectCr1 = newBox.intersectsBox( _rightBox1 ); + result = ( + intersectCl1 && _traverse( + cr2, cl1, matrix1to2, matrix2to1, intersectsRangesFunc, + node2IndexByteOffset, node1IndexByteOffset, depth2, depth1 + 1, + newBox, ! reversed, + ) + ) || ( + intersectCr1 && _traverse( + cr2, cr1, matrix1to2, matrix2to1, intersectsRangesFunc, + node2IndexByteOffset, node1IndexByteOffset, depth2, depth1 + 1, + newBox, ! reversed, + ) + ); + + _boxPool.releasePrimitive( newBox ); + + } + + } + + } + + return result; + +} + +const obb = /* @__PURE__ */ new OrientedBox(); +const tempBox = /* @__PURE__ */ new Box3(); + +class MeshBVH { + + static serialize( bvh, options = {} ) { + + options = { + cloneBuffers: true, + ...options, + }; + + const geometry = bvh.geometry; + const rootData = bvh._roots; + const indirectBuffer = bvh._indirectBuffer; + const indexAttribute = geometry.getIndex(); + let result; + if ( options.cloneBuffers ) { + + result = { + roots: rootData.map( root => root.slice() ), + index: indexAttribute.array.slice(), + indirectBuffer: indirectBuffer ? indirectBuffer.slice() : null, + }; + + } else { + + result = { + roots: rootData, + index: indexAttribute.array, + indirectBuffer: indirectBuffer, + }; + + } + + return result; + + } + + static deserialize( data, geometry, options = {} ) { + + options = { + setIndex: true, + indirect: Boolean( data.indirectBuffer ), + ...options, + }; + + const { index, roots, indirectBuffer } = data; + const bvh = new MeshBVH( geometry, { ...options, [ SKIP_GENERATION ]: true } ); + bvh._roots = roots; + bvh._indirectBuffer = indirectBuffer || null; + + if ( options.setIndex ) { + + const indexAttribute = geometry.getIndex(); + if ( indexAttribute === null ) { + + const newIndex = new BufferAttribute( data.index, 1, false ); + geometry.setIndex( newIndex ); + + } else if ( indexAttribute.array !== index ) { + + indexAttribute.array.set( index ); + indexAttribute.needsUpdate = true; + + } + + } + + return bvh; + + } + + get indirect() { + + return ! ! this._indirectBuffer; + + } + + constructor( geometry, options = {} ) { + + if ( ! geometry.isBufferGeometry ) { + + throw new Error( 'MeshBVH: Only BufferGeometries are supported.' ); + + } else if ( geometry.index && geometry.index.isInterleavedBufferAttribute ) { + + throw new Error( 'MeshBVH: InterleavedBufferAttribute is not supported for the index attribute.' ); + + } + + // default options + options = Object.assign( { + + strategy: CENTER, + maxDepth: 40, + maxLeafTris: 10, + verbose: true, + useSharedArrayBuffer: false, + setBoundingBox: true, + onProgress: null, + indirect: false, + + // undocumented options + + // Whether to skip generating the tree. Used for deserialization. + [ SKIP_GENERATION ]: false, + + }, options ); + + if ( options.useSharedArrayBuffer && ! isSharedArrayBufferSupported() ) { + + throw new Error( 'MeshBVH: SharedArrayBuffer is not available.' ); + + } + + // retain references to the geometry so we can use them it without having to + // take a geometry reference in every function. + this.geometry = geometry; + this._roots = null; + this._indirectBuffer = null; + if ( ! options[ SKIP_GENERATION ] ) { + + buildPackedTree( this, options ); + + if ( ! geometry.boundingBox && options.setBoundingBox ) { + + geometry.boundingBox = this.getBoundingBox( new Box3() ); + + } + + } + + const { _indirectBuffer } = this; + this.resolveTriangleIndex = options.indirect ? i => _indirectBuffer[ i ] : i => i; + + } + + refit( nodeIndices = null ) { + + const refitFunc = this.indirect ? refit_indirect : refit; + return refitFunc( this, nodeIndices ); + + } + + traverse( callback, rootIndex = 0 ) { + + const buffer = this._roots[ rootIndex ]; + const uint32Array = new Uint32Array( buffer ); + const uint16Array = new Uint16Array( buffer ); + _traverse( 0 ); + + function _traverse( node32Index, depth = 0 ) { + + const node16Index = node32Index * 2; + const isLeaf = uint16Array[ node16Index + 15 ] === IS_LEAFNODE_FLAG; + if ( isLeaf ) { + + const offset = uint32Array[ node32Index + 6 ]; + const count = uint16Array[ node16Index + 14 ]; + callback( depth, isLeaf, new Float32Array( buffer, node32Index * 4, 6 ), offset, count ); + + } else { + + // TODO: use node functions here + const left = node32Index + BYTES_PER_NODE / 4; + const right = uint32Array[ node32Index + 6 ]; + const splitAxis = uint32Array[ node32Index + 7 ]; + const stopTraversal = callback( depth, isLeaf, new Float32Array( buffer, node32Index * 4, 6 ), splitAxis ); + + if ( ! stopTraversal ) { + + _traverse( left, depth + 1 ); + _traverse( right, depth + 1 ); + + } + + } + + } + + } + + /* Core Cast Functions */ + raycast( ray, materialOrSide = FrontSide ) { + + const roots = this._roots; + const geometry = this.geometry; + const intersects = []; + const isMaterial = materialOrSide.isMaterial; + const isArrayMaterial = Array.isArray( materialOrSide ); + + const groups = geometry.groups; + const side = isMaterial ? materialOrSide.side : materialOrSide; + const raycastFunc = this.indirect ? raycast_indirect : raycast; + for ( let i = 0, l = roots.length; i < l; i ++ ) { + + const materialSide = isArrayMaterial ? materialOrSide[ groups[ i ].materialIndex ].side : side; + const startCount = intersects.length; + + raycastFunc( this, i, materialSide, ray, intersects ); + + if ( isArrayMaterial ) { + + const materialIndex = groups[ i ].materialIndex; + for ( let j = startCount, jl = intersects.length; j < jl; j ++ ) { + + intersects[ j ].face.materialIndex = materialIndex; + + } + + } + + } + + return intersects; + + } + + raycastFirst( ray, materialOrSide = FrontSide ) { + + const roots = this._roots; + const geometry = this.geometry; + const isMaterial = materialOrSide.isMaterial; + const isArrayMaterial = Array.isArray( materialOrSide ); + + let closestResult = null; + + const groups = geometry.groups; + const side = isMaterial ? materialOrSide.side : materialOrSide; + const raycastFirstFunc = this.indirect ? raycastFirst_indirect : raycastFirst; + for ( let i = 0, l = roots.length; i < l; i ++ ) { + + const materialSide = isArrayMaterial ? materialOrSide[ groups[ i ].materialIndex ].side : side; + const result = raycastFirstFunc( this, i, materialSide, ray ); + if ( result != null && ( closestResult == null || result.distance < closestResult.distance ) ) { + + closestResult = result; + if ( isArrayMaterial ) { + + result.face.materialIndex = groups[ i ].materialIndex; + + } + + } + + } + + return closestResult; + + } + + intersectsGeometry( otherGeometry, geomToMesh ) { + + let result = false; + const roots = this._roots; + const intersectsGeometryFunc = this.indirect ? intersectsGeometry_indirect : intersectsGeometry; + for ( let i = 0, l = roots.length; i < l; i ++ ) { + + result = intersectsGeometryFunc( this, i, otherGeometry, geomToMesh ); + + if ( result ) { + + break; + + } + + } + + return result; + + } + + shapecast( callbacks ) { + + const triangle = ExtendedTrianglePool.getPrimitive(); + const iterateFunc = this.indirect ? iterateOverTriangles_indirect : iterateOverTriangles; + let { + boundsTraverseOrder, + intersectsBounds, + intersectsRange, + intersectsTriangle, + } = callbacks; + + // wrap the intersectsRange function + if ( intersectsRange && intersectsTriangle ) { + + const originalIntersectsRange = intersectsRange; + intersectsRange = ( offset, count, contained, depth, nodeIndex ) => { + + if ( ! originalIntersectsRange( offset, count, contained, depth, nodeIndex ) ) { + + return iterateFunc( offset, count, this, intersectsTriangle, contained, depth, triangle ); + + } + + return true; + + }; + + } else if ( ! intersectsRange ) { + + if ( intersectsTriangle ) { + + intersectsRange = ( offset, count, contained, depth ) => { + + return iterateFunc( offset, count, this, intersectsTriangle, contained, depth, triangle ); + + }; + + } else { + + intersectsRange = ( offset, count, contained ) => { + + return contained; + + }; + + } + + } + + // run shapecast + let result = false; + let byteOffset = 0; + const roots = this._roots; + for ( let i = 0, l = roots.length; i < l; i ++ ) { + + const root = roots[ i ]; + result = shapecast( this, i, intersectsBounds, intersectsRange, boundsTraverseOrder, byteOffset ); + + if ( result ) { + + break; + + } + + byteOffset += root.byteLength; + + } + + ExtendedTrianglePool.releasePrimitive( triangle ); + + return result; + + } + + bvhcast( otherBvh, matrixToLocal, callbacks ) { + + let { + intersectsRanges, + intersectsTriangles, + } = callbacks; + + const triangle1 = ExtendedTrianglePool.getPrimitive(); + const indexAttr1 = this.geometry.index; + const positionAttr1 = this.geometry.attributes.position; + const assignTriangle1 = this.indirect ? + i1 => { + + + const ti = this.resolveTriangleIndex( i1 ); + setTriangle( triangle1, ti * 3, indexAttr1, positionAttr1 ); + + } : + i1 => { + + setTriangle( triangle1, i1 * 3, indexAttr1, positionAttr1 ); + + }; + + const triangle2 = ExtendedTrianglePool.getPrimitive(); + const indexAttr2 = otherBvh.geometry.index; + const positionAttr2 = otherBvh.geometry.attributes.position; + const assignTriangle2 = otherBvh.indirect ? + i2 => { + + const ti2 = otherBvh.resolveTriangleIndex( i2 ); + setTriangle( triangle2, ti2 * 3, indexAttr2, positionAttr2 ); + + } : + i2 => { + + setTriangle( triangle2, i2 * 3, indexAttr2, positionAttr2 ); + + }; + + // generate triangle callback if needed + if ( intersectsTriangles ) { + + const iterateOverDoubleTriangles = ( offset1, count1, offset2, count2, depth1, index1, depth2, index2 ) => { + + for ( let i2 = offset2, l2 = offset2 + count2; i2 < l2; i2 ++ ) { + + assignTriangle2( i2 ); + + triangle2.a.applyMatrix4( matrixToLocal ); + triangle2.b.applyMatrix4( matrixToLocal ); + triangle2.c.applyMatrix4( matrixToLocal ); + triangle2.needsUpdate = true; + + for ( let i1 = offset1, l1 = offset1 + count1; i1 < l1; i1 ++ ) { + + assignTriangle1( i1 ); + + triangle1.needsUpdate = true; + + if ( intersectsTriangles( triangle1, triangle2, i1, i2, depth1, index1, depth2, index2 ) ) { + + return true; + + } + + } + + } + + return false; + + }; + + if ( intersectsRanges ) { + + const originalIntersectsRanges = intersectsRanges; + intersectsRanges = function ( offset1, count1, offset2, count2, depth1, index1, depth2, index2 ) { + + if ( ! originalIntersectsRanges( offset1, count1, offset2, count2, depth1, index1, depth2, index2 ) ) { + + return iterateOverDoubleTriangles( offset1, count1, offset2, count2, depth1, index1, depth2, index2 ); + + } + + return true; + + }; + + } else { + + intersectsRanges = iterateOverDoubleTriangles; + + } + + } + + return bvhcast( this, otherBvh, matrixToLocal, intersectsRanges ); + + } + + + /* Derived Cast Functions */ + intersectsBox( box, boxToMesh ) { + + obb.set( box.min, box.max, boxToMesh ); + obb.needsUpdate = true; + + return this.shapecast( + { + intersectsBounds: box => obb.intersectsBox( box ), + intersectsTriangle: tri => obb.intersectsTriangle( tri ) + } + ); + + } + + intersectsSphere( sphere ) { + + return this.shapecast( + { + intersectsBounds: box => sphere.intersectsBox( box ), + intersectsTriangle: tri => tri.intersectsSphere( sphere ) + } + ); + + } + + closestPointToGeometry( otherGeometry, geometryToBvh, target1 = { }, target2 = { }, minThreshold = 0, maxThreshold = Infinity ) { + + const closestPointToGeometryFunc = this.indirect ? closestPointToGeometry_indirect : closestPointToGeometry; + return closestPointToGeometryFunc( + this, + otherGeometry, + geometryToBvh, + target1, + target2, + minThreshold, + maxThreshold, + ); + + } + + closestPointToPoint( point, target = { }, minThreshold = 0, maxThreshold = Infinity ) { + + return closestPointToPoint( + this, + point, + target, + minThreshold, + maxThreshold, + ); + + } + + getBoundingBox( target ) { + + target.makeEmpty(); + + const roots = this._roots; + roots.forEach( buffer => { + + arrayToBox( 0, new Float32Array( buffer ), tempBox ); + target.union( tempBox ); + + } ); + + return target; + + } + +} + +// converts the given BVH raycast intersection to align with the three.js raycast +// structure (include object, world space distance and point). +function convertRaycastIntersect( hit, object, raycaster ) { + + if ( hit === null ) { + + return null; + + } + + hit.point.applyMatrix4( object.matrixWorld ); + hit.distance = hit.point.distanceTo( raycaster.ray.origin ); + hit.object = object; + + if ( hit.distance < raycaster.near || hit.distance > raycaster.far ) { + + return null; + + } else { + + return hit; + + } + +} + +const ray = /* @__PURE__ */ new Ray(); +const tmpInverseMatrix = /* @__PURE__ */ new Matrix4(); +const origMeshRaycastFunc = Mesh.prototype.raycast; + +function acceleratedRaycast( raycaster, intersects ) { + + if ( this.geometry.boundsTree ) { + + if ( this.material === undefined ) return; + + tmpInverseMatrix.copy( this.matrixWorld ).invert(); + ray.copy( raycaster.ray ).applyMatrix4( tmpInverseMatrix ); + + const bvh = this.geometry.boundsTree; + if ( raycaster.firstHitOnly === true ) { + + const hit = convertRaycastIntersect( bvh.raycastFirst( ray, this.material ), this, raycaster ); + if ( hit ) { + + intersects.push( hit ); + + } + + } else { + + const hits = bvh.raycast( ray, this.material ); + for ( let i = 0, l = hits.length; i < l; i ++ ) { + + const hit = convertRaycastIntersect( hits[ i ], this, raycaster ); + if ( hit ) { + + intersects.push( hit ); + + } + + } + + } + + } else { + + origMeshRaycastFunc.call( this, raycaster, intersects ); + + } + +} + +function computeBoundsTree( options ) { + + this.boundsTree = new MeshBVH( this, options ); + return this.boundsTree; + +} + +function disposeBoundsTree() { + + this.boundsTree = null; + +} + +// Source: https://github.com/gkjohnson/three-mesh-bvh +class BVH { + static apply(geometry) { + if (!BVH.initialized) { + BufferGeometry.prototype.computeBoundsTree = computeBoundsTree; + BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree; + Mesh.prototype.raycast = acceleratedRaycast; + BVH.initialized = true; + } + if (!geometry.boundsTree) { + geometry.computeBoundsTree(); + } } - setMesh(id, mesh) { - const modelID = this.model.modelID; - this.model.ifcAPI.StreamMeshes(modelID, [id], (ifcMesh) => { - mesh.geometry.dispose(); - const { geometryExpressID, flatTransformation } = ifcMesh.geometries.get(0); - const data = this.model.ifcAPI.GetGeometry(modelID, geometryExpressID); - mesh.geometry = this.ifcToThreeGeometry(data); - const matrix = new THREE.Matrix4().fromArray(flatTransformation); - mesh.position.set(0, 0, 0); - mesh.rotation.set(0, 0, 0); - mesh.scale.set(1, 1, 1); - mesh.updateMatrix(); - mesh.applyMatrix4(matrix); - }); + static dispose(geometry) { + geometry.disposeBoundsTree(); } - newThreeMesh() { - const geometry = new THREE.BufferGeometry(); - const mesh = new THREE.InstancedMesh(geometry, this.model.material, 1); - mesh.frustumCulled = false; - const identity = new THREE.Matrix4().identity(); - mesh.setMatrixAt(0, identity); - mesh.instanceMatrix.needsUpdate = true; - return mesh; +} +BVH.initialized = false; + +/* + * Fragments are just a simple wrapper around THREE.InstancedMesh. + * Each fragments can contain Items (identified by ItemID) which + * are mapped to one or many instances inside this THREE.InstancedMesh. + * + * Fragments also implement features like instance buffer resizing and + * hiding out of the box. + * */ +class Fragment { + constructor(geometry, material, count) { + this.ids = new Set(); + this.itemToInstances = new Map(); + this.instanceToItem = new Map(); + this.hiddenItems = new Set(); + this.capacity = 0; + this.capacityOffset = 10; + this.fragments = {}; + this._settingVisibility = false; + this.mesh = new FragmentMesh(geometry, material, count, this); + this.id = this.mesh.uuid; + this.capacity = count; + this.mesh.count = 0; + if (this.mesh.geometry.index.count) { + BVH.apply(this.mesh.geometry); + } } - ifcToThreeGeometry(data) { - const index = this.model.ifcAPI.GetIndexArray(data.GetIndexData(), data.GetIndexDataSize()); - const vertexData = this.model.ifcAPI.GetVertexArray(data.GetVertexData(), data.GetVertexDataSize()); - const position = new Float32Array(vertexData.length / 2); - const normal = new Float32Array(vertexData.length / 2); - for (let i = 0; i < vertexData.length; i += 6) { - position[i / 2] = vertexData[i]; - position[i / 2 + 1] = vertexData[i + 1]; - position[i / 2 + 2] = vertexData[i + 2]; - normal[i / 2] = vertexData[i + 3]; - normal[i / 2 + 1] = vertexData[i + 4]; - normal[i / 2 + 2] = vertexData[i + 5]; + dispose(disposeResources = true) { + this.clear(); + this.group = undefined; + if (this.mesh) { + if (disposeResources) { + for (const mat of this.mesh.material) { + mat.dispose(); + } + this.mesh.material = []; + BVH.dispose(this.mesh.geometry); + this.mesh.geometry.dispose(); + this.mesh.geometry = null; + } + this.mesh.removeFromParent(); + this.mesh.dispose(); + this.mesh.fragment = null; + this.mesh = null; + } + for (const key in this.fragments) { + const frag = this.fragments[key]; + frag.dispose(disposeResources); + } + this.fragments = {}; + } + get(itemID) { + const instanceIDs = this.getInstancesIDs(itemID); + if (!instanceIDs) { + throw new Error("Item not found!"); + } + const transforms = []; + const colorsArray = []; + for (const id of instanceIDs) { + const matrix = new THREE.Matrix4(); + this.mesh.getMatrixAt(id, matrix); + transforms.push(matrix); + if (this.mesh.instanceColor) { + const color = new THREE.Color(); + this.mesh.getColorAt(id, color); + colorsArray.push(color); + } + } + const colors = colorsArray.length ? colorsArray : undefined; + return { id: itemID, transforms, colors }; + } + getItemID(instanceID) { + return this.instanceToItem.get(instanceID) || null; + } + getInstancesIDs(itemID) { + return this.itemToInstances.get(itemID) || null; + } + update() { + if (this.mesh.instanceColor) { + this.mesh.instanceColor.needsUpdate = true; + } + this.mesh.instanceMatrix.needsUpdate = true; + } + add(items) { + var _a; + let size = 0; + for (const item of items) { + size += item.transforms.length; + } + const necessaryCapacity = this.mesh.count + size; + if (necessaryCapacity > this.capacity) { + const newCapacity = necessaryCapacity + this.capacityOffset; + const newMesh = new FragmentMesh(this.mesh.geometry, this.mesh.material, newCapacity, this); + newMesh.count = this.mesh.count; + this.capacity = newCapacity; + const oldMesh = this.mesh; + (_a = oldMesh.parent) === null || _a === void 0 ? void 0 : _a.add(newMesh); + oldMesh.removeFromParent(); + this.mesh = newMesh; + const tempMatrix = new THREE.Matrix4(); + for (let i = 0; i < oldMesh.instanceMatrix.count; i++) { + oldMesh.getMatrixAt(i, tempMatrix); + newMesh.setMatrixAt(i, tempMatrix); + } + if (oldMesh.instanceColor) { + const tempColor = new THREE.Color(); + for (let i = 0; i < oldMesh.instanceColor.count; i++) { + oldMesh.getColorAt(i, tempColor); + newMesh.setColorAt(i, tempColor); + } + } + oldMesh.dispose(); + } + for (let i = 0; i < items.length; i++) { + const { transforms, colors, id } = items[i]; + if (!this.itemToInstances.has(id)) { + this.itemToInstances.set(id, new Set()); + } + const instances = this.itemToInstances.get(id); + this.ids.add(id); + for (let j = 0; j < transforms.length; j++) { + const transform = transforms[j]; + const newInstanceID = this.mesh.count; + this.mesh.setMatrixAt(newInstanceID, transform); + if (colors) { + const color = colors[j]; + this.mesh.setColorAt(newInstanceID, color); + } + instances.add(newInstanceID); + this.instanceToItem.set(newInstanceID, id); + this.mesh.count++; + } + } + this.update(); + } + remove(itemsIDs) { + if (this.mesh.count === 0) { + return; + } + for (const itemID of itemsIDs) { + const instancesToDelete = this.itemToInstances.get(itemID); + if (instancesToDelete === undefined) { + throw new Error("Instances not found!"); + } + for (const instanceID of instancesToDelete) { + if (this.mesh.count === 0) + throw new Error("Errow with mesh count!"); + this.putLast(instanceID); + this.instanceToItem.delete(instanceID); + this.mesh.count--; + } + this.itemToInstances.delete(itemID); + this.ids.delete(itemID); + } + this.update(); + } + clear() { + this.hiddenItems.clear(); + this.ids.clear(); + this.instanceToItem.clear(); + this.itemToInstances.clear(); + this.mesh.count = 0; + } + addFragment(id, material = this.mesh.material) { + const newGeometry = new THREE.BufferGeometry(); + const attrs = this.mesh.geometry.attributes; + newGeometry.setAttribute("position", attrs.position); + newGeometry.setAttribute("normal", attrs.normal); + newGeometry.setIndex(Array.from(this.mesh.geometry.index.array)); + const newFragment = new Fragment(newGeometry, material, this.capacity); + const items = []; + for (const id of this.ids) { + const item = this.get(id); + items.push(item); + } + newFragment.add(items); + newFragment.mesh.applyMatrix4(this.mesh.matrix); + newFragment.mesh.updateMatrix(); + this.fragments[id] = newFragment; + return this.fragments[id]; + } + removeFragment(id) { + const fragment = this.fragments[id]; + if (fragment) { + fragment.dispose(false); + delete this.fragments[id]; + } + } + setVisibility(visible, itemIDs = this.ids) { + if (this._settingVisibility) + return; + this._settingVisibility = true; + if (visible) { + for (const itemID of itemIDs) { + if (!this.ids.has(itemID)) { + throw new Error(`This item doesn't exist here: ${itemID}`); + } + if (!this.hiddenItems.has(itemID)) { + continue; + } + const instances = this.itemToInstances.get(itemID); + if (!instances) + throw new Error("Instances not found!"); + for (const instance of new Set(instances)) { + this.mesh.count++; + this.putLast(instance); + } + this.hiddenItems.delete(itemID); + } + } + else { + for (const itemID of itemIDs) { + if (!this.ids.has(itemID)) { + throw new Error(`This item doesn't exist here: ${itemID}`); + } + if (this.hiddenItems.has(itemID)) { + continue; + } + const instances = this.itemToInstances.get(itemID); + if (!instances) + throw new Error("Instances not found!"); + for (const instance of new Set(instances)) { + this.putLast(instance); + this.mesh.count--; + } + this.hiddenItems.add(itemID); + } + } + this.update(); + this._settingVisibility = false; + } + applyTransform(itemIDs, transform) { + const tempMatrix = new THREE.Matrix4(); + for (const itemID of itemIDs) { + const instances = this.getInstancesIDs(itemID); + if (instances === null) { + continue; + } + for (const instanceID of instances) { + this.mesh.getMatrixAt(instanceID, tempMatrix); + tempMatrix.premultiply(transform); + this.mesh.setMatrixAt(instanceID, tempMatrix); + } + } + this.update(); + } + exportData() { + const geometry = this.mesh.exportData(); + const ids = Array.from(this.ids); + const id = this.id; + return { ...geometry, ids, id }; + } + putLast(instanceID) { + if (this.mesh.count === 0) + return; + const id1 = this.instanceToItem.get(instanceID); + const instanceID2 = this.mesh.count - 1; + if (instanceID2 === instanceID) { + return; + } + const id2 = this.instanceToItem.get(instanceID2); + if (id1 === undefined || id2 === undefined) { + throw new Error("Keys not found"); + } + if (id1 !== id2) { + const instances1 = this.itemToInstances.get(id1); + const instances2 = this.itemToInstances.get(id2); + if (!instances1 || !instances2) { + throw new Error("Instances not found"); + } + if (!instances1.has(instanceID) || !instances2.has(instanceID2)) { + throw new Error("Malformed fragment structure"); + } + instances1.delete(instanceID); + instances2.delete(instanceID2); + instances1.add(instanceID2); + instances2.add(instanceID); + this.instanceToItem.set(instanceID, id2); + this.instanceToItem.set(instanceID2, id1); + } + const transform1 = new THREE.Matrix4(); + const transform2 = new THREE.Matrix4(); + this.mesh.getMatrixAt(instanceID, transform1); + this.mesh.getMatrixAt(instanceID2, transform2); + this.mesh.setMatrixAt(instanceID, transform2); + this.mesh.setMatrixAt(instanceID2, transform1); + if (this.mesh.instanceColor !== null) { + const color1 = new THREE.Color(); + const color2 = new THREE.Color(); + this.mesh.getColorAt(instanceID, color1); + this.mesh.getColorAt(instanceID2, color2); + this.mesh.setColorAt(instanceID, color2); + this.mesh.setColorAt(instanceID2, color1); } - const geometry = new THREE.BufferGeometry(); - geometry.setAttribute("position", new THREE.BufferAttribute(position, 3)); - geometry.setAttribute("normal", new THREE.BufferAttribute(normal, 3)); - geometry.setIndex(Array.from(index)); - return geometry; + } +} + +const int32 = new Int32Array(2); +new Float32Array(int32.buffer); +new Float64Array(int32.buffer); +new Uint16Array(new Uint8Array([1, 0]).buffer)[0] === 1; + +var Encoding; +(function (Encoding) { + Encoding[Encoding["UTF8_BYTES"] = 1] = "UTF8_BYTES"; + Encoding[Encoding["UTF16_STRING"] = 2] = "UTF16_STRING"; +})(Encoding || (Encoding = {})); + +/* unzipit@1.4.3, license MIT */ +/* global SharedArrayBuffer, process */ + + +const isNode = + (typeof process !== 'undefined') && + process.versions && + (typeof process.versions.node !== 'undefined') && + (typeof process.versions.electron === 'undefined'); + +function makeCodes(tree, MAX_BITS) { // code, length + var max_code = tree.length; + var code, bits, n, i, len; + + var bl_count = U.bl_count; for(var i=0; i<=MAX_BITS; i++) bl_count[i]=0; + for(i=1; i>1; + var cl = tree[i+1], val = (lit<<4)|cl; // : (0x8000 | (U.of0[lit-257]<<7) | (U.exb[lit-257]<<4) | cl); + var rest = (MAX_BITS-cl), i0 = tree[i]<>>(15-MAX_BITS); + while(i0!=i1) { + var p0 = r15[i0]>>>(15-MAX_BITS); + map[p0]=val; i0++; + } + } +} +function revCodes(tree, MAX_BITS) { + var r15 = U.rev15, imb = 15-MAX_BITS; + for(var i=0; i>>imb; } +} +const U = function(){ + var u16=Uint16Array, u32=Uint32Array; + return { + next_code : new u16(16), + bl_count : new u16(16), + ordr : [ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 ], + of0 : [3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,999,999,999], + exb : [0,0,0,0,0,0,0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 0, 0, 0], + ldef : new u16(32), + df0 : [1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577, 65535, 65535], + dxb : [0,0,0,0,1,1,2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 0, 0], + ddef : new u32(32), + flmap: new u16( 512), fltree: [], + fdmap: new u16( 32), fdtree: [], + lmap : new u16(32768), ltree : [], ttree:[], + dmap : new u16(32768), dtree : [], + imap : new u16( 512), itree : [], + //rev9 : new u16( 512) + rev15: new u16(1<<15), + lhst : new u32(286), dhst : new u32( 30), ihst : new u32(19), + lits : new u32(15000), + strt : new u16(1<<16), + prev : new u16(1<<15) + }; +} (); + +(function(){ + var len = 1<<15; + for(var i=0; i>> 1) | ((x & 0x55555555) << 1)); + x = (((x & 0xcccccccc) >>> 2) | ((x & 0x33333333) << 2)); + x = (((x & 0xf0f0f0f0) >>> 4) | ((x & 0x0f0f0f0f) << 4)); + x = (((x & 0xff00ff00) >>> 8) | ((x & 0x00ff00ff) << 8)); + U.rev15[i] = (((x >>> 16) | (x << 16)))>>>17; + } + + function pushV(tgt, n, sv) { while(n--!=0) tgt.push(0,sv); } + + for(var i=0; i<32; i++) { U.ldef[i]=(U.of0[i]<<3)|U.exb[i]; U.ddef[i]=(U.df0[i]<<4)|U.dxb[i]; } + + pushV(U.fltree, 144, 8); pushV(U.fltree, 255-143, 9); pushV(U.fltree, 279-255, 7); pushV(U.fltree,287-279,8); + /* + var i = 0; + for(; i<=143; i++) U.fltree.push(0,8); + for(; i<=255; i++) U.fltree.push(0,9); + for(; i<=279; i++) U.fltree.push(0,7); + for(; i<=287; i++) U.fltree.push(0,8); + */ + makeCodes(U.fltree, 9); + codes2map(U.fltree, 9, U.flmap); + revCodes (U.fltree, 9); + + pushV(U.fdtree,32,5); + //for(i=0;i<32; i++) U.fdtree.push(0,5); + makeCodes(U.fdtree, 5); + codes2map(U.fdtree, 5, U.fdmap); + revCodes (U.fdtree, 5); + + pushV(U.itree,19,0); pushV(U.ltree,286,0); pushV(U.dtree,30,0); pushV(U.ttree,320,0); + /* + for(var i=0; i< 19; i++) U.itree.push(0,0); + for(var i=0; i<286; i++) U.ltree.push(0,0); + for(var i=0; i< 30; i++) U.dtree.push(0,0); + for(var i=0; i<320; i++) U.ttree.push(0,0); + */ +})(); + +const crc = { + table : ( function() { + var tab = new Uint32Array(256); + for (var n=0; n<256; n++) { + var c = n; + for (var k=0; k<8; k++) { + if (c & 1) c = 0xedb88320 ^ (c >>> 1); + else c = c >>> 1; + } + tab[n] = c; } + return tab; })(), + update : function(c, buf, off, len) { + for (var i=0; i>> 8); + return c; + }, + crc : function(b,o,l) { return crc.update(0xffffffff,b,o,l) ^ 0xffffffff; } +}; + +/* global module */ + +const config = { + numWorkers: 1, + workerURL: '', + useWorkers: false, +}; + +// Because Firefox uses non-standard onerror to signal an error. +function startWorker$1(url) { + return new Promise((resolve, reject) => { + const worker = new Worker(url); + worker.onmessage = (e) => { + if (e.data === 'start') { + worker.onerror = undefined; + worker.onmessage = undefined; + resolve(worker); + } else { + reject(new Error(`unexpected message: ${e.data}`)); + } + }; + worker.onerror = reject; + }); +} + +function dynamicRequire(mod, request) { + return mod.require ? mod.require(request) : {}; +} + +((function() { + if (isNode) { + // We need to use `dynamicRequire` because `require` on it's own will be optimized by webpack. + const {Worker} = dynamicRequire(module, 'worker_threads'); + return { + async createWorker(url) { + return new Worker(url); + }, + addEventListener(worker, fn) { + worker.on('message', (data) => { + fn({target: worker, data}); + }); + }, + async terminate(worker) { + await worker.terminate(); + }, + }; + } else { + return { + async createWorker(url) { + // I don't understand this security issue + // Apparently there is some iframe setting or http header + // that prevents cross domain workers. But, I can manually + // download the text and do it. I reported this to Chrome + // and they said it was fine so ¯\_(ツ)_/¯ + try { + const worker = await startWorker$1(url); + return worker; + } catch (e) { + console.warn('could not load worker:', url); + } + + let text; + try { + const req = await fetch(url, {mode: 'cors'}); + if (!req.ok) { + throw new Error(`could not load: ${url}`); + } + text = await req.text(); + url = URL.createObjectURL(new Blob([text], {type: 'application/javascript'})); + const worker = await startWorker$1(url); + config.workerURL = url; // this is a hack. What's a better way to structure this code? + return worker; + } catch (e) { + console.warn('could not load worker via fetch:', url); + } + + if (text !== undefined) { + try { + url = `data:application/javascript;base64,${btoa(text)}`; + const worker = await startWorker$1(url); + config.workerURL = url; + return worker; + } catch (e) { + console.warn('could not load worker via dataURI'); + } + } + + console.warn('workers will not be used'); + throw new Error('can not start workers'); + }, + addEventListener(worker, fn) { + worker.addEventListener('message', fn); + }, + async terminate(worker) { + worker.terminate(); + }, + }; + } +})()); + +/* eslint-disable no-irregular-whitespace */ +// const decodeCP437 = (function() { +// const cp437 = '\u0000☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼ !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~⌂ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ '; +// +// return function(uint8view) { +// return Array.from(uint8view).map(v => cp437[v]).join(''); +// }; +// }()); +/* eslint-enable no-irregular-whitespace */ + +new TextDecoder(); + +class ClayObject { + constructor(model) { + this.model = model; } } @@ -77581,15 +83694,27 @@ class Model { set(item) { this.ifcAPI.WriteLine(this.modelID, item); } - delete(item) { + delete(item, recursive = false) { if (item === null) { return; } + let foundItem; if (item instanceof Handle) { - this.ifcAPI.DeleteLine(this.modelID, item.value); - return; + foundItem = this.ifcAPI.GetLine(this.modelID, item.value); + } + else { + foundItem = item; + } + if (recursive) { + for (const key in foundItem) { + // @ts-ignore + const value = foundItem[key]; + if (value instanceof Handle) { + this.delete(value); + } + } } - this.ifcAPI.DeleteLine(this.modelID, item.expressID); + this.ifcAPI.DeleteLine(this.modelID, foundItem.expressID); } get(item) { if (item === null) { @@ -77611,6 +83736,223 @@ class Model { } } +class ElementType extends ClayObject { + constructor() { + super(...arguments); + this.geometries = new Map(); + this.elements = new Map(); + this.fragments = new Map(); + } + newFragment() { + const geometry = new THREE.BufferGeometry(); + geometry.setIndex([]); + const fragment = new Fragment(geometry, this.model.material, 0); + fragment.mesh.frustumCulled = false; + return fragment; + } +} + +class StaticElementType extends ElementType { + addInstance() { + const element = this.createElement(); + const id = element.attributes.expressID; + this.elements.set(id, element); + for (const [_geometryID, fragment] of this.fragments) { + const colors = [new THREE.Color(1, 1, 1)]; + const transforms = [new THREE.Matrix4().identity()]; + fragment.add([{ id, colors, transforms }]); + } + element.update(true); + return element; + } + deleteInstance(id) { + const element = this.elements.get(id); + if (!element) { + throw new Error("Element does not exist!"); + } + element.attributes.Representation = null; + this.model.set(element.attributes); + this.model.delete(element.attributes, true); + for (const [_geometryID, fragment] of this.fragments) { + fragment.remove([id]); + } + } + update(updateGeometry = false) { + let first = updateGeometry; + for (const [_id, element] of this.elements) { + element.update(first); + first = false; + } + } +} + +class MathUtils { + static basisFromEuler(rotation) { + const dirs = new THREE.Matrix4(); + dirs.makeRotationFromEuler(rotation); + const dirX = new THREE.Vector3(); + const dirY = new THREE.Vector3(); + const dirZ = new THREE.Vector3(); + dirs.extractBasis(dirX, dirY, dirZ); + return { dirX, dirY, dirZ }; + } +} + +class IfcUtils { + static direction(vector) { + return new IFC4X3.IfcDirection([ + new IFC4X3.IfcReal(vector.x), + new IFC4X3.IfcReal(vector.y), + new IFC4X3.IfcReal(vector.z), + ]); + } + static point(vector) { + return new IFC4X3.IfcCartesianPoint([ + new IFC4X3.IfcLengthMeasure(vector.x), + new IFC4X3.IfcLengthMeasure(vector.y), + new IFC4X3.IfcLengthMeasure(vector.z), + ]); + } + static localPlacement(location = new THREE.Vector3(0, 0, 0), zDirection = new THREE.Vector3(0, 0, 1), xDirection = new THREE.Vector3(1, 0, 0)) { + return new IFC4X3.IfcLocalPlacement(null, new IFC4X3.IfcAxis2Placement3D(IfcUtils.point(location), IfcUtils.direction(zDirection), IfcUtils.direction(xDirection))); + } + static productDefinitionShape(model, items) { + const representation = this.shape(model, items); + return new IFC4X3.IfcProductDefinitionShape(null, null, [representation]); + } + static shape(model, items) { + return new IFC4X3.IfcShapeRepresentation(model.context, null, null, items); + } + static setAxis2Placement(model, placement, position, rotation) { + const location = model.get(placement.Location); + location.Coordinates[0].value = position.x; + location.Coordinates[1].value = position.y; + location.Coordinates[2].value = position.z; + model.set(location); + const { dirX, dirZ } = MathUtils.basisFromEuler(rotation); + if (placement instanceof IFC4X3.IfcAxis2Placement3D) { + const zDirection = model.get(placement.Axis); + zDirection.DirectionRatios[0].value = dirZ.x; + zDirection.DirectionRatios[1].value = dirZ.y; + zDirection.DirectionRatios[2].value = dirZ.z; + model.set(zDirection); + } + const xDirection = model.get(placement.RefDirection); + xDirection.DirectionRatios[0].value = dirX.x; + xDirection.DirectionRatios[1].value = dirX.y; + xDirection.DirectionRatios[2].value = dirX.z; + model.set(xDirection); + } +} + +class Element extends ClayObject { + get meshes() { + const meshes = []; + for (const id of this.geometries) { + const fragment = this.type.fragments.get(id); + if (!fragment) { + throw new Error("Fragment not found!"); + } + meshes.push(fragment.mesh); + } + return meshes; + } + constructor(model, type) { + super(model); + this.position = new THREE.Vector3(); + this.rotation = new THREE.Euler(); + this.geometries = new Set(); + this.openings = new Map(); + this.type = type; + } + update(updateGeometry = false) { + this.updateIfcElement(); + const modelID = this.model.modelID; + const id = this.attributes.expressID; + const tempMatrix = new THREE.Matrix4(); + this.model.ifcAPI.StreamMeshes(modelID, [id], (ifcMesh) => { + const size = ifcMesh.geometries.size(); + for (let i = 0; i < size; i++) { + const geometry = ifcMesh.geometries.get(i); + const geomID = geometry.geometryExpressID; + const transformArray = geometry.flatTransformation; + const fragment = this.type.fragments.get(geomID); + if (!fragment) { + throw new Error("Fragment not found!"); + } + const instances = fragment.getInstancesIDs(id); + if (!instances) { + throw new Error("Instances not found!"); + } + tempMatrix.fromArray(transformArray); + for (const instance of instances) { + fragment.mesh.setMatrixAt(instance, tempMatrix); + } + fragment.mesh.instanceMatrix.needsUpdate = true; + if (updateGeometry) { + fragment.mesh.geometry.dispose(); + const data = this.model.ifcAPI.GetGeometry(modelID, geomID); + fragment.mesh.geometry = this.ifcToThreeGeometry(data); + const size = fragment.mesh.geometry.index.count; + fragment.mesh.geometry.clearGroups(); + fragment.mesh.geometry.addGroup(0, size); + } + } + }); + } + updateIfcElement() { + const placement = this.model.get(this.attributes.ObjectPlacement); + const relPlacement = this.model.get(placement.RelativePlacement); + IfcUtils.setAxis2Placement(this.model, relPlacement, this.position, this.rotation); + this.model.set(this.attributes); + } + addOpening(opening) { + const voids = new IFC4X3.IfcRelVoidsElement(new IFC4X3.IfcGloballyUniqueId(v4()), null, null, null, this.attributes, opening.attributes); + this.model.set(voids); + const id = opening.attributes.expressID; + this.openings.set(id, voids); + this.model.update(); + } + removeOpening(opening) { + const id = opening.attributes.expressID; + const found = this.openings.get(id); + if (!found) + return; + this.model.delete(found); + this.model.update(); + } + ifcToThreeGeometry(data) { + const index = this.model.ifcAPI.GetIndexArray(data.GetIndexData(), data.GetIndexDataSize()); + const vertexData = this.model.ifcAPI.GetVertexArray(data.GetVertexData(), data.GetVertexDataSize()); + const position = new Float32Array(vertexData.length / 2); + const normal = new Float32Array(vertexData.length / 2); + for (let i = 0; i < vertexData.length; i += 6) { + position[i / 2] = vertexData[i]; + position[i / 2 + 1] = vertexData[i + 1]; + position[i / 2 + 2] = vertexData[i + 2]; + normal[i / 2] = vertexData[i + 3]; + normal[i / 2 + 1] = vertexData[i + 4]; + normal[i / 2 + 2] = vertexData[i + 5]; + } + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute("position", new THREE.BufferAttribute(position, 3)); + geometry.setAttribute("normal", new THREE.BufferAttribute(normal, 3)); + geometry.setIndex(Array.from(index)); + return geometry; + } +} + +class SimpleOpening extends Element { + constructor(model, type) { + super(model, type); + this.type = type; + const placement = IfcUtils.localPlacement(); + this.geometries.add(type.body.attributes.expressID); + this.attributes = new IFC4X3.IfcOpeningElement(new IFC4X3.IfcGloballyUniqueId(v4()), null, null, null, null, placement, type.shape, null, null); + this.model.set(this.attributes); + } +} + class ClayGeometry extends ClayObject { constructor() { super(...arguments); @@ -77618,14 +83960,15 @@ class ClayGeometry extends ClayObject { this.lastClipping = null; this.clippings = new Map(); } + delete() { } addSubtraction(geometry) { - const item = geometry.ifcData; + const item = geometry.attributes; if (this.clippings.has(item.expressID)) { return; } // Create bool between the given item and the current geometry // (might be another bool operation) - const bool = new IFC4X3.IfcBooleanClippingResult(IFC4X3.IfcBooleanOperator.DIFFERENCE, this.ifcData, item); + const bool = new IFC4X3.IfcBooleanClippingResult(IFC4X3.IfcBooleanOperator.DIFFERENCE, this.attributes, item); this.model.set(bool); // If it's the first clipping, reference it const isFirstClipping = this.clippings.size === 0; @@ -77644,7 +83987,7 @@ class ClayGeometry extends ClayObject { const previous = this.lastClipping; this.clippings.set(item.expressID, { bool, previous, next: null }); // Make this bool the current geometry - this.ifcData = bool; + this.attributes = bool; this.update(); } removeSubtraction(item) { @@ -77655,7 +83998,7 @@ class ClayGeometry extends ClayObject { const { bool, next, previous } = found; if (previous === null && next === null) { // This was the only bool in the list - this.ifcData = this.core; + this.attributes = this.core; this.firstClipping = null; this.lastClipping = null; } @@ -77666,7 +84009,7 @@ class ClayGeometry extends ClayObject { throw new Error("Malformed bool structure!"); } newLast.next = null; - this.ifcData = newLast.bool; + this.attributes = newLast.bool; } else if (previous === null && next !== null) { // The deleted bool was the first one in the list @@ -77700,61 +84043,6 @@ class ClayGeometry extends ClayObject { } } -class MathUtils { - static basisFromEuler(rotation) { - const dirs = new THREE.Matrix4(); - dirs.makeRotationFromEuler(rotation); - const dirX = new THREE.Vector3(); - const dirY = new THREE.Vector3(); - const dirZ = new THREE.Vector3(); - dirs.extractBasis(dirX, dirY, dirZ); - return { dirX, dirY, dirZ }; - } -} - -class IfcUtils { - static direction(vector) { - return new IFC4X3.IfcDirection([ - new IFC4X3.IfcReal(vector.x), - new IFC4X3.IfcReal(vector.y), - new IFC4X3.IfcReal(vector.z), - ]); - } - static point(vector) { - return new IFC4X3.IfcCartesianPoint([ - new IFC4X3.IfcLengthMeasure(vector.x), - new IFC4X3.IfcLengthMeasure(vector.y), - new IFC4X3.IfcLengthMeasure(vector.z), - ]); - } - static localPlacement(location = new THREE.Vector3(0, 0, 0), zDirection = new THREE.Vector3(0, 0, 1), xDirection = new THREE.Vector3(1, 0, 0)) { - return new IFC4X3.IfcLocalPlacement(null, new IFC4X3.IfcAxis2Placement3D(IfcUtils.point(location), IfcUtils.direction(zDirection), IfcUtils.direction(xDirection))); - } - static shapeRepresentation(model) { - return new IFC4X3.IfcShapeRepresentation(model.context, new IFC4X3.IfcLabel("Body"), new IFC4X3.IfcLabel("SweptSolid"), []); - } - static setAxis2Placement(model, placement, position, rotation) { - const location = model.get(placement.Location); - location.Coordinates[0].value = position.x; - location.Coordinates[1].value = position.y; - location.Coordinates[2].value = position.z; - model.set(location); - const { dirX, dirZ } = MathUtils.basisFromEuler(rotation); - if (placement instanceof IFC4X3.IfcAxis2Placement3D) { - const zDirection = model.get(placement.Axis); - zDirection.DirectionRatios[0].value = dirZ.x; - zDirection.DirectionRatios[1].value = dirZ.y; - zDirection.DirectionRatios[2].value = dirZ.z; - model.set(zDirection); - } - const xDirection = model.get(placement.RefDirection); - xDirection.DirectionRatios[0].value = dirX.x; - xDirection.DirectionRatios[1].value = dirX.y; - xDirection.DirectionRatios[2].value = dirX.z; - model.set(xDirection); - } -} - class Extrusion extends ClayGeometry { constructor(model, profile) { super(model); @@ -77766,8 +84054,8 @@ class Extrusion extends ClayGeometry { const { dirX, dirZ } = MathUtils.basisFromEuler(this.rotation); const placement = new IFC4X3.IfcAxis2Placement3D(IfcUtils.point(this.position), IfcUtils.direction(dirZ), IfcUtils.direction(dirX)); const direction = IfcUtils.direction(this.direction); - this.core = new IFC4X3.IfcExtrudedAreaSolid(profile.ifcData, placement, direction, new IFC4X3.IfcPositiveLengthMeasure(this.depth)); - this.ifcData = this.core; + this.core = new IFC4X3.IfcExtrudedAreaSolid(profile.attributes, placement, direction, new IFC4X3.IfcPositiveLengthMeasure(this.depth)); + this.attributes = this.core; this.update(); } update() { @@ -77808,7 +84096,7 @@ class Brep extends ClayGeometry { const ifcClosedShell = new IFC4X3.IfcClosedShell([]); this.core = new IFC4X3.IfcFacetedBrep(ifcClosedShell); this.regenerateBrep(); - this.ifcData = this.core; + this.attributes = this.core; this.update(); } update() { @@ -77869,123 +84157,123 @@ class RectangleProfile extends Profile { this.rotation = new THREE.Euler(0, 0, 0); this.position = new THREE.Vector3(0, 0, 0); const placement = new IFC4X3.IfcAxis2Placement2D(IfcUtils.point(this.position), IfcUtils.direction(new THREE.Vector3(1, 0, 0))); - this.ifcData = new IFC4X3.IfcRectangleProfileDef(IFC4X3.IfcProfileTypeEnum.AREA, null, placement, new IFC4X3.IfcPositiveLengthMeasure(this.dimension.x), new IFC4X3.IfcPositiveLengthMeasure(this.dimension.y)); - this.model.set(this.ifcData); + this.attributes = new IFC4X3.IfcRectangleProfileDef(IFC4X3.IfcProfileTypeEnum.AREA, null, placement, new IFC4X3.IfcPositiveLengthMeasure(this.dimension.x), new IFC4X3.IfcPositiveLengthMeasure(this.dimension.y)); + this.model.set(this.attributes); } update() { - this.ifcData.XDim.value = this.dimension.x; - this.ifcData.YDim.value = this.dimension.y; - const placement = this.model.get(this.ifcData.Position); + this.attributes.XDim.value = this.dimension.x; + this.attributes.YDim.value = this.dimension.y; + const placement = this.model.get(this.attributes.Position); IfcUtils.setAxis2Placement(this.model, placement, this.position, this.rotation); - this.model.set(this.ifcData); + this.model.set(this.attributes); } } -class Element extends ClayObject { +class SimpleOpeningType extends StaticElementType { + get body() { + const geoms = this.geometries.values(); + return geoms.next().value; + } constructor(model) { super(model); - this.position = new THREE.Vector3(); - this.rotation = new THREE.Euler(); - this.openings = new Map(); - this.mesh = this.newThreeMesh(); - } - addOpening(opening) { - const voids = new IFC4X3.IfcRelVoidsElement(new IFC4X3.IfcGloballyUniqueId(v4()), null, null, null, this.ifcData, opening.ifcData); - this.model.set(voids); - const id = opening.ifcData.expressID; - this.openings.set(id, voids); - this.model.update(); - } - removeOpening(opening) { - const id = opening.ifcData.expressID; - const found = this.openings.get(id); - if (!found) - return; - this.model.delete(found); - this.model.update(); - } - updateElement() { - const placement = this.model.get(this.ifcData.ObjectPlacement); - const relPlacement = this.model.get(placement.RelativePlacement); - IfcUtils.setAxis2Placement(this.model, relPlacement, this.position, this.rotation); - this.model.set(this.ifcData); - this.setMesh(this.ifcData.expressID, this.mesh); + this.height = 1; + this.width = 1; + this.length = 1; + const profile = new RectangleProfile(model); + const body = new Extrusion(model, profile); + const id = body.attributes.expressID; + this.geometries.set(id, body); + this.shape = IfcUtils.productDefinitionShape(model, [body.attributes]); + const fragment = this.newFragment(); + fragment.mesh.material = [this.model.materialT]; + this.fragments.set(id, fragment); + this.attributes = new IFC4X3.IfcElementType(new IFC4X3.IfcGloballyUniqueId(v4()), null, null, null, null, null, null, null, null); + } + update(updateGeometry = false) { + this.body.profile.dimension.x = this.width; + this.body.profile.dimension.y = this.length; + this.body.profile.update(); + this.body.depth = this.height; + this.body.update(); + super.update(updateGeometry); + } + createElement() { + return new SimpleOpening(this.model, this); } } -class Opening extends Element { - get baseDimension() { - return this.geometries.body.profile.dimension; - } - set baseDimension(value) { - this.geometries.body.profile.dimension = value; - } - get direction() { - return this.geometries.body.direction; - } - set direction(value) { - this.geometries.body.direction = value; - } - get height() { - return this.geometries.body.depth; +class DynamicElementType extends ElementType { + addInstance() { + const element = this.createElement(); + const id = element.attributes.expressID; + for (const geomID of element.geometries) { + const fragment = this.newFragment(); + const colors = [new THREE.Color(1, 1, 1)]; + const transforms = [new THREE.Matrix4().identity()]; + fragment.add([{ id, colors, transforms }]); + this.fragments.set(geomID, fragment); + } + element.update(true); + this.elements.set(id, element); + return element; + } + deleteInstance(id) { + const element = this.elements.get(id); + if (!element) { + throw new Error("Element does not exist!"); + } + this.model.delete(element.attributes, true); + for (id of element.geometries) { + const fragment = this.fragments.get(id); + if (!fragment) { + throw new Error("Fragment not found!"); + } + fragment.dispose(false); + this.fragments.delete(id); + const geometry = this.geometries.get(id); + if (!geometry) { + throw new Error("Geometry not found!"); + } + geometry.delete(); + this.geometries.delete(id); + } } - set height(value) { - this.geometries.body.depth = value; + update(updateGeometry = false) { + for (const [_id, element] of this.elements) { + element.update(updateGeometry); + } } - constructor(model) { - super(model); +} + +class SimpleSlab extends Element { + constructor(model, type) { + super(model, type); + this.thickness = 0.3; + this.type = type; const profile = new RectangleProfile(model); - this.geometries = { body: new Extrusion(model, profile) }; - const { body } = this.geometries; - this.mesh.material = model.materialT; - const representation = IfcUtils.shapeRepresentation(this.model); - representation.Items = [body.ifcData]; + this.body = new Extrusion(model, profile); + const id = this.body.attributes.expressID; + this.type.geometries.set(id, this.body); + this.geometries.add(id); const placement = IfcUtils.localPlacement(); - const shape = new IFC4X3.IfcProductDefinitionShape(null, null, [ - representation, - ]); - const label = "Opening"; - this.ifcData = new IFC4X3.IfcOpeningElement(new IFC4X3.IfcGloballyUniqueId(v4()), null, new IFC4X3.IfcLabel(label), null, new IFC4X3.IfcLabel(label), placement, shape, new IFC4X3.IfcIdentifier(label), null); + const shape = IfcUtils.productDefinitionShape(model, [this.body.attributes]); + this.attributes = new IFC4X3.IfcSlab(new IFC4X3.IfcGloballyUniqueId(v4()), null, null, null, null, placement, shape, null, null); this.update(); } - update() { - const { body } = this.geometries; - body.profile.update(); - body.update(); - this.updateElement(); + update(updateGeometry = false) { + this.body.depth = this.thickness; + this.body.update(); + super.update(updateGeometry); } } -class SimpleSlab extends Element { - get thickness() { - return this.geometries.body.depth; - } - set thickness(value) { - this.geometries.body.depth = value; - } +class SimpleSlabType extends DynamicElementType { constructor(model) { super(model); - const profile = new RectangleProfile(model); - this.geometries = { body: new Extrusion(model, profile) }; - const { body } = this.geometries; - body.depth = 0.3; - body.profile.dimension.x = 5; - body.profile.dimension.y = 10; - const representation = IfcUtils.shapeRepresentation(this.model); - representation.Items = [body.ifcData]; - const placement = IfcUtils.localPlacement(); - const shape = new IFC4X3.IfcProductDefinitionShape(null, null, [ - representation, - ]); - const label = "Simple slab"; - this.ifcData = new IFC4X3.IfcSlab(new IFC4X3.IfcGloballyUniqueId(v4()), null, new IFC4X3.IfcLabel(label), null, new IFC4X3.IfcLabel(label), placement, shape, new IFC4X3.IfcIdentifier(label), null); - this.update(); + this.attributes = new IFC4X3.IfcSlabType(new IFC4X3.IfcGloballyUniqueId(v4()), null, null, null, null, null, null, null, null, IFC4X3.IfcSlabTypeEnum.FLOOR); } - update() { - const { body } = this.geometries; - body.profile.update(); - body.update(); - this.updateElement(); + createElement() { + return new SimpleSlab(this.model, this); } } @@ -78002,47 +84290,47 @@ class SimpleWall extends Element { vector.normalize(); return vector; } - constructor(model) { - super(model); - this.width = 0.2; + constructor(model, type) { + super(model, type); this.height = 3; this.startPoint = new THREE.Vector3(0, 0, 0); this.endPoint = new THREE.Vector3(1, 0, 0); this._openings = new Map(); + this.type = type; const profile = new RectangleProfile(model); - this.geometries = { body: new Extrusion(model, profile) }; - const { body } = this.geometries; - const representation = IfcUtils.shapeRepresentation(this.model); - representation.Items = [body.ifcData]; + this.body = new Extrusion(model, profile); + const id = this.body.attributes.expressID; + this.type.geometries.set(id, this.body); + this.geometries.add(id); const placement = IfcUtils.localPlacement(); - const shape = new IFC4X3.IfcProductDefinitionShape(null, null, [ - representation, + const shape = IfcUtils.productDefinitionShape(model, [ + this.body.attributes, ]); - const label = "Simple Wall"; - this.ifcData = new IFC4X3.IfcWall(new IFC4X3.IfcGloballyUniqueId(v4()), null, new IFC4X3.IfcLabel(label), null, new IFC4X3.IfcLabel(label), placement, shape, new IFC4X3.IfcIdentifier(label), null); - this.update(); + this.attributes = new IFC4X3.IfcWall(new IFC4X3.IfcGloballyUniqueId(v4()), null, null, null, null, placement, shape, null, null); + this.model.set(this.attributes); } - update() { + update(updateGeometry = false) { this.updateAllOpenings(); - const profile = this.geometries.body.profile; + const profile = this.body.profile; profile.dimension.x = this.length; - profile.dimension.y = this.width; + profile.dimension.y = this.type.width; profile.update(); - const { body } = this.geometries; - body.depth = this.height; - body.update(); + this.body.depth = this.height; + this.body.update(); const dir = this.direction; this.rotation.z = Math.atan2(dir.y, dir.x); this.position = this.midPoint; - this.updateElement(); + super.update(updateGeometry); } addOpening(opening) { super.addOpening(opening); this.setOpening(opening); + this.updateGeometryID(); } removeOpening(opening) { super.removeOpening(opening); - this._openings.delete(opening.ifcData.expressID); + this._openings.delete(opening.attributes.expressID); + this.updateGeometryID(); } setOpening(opening) { const wallPlane = new THREE.Plane(); @@ -78060,7 +84348,7 @@ class SimpleWall extends Element { vector.subVectors(newPosition, this.startPoint); const dotProduct = vector.dot(this.direction); distance *= dotProduct > 0 ? 1 : -1; - const id = opening.ifcData.expressID; + const id = opening.attributes.expressID; this._openings.set(id, { opening, distance }); } updateAllOpenings() { @@ -78075,48 +84363,96 @@ class SimpleWall extends Element { opening.update(); } } + updateGeometryID() { + const modelID = this.model.modelID; + const id = this.attributes.expressID; + this.model.ifcAPI.StreamMeshes(modelID, [id], (ifcMesh) => { + const newGeometry = ifcMesh.geometries.get(0); + const newGeomID = newGeometry.geometryExpressID; + const oldGeomID = this.geometries.values().next().value; + this.geometries.clear(); + this.geometries.add(newGeomID); + const frag = this.type.fragments.get(oldGeomID); + this.type.fragments.delete(oldGeomID); + this.type.fragments.set(newGeomID, frag); + const geometry = this.type.geometries.get(oldGeomID); + this.type.geometries.delete(oldGeomID); + this.type.geometries.set(newGeomID, geometry); + }); + } } -class SimpleFurniture extends Element { +class SimpleWallType extends DynamicElementType { constructor(model) { super(model); - this.geometries = { body: new Brep(model) }; - const { body } = this.geometries; - const representation = IfcUtils.shapeRepresentation(this.model); - representation.Items = [body.ifcData]; + this.width = 0.2; + this.attributes = new IFC4X3.IfcSlabType(new IFC4X3.IfcGloballyUniqueId(v4()), null, null, null, null, null, null, null, null, IFC4X3.IfcSlabTypeEnum.FLOOR); + } + createElement() { + return new SimpleWall(this.model, this); + } +} + +class SimpleFurniture extends Element { + constructor(model, type) { + super(model, type); + this.type = type; const placement = IfcUtils.localPlacement(); - const shape = new IFC4X3.IfcProductDefinitionShape(null, null, [ - representation, - ]); - this.ifcData = new IFC4X3.IfcFurnishingElement(new IFC4X3.IfcGloballyUniqueId(v4()), null, null, null, null, placement, shape, null); + this.geometries.add(type.body.attributes.expressID); + this.attributes = new IFC4X3.IfcFurnishingElement(new IFC4X3.IfcGloballyUniqueId(v4()), null, null, null, null, placement, type.shape, null); this.update(); } - update() { - const { body } = this.geometries; - body.update(); - this.updateElement(); - } } -class SimpleWindow extends Element { +class SimpleFurnitureType extends StaticElementType { + get body() { + const geoms = this.geometries.values(); + return geoms.next().value; + } constructor(model) { super(model); - this.geometries = { body: new Brep(model) }; - const { body } = this.geometries; - const representation = IfcUtils.shapeRepresentation(this.model); - representation.Items = [body.ifcData]; + const body = new Brep(model); + const id = body.attributes.expressID; + this.geometries.set(id, body); + this.shape = IfcUtils.productDefinitionShape(model, [body.attributes]); + const fragment = this.newFragment(); + this.fragments.set(id, fragment); + this.attributes = new IFC4X3.IfcFurnishingElementType(new IFC4X3.IfcGloballyUniqueId(v4()), null, null, null, null, null, null, null, null); + } + createElement() { + return new SimpleFurniture(this.model, this); + } +} + +class SimpleWindow extends Element { + constructor(model, type) { + super(model, type); + this.type = type; const placement = IfcUtils.localPlacement(); - const shape = new IFC4X3.IfcProductDefinitionShape(null, null, [ - representation, - ]); - this.ifcData = new IFC4X3.IfcFurnishingElement(new IFC4X3.IfcGloballyUniqueId(v4()), null, null, null, null, placement, shape, null); + this.geometries.add(type.body.attributes.expressID); + this.attributes = new IFC4X3.IfcFurnishingElement(new IFC4X3.IfcGloballyUniqueId(v4()), null, null, null, null, placement, type.shape, null); this.update(); } - update() { - const { body } = this.geometries; - body.update(); - this.updateElement(); +} + +class SimpleWindowType extends StaticElementType { + get body() { + const geoms = this.geometries.values(); + return geoms.next().value; + } + constructor(model) { + super(model); + const body = new Brep(model); + const id = body.attributes.expressID; + this.geometries.set(id, body); + this.shape = IfcUtils.productDefinitionShape(model, [body.attributes]); + const fragment = this.newFragment(); + this.fragments.set(id, fragment); + this.attributes = new IFC4X3.IfcFurnishingElementType(new IFC4X3.IfcGloballyUniqueId(v4()), null, null, null, null, null, null, null, null); + } + createElement() { + return new SimpleWindow(this.model, this); } } -export { Brep, BufferManager, ClayObject, Control, Extrusion, Extrusions, Faces, IdIndexMap, Lines, Model, OffsetFaces, Opening, Primitive, Raycaster, RectangleProfile, Selector, SimpleFurniture, SimpleSlab, SimpleWall, SimpleWindow, Vector, Vertices, Walls }; +export { Brep, BufferManager, ClayObject, Control, Extrusion, Extrusions, Faces, IdIndexMap, Lines, Model, OffsetFaces, Primitive, Raycaster, RectangleProfile, Selector, SimpleFurniture, SimpleFurnitureType, SimpleOpening, SimpleOpeningType, SimpleSlab, SimpleSlabType, SimpleWall, SimpleWallType, SimpleWindow, SimpleWindowType, Vector, Vertices, Walls }; diff --git a/src/base/clay-object.ts b/src/base/clay-object.ts index 98fd941..20d374e 100644 --- a/src/base/clay-object.ts +++ b/src/base/clay-object.ts @@ -1,73 +1,14 @@ import * as WEBIFC from "web-ifc"; -import * as THREE from "three"; import { Model } from "./model"; export abstract class ClayObject { model: Model; - abstract ifcData: WEBIFC.IfcLineObject; + abstract attributes: WEBIFC.IfcLineObject; abstract update(): void; protected constructor(model: Model) { this.model = model; } - - setMesh(id: number, mesh: THREE.Mesh) { - const modelID = this.model.modelID; - this.model.ifcAPI.StreamMeshes(modelID, [id], (ifcMesh) => { - mesh.geometry.dispose(); - const { geometryExpressID, flatTransformation } = - ifcMesh.geometries.get(0); - const data = this.model.ifcAPI.GetGeometry(modelID, geometryExpressID); - mesh.geometry = this.ifcToThreeGeometry(data); - const matrix = new THREE.Matrix4().fromArray(flatTransformation); - mesh.position.set(0, 0, 0); - mesh.rotation.set(0, 0, 0); - mesh.scale.set(1, 1, 1); - mesh.updateMatrix(); - mesh.applyMatrix4(matrix); - }); - } - - newThreeMesh() { - const geometry = new THREE.BufferGeometry(); - const mesh = new THREE.InstancedMesh(geometry, this.model.material, 1); - mesh.frustumCulled = false; - const identity = new THREE.Matrix4().identity(); - mesh.setMatrixAt(0, identity); - mesh.instanceMatrix.needsUpdate = true; - return mesh; - } - - private ifcToThreeGeometry(data: WEBIFC.IfcGeometry) { - const index = this.model.ifcAPI.GetIndexArray( - data.GetIndexData(), - data.GetIndexDataSize() - ); - - const vertexData = this.model.ifcAPI.GetVertexArray( - data.GetVertexData(), - data.GetVertexDataSize() - ); - - const position = new Float32Array(vertexData.length / 2); - const normal = new Float32Array(vertexData.length / 2); - - for (let i = 0; i < vertexData.length; i += 6) { - position[i / 2] = vertexData[i]; - position[i / 2 + 1] = vertexData[i + 1]; - position[i / 2 + 2] = vertexData[i + 2]; - - normal[i / 2] = vertexData[i + 3]; - normal[i / 2 + 1] = vertexData[i + 4]; - normal[i / 2 + 2] = vertexData[i + 5]; - } - - const geometry = new THREE.BufferGeometry(); - geometry.setAttribute("position", new THREE.BufferAttribute(position, 3)); - geometry.setAttribute("normal", new THREE.BufferAttribute(normal, 3)); - geometry.setIndex(Array.from(index)); - return geometry; - } } diff --git a/src/base/model.ts b/src/base/model.ts index 9525661..e1a5a01 100644 --- a/src/base/model.ts +++ b/src/base/model.ts @@ -43,15 +43,32 @@ export class Model { this.ifcAPI.WriteLine(this.modelID, item); } - delete(item: WEBIFC.IfcLineObject | WEBIFC.Handle | null) { + delete( + item: WEBIFC.IfcLineObject | WEBIFC.Handle | null, + recursive = false + ) { if (item === null) { return; } + + let foundItem: WEBIFC.IfcLineObject; if (item instanceof WEBIFC.Handle) { - this.ifcAPI.DeleteLine(this.modelID, item.value); - return; + foundItem = this.ifcAPI.GetLine(this.modelID, item.value); + } else { + foundItem = item; } - this.ifcAPI.DeleteLine(this.modelID, item.expressID); + + if (recursive) { + for (const key in foundItem) { + // @ts-ignore + const value = foundItem[key]; + if (value instanceof WEBIFC.Handle) { + this.delete(value); + } + } + } + + this.ifcAPI.DeleteLine(this.modelID, foundItem.expressID); } get(item: WEBIFC.Handle | T | null) { diff --git a/src/elements/Element/index.ts b/src/elements/Element/index.ts deleted file mode 100644 index 2cb492f..0000000 --- a/src/elements/Element/index.ts +++ /dev/null @@ -1,70 +0,0 @@ -import * as THREE from "three"; -import { v4 as uuidv4 } from "uuid"; -import { IFC4X3 as IFC } from "web-ifc"; -import { ClayObject, Model } from "../../base"; -import { Opening } from "../Opening"; -import { IfcUtils } from "../../utils/ifc-utils"; - -export abstract class Element extends ClayObject { - abstract ifcData: IFC.IfcElement; - abstract geometries: { [name: string]: ClayObject }; - - mesh: THREE.InstancedMesh; - - position = new THREE.Vector3(); - - rotation = new THREE.Euler(); - - openings = new Map(); - - constructor(model: Model) { - super(model); - this.mesh = this.newThreeMesh(); - } - - addOpening(opening: Opening) { - const voids = new IFC.IfcRelVoidsElement( - new IFC.IfcGloballyUniqueId(uuidv4()), - null, - null, - null, - this.ifcData, - opening.ifcData - ); - - this.model.set(voids); - - const id = opening.ifcData.expressID; - this.openings.set(id, voids); - - this.model.update(); - } - - removeOpening(opening: Opening) { - const id = opening.ifcData.expressID; - const found = this.openings.get(id); - if (!found) return; - this.model.delete(found); - this.model.update(); - } - - updateElement() { - const placement = this.model.get( - this.ifcData.ObjectPlacement - ) as IFC.IfcLocalPlacement; - - const relPlacement = this.model.get( - placement.RelativePlacement - ) as IFC.IfcAxis2Placement3D; - - IfcUtils.setAxis2Placement( - this.model, - relPlacement, - this.position, - this.rotation - ); - - this.model.set(this.ifcData); - this.setMesh(this.ifcData.expressID, this.mesh); - } -} diff --git a/src/elements/Elements/DynamicElementType/index.ts b/src/elements/Elements/DynamicElementType/index.ts new file mode 100644 index 0000000..5650093 --- /dev/null +++ b/src/elements/Elements/DynamicElementType/index.ts @@ -0,0 +1,57 @@ +import * as THREE from "three"; +import { IFC4X3 as IFC } from "web-ifc"; +import { Element } from "../Element"; +import { ElementType } from "../ElementType"; + +export abstract class DynamicElementType< + T extends Element +> extends ElementType { + abstract attributes: IFC.IfcElementType; + + addInstance(): T { + const element = this.createElement(); + const id = element.attributes.expressID; + for (const geomID of element.geometries) { + const fragment = this.newFragment(); + const colors = [new THREE.Color(1, 1, 1)]; + const transforms = [new THREE.Matrix4().identity()]; + fragment.add([{ id, colors, transforms }]); + this.fragments.set(geomID, fragment); + } + element.update(true); + this.elements.set(id, element); + return element; + } + + deleteInstance(id: number) { + const element = this.elements.get(id); + if (!element) { + throw new Error("Element does not exist!"); + } + this.model.delete(element.attributes, true); + + for (id of element.geometries) { + const fragment = this.fragments.get(id); + if (!fragment) { + throw new Error("Fragment not found!"); + } + fragment.dispose(false); + this.fragments.delete(id); + + const geometry = this.geometries.get(id); + if (!geometry) { + throw new Error("Geometry not found!"); + } + geometry.delete(); + this.geometries.delete(id); + } + } + + update(updateGeometry = false) { + for (const [_id, element] of this.elements) { + element.update(updateGeometry); + } + } + + protected abstract createElement(): T; +} diff --git a/src/elements/Elements/Element/index.ts b/src/elements/Elements/Element/index.ts new file mode 100644 index 0000000..5556d0b --- /dev/null +++ b/src/elements/Elements/Element/index.ts @@ -0,0 +1,153 @@ +import * as THREE from "three"; +import * as FRAGS from "bim-fragment"; +import * as WEBIFC from "web-ifc"; +import { v4 as uuidv4 } from "uuid"; +import { IFC4X3 as IFC } from "web-ifc"; +import { ClayObject, Model } from "../../../base"; +import { ElementType } from "../ElementType"; +import { IfcUtils } from "../../../utils/ifc-utils"; +import { SimpleOpening } from "../../Openings"; + +export abstract class Element extends ClayObject { + abstract attributes: IFC.IfcElement; + + position = new THREE.Vector3(); + + rotation = new THREE.Euler(); + + type: ElementType; + + geometries = new Set(); + + openings = new Map(); + + get meshes() { + const meshes: FRAGS.FragmentMesh[] = []; + for (const id of this.geometries) { + const fragment = this.type.fragments.get(id); + if (!fragment) { + throw new Error("Fragment not found!"); + } + meshes.push(fragment.mesh); + } + return meshes; + } + + protected constructor(model: Model, type: ElementType) { + super(model); + this.type = type; + } + + update(updateGeometry = false) { + this.updateIfcElement(); + const modelID = this.model.modelID; + const id = this.attributes.expressID; + const tempMatrix = new THREE.Matrix4(); + this.model.ifcAPI.StreamMeshes(modelID, [id], (ifcMesh) => { + const size = ifcMesh.geometries.size(); + for (let i = 0; i < size; i++) { + const geometry = ifcMesh.geometries.get(i); + const geomID = geometry.geometryExpressID; + const transformArray = geometry.flatTransformation; + const fragment = this.type.fragments.get(geomID); + if (!fragment) { + throw new Error("Fragment not found!"); + } + const instances = fragment.getInstancesIDs(id); + if (!instances) { + throw new Error("Instances not found!"); + } + tempMatrix.fromArray(transformArray); + for (const instance of instances) { + fragment.mesh.setMatrixAt(instance, tempMatrix); + } + fragment.mesh.instanceMatrix.needsUpdate = true; + + if (updateGeometry) { + fragment.mesh.geometry.dispose(); + const data = this.model.ifcAPI.GetGeometry(modelID, geomID); + fragment.mesh.geometry = this.ifcToThreeGeometry(data); + const size = fragment.mesh.geometry.index.count; + fragment.mesh.geometry.clearGroups(); + fragment.mesh.geometry.addGroup(0, size); + } + } + }); + } + + private updateIfcElement() { + const placement = this.model.get( + this.attributes.ObjectPlacement + ) as IFC.IfcLocalPlacement; + + const relPlacement = this.model.get( + placement.RelativePlacement + ) as IFC.IfcAxis2Placement3D; + + IfcUtils.setAxis2Placement( + this.model, + relPlacement, + this.position, + this.rotation + ); + + this.model.set(this.attributes); + } + + addOpening(opening: SimpleOpening) { + const voids = new IFC.IfcRelVoidsElement( + new IFC.IfcGloballyUniqueId(uuidv4()), + null, + null, + null, + this.attributes, + opening.attributes + ); + + this.model.set(voids); + + const id = opening.attributes.expressID; + this.openings.set(id, voids); + + this.model.update(); + } + + removeOpening(opening: SimpleOpening) { + const id = opening.attributes.expressID; + const found = this.openings.get(id); + if (!found) return; + this.model.delete(found); + this.model.update(); + } + + protected ifcToThreeGeometry(data: WEBIFC.IfcGeometry) { + const index = this.model.ifcAPI.GetIndexArray( + data.GetIndexData(), + data.GetIndexDataSize() + ); + + const vertexData = this.model.ifcAPI.GetVertexArray( + data.GetVertexData(), + data.GetVertexDataSize() + ); + + const position = new Float32Array(vertexData.length / 2); + const normal = new Float32Array(vertexData.length / 2); + + for (let i = 0; i < vertexData.length; i += 6) { + position[i / 2] = vertexData[i]; + position[i / 2 + 1] = vertexData[i + 1]; + position[i / 2 + 2] = vertexData[i + 2]; + + normal[i / 2] = vertexData[i + 3]; + normal[i / 2 + 1] = vertexData[i + 4]; + normal[i / 2 + 2] = vertexData[i + 5]; + } + + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute("position", new THREE.BufferAttribute(position, 3)); + geometry.setAttribute("normal", new THREE.BufferAttribute(normal, 3)); + geometry.setIndex(Array.from(index)); + return geometry as FRAGS.IndexedGeometry; + } +} diff --git a/src/elements/Elements/ElementType/index.ts b/src/elements/Elements/ElementType/index.ts new file mode 100644 index 0000000..4e85855 --- /dev/null +++ b/src/elements/Elements/ElementType/index.ts @@ -0,0 +1,30 @@ +import * as FRAGS from "bim-fragment"; +import { IFC4X3 as IFC } from "web-ifc"; +import * as THREE from "three"; +import { ClayObject } from "../../../base"; +import { ClayGeometry } from "../../../geometries/Geometry"; +import { Element } from "../Element"; + +export abstract class ElementType< + T extends Element = Element +> extends ClayObject { + abstract attributes: IFC.IfcElementType; + + geometries = new Map(); + + elements = new Map(); + + fragments = new Map(); + + abstract addInstance(): T; + + abstract deleteInstance(id: number): void; + + protected newFragment() { + const geometry = new THREE.BufferGeometry(); + geometry.setIndex([]); + const fragment = new FRAGS.Fragment(geometry, this.model.material, 0); + fragment.mesh.frustumCulled = false; + return fragment; + } +} diff --git a/src/elements/Elements/StaticElementType/index.ts b/src/elements/Elements/StaticElementType/index.ts new file mode 100644 index 0000000..72c1e9a --- /dev/null +++ b/src/elements/Elements/StaticElementType/index.ts @@ -0,0 +1,52 @@ +import { IFC4X3 as IFC } from "web-ifc"; +import * as THREE from "three"; +import { Element } from "../Element"; +import { ElementType } from "../ElementType"; + +export abstract class StaticElementType< + T extends Element +> extends ElementType { + abstract attributes: IFC.IfcElementType; + + abstract shape: IFC.IfcProductDefinitionShape; + + addInstance(): T { + const element = this.createElement(); + const id = element.attributes.expressID; + this.elements.set(id, element); + + for (const [_geometryID, fragment] of this.fragments) { + const colors = [new THREE.Color(1, 1, 1)]; + const transforms = [new THREE.Matrix4().identity()]; + fragment.add([{ id, colors, transforms }]); + } + + element.update(true); + + return element; + } + + deleteInstance(id: number) { + const element = this.elements.get(id); + if (!element) { + throw new Error("Element does not exist!"); + } + element.attributes.Representation = null; + this.model.set(element.attributes); + this.model.delete(element.attributes, true); + + for (const [_geometryID, fragment] of this.fragments) { + fragment.remove([id]); + } + } + + update(updateGeometry = false) { + let first = updateGeometry; + for (const [_id, element] of this.elements) { + element.update(first); + first = false; + } + } + + protected abstract createElement(): T; +} diff --git a/src/elements/Elements/index.ts b/src/elements/Elements/index.ts new file mode 100644 index 0000000..fbe264c --- /dev/null +++ b/src/elements/Elements/index.ts @@ -0,0 +1,4 @@ +export * from "./ElementType"; +export * from "./Element"; +export * from "./StaticElementType"; +export * from "./DynamicElementType"; diff --git a/src/elements/Furniture/SimpleFurniture/index.ts b/src/elements/Furniture/SimpleFurniture/index.ts index c5221b1..0df3485 100644 --- a/src/elements/Furniture/SimpleFurniture/index.ts +++ b/src/elements/Furniture/SimpleFurniture/index.ts @@ -1,46 +1,48 @@ import { IFC4X3 as IFC } from "web-ifc"; import { v4 as uuidv4 } from "uuid"; import { Model } from "../../../base"; -import { Element } from "../../Element"; +import { StaticElementType } from "../../Elements/StaticElementType"; +import { SimpleFurniture } from "./src"; import { Brep } from "../../../geometries"; import { IfcUtils } from "../../../utils/ifc-utils"; -export class SimpleFurniture extends Element { - ifcData: IFC.IfcFurnishingElement; +export * from "./src"; - geometries: { body: Brep }; +export class SimpleFurnitureType extends StaticElementType { + attributes: IFC.IfcFurnishingElementType; + + shape: IFC.IfcProductDefinitionShape; + + get body() { + const geoms = this.geometries.values(); + return geoms.next().value as Brep; + } constructor(model: Model) { super(model); - this.geometries = { body: new Brep(model) }; - - const { body } = this.geometries; + const body = new Brep(model); + const id = body.attributes.expressID; + this.geometries.set(id, body); + this.shape = IfcUtils.productDefinitionShape(model, [body.attributes]); - const representation = IfcUtils.shapeRepresentation(this.model); - representation.Items = [body.ifcData]; - const placement = IfcUtils.localPlacement(); - const shape = new IFC.IfcProductDefinitionShape(null, null, [ - representation, - ]); + const fragment = this.newFragment(); + this.fragments.set(id, fragment); - this.ifcData = new IFC.IfcFurnishingElement( + this.attributes = new IFC.IfcFurnishingElementType( new IFC.IfcGloballyUniqueId(uuidv4()), null, null, null, null, - placement, - shape, + null, + null, + null, null ); - - this.update(); } - update(): void { - const { body } = this.geometries; - body.update(); - this.updateElement(); + protected createElement() { + return new SimpleFurniture(this.model, this); } } diff --git a/src/elements/Furniture/SimpleFurniture/src/index.ts b/src/elements/Furniture/SimpleFurniture/src/index.ts new file mode 100644 index 0000000..b026b26 --- /dev/null +++ b/src/elements/Furniture/SimpleFurniture/src/index.ts @@ -0,0 +1,34 @@ +import { IFC4X3 as IFC } from "web-ifc"; +import { v4 as uuidv4 } from "uuid"; +import { Model } from "../../../../base"; +import { IfcUtils } from "../../../../utils/ifc-utils"; +import { Element } from "../../../Elements/Element"; +import { SimpleFurnitureType } from "../index"; + +export class SimpleFurniture extends Element { + attributes: IFC.IfcFurnishingElement; + + type: SimpleFurnitureType; + + constructor(model: Model, type: SimpleFurnitureType) { + super(model, type); + this.type = type; + + const placement = IfcUtils.localPlacement(); + + this.geometries.add(type.body.attributes.expressID); + + this.attributes = new IFC.IfcFurnishingElement( + new IFC.IfcGloballyUniqueId(uuidv4()), + null, + null, + null, + null, + placement, + type.shape, + null + ); + + this.update(); + } +} diff --git a/src/elements/Opening/index.ts b/src/elements/Opening/index.ts deleted file mode 100644 index fdb614f..0000000 --- a/src/elements/Opening/index.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { IFC4X3 as IFC } from "web-ifc"; -import * as THREE from "three"; -import { v4 as uuidv4 } from "uuid"; -import { Model } from "../../base"; -import { Extrusion, RectangleProfile } from "../../geometries"; -import { Element } from "../Element"; -import { IfcUtils } from "../../utils/ifc-utils"; - -export class Opening extends Element { - ifcData: IFC.IfcOpeningElement; - - geometries: { body: Extrusion }; - - get baseDimension() { - return this.geometries.body.profile.dimension; - } - - set baseDimension(value: THREE.Vector3) { - this.geometries.body.profile.dimension = value; - } - - get direction() { - return this.geometries.body.direction; - } - - set direction(value: THREE.Vector3) { - this.geometries.body.direction = value; - } - - get height() { - return this.geometries.body.depth; - } - - set height(value: number) { - this.geometries.body.depth = value; - } - - constructor(model: Model) { - super(model); - - const profile = new RectangleProfile(model); - this.geometries = { body: new Extrusion(model, profile) }; - - const { body } = this.geometries; - this.mesh.material = model.materialT; - - const representation = IfcUtils.shapeRepresentation(this.model); - representation.Items = [body.ifcData]; - const placement = IfcUtils.localPlacement(); - const shape = new IFC.IfcProductDefinitionShape(null, null, [ - representation, - ]); - - const label = "Opening"; - - this.ifcData = new IFC.IfcOpeningElement( - new IFC.IfcGloballyUniqueId(uuidv4()), - null, - new IFC.IfcLabel(label), - null, - new IFC.IfcLabel(label), - placement, - shape, - new IFC.IfcIdentifier(label), - null - ); - - this.update(); - } - - update() { - const { body } = this.geometries; - body.profile.update(); - body.update(); - this.updateElement(); - } -} diff --git a/src/elements/Opening/index.html b/src/elements/Openings/SimpleOpening/index.html similarity index 100% rename from src/elements/Opening/index.html rename to src/elements/Openings/SimpleOpening/index.html diff --git a/src/elements/Openings/SimpleOpening/index.ts b/src/elements/Openings/SimpleOpening/index.ts new file mode 100644 index 0000000..9ea04d4 --- /dev/null +++ b/src/elements/Openings/SimpleOpening/index.ts @@ -0,0 +1,66 @@ +import { IFC4X3, IFC4X3 as IFC } from "web-ifc"; +import { v4 as uuidv4 } from "uuid"; +import { StaticElementType } from "../../Elements/StaticElementType"; +import { SimpleOpening } from "./src"; +import { Extrusion, RectangleProfile } from "../../../geometries"; +import { Model } from "../../../base"; +import { IfcUtils } from "../../../utils/ifc-utils"; + +export * from "./src"; + +export class SimpleOpeningType extends StaticElementType { + attributes: IFC4X3.IfcElementType; + + shape: IFC4X3.IfcProductDefinitionShape; + + height = 1; + + width = 1; + + length = 1; + + get body() { + const geoms = this.geometries.values(); + return geoms.next().value as Extrusion; + } + + constructor(model: Model) { + super(model); + + const profile = new RectangleProfile(model); + const body = new Extrusion(model, profile); + const id = body.attributes.expressID; + this.geometries.set(id, body); + this.shape = IfcUtils.productDefinitionShape(model, [body.attributes]); + + const fragment = this.newFragment(); + fragment.mesh.material = [this.model.materialT]; + this.fragments.set(id, fragment); + + this.attributes = new IFC.IfcElementType( + new IFC.IfcGloballyUniqueId(uuidv4()), + null, + null, + null, + null, + null, + null, + null, + null + ); + } + + update(updateGeometry: boolean = false) { + this.body.profile.dimension.x = this.width; + this.body.profile.dimension.y = this.length; + this.body.profile.update(); + this.body.depth = this.height; + this.body.update(); + + super.update(updateGeometry); + } + + protected createElement() { + return new SimpleOpening(this.model, this); + } +} diff --git a/src/elements/Openings/SimpleOpening/src/index.ts b/src/elements/Openings/SimpleOpening/src/index.ts new file mode 100644 index 0000000..0af15ec --- /dev/null +++ b/src/elements/Openings/SimpleOpening/src/index.ts @@ -0,0 +1,35 @@ +import { IFC4X3 as IFC } from "web-ifc"; +import { v4 as uuidv4 } from "uuid"; +import { Element } from "../../../Elements/Element"; +import { SimpleOpeningType } from "../index"; +import { Model } from "../../../../base"; +import { IfcUtils } from "../../../../utils/ifc-utils"; + +export class SimpleOpening extends Element { + attributes: IFC.IfcOpeningElement; + + type: SimpleOpeningType; + + constructor(model: Model, type: SimpleOpeningType) { + super(model, type); + this.type = type; + + const placement = IfcUtils.localPlacement(); + + this.geometries.add(type.body.attributes.expressID); + + this.attributes = new IFC.IfcOpeningElement( + new IFC.IfcGloballyUniqueId(uuidv4()), + null, + null, + null, + null, + placement, + type.shape, + null, + null + ); + + this.model.set(this.attributes); + } +} diff --git a/src/elements/Openings/index.ts b/src/elements/Openings/index.ts new file mode 100644 index 0000000..5a03f71 --- /dev/null +++ b/src/elements/Openings/index.ts @@ -0,0 +1 @@ +export * from "./SimpleOpening"; diff --git a/src/elements/Slabs/SimpleSlab/index.ts b/src/elements/Slabs/SimpleSlab/index.ts index 56167f6..0379dce 100644 --- a/src/elements/Slabs/SimpleSlab/index.ts +++ b/src/elements/Slabs/SimpleSlab/index.ts @@ -1,62 +1,32 @@ import { v4 as uuidv4 } from "uuid"; -import { IFC4X3 as IFC } from "web-ifc"; +import { IFC4X3, IFC4X3 as IFC } from "web-ifc"; import { Model } from "../../../base"; -import { Element } from "../../Element"; -import { Extrusion, RectangleProfile } from "../../../geometries"; -import { IfcUtils } from "../../../utils/ifc-utils"; +import { DynamicElementType } from "../../Elements"; +import { SimpleSlab } from "./src"; -export class SimpleSlab extends Element { - ifcData: IFC.IfcSlab; +export * from "./src"; - geometries: { body: Extrusion }; - - get thickness() { - return this.geometries.body.depth; - } - - set thickness(value: number) { - this.geometries.body.depth = value; - } +export class SimpleSlabType extends DynamicElementType { + attributes: IFC4X3.IfcSlabType; constructor(model: Model) { super(model); - const profile = new RectangleProfile(model); - this.geometries = { body: new Extrusion(model, profile) }; - - const { body } = this.geometries; - body.depth = 0.3; - body.profile.dimension.x = 5; - body.profile.dimension.y = 10; - - const representation = IfcUtils.shapeRepresentation(this.model); - representation.Items = [body.ifcData]; - const placement = IfcUtils.localPlacement(); - const shape = new IFC.IfcProductDefinitionShape(null, null, [ - representation, - ]); - - const label = "Simple slab"; - - this.ifcData = new IFC.IfcSlab( + this.attributes = new IFC.IfcSlabType( new IFC.IfcGloballyUniqueId(uuidv4()), null, - new IFC.IfcLabel(label), null, - new IFC.IfcLabel(label), - placement, - shape, - new IFC.IfcIdentifier(label), - null + null, + null, + null, + null, + null, + null, + IFC.IfcSlabTypeEnum.FLOOR ); - - this.update(); } - update(): void { - const { body } = this.geometries; - body.profile.update(); - body.update(); - this.updateElement(); + protected createElement() { + return new SimpleSlab(this.model, this); } } diff --git a/src/elements/Slabs/SimpleSlab/src/index.ts b/src/elements/Slabs/SimpleSlab/src/index.ts new file mode 100644 index 0000000..ba6f6cc --- /dev/null +++ b/src/elements/Slabs/SimpleSlab/src/index.ts @@ -0,0 +1,51 @@ +import { v4 as uuidv4 } from "uuid"; +import { IFC4X3 as IFC } from "web-ifc"; +import { Model } from "../../../../base"; +import { IfcUtils } from "../../../../utils/ifc-utils"; +import { Element } from "../../../Elements"; +import { SimpleSlabType } from "../index"; +import { Extrusion, RectangleProfile } from "../../../../geometries"; + +export class SimpleSlab extends Element { + attributes: IFC.IfcSlab; + + type: SimpleSlabType; + + body: Extrusion; + + thickness = 0.3; + + constructor(model: Model, type: SimpleSlabType) { + super(model, type); + this.type = type; + + const profile = new RectangleProfile(model); + this.body = new Extrusion(model, profile); + const id = this.body.attributes.expressID; + this.type.geometries.set(id, this.body); + this.geometries.add(id); + + const placement = IfcUtils.localPlacement(); + const shape = IfcUtils.productDefinitionShape(model, [this.body.attributes]); + + this.attributes = new IFC.IfcSlab( + new IFC.IfcGloballyUniqueId(uuidv4()), + null, + null, + null, + null, + placement, + shape, + null, + null + ); + + this.update(); + } + + update(updateGeometry: boolean = false) { + this.body.depth = this.thickness; + this.body.update(); + super.update(updateGeometry); + } +} diff --git a/src/elements/Walls/SimpleWall/index.html b/src/elements/Walls/SimpleWall/index.html index 18a9866..0847cd2 100644 --- a/src/elements/Walls/SimpleWall/index.html +++ b/src/elements/Walls/SimpleWall/index.html @@ -92,51 +92,60 @@ model.ifcAPI.SetWasmPath("https://unpkg.com/web-ifc@0.0.50/", true); await model.init(); - const wall = new CLAY.SimpleWall(model); - scene.add(wall.mesh); + const simpleWallType = new CLAY.SimpleWallType(model); + const wall = simpleWallType.addInstance(); + scene.add(...wall.meshes); - const opening = new CLAY.Opening(model); - scene.add(opening.mesh); + const simpleOpeningType = new CLAY.SimpleOpeningType(model); + const opening = simpleOpeningType.addInstance(); + scene.add(...opening.meshes); + console.log(simpleOpeningType); wall.addOpening(opening); - wall.update(); + wall.update(true); + + window.addEventListener("keydown", () => { + wall.removeOpening(opening); + wall.update(true); + }) + // Set up GUI const gui = new dat.GUI(); gui.add(wall.startPoint, "x").name("Start X").min(-5).max(5).step(0.1).onChange(() => { - wall.update(); + wall.update(true); }); gui.add(wall.startPoint, "y").name("Start Y").min(-5).max(5).step(0.1).onChange(() => { - wall.update(); + wall.update(true); }); gui.add(wall.endPoint, "x").name("End X").min(-5).max(5).step(0.1).onChange(() => { - wall.update(); + wall.update(true); }); gui.add(wall.endPoint, "y").name("End Y").min(-5).max(5).step(0.1).onChange(() => { - wall.update(); + wall.update(true); }); - gui.add(wall, "width").name("Width").min(0.1).max(0.5).step(0.05).onChange(() => { - wall.update(); + gui.add(wall, "height").name("Height").min(1).max(4).step(0.05).onChange(() => { + wall.update(true); }); - gui.add(wall, "height").name("Height").min(1).max(4).step(0.05).onChange(() => { - wall.update(); + gui.add(simpleWallType, "width").name("Width").min(0.1).max(0.5).step(0.05).onChange(() => { + simpleWallType.update(true); }); gui.add(opening.position, "x").name("Opening X Position").min(-5).max(5).step(0.1).onChange(() => { wall.setOpening(opening); - wall.update(); + wall.update(true); }); gui.add(opening.position, "y").name("Opening Y Position").min(-5).max(5).step(0.1).onChange(() => { wall.setOpening(opening); - wall.update(); + wall.update(true); }); diff --git a/src/elements/Walls/SimpleWall/index.ts b/src/elements/Walls/SimpleWall/index.ts index 72912b2..e538ce3 100644 --- a/src/elements/Walls/SimpleWall/index.ts +++ b/src/elements/Walls/SimpleWall/index.ts @@ -1,144 +1,34 @@ -import { IFC4X3 as IFC } from "web-ifc"; -import * as THREE from "three"; import { v4 as uuidv4 } from "uuid"; +import { IFC4X3, IFC4X3 as IFC } from "web-ifc"; import { Model } from "../../../base"; -import { Extrusion, RectangleProfile } from "../../../geometries"; -import { Element } from "../../Element"; -import { Opening } from "../../Opening"; -import { IfcUtils } from "../../../utils/ifc-utils"; +import { DynamicElementType } from "../../Elements"; +import { SimpleWall } from "./src"; -export class SimpleWall extends Element { - ifcData: IFC.IfcWall; +export * from "./src"; - geometries: { body: Extrusion }; +export class SimpleWallType extends DynamicElementType { + attributes: IFC4X3.IfcWallType; width = 0.2; - height = 3; - - startPoint = new THREE.Vector3(0, 0, 0); - - endPoint = new THREE.Vector3(1, 0, 0); - - private _openings = new Map(); - - get length() { - return this.startPoint.distanceTo(this.endPoint); - } - - get midPoint() { - return new THREE.Vector3( - (this.startPoint.x + this.endPoint.x) / 2, - (this.startPoint.y + this.endPoint.y) / 2, - (this.startPoint.z + this.endPoint.z) / 2 - ); - } - - get direction() { - const vector = new THREE.Vector3(); - vector.subVectors(this.endPoint, this.startPoint); - vector.normalize(); - return vector; - } - constructor(model: Model) { super(model); - const profile = new RectangleProfile(model); - this.geometries = { body: new Extrusion(model, profile) }; - - const { body } = this.geometries; - - const representation = IfcUtils.shapeRepresentation(this.model); - representation.Items = [body.ifcData]; - const placement = IfcUtils.localPlacement(); - const shape = new IFC.IfcProductDefinitionShape(null, null, [ - representation, - ]); - - const label = "Simple Wall"; - - this.ifcData = new IFC.IfcWall( + this.attributes = new IFC.IfcSlabType( new IFC.IfcGloballyUniqueId(uuidv4()), null, - new IFC.IfcLabel(label), null, - new IFC.IfcLabel(label), - placement, - shape, - new IFC.IfcIdentifier(label), - null + null, + null, + null, + null, + null, + null, + IFC.IfcSlabTypeEnum.FLOOR ); - - this.update(); - } - - update() { - this.updateAllOpenings(); - - const profile = this.geometries.body.profile; - profile.dimension.x = this.length; - profile.dimension.y = this.width; - profile.update(); - - const { body } = this.geometries; - body.depth = this.height; - body.update(); - - const dir = this.direction; - this.rotation.z = Math.atan2(dir.y, dir.x); - this.position = this.midPoint; - - this.updateElement(); - } - - addOpening(opening: Opening) { - super.addOpening(opening); - this.setOpening(opening); - } - - removeOpening(opening: Opening) { - super.removeOpening(opening); - this._openings.delete(opening.ifcData.expressID); - } - - setOpening(opening: Opening) { - const wallPlane = new THREE.Plane(); - - const tempPoint = this.startPoint.clone(); - tempPoint.z += 1; - wallPlane.setFromCoplanarPoints(tempPoint, this.startPoint, this.endPoint); - const newPosition = new THREE.Vector3(); - wallPlane.projectPoint(opening.position, newPosition); - - opening.position.copy(newPosition); - opening.update(); - - // The distance is signed, so that it also supports openings that are - // before the startPoint by using the dot product - let distance = newPosition.distanceTo(this.startPoint); - const vector = new THREE.Vector3(); - vector.subVectors(newPosition, this.startPoint); - const dotProduct = vector.dot(this.direction); - distance *= dotProduct > 0 ? 1 : -1; - - const id = opening.ifcData.expressID; - - this._openings.set(id, { opening, distance }); } - private updateAllOpenings() { - const start = this.startPoint; - const dir = this.direction; - for (const [_id, { opening, distance }] of this._openings) { - const pos = dir.clone().multiplyScalar(distance).add(start); - - // Align opening to wall - opening.position.x = pos.x; - opening.position.y = pos.y; - opening.rotation.z = this.rotation.z; - - opening.update(); - } + protected createElement() { + return new SimpleWall(this.model, this); } } diff --git a/src/elements/Walls/SimpleWall/src/index.ts b/src/elements/Walls/SimpleWall/src/index.ts new file mode 100644 index 0000000..1309ab1 --- /dev/null +++ b/src/elements/Walls/SimpleWall/src/index.ts @@ -0,0 +1,170 @@ +import * as THREE from "three"; +import * as FRAGS from "bim-fragment"; +import { v4 as uuidv4 } from "uuid"; +import { IFC4X3 as IFC } from "web-ifc"; +import { Model } from "../../../../base"; +import { IfcUtils } from "../../../../utils/ifc-utils"; +import { Element } from "../../../Elements"; +import { Extrusion, RectangleProfile } from "../../../../geometries"; +import { SimpleWallType } from "../index"; +import { SimpleOpening } from "../../../Openings"; +import { ClayGeometry } from "../../../../geometries/Geometry"; + +export class SimpleWall extends Element { + attributes: IFC.IfcWall; + + type: SimpleWallType; + + body: Extrusion; + + height = 3; + + startPoint = new THREE.Vector3(0, 0, 0); + + endPoint = new THREE.Vector3(1, 0, 0); + + private _openings = new Map< + number, + { opening: SimpleOpening; distance: number } + >(); + + get length() { + return this.startPoint.distanceTo(this.endPoint); + } + + get midPoint() { + return new THREE.Vector3( + (this.startPoint.x + this.endPoint.x) / 2, + (this.startPoint.y + this.endPoint.y) / 2, + (this.startPoint.z + this.endPoint.z) / 2 + ); + } + + get direction() { + const vector = new THREE.Vector3(); + vector.subVectors(this.endPoint, this.startPoint); + vector.normalize(); + return vector; + } + + constructor(model: Model, type: SimpleWallType) { + super(model, type); + this.type = type; + + const profile = new RectangleProfile(model); + this.body = new Extrusion(model, profile); + const id = this.body.attributes.expressID; + this.type.geometries.set(id, this.body); + this.geometries.add(id); + + const placement = IfcUtils.localPlacement(); + const shape = IfcUtils.productDefinitionShape(model, [ + this.body.attributes, + ]); + + this.attributes = new IFC.IfcWall( + new IFC.IfcGloballyUniqueId(uuidv4()), + null, + null, + null, + null, + placement, + shape, + null, + null + ); + + this.model.set(this.attributes); + } + + update(updateGeometry: boolean = false) { + this.updateAllOpenings(); + + const profile = this.body.profile; + profile.dimension.x = this.length; + profile.dimension.y = this.type.width; + profile.update(); + + this.body.depth = this.height; + this.body.update(); + + const dir = this.direction; + this.rotation.z = Math.atan2(dir.y, dir.x); + this.position = this.midPoint; + + super.update(updateGeometry); + } + + addOpening(opening: SimpleOpening) { + super.addOpening(opening); + this.setOpening(opening); + this.updateGeometryID(); + } + + removeOpening(opening: SimpleOpening) { + super.removeOpening(opening); + this._openings.delete(opening.attributes.expressID); + this.updateGeometryID(); + } + + setOpening(opening: SimpleOpening) { + const wallPlane = new THREE.Plane(); + + const tempPoint = this.startPoint.clone(); + tempPoint.z += 1; + wallPlane.setFromCoplanarPoints(tempPoint, this.startPoint, this.endPoint); + const newPosition = new THREE.Vector3(); + wallPlane.projectPoint(opening.position, newPosition); + + opening.position.copy(newPosition); + opening.update(); + + // The distance is signed, so that it also supports openings that are + // before the startPoint by using the dot product + let distance = newPosition.distanceTo(this.startPoint); + const vector = new THREE.Vector3(); + vector.subVectors(newPosition, this.startPoint); + const dotProduct = vector.dot(this.direction); + distance *= dotProduct > 0 ? 1 : -1; + + const id = opening.attributes.expressID; + + this._openings.set(id, { opening, distance }); + } + + private updateAllOpenings() { + const start = this.startPoint; + const dir = this.direction; + for (const [_id, { opening, distance }] of this._openings) { + const pos = dir.clone().multiplyScalar(distance).add(start); + + // Align opening to wall + opening.position.x = pos.x; + opening.position.y = pos.y; + opening.rotation.z = this.rotation.z; + + opening.update(); + } + } + + private updateGeometryID() { + const modelID = this.model.modelID; + const id = this.attributes.expressID; + this.model.ifcAPI.StreamMeshes(modelID, [id], (ifcMesh) => { + const newGeometry = ifcMesh.geometries.get(0); + const newGeomID = newGeometry.geometryExpressID; + const oldGeomID = this.geometries.values().next().value; + + this.geometries.clear(); + this.geometries.add(newGeomID); + + const frag = this.type.fragments.get(oldGeomID) as FRAGS.Fragment; + this.type.fragments.delete(oldGeomID); + this.type.fragments.set(newGeomID, frag); + + const geometry = this.type.geometries.get(oldGeomID) as ClayGeometry; + this.type.geometries.delete(oldGeomID); + this.type.geometries.set(newGeomID, geometry); + }); + } +} diff --git a/src/elements/Windows/SimpleWindow/index.ts b/src/elements/Windows/SimpleWindow/index.ts index bf9403e..0380a46 100644 --- a/src/elements/Windows/SimpleWindow/index.ts +++ b/src/elements/Windows/SimpleWindow/index.ts @@ -1,46 +1,48 @@ -import { IFC4X3 as IFC } from "web-ifc"; +import { IFC4X3, IFC4X3 as IFC } from "web-ifc"; import { v4 as uuidv4 } from "uuid"; import { Model } from "../../../base"; -import { Element } from "../../Element"; import { Brep } from "../../../geometries"; import { IfcUtils } from "../../../utils/ifc-utils"; +import { StaticElementType } from "../../Elements"; +import { SimpleWindow } from "./src"; -export class SimpleWindow extends Element { - ifcData: IFC.IfcFurnishingElement; +export * from "./src"; - geometries: { body: Brep }; +export class SimpleWindowType extends StaticElementType { + attributes: IFC4X3.IfcFurnishingElementType; + + shape: IFC4X3.IfcProductDefinitionShape; + + get body() { + const geoms = this.geometries.values(); + return geoms.next().value as Brep; + } constructor(model: Model) { super(model); - this.geometries = { body: new Brep(model) }; - - const { body } = this.geometries; + const body = new Brep(model); + const id = body.attributes.expressID; + this.geometries.set(id, body); + this.shape = IfcUtils.productDefinitionShape(model, [body.attributes]); - const representation = IfcUtils.shapeRepresentation(this.model); - representation.Items = [body.ifcData]; - const placement = IfcUtils.localPlacement(); - const shape = new IFC.IfcProductDefinitionShape(null, null, [ - representation, - ]); + const fragment = this.newFragment(); + this.fragments.set(id, fragment); - this.ifcData = new IFC.IfcFurnishingElement( + this.attributes = new IFC.IfcFurnishingElementType( new IFC.IfcGloballyUniqueId(uuidv4()), null, null, null, null, - placement, - shape, + null, + null, + null, null ); - - this.update(); } - update(): void { - const { body } = this.geometries; - body.update(); - this.updateElement(); + protected createElement() { + return new SimpleWindow(this.model, this); } } diff --git a/src/elements/Windows/SimpleWindow/src/index.ts b/src/elements/Windows/SimpleWindow/src/index.ts new file mode 100644 index 0000000..44cdb4a --- /dev/null +++ b/src/elements/Windows/SimpleWindow/src/index.ts @@ -0,0 +1,34 @@ +import { IFC4X3 as IFC } from "web-ifc"; +import { v4 as uuidv4 } from "uuid"; +import { Model } from "../../../../base"; +import { IfcUtils } from "../../../../utils/ifc-utils"; +import { Element } from "../../../Elements/Element"; +import { SimpleWindowType } from "../index"; + +export class SimpleWindow extends Element { + attributes: IFC.IfcFurnishingElement; + + type: SimpleWindowType; + + constructor(model: Model, type: SimpleWindowType) { + super(model, type); + this.type = type; + + const placement = IfcUtils.localPlacement(); + + this.geometries.add(type.body.attributes.expressID); + + this.attributes = new IFC.IfcFurnishingElement( + new IFC.IfcGloballyUniqueId(uuidv4()), + null, + null, + null, + null, + placement, + type.shape, + null + ); + + this.update(); + } +} diff --git a/src/elements/index.ts b/src/elements/index.ts index 9b5f3e2..4ee9c41 100644 --- a/src/elements/index.ts +++ b/src/elements/index.ts @@ -1,4 +1,4 @@ -export * from "./Opening"; +export * from "./Openings"; export * from "./Slabs"; export * from "./Walls"; export * from "./Furniture"; diff --git a/src/geometries/Brep/index.ts b/src/geometries/Brep/index.ts index 714c0b2..6e60155 100644 --- a/src/geometries/Brep/index.ts +++ b/src/geometries/Brep/index.ts @@ -5,7 +5,7 @@ import { Model } from "../../base"; import { IfcUtils } from "../../utils/ifc-utils"; export class Brep extends ClayGeometry { - ifcData: IFC.IfcFacetedBrep | IFC.IfcBooleanClippingResult; + attributes: IFC.IfcFacetedBrep | IFC.IfcBooleanClippingResult; core: IFC.IfcFacetedBrep; @@ -41,7 +41,7 @@ export class Brep extends ClayGeometry { this.core = new IFC.IfcFacetedBrep(ifcClosedShell); this.regenerateBrep(); - this.ifcData = this.core; + this.attributes = this.core; this.update(); } diff --git a/src/geometries/Extrusion/index.ts b/src/geometries/Extrusion/index.ts index 65ac61c..b93bef5 100644 --- a/src/geometries/Extrusion/index.ts +++ b/src/geometries/Extrusion/index.ts @@ -7,7 +7,7 @@ import { MathUtils } from "../../utils/math-utils"; import { IfcUtils } from "../../utils/ifc-utils"; export class Extrusion extends ClayGeometry { - ifcData: IFC.IfcExtrudedAreaSolid | IFC.IfcBooleanClippingResult; + attributes: IFC.IfcExtrudedAreaSolid | IFC.IfcBooleanClippingResult; core: IFC.IfcExtrudedAreaSolid; @@ -36,13 +36,13 @@ export class Extrusion extends ClayGeometry { const direction = IfcUtils.direction(this.direction); this.core = new IFC.IfcExtrudedAreaSolid( - profile.ifcData, + profile.attributes, placement, direction, new IFC.IfcPositiveLengthMeasure(this.depth) ); - this.ifcData = this.core; + this.attributes = this.core; this.update(); } diff --git a/src/geometries/Geometry/index.ts b/src/geometries/Geometry/index.ts index 25fbe17..6bbcd0c 100644 --- a/src/geometries/Geometry/index.ts +++ b/src/geometries/Geometry/index.ts @@ -2,7 +2,7 @@ import { IFC4X3 as IFC, IfcLineObject } from "web-ifc"; import { ClayObject } from "../../base"; export abstract class ClayGeometry extends ClayObject { - abstract ifcData: + abstract attributes: | IFC.IfcGeometricRepresentationItem | IFC.IfcBooleanClippingResult; @@ -20,8 +20,10 @@ export abstract class ClayGeometry extends ClayObject { } >(); + delete() {} + addSubtraction(geometry: ClayGeometry) { - const item = geometry.ifcData; + const item = geometry.attributes; if (this.clippings.has(item.expressID)) { return; @@ -31,7 +33,7 @@ export abstract class ClayGeometry extends ClayObject { // (might be another bool operation) const bool = new IFC.IfcBooleanClippingResult( IFC.IfcBooleanOperator.DIFFERENCE, - this.ifcData, + this.attributes, item ); @@ -57,7 +59,7 @@ export abstract class ClayGeometry extends ClayObject { this.clippings.set(item.expressID, { bool, previous, next: null }); // Make this bool the current geometry - this.ifcData = bool; + this.attributes = bool; this.update(); } @@ -71,7 +73,7 @@ export abstract class ClayGeometry extends ClayObject { if (previous === null && next === null) { // This was the only bool in the list - this.ifcData = this.core; + this.attributes = this.core; this.firstClipping = null; this.lastClipping = null; } else if (previous !== null && next === null) { @@ -81,7 +83,7 @@ export abstract class ClayGeometry extends ClayObject { throw new Error("Malformed bool structure!"); } newLast.next = null; - this.ifcData = newLast.bool; + this.attributes = newLast.bool; } else if (previous === null && next !== null) { // The deleted bool was the first one in the list const newFirst = this.clippings.get(next); diff --git a/src/geometries/MappedItems/index.ts b/src/geometries/MappedItems/index.ts index 2ae0143..fa0e62b 100644 --- a/src/geometries/MappedItems/index.ts +++ b/src/geometries/MappedItems/index.ts @@ -11,7 +11,7 @@ import { IfcUtils } from "../../utils/ifc-utils"; // IFCSHAPEREPRESENTATION export class RepresentationMap extends ClayObject { - ifcData: IFC.IfcRepresentationMap; + attributes: IFC.IfcRepresentationMap; geometries: T; @@ -24,8 +24,8 @@ export class RepresentationMap extends ClayObject { super(model); this.geometries = geometries; - const representation = IfcUtils.shapeRepresentation(this.model); - representation.Items = geometries.map((geom) => geom.ifcData); + const items = geometries.map((geom) => geom.attributes); + const representation = IfcUtils.shape(this.model, items); const placement = new IFC.IfcAxis2Placement3D( IfcUtils.point(new THREE.Vector3()), @@ -33,7 +33,7 @@ export class RepresentationMap extends ClayObject { null ); - this.ifcData = new IFC.IfcRepresentationMap(placement, representation); + this.attributes = new IFC.IfcRepresentationMap(placement, representation); this.update(); } @@ -49,7 +49,7 @@ export class RepresentationMap extends ClayObject { IfcUtils.direction(zDir) ); - const item = new IFC.IfcMappedItem(this.ifcData, operator); + const item = new IFC.IfcMappedItem(this.attributes, operator); this.model.set(item); const transform = transformation.clone(); @@ -110,7 +110,7 @@ export class RepresentationMap extends ClayObject { } update(): void { - this.model.set(this.ifcData); + this.model.set(this.attributes); } private getItem(id: number) { diff --git a/src/geometries/Profiles/Profile/index.ts b/src/geometries/Profiles/Profile/index.ts index 4be8fd5..4c8de15 100644 --- a/src/geometries/Profiles/Profile/index.ts +++ b/src/geometries/Profiles/Profile/index.ts @@ -2,5 +2,5 @@ import * as WEBIFC from "web-ifc"; import { ClayObject } from "../../../base/clay-object"; export abstract class Profile extends ClayObject { - abstract ifcData: WEBIFC.IFC4X3.IfcProfileDef; + abstract attributes: WEBIFC.IFC4X3.IfcProfileDef; } diff --git a/src/geometries/Profiles/RectangleProfile/index.ts b/src/geometries/Profiles/RectangleProfile/index.ts index 5b14b83..360c5cd 100644 --- a/src/geometries/Profiles/RectangleProfile/index.ts +++ b/src/geometries/Profiles/RectangleProfile/index.ts @@ -5,7 +5,7 @@ import { Model } from "../../../base"; import { IfcUtils } from "../../../utils/ifc-utils"; export class RectangleProfile extends Profile { - ifcData: IFC.IfcRectangleProfileDef; + attributes: IFC.IfcRectangleProfileDef; dimension = new THREE.Vector3(1, 1, 0); @@ -21,7 +21,7 @@ export class RectangleProfile extends Profile { IfcUtils.direction(new THREE.Vector3(1, 0, 0)) ); - this.ifcData = new IFC.IfcRectangleProfileDef( + this.attributes = new IFC.IfcRectangleProfileDef( IFC.IfcProfileTypeEnum.AREA, null, placement, @@ -29,14 +29,14 @@ export class RectangleProfile extends Profile { new IFC.IfcPositiveLengthMeasure(this.dimension.y) ); - this.model.set(this.ifcData); + this.model.set(this.attributes); } update() { - this.ifcData.XDim.value = this.dimension.x; - this.ifcData.YDim.value = this.dimension.y; + this.attributes.XDim.value = this.dimension.x; + this.attributes.YDim.value = this.dimension.y; - const placement = this.model.get(this.ifcData.Position); + const placement = this.model.get(this.attributes.Position); IfcUtils.setAxis2Placement( this.model, @@ -45,6 +45,6 @@ export class RectangleProfile extends Profile { this.rotation ); - this.model.set(this.ifcData); + this.model.set(this.attributes); } } diff --git a/src/utils/ifc-utils.ts b/src/utils/ifc-utils.ts index 1242193..3c1901e 100644 --- a/src/utils/ifc-utils.ts +++ b/src/utils/ifc-utils.ts @@ -35,13 +35,16 @@ export class IfcUtils { ); } - static shapeRepresentation(model: Model) { - return new IFC.IfcShapeRepresentation( - model.context, - new IFC.IfcLabel("Body"), - new IFC.IfcLabel("SweptSolid"), - [] - ); + static productDefinitionShape( + model: Model, + items: IFC.IfcRepresentationItem[] + ) { + const representation = this.shape(model, items); + return new IFC.IfcProductDefinitionShape(null, null, [representation]); + } + + static shape(model: Model, items: IFC.IfcRepresentationItem[]) { + return new IFC.IfcShapeRepresentation(model.context, null, null, items); } static setAxis2Placement( diff --git a/yarn.lock b/yarn.lock index b253cb5..176bbc7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1773,16 +1773,16 @@ __metadata: languageName: node linkType: hard -"bim-fragment@npm:1.4.1": - version: 1.4.1 - resolution: "bim-fragment@npm:1.4.1" +"bim-fragment@npm:1.4.2": + version: 1.4.2 + resolution: "bim-fragment@npm:1.4.2" dependencies: flatbuffers: ^23.3.3 three-mesh-bvh: 0.7.0 unzipit: 1.4.3 peerDependencies: three: ^0.160.1 - checksum: 7a1fa3dba068819c8b8f0224a0de342d8a4424448f991c3f8f52064e7269ff8ba0627e9f25548adcc866c4490438e5e5b03d5d31a82f02b7b76ae0e35d1bdf62 + checksum: e86705f4fa75243b51c09d9219fdc652f4397e69879fd856927a3c58762b2e535d5e050bb970ad318776d39ae1b5c22a32303dc2b6658d1cf124d63c97581684 languageName: node linkType: hard @@ -5642,7 +5642,7 @@ __metadata: "@types/uuid": 9.0.8 "@typescript-eslint/eslint-plugin": ^4.27.0 "@typescript-eslint/parser": ^4.27.0 - bim-fragment: 1.4.1 + bim-fragment: 1.4.2 cpy-cli: ^3.1.1 earcut: ^2.2.4 eslint: ^7.28.0 @@ -5661,7 +5661,7 @@ __metadata: uuid: ^9.0.1 web-ifc: 0.0.51 peerDependencies: - bim-fragment: 1.4.1 + bim-fragment: 1.4.2 three: ^0.160.1 web-ifc: ^0.0.51 languageName: unknown