From fb999461ce2e654321b0b6dcab197909c2f11429 Mon Sep 17 00:00:00 2001 From: menes97 <38889017+menes97@users.noreply.github.com> Date: Thu, 12 Nov 2026 23:10:23 +0300 Subject: [PATCH] IFC: compute element dimensions from the exported geometry Pset_NwcElementProperties used the mesh's local bounding box, so the dimensions ignored the length-unit scale and the mesh transform, and clipped meshes, which have no bounding box, lost them. Both export paths now pass the exported vertices and scale, and the values are written as IFCLENGTHMEASURE. Fixes #41 Co-Authored-By: Claude Opus 5.5 --- src/NwcIO/Internal/Export/IfcExporter.cs | 33 ++++++--- tests/NwcIO.Tests/IfcExportTests.cs | 92 ++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 10 deletions(-) diff --git a/src/NwcIO/Internal/Export/IfcExporter.cs b/src/NwcIO/Internal/Export/IfcExporter.cs index 6f84abc..277b3d3 100644 --- a/src/NwcIO/Internal/Export/IfcExporter.cs +++ b/src/NwcIO/Internal/Export/IfcExporter.cs @@ -210,7 +210,7 @@ public static void Write( // Export properties if requested if (options.ExportProperties) { - WriteMeshProperties(w, elemId, mesh, ownerHistoryId); + WriteMeshProperties(w, elemId, mesh, ownerHistoryId, vertices, scale); } } @@ -437,7 +437,7 @@ public static void WriteAssembly( if (options.ExportProperties) { - WriteMeshProperties(w, elemId, mesh, ownerHistoryId); + WriteMeshProperties(w, elemId, mesh, ownerHistoryId, vertices, scale); } } @@ -587,7 +587,13 @@ private static int AddIfcProduct( }; } - private static void WriteMeshProperties(StepWriter w, int elementId, NwcMesh mesh, int ownerHistoryId) + private static void WriteMeshProperties( + StepWriter w, + int elementId, + NwcMesh mesh, + int ownerHistoryId, + IReadOnlyList vertices, + double scale) { var propIds = new List(); @@ -610,14 +616,21 @@ private static void WriteMeshProperties(StepWriter w, int elementId, NwcMesh mes propIds.Add(pMat); } - if (mesh.BoundingBox is { } bbox && !bbox.IsEmpty) + var bounds = BoundingBox3d.Empty; + foreach (var vertex in vertices) + { + bounds = bounds.Union(vertex); + } + + if (!bounds.IsEmpty) { - int pDimX = w.Add("IFCPROPERTYSINGLEVALUE", - $"'Length_X',$,IFCREAL({StepWriter.Real(bbox.Size.X)}),$"); - int pDimY = w.Add("IFCPROPERTYSINGLEVALUE", - $"'Width_Y',$,IFCREAL({StepWriter.Real(bbox.Size.Y)}),$"); - int pDimZ = w.Add("IFCPROPERTYSINGLEVALUE", - $"'Height_Z',$,IFCREAL({StepWriter.Real(bbox.Size.Z)}),$"); + var size = bounds.Size * scale; + var pDimX = w.Add("IFCPROPERTYSINGLEVALUE", + $"'Length_X',$,IFCLENGTHMEASURE({StepWriter.Real(size.X)}),$"); + var pDimY = w.Add("IFCPROPERTYSINGLEVALUE", + $"'Width_Y',$,IFCLENGTHMEASURE({StepWriter.Real(size.Y)}),$"); + var pDimZ = w.Add("IFCPROPERTYSINGLEVALUE", + $"'Height_Z',$,IFCLENGTHMEASURE({StepWriter.Real(size.Z)}),$"); propIds.Add(pDimX); propIds.Add(pDimY); propIds.Add(pDimZ); diff --git a/tests/NwcIO.Tests/IfcExportTests.cs b/tests/NwcIO.Tests/IfcExportTests.cs index 89eaf19..f0571c9 100644 --- a/tests/NwcIO.Tests/IfcExportTests.cs +++ b/tests/NwcIO.Tests/IfcExportTests.cs @@ -1,4 +1,5 @@ using NwcIO.Model; +using NwcIO.Model.Sectioning; using Xunit; namespace NwcIO.Tests; @@ -217,6 +218,97 @@ public void SaveIfc_WithMetreUnits_ScalesCoordinates() Assert.Contains("1.", ifcM); } + [Theory] + [InlineData(false, NwcIfcLengthUnit.Millimetre, true, "1000.")] + [InlineData(true, NwcIfcLengthUnit.Millimetre, true, "1000.")] + [InlineData(false, NwcIfcLengthUnit.Metre, true, "1.")] + [InlineData(true, NwcIfcLengthUnit.Metre, true, "1.")] + [InlineData(false, NwcIfcLengthUnit.Metre, false, "1000.")] + [InlineData(true, NwcIfcLengthUnit.Metre, false, "1000.")] + public void SaveIfc_Dimensions_UseExportedLengthUnits( + bool assembly, NwcIfcLengthUnit unit, bool convert, string expected) + { + var doc = CreateSyntheticDocument(); + var ifc = ExportDimensions(doc, assembly, new NwcIfcExportOptions + { + LengthUnit = unit, + ConvertMmToMetres = convert, + }); + + AssertDimensions(ifc, expected, expected, expected); + } + + [Theory] + [InlineData(false, true)] + [InlineData(true, true)] + [InlineData(false, false)] + [InlineData(true, false)] + public void SaveIfc_Dimensions_UseSelectedCoordinateSpace(bool assembly, bool worldCoordinates) + { + var doc = CreateSyntheticDocument(); + doc.Meshes[0].Transform = new NwcTransform + { + Id = 1, + Translation = new Vector3d(5000, -3000, 2000), + // Rotate the cube 45 degrees about Z, increasing its X and Y extents. + Rotation = new Quaterniond(0, 0, Math.Sin(Math.PI / 8), Math.Cos(Math.PI / 8)), + }; + var ifc = ExportDimensions(doc, assembly, new NwcIfcExportOptions + { + WorldCoordinates = worldCoordinates, + LengthUnit = NwcIfcLengthUnit.Metre, + ConvertMmToMetres = true, + }); + + foreach (var name in new[] { "Length_X", "Width_Y", "Height_Z" }) + { + var match = System.Text.RegularExpressions.Regex.Match( + ifc, $@"IFCPROPERTYSINGLEVALUE\('{name}',\$,IFCLENGTHMEASURE\(([^)]+)\),\$\)"); + Assert.True(match.Success, $"Missing length measure for {name}"); + var actual = double.Parse(match.Groups[1].Value, System.Globalization.CultureInfo.InvariantCulture); + var expected = worldCoordinates && name != "Height_Z" ? Math.Sqrt(2) : 1; + Assert.Equal(expected, actual, 6); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void SaveIfc_Dimensions_RetainPostClipExtents(bool assembly) + { + var doc = CreateSyntheticDocument(); + var ifc = ExportDimensions(doc, assembly, new NwcIfcExportOptions + { + LengthUnit = NwcIfcLengthUnit.Metre, + ConvertMmToMetres = true, + SectionPlanes = new[] { new NwcSectionPlane { Normal = new Vector3d(1, 0, 0), Distance = 250 } }, + }); + + AssertDimensions(ifc, "0.75", "1.", "1."); + } + + private static string ExportDimensions(NwcDocument doc, bool assembly, NwcIfcExportOptions options) + { + using var sw = new StringWriter(); + if (assembly) + { + NwcAssembly.Create("Dimensions", doc).SaveIfc(sw, options); + } + else + { + doc.SaveIfc(sw, options); + } + + return sw.ToString(); + } + + private static void AssertDimensions(string ifc, string x, string y, string z) + { + Assert.Contains($"IFCPROPERTYSINGLEVALUE('Length_X',$,IFCLENGTHMEASURE({x}),$)", ifc); + Assert.Contains($"IFCPROPERTYSINGLEVALUE('Width_Y',$,IFCLENGTHMEASURE({y}),$)", ifc); + Assert.Contains($"IFCPROPERTYSINGLEVALUE('Height_Z',$,IFCLENGTHMEASURE({z}),$)", ifc); + } + [Fact] public void SaveIfc_SharedVerticesTrue_EmitsExactlyUniqueMeshPoints() {