Skip to content

IFC import: plain profile-extruded IfcSlab renders offset with wrong thickness #629

Description

@thebossnow

What happened

Imported a minimal hand-authored IFC4 file (IfcProject → IfcSite → IfcBuilding → IfcBuildingStorey, one IfcSlab, four IfcWalls forming its perimeter) through editor.pascal.app/ifc.

  • The 4 walls round-tripped perfectly: reported Length 10.00ft, Height 9.84ft, Thickness 0.656ft — exact matches for the 3.048m / 3m / 0.2m in the source file.
  • The slab (IfcArbitraryClosedProfileDef + IfcExtrudedAreaSolid, no material layer set, PredefinedType = .BASESLAB.) came in as "Footprint 1 slab" but rendered offset to the side of the wall footprint instead of underneath it, and reports Thickness 0.16ft (≈0.049m) instead of the file's 0.15m.

Expected: the slab occupies the same footprint as the walls (same rectangle, verified below) and reports 0.15m / ~0.492ft thickness.

What I ruled out

  • The file is internally consistent: the slab's profile points (#42–#46, (1.524,1.524)→(4.572,3.9624)) are the exact same absolute coordinates as the wall corner placements (#86, #108, #130, #152). The stored IfcExtrudedAreaSolid depth is 0.14999999999999999 (0.15m), not ~0.049m.
  • Suspected a missing ObjectPlacement on the spatial tree/slab (my exporter originally left it null). Added an explicit identity IfcLocalPlacement on IfcSite/IfcBuilding/IfcBuildingStorey and the slab, re-exported, re-tested. Identical result — same offset, same 0.16ft thickness. So it isn't a null-placement artifact; the attached file already has the explicit placements.

A hypothesis, unconfirmed: the slab's extrusion is authored with the local placement's Z-axis flipped ((0,0,-1)) and a local extrusion direction of (0,0,1), so it extrudes downward in world space with a positive depth value (top surface at the placement origin, base below it) — a valid but slightly unusual IFC authoring pattern. The walls in the same file don't do this (local Z stays (0,0,1), and the wall's placement carries the position instead of baking it into the profile). If your depth/extent-recovery code assumes a non-flipped local frame, that combination might be what's tripping it up specifically for the slab. I haven't looked at packages/ifc-converter's source to confirm this — just flagging it as the one structural difference I can see between the wall (works) and slab (doesn't) authoring in this file.

Repro file (IFC4, ~5.5KB, walls + slab as described above):

