From 844bdada9ce444aadf252cdf7073a28365b4b39f Mon Sep 17 00:00:00 2001 From: Furgo <148809153+furgo16@users.noreply.github.com> Date: Fri, 9 May 2025 01:35:12 +0200 Subject: [PATCH] BIM: add area calculation unit tests for Arch Components --- src/Mod/BIM/CMakeLists.txt | 1 + src/Mod/BIM/TestArch.py | 4 +- src/Mod/BIM/bimtests/TestArchComponent.py | 184 ++++++++++++++++++++++ 3 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 src/Mod/BIM/bimtests/TestArchComponent.py diff --git a/src/Mod/BIM/CMakeLists.txt b/src/Mod/BIM/CMakeLists.txt index 895f30a8a0..f7ecd19a50 100644 --- a/src/Mod/BIM/CMakeLists.txt +++ b/src/Mod/BIM/CMakeLists.txt @@ -199,6 +199,7 @@ SET(nativeifc_SRCS SET(bimtests_SRCS bimtests/TestArchBase.py + bimtests/TestArchComponent.py bimtests/TestArchRoof.py bimtests/TestArchSpace.py bimtests/TestArchWall.py diff --git a/src/Mod/BIM/TestArch.py b/src/Mod/BIM/TestArch.py index b1da58a951..059ac6a7da 100644 --- a/src/Mod/BIM/TestArch.py +++ b/src/Mod/BIM/TestArch.py @@ -38,6 +38,7 @@ import WorkingPlane from bimtests.TestArchRoof import TestArchRoof from bimtests.TestArchSpace import TestArchSpace from bimtests.TestArchWall import TestArchWall +from bimtests.TestArchComponent import TestArchComponent from draftutils.messages import _msg @@ -215,4 +216,5 @@ class ArchTest(unittest.TestCase): # Use the modules so that code checkers don't complain (flake8) True if TestArchSpace else False True if TestArchRoof else False -True if TestArchWall else False \ No newline at end of file +True if TestArchWall else False +True if TestArchComponent else False diff --git a/src/Mod/BIM/bimtests/TestArchComponent.py b/src/Mod/BIM/bimtests/TestArchComponent.py new file mode 100644 index 0000000000..3d23d8668c --- /dev/null +++ b/src/Mod/BIM/bimtests/TestArchComponent.py @@ -0,0 +1,184 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# *************************************************************************** +# * * +# * Copyright (c) 2025 Furgo * +# * * +# * This file is part of FreeCAD. * +# * * +# * FreeCAD is free software: you can redistribute it and/or modify it * +# * under the terms of the GNU Lesser General Public License as * +# * published by the Free Software Foundation, either version 2.1 of the * +# * License, or (at your option) any later version. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, but * +# * WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * +# * Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Lesser General Public * +# * License along with FreeCAD. If not, see * +# * . * +# * * +# *************************************************************************** + +# Unit tests for the ArchComponent module + +import Arch +import Draft +import FreeCAD as App +from bimtests import TestArchBase +from draftutils.messages import _msg + +from math import pi, cos, sin, radians + +class TestArchComponent(TestArchBase.TestArchBase): + + def testBsplineSlabAreas(self): + """Test the HorizontalArea and VerticalArea properties of a Bspline-based slab. + + See https://github.com/FreeCAD/FreeCAD/issues/20989. + """ + + operation = "Checking Bspline slab area calculation..." + self.printTestMessage(operation) + + doc = App.ActiveDocument + + # Parameters + radius = 10000 # 10 meters in mm + extrusionLength = 100 # 10 cm in mm + numPoints = 50 # Number of points for B-spline + startAngle = 0 # Start at 0 degrees (right) + endAngle = 180 # End at 180 degrees (left) + + # Create points for semicircle + points = [] + angleStep = (endAngle - startAngle) / (numPoints - 1) + for i in range(numPoints): + angleDeg = startAngle + i * angleStep + angleRad = radians(angleDeg) + x = radius * cos(angleRad) + y = radius * sin(angleRad) + points.append(App.Vector(x, y, 0)) + + # Create Draft objects + bspline = Draft.makeBSpline(points, closed=False) + closingLine = Draft.makeLine(points[-1], points[0]) + doc.recompute() + + # Create sketch + # We do this because Draft.make_wires does not support B-splines + # and we need a closed wire for the slab + sketch = Draft.makeSketch([bspline, closingLine], + autoconstraints=True, + delete=True) + if sketch is None: + self.fail("Sketch creation failed") + sketch.recompute() + + # Create slab + slab = Arch.makeStructure(sketch, length=extrusionLength, name="Slab") + slab.recompute() + + # Calculate theoretical areas + radiusMeters = radius / 1000 + heightMeters = extrusionLength / 1000 + theoreticalHorizontalArea = (pi * radiusMeters**2) / 2 + theoreticalVerticalArea = (pi * radiusMeters + 2 * radiusMeters) * heightMeters + + # Get actual areas + actualHorizontalArea = slab.HorizontalArea.getValueAs("m^2").Value + actualVerticalArea = slab.VerticalArea.getValueAs("m^2").Value + + # Optimally wrapped assertions + self.assertAlmostEqual( + actualHorizontalArea, theoreticalHorizontalArea, places=3, + msg=( + "Horizontal area > 0.1% tolerance | " + f"Exp: {theoreticalHorizontalArea:.3f} m² | " + f"Got: {actualHorizontalArea:.3f} m²" + ) + ) + + self.assertAlmostEqual( + actualVerticalArea, theoreticalVerticalArea, places=3, + msg=( + "Vertical area > 0.1% tolerance | " + f"Exp: {theoreticalVerticalArea:.3f} m² | " + f"Got: {actualVerticalArea:.3f} m²" + ) + ) + + def testHouseSpaceAreas(self): + """Test the HorizontalArea and VerticalArea properties of a house-like space. + + See https://github.com/FreeCAD/FreeCAD/issues/14687. + """ + + operation = "Checking house space area calculation..." + self.printTestMessage(operation) + + doc = App.ActiveDocument + + # Dimensional parameters (all in mm) + baseLength = 5000 # 5m along X-axis + baseWidth = 5000 # 5m along Y-axis (extrusion depth) + rectangleHeight = 2500 # 2.5m lower rectangular portion + triangleHeight = 2500 # 2.5m upper triangular portion + totalHeight = rectangleHeight + triangleHeight # 5m total height + + # Create envelope profile points (XZ plane) + points = [ + App.Vector(0, 0, 0), + App.Vector(baseLength, 0, 0), + App.Vector(baseLength, 0, rectangleHeight), + App.Vector(baseLength/2, 0, totalHeight), + App.Vector(0, 0, rectangleHeight) + ] + + # Create wire with automatic face creation + wire = Draft.makeWire(points, closed=True, face=True) + doc.recompute() + + # Extrude the wire + extrudedObj = Draft.extrude(wire, App.Vector(0, baseWidth, 0)) + extrudedObj.Label = "Extruded house" + doc.recompute() + + # Create Arch Space from the extrusion + space = Arch.makeSpace(extrudedObj) + space.Label = "House space" + doc.recompute() + + # Calculate theoretical areas + # Horizontal area (only bottom face on XY plane) + theoreticalHorizontalArea = (baseLength * baseWidth) / 1e6 # 25 m² + + # Vertical areas + # Side faces (YZ plane) - two rectangles + sideFaceArea = (rectangleHeight * baseWidth) / 1e6 # 12.5 m² each + totalSides = sideFaceArea * 2 # 25 m² + + # Front/back faces (XZ plane) + rectangularPart = (baseLength * rectangleHeight) / 1e6 # 12.5 m² + triangularPart = (baseLength * triangleHeight / 2) / 1e6 # 6.25 m² + totalFrontBack = (rectangularPart + triangularPart) * 2 # 37.5 m² + + theoreticalVerticalArea = totalSides + totalFrontBack # 62.5 m² + + # Get actual areas from space + actualHorizontalArea = space.HorizontalArea.getValueAs("m^2").Value + actualVerticalArea = space.VerticalArea.getValueAs("m^2").Value + + self.assertAlmostEqual( + actualHorizontalArea, theoreticalHorizontalArea, places=3, + msg=f"Horizontal area > 0.1% | Exp: {theoreticalHorizontalArea:.3f} | " + f"Got: {actualHorizontalArea:.3f}" + ) + + self.assertAlmostEqual( + actualVerticalArea, theoreticalVerticalArea, places=3, + msg=f"Vertical area > 0.1% | Exp: {theoreticalVerticalArea:.3f} | " + f"Got: {actualVerticalArea:.3f}" + )