@@ -621,6 +621,16 @@ function sourceColor(color: { x?: number; y?: number; z?: number } | undefined):
621621 return `#${ channel ( color ?. x ) } ${ channel ( color ?. y ) } ${ channel ( color ?. z ) } `
622622}
623623
624+ function roundMeshPosition ( value : number ) : number {
625+ const rounded = Math . round ( value * 10_000 ) / 10_000
626+ return rounded === 0 ? 0 : rounded
627+ }
628+
629+ function roundMeshNormal ( value : number ) : number {
630+ const rounded = Math . round ( value * 1000 ) / 1000
631+ return rounded === 0 ? 0 : rounded
632+ }
633+
624634function extractImportedMeshPrimitives (
625635 ifcApi : WebIFC . IfcAPI ,
626636 modelID : number ,
@@ -678,24 +688,21 @@ function extractImportedMeshPrimitives(
678688 // STEP `swapYZ` transform here a second time makes plan depth look
679689 // like height (and height look like plan depth), exploding fallback
680690 // walls and railings across the scene.
681- if ( swapYZ ) {
682- positions . push (
683- ( world [ 0 ] ! - originOffset [ 0 ] ! ) * unitFactor ,
684- ( world [ 1 ] ! - originOffset [ 2 ] ! ) * unitFactor - levelElevation ,
685- - ( world [ 2 ] ! + originOffset [ 1 ] ! ) * unitFactor ,
686- )
687- } else {
688- positions . push (
689- ...toPascalPoint (
691+ const mappedPosition : [ number , number , number ] = swapYZ
692+ ? [
693+ world [ 0 ] ! - originOffset [ 0 ] ! * unitFactor ,
694+ world [ 1 ] ! - originOffset [ 2 ] ! * unitFactor - levelElevation ,
695+ - ( world [ 2 ] ! + originOffset [ 1 ] ! * unitFactor ) ,
696+ ]
697+ : toPascalPoint (
690698 [
691- ( world [ 0 ] ! - originOffset [ 0 ] ! ) * unitFactor ,
692- ( world [ 1 ] ! - originOffset [ 1 ] ! ) * unitFactor ,
693- ( world [ 2 ] ! - originOffset [ 2 ] ! ) * unitFactor ,
699+ world [ 0 ] ! - originOffset [ 0 ] ! * unitFactor ,
700+ world [ 1 ] ! - originOffset [ 1 ] ! * unitFactor ,
701+ world [ 2 ] ! - originOffset [ 2 ] ! * unitFactor ,
694702 ] ,
695703 levelElevation ,
696- ) ,
697- )
698- }
704+ )
705+ positions . push ( ...mappedPosition . map ( roundMeshPosition ) )
699706
700707 const nx = vertices [ vertex + 3 ] !
701708 const ny = vertices [ vertex + 4 ] !
@@ -710,9 +717,9 @@ function extractImportedMeshPrimitives(
710717 : worldNormal
711718 const normalLength = Math . hypot ( ...mappedNormal ) || 1
712719 normals . push (
713- mappedNormal [ 0 ] ! / normalLength ,
714- mappedNormal [ 1 ] ! / normalLength ,
715- mappedNormal [ 2 ] ! / normalLength ,
720+ roundMeshNormal ( mappedNormal [ 0 ] ! / normalLength ) ,
721+ roundMeshNormal ( mappedNormal [ 1 ] ! / normalLength ) ,
722+ roundMeshNormal ( mappedNormal [ 2 ] ! / normalLength ) ,
716723 )
717724 }
718725
@@ -1042,6 +1049,24 @@ export async function convertIfcToPascal(
10421049 return storeyExpressId == null ? 0 : ( storeyElevationByExpressId . get ( storeyExpressId ) ?? 0 )
10431050 }
10441051
1052+ const importedPrimitivesByExpressId = new Map < number , ImportedMeshPrimitiveValue [ ] > ( )
1053+ function importedMeshPrimitivesFor ( expressId : number ) : ImportedMeshPrimitiveValue [ ] {
1054+ const cached = importedPrimitivesByExpressId . get ( expressId )
1055+ if ( cached ) return cached
1056+ const primitives = extractImportedMeshPrimitives (
1057+ ifcApi ,
1058+ modelID ,
1059+ expressId ,
1060+ unitFactor ,
1061+ originOffset ,
1062+ elementLevelElevation ( expressId ) ,
1063+ toPascalPoint ,
1064+ opts . swapYZ ,
1065+ )
1066+ importedPrimitivesByExpressId . set ( expressId , primitives )
1067+ return primitives
1068+ }
1069+
10451070 progress ( 'Processing sites...' , 30 )
10461071 // Process sites
10471072 const sites = ifcApi . GetLineIDsWithType ( modelID , WebIFC . IFCSITE )
@@ -1763,7 +1788,12 @@ export async function convertIfcToPascal(
17631788 // landings as exact meshes: roofs may be sloped, while a local landing is
17641789 // not a storey floor and must not raise adjacent wall bases.
17651790 const slabPredefinedType = String ( slab . PredefinedType ?. value ?? '' ) . toUpperCase ( )
1766- if ( slabPredefinedType === 'ROOF' || slabPredefinedType === 'LANDING' ) continue
1791+ if (
1792+ ( slabPredefinedType === 'ROOF' || slabPredefinedType === 'LANDING' ) &&
1793+ importedMeshPrimitivesFor ( slabExpressID ) . length > 0
1794+ ) {
1795+ continue
1796+ }
17671797
17681798 const nodeId = generateId ( 'slab' )
17691799 expressIdToNodeId . set ( slabExpressID , nodeId )
@@ -1851,6 +1881,7 @@ export async function convertIfcToPascal(
18511881 ifcType : 'IFCSLAB' ,
18521882 expressID : slabExpressID ,
18531883 globalId : slab . GlobalId ?. value ,
1884+ predefinedType : slab . PredefinedType ?. value ,
18541885 thickness,
18551886 } ) ,
18561887 } )
@@ -1863,6 +1894,24 @@ export async function convertIfcToPascal(
18631894 }
18641895
18651896 // Process stairs
1897+ const stairFlightExpressIds = new Set < number > ( )
1898+ const stairFlights = ifcApi . GetLineIDsWithType ( modelID , WebIFC . IFCSTAIRFLIGHT )
1899+ for ( let i = 0 ; i < stairFlights . size ( ) ; i ++ ) stairFlightExpressIds . add ( stairFlights . get ( i ) )
1900+
1901+ function claimStairFlightDescendants ( stairExpressId : number , stairNodeId : string ) {
1902+ const pending = [ ...( childrenMap . get ( stairExpressId ) ?? [ ] ) ]
1903+ const visited = new Set < number > ( )
1904+ while ( pending . length > 0 ) {
1905+ const descendant = pending . pop ( ) !
1906+ if ( visited . has ( descendant ) ) continue
1907+ visited . add ( descendant )
1908+ if ( stairFlightExpressIds . has ( descendant ) ) {
1909+ expressIdToNodeId . set ( descendant , stairNodeId )
1910+ }
1911+ pending . push ( ...( childrenMap . get ( descendant ) ?? [ ] ) )
1912+ }
1913+ }
1914+
18661915 const stairs = ifcApi . GetLineIDsWithType ( modelID , WebIFC . IFCSTAIR )
18671916 for ( let i = 0 ; i < stairs . size ( ) ; i ++ ) {
18681917 const stairExpressID = stairs . get ( i )
@@ -1871,6 +1920,7 @@ export async function convertIfcToPascal(
18711920 const stair = ifcApi . GetLine ( modelID , stairExpressID )
18721921 const nodeId = generateId ( 'stair' )
18731922 expressIdToNodeId . set ( stairExpressID , nodeId )
1923+ claimStairFlightDescendants ( stairExpressID , nodeId )
18741924
18751925 const parentNodeId = resolveElementParent ( stairExpressID )
18761926
@@ -2165,87 +2215,80 @@ export async function convertIfcToPascal(
21652215 try {
21662216 const spaces = ifcApi . GetLineIDsWithType ( modelID , WebIFC . IFCSPACE )
21672217 for ( let i = 0 ; i < spaces . size ( ) ; i ++ ) {
2168- const spaceExpressId = spaces . get ( i )
2169- if ( expressIdToNodeId . has ( spaceExpressId ) ) continue
2170- const space = ifcApi . GetLine ( modelID , spaceExpressId )
2171- const parentNodeId = resolveElementParent ( spaceExpressId )
2172- const levelElevation = elementLevelElevation ( spaceExpressId )
2173- const primitives = extractImportedMeshPrimitives (
2174- ifcApi ,
2175- modelID ,
2176- spaceExpressId ,
2177- unitFactor ,
2178- originOffset ,
2179- levelElevation ,
2180- toPascalPoint ,
2181- opts . swapYZ ,
2182- )
2183- let polygon : [ number , number ] [ ] | null = null
2184- let ceilingHeight = DEFAULT_LEVEL_HEIGHT
21852218 try {
2186- const worldMat = space . ObjectPlacement ?. value
2187- ? resolveWorldTransform ( ifcApi , modelID , space . ObjectPlacement . value )
2188- : identity ( )
2189- const body = getBodyExtrusionData ( ifcApi , modelID , space )
2190- const extrusionMat = getExtrusionPosition ( ifcApi , modelID , space )
2191- if ( body . profilePoints && body . profilePoints . length >= 3 ) {
2192- const combinedMat = extrusionMat ? multiply ( worldMat , extrusionMat ) : worldMat
2193- polygon = body . profilePoints . map ( ( point ) => {
2194- const scene = worldToScene ( transformPoint3 ( combinedMat , [ point [ 0 ] , point [ 1 ] , 0 ] ) )
2195- return [ scene [ 0 ] , scene [ 1 ] ] as [ number , number ]
2196- } )
2197- const first = polygon [ 0 ]
2198- const last = polygon . at ( - 1 )
2199- if (
2200- polygon . length > 3 &&
2201- last &&
2202- Math . abs ( first [ 0 ] - last [ 0 ] ) < 1e-6 &&
2203- Math . abs ( first [ 1 ] - last [ 1 ] ) < 1e-6
2204- ) {
2205- polygon . pop ( )
2219+ const spaceExpressId = spaces . get ( i )
2220+ if ( expressIdToNodeId . has ( spaceExpressId ) ) continue
2221+ const space = ifcApi . GetLine ( modelID , spaceExpressId )
2222+ const parentNodeId = resolveElementParent ( spaceExpressId )
2223+ const primitives = importedMeshPrimitivesFor ( spaceExpressId )
2224+ let polygon : [ number , number ] [ ] | null = null
2225+ let ceilingHeight = DEFAULT_LEVEL_HEIGHT
2226+ try {
2227+ const worldMat = space . ObjectPlacement ?. value
2228+ ? resolveWorldTransform ( ifcApi , modelID , space . ObjectPlacement . value )
2229+ : identity ( )
2230+ const body = getBodyExtrusionData ( ifcApi , modelID , space )
2231+ const extrusionMat = getExtrusionPosition ( ifcApi , modelID , space )
2232+ if ( body . profilePoints && body . profilePoints . length >= 3 ) {
2233+ const combinedMat = extrusionMat ? multiply ( worldMat , extrusionMat ) : worldMat
2234+ polygon = body . profilePoints . map ( ( point ) => {
2235+ const scene = worldToScene ( transformPoint3 ( combinedMat , [ point [ 0 ] , point [ 1 ] , 0 ] ) )
2236+ return [ scene [ 0 ] , scene [ 1 ] ] as [ number , number ]
2237+ } )
2238+ const first = polygon [ 0 ]
2239+ const last = polygon . at ( - 1 )
2240+ if (
2241+ polygon . length > 3 &&
2242+ last &&
2243+ Math . abs ( first [ 0 ] - last [ 0 ] ) < 1e-6 &&
2244+ Math . abs ( first [ 1 ] - last [ 1 ] ) < 1e-6
2245+ ) {
2246+ polygon . pop ( )
2247+ }
22062248 }
2249+ if ( body . depth ) ceilingHeight = body . depth * unitFactor
2250+ } catch {
2251+ /* mesh fallback below */
2252+ }
2253+ polygon ??= meshFootprint ( primitives )
2254+ if ( ! polygon || polygon . length < 3 ) continue
2255+ if ( primitives . length > 0 ) {
2256+ const ys = primitives . flatMap ( ( primitive ) =>
2257+ primitive . positions . filter ( ( _ , index ) => index % 3 === 1 ) ,
2258+ )
2259+ if ( ys . length > 0 ) ceilingHeight = Math . max ( ...ys ) - Math . min ( ...ys )
22072260 }
2208- if ( body . depth ) ceilingHeight = body . depth * unitFactor
2209- } catch {
2210- /* mesh fallback below */
2211- }
2212- polygon ??= meshFootprint ( primitives )
2213- if ( ! polygon || polygon . length < 3 ) continue
2214- if ( primitives . length > 0 ) {
2215- const ys = primitives . flatMap ( ( primitive ) =>
2216- primitive . positions . filter ( ( _ , index ) => index % 3 === 1 ) ,
2217- )
2218- if ( ys . length > 0 ) ceilingHeight = Math . max ( ...ys ) - Math . min ( ...ys )
2219- }
22202261
2221- const nodeId = generateId ( 'zone' )
2222- const zone = tryParse ( ZoneNode , 'zone' , {
2223- object : 'node' ,
2224- id : nodeId ,
2225- type : 'zone' ,
2226- name : space . LongName ?. value || space . Name ?. value || `Space ${ i + 1 } ` ,
2227- parentId : parentNodeId ,
2228- visible : true ,
2229- polygon,
2230- spaceRole : 'room' ,
2231- roomNumber :
2232- space . LongName ?. value && space . Name ?. value !== space . LongName ?. value
2233- ? space . Name . value
2234- : '' ,
2235- ceilingHeight : Math . max ( 0.1 , ceilingHeight ) ,
2236- metadata : buildMetadata ( {
2237- ifcType : 'IFCSPACE' ,
2238- expressID : spaceExpressId ,
2239- globalId : space . GlobalId ?. value ,
2240- predefinedType : space . PredefinedType ?. value ,
2241- } ) ,
2242- } )
2243- expressIdToNodeId . set ( spaceExpressId , nodeId )
2244- nodes [ nodeId ] = zone
2245- if ( parentNodeId && nodes [ parentNodeId ] ) {
2246- ; ( nodes [ parentNodeId ] as { children ?: string [ ] } ) . children ?. push ( nodeId )
2262+ const spaceName = space . Name ?. value
2263+ const longName = space . LongName ?. value
2264+ const nodeId = generateId ( 'zone' )
2265+ const zone = tryParse ( ZoneNode , 'zone' , {
2266+ object : 'node' ,
2267+ id : nodeId ,
2268+ type : 'zone' ,
2269+ name : longName || spaceName || `Space ${ i + 1 } ` ,
2270+ parentId : parentNodeId ,
2271+ visible : true ,
2272+ polygon,
2273+ spaceRole : 'room' ,
2274+ roomNumber : longName && spaceName !== longName ? ( spaceName ?? '' ) : '' ,
2275+ ceilingHeight : Math . max ( 0.1 , ceilingHeight ) ,
2276+ metadata : buildMetadata ( {
2277+ ifcType : 'IFCSPACE' ,
2278+ expressID : spaceExpressId ,
2279+ globalId : space . GlobalId ?. value ,
2280+ predefinedType : space . PredefinedType ?. value ,
2281+ } ) ,
2282+ } )
2283+ expressIdToNodeId . set ( spaceExpressId , nodeId )
2284+ nodes [ nodeId ] = zone
2285+ if ( parentNodeId && nodes [ parentNodeId ] ) {
2286+ ; ( nodes [ parentNodeId ] as { children ?: string [ ] } ) . children ?. push ( nodeId )
2287+ }
2288+ importedSpaceCount ++
2289+ } catch {
2290+ /* skip malformed space */
22472291 }
2248- importedSpaceCount ++
22492292 }
22502293 } catch {
22512294 /* IFC schema may not expose spaces */
@@ -2291,16 +2334,7 @@ export async function convertIfcToPascal(
22912334 if ( expressIdToNodeId . has ( expressId ) ) continue
22922335 const element = ifcApi . GetLine ( modelID , expressId )
22932336 const parentNodeId = resolveElementParent ( expressId )
2294- const primitives = extractImportedMeshPrimitives (
2295- ifcApi ,
2296- modelID ,
2297- expressId ,
2298- unitFactor ,
2299- originOffset ,
2300- elementLevelElevation ( expressId ) ,
2301- toPascalPoint ,
2302- opts . swapYZ ,
2303- )
2337+ const primitives = importedMeshPrimitivesFor ( expressId )
23042338 if ( primitives . length === 0 ) continue
23052339
23062340 const nodeId = generateId ( 'imesh' )
0 commit comments