pascal_test2.ifc
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
FILE_NAME('','2026-08-10T14:52:59',(''),(''),'IfcOpenShell 0.8.5-1c5b825','IfcOpenShell 0.8.5-1c5b825','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROJECT('0X2rkhFXDCOAqVNZAthnn4',$,'untitled',$,$,$,$,(#10),#5);
#2=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#3=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
#4=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#5=IFCUNITASSIGNMENT((#4,#2,#3));
#6=IFCCARTESIANPOINT((0.,0.,0.));
#7=IFCDIRECTION((0.,0.,1.));
#8=IFCDIRECTION((1.,0.,0.));
#9=IFCAXIS2PLACEMENT3D(#6,#7,#8);
#10=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.0000000000000001E-05,#9,$);
#11=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#10,$,.MODEL_VIEW.,$);
#12=IFCSITE('0q2RAHQ9zFNOG5yVK015mH',$,'Site',$,$,#22,#41,$,$,$,$,$,$,$);
#13=IFCBUILDING('2ivlxUwjjAHAPN_qSWj3OP',$,'Building',$,$,#27,$,$,$,$,$,$);
#14=IFCBUILDINGSTOREY('03qMmw5mX1fhw_uEBhpvQo',$,'Ground Floor',$,$,#32,$,$,$,$);
#15=IFCRELAGGREGATES('1Fex0KvXr9k8qi5ztnhGvk',$,$,$,#1,(#12));
#16=IFCRELAGGREGATES('21lLDdwILDU8sBR_j_qwlQ',$,$,$,#12,(#13));
#17=IFCRELAGGREGATES('3yquw45zzFgwkWPEJr70jf',$,$,$,#13,(#14));
#18=IFCCARTESIANPOINT((0.,0.,0.));
#19=IFCDIRECTION((0.,0.,1.));
#20=IFCDIRECTION((1.,0.,0.));
#21=IFCAXIS2PLACEMENT3D(#18,#19,#20);
#22=IFCLOCALPLACEMENT($,#21);
#23=IFCCARTESIANPOINT((0.,0.,0.));
#24=IFCDIRECTION((0.,0.,1.));
#25=IFCDIRECTION((1.,0.,0.));
#26=IFCAXIS2PLACEMENT3D(#23,#24,#25);
#27=IFCLOCALPLACEMENT(#22,#26);
#28=IFCCARTESIANPOINT((0.,0.,0.));
#29=IFCDIRECTION((0.,0.,1.));
#30=IFCDIRECTION((1.,0.,0.));
#31=IFCAXIS2PLACEMENT3D(#28,#29,#30);
#32=IFCLOCALPLACEMENT(#27,#31);
#33=IFCCARTESIANPOINT((0.,0.));
#34=IFCCARTESIANPOINT((9.1440000000000001,0.));
#35=IFCCARTESIANPOINT((9.1440000000000001,15.24));
#36=IFCCARTESIANPOINT((0.,15.24));
#37=IFCCARTESIANPOINT((0.,0.));
#38=IFCPOLYLINE((#33,#34,#35,#36,#37));
#39=IFCGEOMETRICCURVESET((#38));
#40=IFCSHAPEREPRESENTATION(#11,'Body','GeometricCurveSet',(#39));
#41=IFCPRODUCTDEFINITIONSHAPE($,$,(#40));
#42=IFCCARTESIANPOINT((1.524,1.524));
#43=IFCCARTESIANPOINT((4.5720000000000001,1.524));
#44=IFCCARTESIANPOINT((4.5720000000000001,3.9624000000000001));
#45=IFCCARTESIANPOINT((1.524,3.9624000000000001));
#46=IFCCARTESIANPOINT((1.524,1.524));
#47=IFCPOLYLINE((#42,#43,#44,#45,#46));
#48=IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#47);
#49=IFCSLAB('1a_QZryoL4RA$jFb_MWudY',$,'Footprint 1 slab',$,$,#68,#57,$,.BASESLAB.);
#50=IFCCARTESIANPOINT((0.,0.,0.));
#51=IFCDIRECTION((0.,0.,-1.));
#52=IFCDIRECTION((1.,0.,0.));
#53=IFCAXIS2PLACEMENT3D(#50,#51,#52);
#54=IFCDIRECTION((0.,0.,1.));
#55=IFCEXTRUDEDAREASOLID(#48,#53,#54,0.14999999999999999);
#56=IFCSHAPEREPRESENTATION(#11,'Body','SweptSolid',(#55));
#57=IFCPRODUCTDEFINITIONSHAPE($,$,(#56));
#63=IFCRELCONTAINEDINSPATIALSTRUCTURE('2onKV5145BKe963NFyNzps',$,$,$,(#91,#135,#113,#49,#69),#14);
#64=IFCCARTESIANPOINT((0.,0.,0.));
#65=IFCDIRECTION((0.,0.,1.));
#66=IFCDIRECTION((1.,0.,0.));
#67=IFCAXIS2PLACEMENT3D(#64,#65,#66);
#68=IFCLOCALPLACEMENT(#32,#67);
#69=IFCWALL('2LU6EYJkH6yPANWtd$EcwV',$,'Footprint 1 wall 1',$,$,#90,#85,$,$);
#70=IFCCARTESIANPOINTLIST2D(((0.,0.),(0.,0.20000000000000001),(3.048,0.20000000000000001),(3.048,0.),(0.,0.)));
#71=IFCINDEXEDPOLYCURVE(#70,$,.F.);
#72=IFCDIRECTION((0.,0.,1.));
#73=IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#71);
#74=IFCCARTESIANPOINT((0.,0.,0.));
#75=IFCDIRECTION((0.,0.,1.));
#76=IFCDIRECTION((1.,0.,0.));
#77=IFCAXIS2PLACEMENT3D(#74,#75,#76);
#78=IFCEXTRUDEDAREASOLID(#73,#77,#72,3.);
#79=IFCSHAPEREPRESENTATION(#11,'Body','SweptSolid',(#78));
#85=IFCPRODUCTDEFINITIONSHAPE($,$,(#79));
#86=IFCCARTESIANPOINT((1.524,1.524,0.));
#87=IFCDIRECTION((0.,0.,1.));
#88=IFCDIRECTION((1.,0.,0.));
#89=IFCAXIS2PLACEMENT3D(#86,#87,#88);
#90=IFCLOCALPLACEMENT(#32,#89);
#91=IFCWALL('1wt2kQo0b6qQ1SsGQnsZiL',$,'Footprint 1 wall 2',$,$,#112,#107,$,$);
#92=IFCCARTESIANPOINTLIST2D(((0.,0.),(0.,0.20000000000000001),(2.4384000000000001,0.20000000000000001),(2.4384000000000001,0.),(0.,0.)));
#93=IFCINDEXEDPOLYCURVE(#92,$,.F.);
#94=IFCDIRECTION((0.,0.,1.));
#95=IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#93);
#96=IFCCARTESIANPOINT((0.,0.,0.));
#97=IFCDIRECTION((0.,0.,1.));
#98=IFCDIRECTION((1.,0.,0.));
#99=IFCAXIS2PLACEMENT3D(#96,#97,#98);
#100=IFCEXTRUDEDAREASOLID(#95,#99,#94,3.);
#101=IFCSHAPEREPRESENTATION(#11,'Body','SweptSolid',(#100));
#107=IFCPRODUCTDEFINITIONSHAPE($,$,(#101));
#108=IFCCARTESIANPOINT((4.5720000000000001,1.524,0.));
#109=IFCDIRECTION((0.,0.,1.));
#110=IFCDIRECTION((0.,1.,0.));
#111=IFCAXIS2PLACEMENT3D(#108,#109,#110);
#112=IFCLOCALPLACEMENT(#32,#111);
#113=IFCWALL('09Cl9kPrPEWuLu8h9oLi4e',$,'Footprint 1 wall 3',$,$,#134,#129,$,$);
#114=IFCCARTESIANPOINTLIST2D(((0.,0.),(0.,0.20000000000000001),(3.048,0.20000000000000001),(3.048,0.),(0.,0.)));
#115=IFCINDEXEDPOLYCURVE(#114,$,.F.);
#116=IFCDIRECTION((0.,0.,1.));
#117=IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#115);
#118=IFCCARTESIANPOINT((0.,0.,0.));
#119=IFCDIRECTION((0.,0.,1.));
#120=IFCDIRECTION((1.,0.,0.));
#121=IFCAXIS2PLACEMENT3D(#118,#119,#120);
#122=IFCEXTRUDEDAREASOLID(#117,#121,#116,3.);
#123=IFCSHAPEREPRESENTATION(#11,'Body','SweptSolid',(#122));
#129=IFCPRODUCTDEFINITIONSHAPE($,$,(#123));
#130=IFCCARTESIANPOINT((4.5720000000000001,3.9624000000000001,0.));
#131=IFCDIRECTION((0.,0.,1.));
#132=IFCDIRECTION((-1.,0.,0.));
#133=IFCAXIS2PLACEMENT3D(#130,#131,#132);
#134=IFCLOCALPLACEMENT(#32,#133);
#135=IFCWALL('10Q_ZH6RP6MRUKqbM2I9_f',$,'Footprint 1 wall 4',$,$,#156,#151,$,$);
#136=IFCCARTESIANPOINTLIST2D(((0.,0.),(0.,0.20000000000000001),(2.4384000000000001,0.20000000000000001),(2.4384000000000001,0.),(0.,0.)));
#137=IFCINDEXEDPOLYCURVE(#136,$,.F.);
#138=IFCDIRECTION((0.,0.,1.));
#139=IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#137);
#140=IFCCARTESIANPOINT((0.,0.,0.));
#141=IFCDIRECTION((0.,0.,1.));
#142=IFCDIRECTION((1.,0.,0.));
#143=IFCAXIS2PLACEMENT3D(#140,#141,#142);
#144=IFCEXTRUDEDAREASOLID(#139,#143,#138,3.);
#145=IFCSHAPEREPRESENTATION(#11,'Body','SweptSolid',(#144));
#151=IFCPRODUCTDEFINITIONSHAPE($,$,(#145));
#152=IFCCARTESIANPOINT((1.524,3.9624000000000001,0.));
#153=IFCDIRECTION((0.,0.,1.));
#154=IFCDIRECTION((0.,-1.,0.));
#155=IFCAXIS2PLACEMENT3D(#152,#153,#154);
#156=IFCLOCALPLACEMENT(#32,#155);
ENDSEC;
END-ISO-10303-21;

Generated by aiblueprint-mcp's new (minimal) IFC exporter while validating it against your importer — noticed this while testing rather than going looking for it. Happy to provide more variants if useful (e.g. a slab with local Z not flipped, to test the hypothesis above).

Activity

  1. Aymericr commented on Aug 28, 2026

    @Aymericr
    Contributor

    Excellent report — the repro file did most of the work. Two findings, and they split cleanly.

    The thickness is our bug, and it's a one-liner. The converter parses your 0.15 m depth correctly and then throws it away. In packages/ifc-converter/src/index.ts the slab node is built with the thickness stuffed into metadata under this comment:

    // TODO(ifc-fix): Pascal SlabNode has no `thickness` field — moved
    // to metadata so the IFC value isn't lost.
    

    That comment is simply out of date — SlabNode has had thickness: z.number().default(0.05) for a while. So the field falls back to 0.05 m, which is 0.164 ft — your 0.16 ft, exactly. Fixing this is passing one value through and deleting a stale comment.

    The offset is your exporter, and your hypothesis had the right mechanism but the wrong culprit. You spotted the flipped Z in #51 = IFCDIRECTION((0.,0.,-1.)), and that is the cause — but our handling of it is spec-correct. Per IFC, the derived Y axis is Z × X, so (0,0,-1) × (1,0,0) = (0,-1,0). Your profile point (1.524, 1.524) therefore maps to world (1.524, -1.524): because you baked absolute coordinates into the profile and flipped the local Z, the footprint gets mirrored about the world X axis. The file is asking for a mirrored slab, and any conforming viewer will put it where we put it. That's also why your walls round-trip perfectly — they keep Z at (0,0,1) and carry position in the placement instead of the profile.

    Your offered variant with local Z unflipped would confirm this in one round trip, and I'd genuinely like to see it — partly to close the loop, partly because it's a useful fixture for us either way.

    I'll take the thickness fix. Neither of the open IFC PRs (#553, #603) touches this path, so there's no conflict. And thanks for flagging this from exporter-validation work rather than going looking for it — this is the most useful kind of bug report.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinggood first issueGood for newcomers

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions