# SPDX-License-Identifier: LGPL-2.1-or-later # *************************************************************************** # * * # * Copyright (c) 2013 Yorik van Havre * # * * # * 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 Arch wall module import os import Arch import Draft import Part import FreeCAD as App from bimtests import TestArchBase class TestArchWall(TestArchBase.TestArchBase): def testWall(self): operation = "Checking Arch Wall..." self.printTestMessage(operation) l = Draft.makeLine(App.Vector(0, 0, 0), App.Vector(-2, 0, 0)) w = Arch.makeWall(l) self.assertTrue(w, "Arch Wall failed") def testWallMultiMatAlign(self): operation = "Checking Arch Wall with MultiMaterial and 3 alignments..." self.printTestMessage(operation) matA = Arch.makeMaterial() matB = Arch.makeMaterial() matMulti = Arch.makeMultiMaterial() matMulti.Materials = [matA, matB] matMulti.Thicknesses = [100, 200] # total width different from default 200 pts = [ App.Vector(0, 0, 0), App.Vector(1000, 0, 0), App.Vector(1000, 1000, 0), App.Vector(2000, 1000, 0), ] # wall based on wire: wire = Draft.makeWire(pts) wallWire = Arch.makeWall(wire) wallWire.Material = matMulti # wall based on sketch: sketch = App.activeDocument().addObject("Sketcher::SketchObject", "Sketch") sketch.addGeometry( [ Part.LineSegment(pts[0], pts[1]), Part.LineSegment(pts[1], pts[2]), Part.LineSegment(pts[2], pts[3]), ] ) wallSketch = Arch.makeWall(sketch) wallSketch.Material = matMulti alignLst = ["Left", "Center", "Right"] checkLst = [ [App.Vector(0, -300, 0), App.Vector(2000, 1000, 0)], [App.Vector(0, -150, 0), App.Vector(2000, 1150, 0)], [App.Vector(0, 0, 0), App.Vector(2000, 1300, 0)], ] for i in range(3): wallWire.Align = alignLst[i] wallSketch.Align = alignLst[i] App.ActiveDocument.recompute() for box in [wallWire.Shape.BoundBox, wallSketch.Shape.BoundBox]: ptMin = App.Vector(box.XMin, box.YMin, 0) self.assertTrue( ptMin.isEqual(checkLst[i][0], 1e-8), "Arch Wall with MultiMaterial and 3 alignments failed", ) ptMax = App.Vector(box.XMax, box.YMax, 0) self.assertTrue( ptMax.isEqual(checkLst[i][1], 1e-8), "Arch Wall with MultiMaterial and 3 alignments failed", ) def test_makeWall(self): """Test the makeWall function.""" operation = "Testing makeWall function" self.printTestMessage(operation) wall = Arch.makeWall(length=5000, width=200, height=3000) self.assertIsNotNone(wall, "makeWall failed to create a wall object.") self.assertEqual(wall.Label, "Wall", "Wall label is incorrect.") def test_joinWalls(self): """Test the joinWalls function.""" operation = "Testing joinWalls function" self.printTestMessage(operation) base_line1 = Draft.makeLine(App.Vector(0, 0, 0), App.Vector(5000, 0, 0)) base_line2 = Draft.makeLine(App.Vector(5000, 0, 0), App.Vector(5000, 3000, 0)) wall1 = Arch.makeWall(base_line1, width=200, height=3000) wall2 = Arch.makeWall(base_line2, width=200, height=3000) joined_wall = Arch.joinWalls([wall1, wall2]) self.assertIsNotNone(joined_wall, "joinWalls failed to join walls.") def test_remove_base_from_wall_without_host(self): """ Tests that removing a debasable wall's base using removeComponents successfully unlinks the base. """ self.printTestMessage("Testing removal of a wall's base component...") # 1. Arrange: Create a wall with a base line = Draft.makeLine(App.Vector(0, 0, 0), App.Vector(2000, 0, 0)) # Ensure the base object's shape is computed, making the wall debasable. line.recompute() wall = Arch.makeWall(line) self.document.recompute() # Ensure wall is fully formed self.assertIsNotNone(wall.Base, "Pre-condition failed: Wall should have a base.") self.assertTrue( Arch.is_debasable(wall), "Pre-condition failed: The test wall is not debasable." ) # 2. Act: Call removeComponents on the base. # This will trigger the is_debasable -> True -> debaseWall() path. Arch.removeComponents([wall.Base]) self.document.recompute() # 3. Assert: The base should now be None because debaseWall was successful. self.assertIsNone(wall.Base, "The wall's Base property was not cleared after removal.") def test_is_debasable_with_valid_line_base(self): """Tests that a wall based on a single Draft.Line is debasable.""" self.printTestMessage("Checking is_debasable with Draft.Line...") line = Draft.makeLine(App.Vector(0, 0, 0), App.Vector(1000, 0, 0)) line.recompute() wall = Arch.makeWall(line) self.document.recompute() self.assertTrue(Arch.is_debasable(wall), "Wall on Draft.Line should be debasable.") def test_is_debasable_with_valid_sketch_base(self): """Tests that a wall based on a Sketch with a single line is debasable.""" self.printTestMessage("Checking is_debasable with single-line Sketch...") sketch = self.document.addObject("Sketcher::SketchObject", "SingleLineSketch") sketch.addGeometry(Part.LineSegment(App.Vector(0, 0, 0), App.Vector(1000, 0, 0))) self.document.recompute() wall = Arch.makeWall(sketch) self.assertTrue(Arch.is_debasable(wall), "Wall on single-line Sketch should be debasable.") def test_is_debasable_with_multi_edge_base(self): """Tests that a wall based on a multi-segment wire is not debasable.""" self.printTestMessage("Checking is_debasable with multi-segment Wire...") wire = Draft.makeWire( [App.Vector(0, 0, 0), App.Vector(1000, 0, 0), App.Vector(1000, 1000, 0)] ) wall = Arch.makeWall(wire) self.assertFalse( Arch.is_debasable(wall), "Wall on multi-segment wire should not be debasable." ) def test_is_debasable_with_curved_base(self): """Tests that a wall based on an arc is not debasable.""" self.printTestMessage("Checking is_debasable with curved base...") arc = Draft.make_circle(radius=500, startangle=0, endangle=90) self.document.recompute() wall = Arch.makeWall(arc) self.document.recompute() self.assertFalse(Arch.is_debasable(wall), "Wall on curved base should not be debasable.") def test_is_debasable_with_no_base(self): """Tests that a baseless wall is not debasable.""" self.printTestMessage("Checking is_debasable with no base...") wall = Arch.makeWall(length=1000) self.assertFalse(Arch.is_debasable(wall), "Baseless wall should not be debasable.") def test_debase_wall_preserves_global_position(self): """ Tests that debaseWall correctly transfers the base's placement to the wall, preserving its global position and dimensions. """ self.printTestMessage("Checking debaseWall preserves global position...") # 1. Arrange: Create a rotated and translated line, and a wall from it. pl = App.Placement(App.Vector(1000, 500, 200), App.Rotation(App.Vector(0, 0, 1), 45)) line = Draft.makeLine(App.Vector(0, 0, 0), App.Vector(2000, 0, 0)) line.Placement = pl line.recompute() # Use object-level recompute wall = Arch.makeWall(line, width=200, height=1500, align="Left") self.document.recompute() # Store the wall's original state original_bb = wall.Shape.BoundBox original_volume = wall.Shape.Volume original_length = wall.Length.Value # 2. Act: Debase the wall success = Arch.debaseWall(wall) self.document.recompute() # 3. Assert self.assertTrue(success, "debaseWall should return True for a valid wall.") self.assertIsNone(wall.Base, "Wall's Base should be None after debasing.") # Core assertions for preserving geometry and placement self.assertAlmostEqual( original_volume, wall.Shape.Volume, delta=1e-6, msg="Wall volume should not change after debasing.", ) # Compare individual properties of the BoundBox with a tolerance final_bb = wall.Shape.BoundBox self.assertAlmostEqual( original_bb.XMin, final_bb.XMin, delta=1e-6, msg="Bounding box XMin does not match." ) self.assertAlmostEqual( original_bb.XMax, final_bb.XMax, delta=1e-6, msg="Bounding box XMax does not match." ) self.assertAlmostEqual( original_bb.YMin, final_bb.YMin, delta=1e-6, msg="Bounding box YMin does not match." ) self.assertAlmostEqual( original_bb.YMax, final_bb.YMax, delta=1e-6, msg="Bounding box YMax does not match." ) self.assertAlmostEqual( original_bb.ZMin, final_bb.ZMin, delta=1e-6, msg="Bounding box ZMin does not match." ) self.assertAlmostEqual( original_bb.ZMax, final_bb.ZMax, delta=1e-6, msg="Bounding box ZMax does not match." ) # Check parametric integrity self.assertAlmostEqual( wall.Length.Value, original_length, delta=1e-6, msg="Wall's Length property should be preserved.", ) # Verify it remains parametric by changing a property wall.Height = 2000 self.document.recompute() self.assertNotAlmostEqual( original_volume, wall.Shape.Volume, delta=1e-6, msg="Wall should remain parametric and its volume should change with height.", ) def test_makeWall_baseless_alignment(self): """ Tests that Arch.makeWall correctly creates a baseless wall with the specified alignment. """ self.printTestMessage("Checking baseless wall alignment from makeWall...") # Define the test cases: (Alignment Mode, Expected final Y-center) test_cases = [ ("Center", 0.0), ("Left", -100.0), ("Right", 100.0), ] for align_mode, expected_y_center in test_cases: with self.subTest(alignment=align_mode): # 1. Arrange & Act: Create a baseless wall using the API call. wall = Arch.makeWall(length=2000, width=200, height=1500, align=align_mode) self.document.recompute() # 2. Assert Geometry: Verify the shape is valid. self.assertFalse( wall.Shape.isNull(), msg=f"[{align_mode}] Shape should not be null." ) expected_volume = 2000 * 200 * 1500 self.assertAlmostEqual( wall.Shape.Volume, expected_volume, delta=1e-6, msg=f"[{align_mode}] Wall volume is incorrect.", ) # 3. Assert Placement and Alignment. # The wall's Placement should be at the origin. self.assertTrue( wall.Placement.Base.isEqual(App.Vector(0, 0, 0), 1e-6), msg=f"[{align_mode}] Default placement Base should be at the origin.", ) self.assertAlmostEqual( wall.Placement.Rotation.Angle, 0.0, delta=1e-6, msg=f"[{align_mode}] Default placement Rotation should be zero.", ) # The shape's center should be offset according to the alignment. shape_center = wall.Shape.BoundBox.Center expected_center = App.Vector(0, expected_y_center, 750) self.assertTrue( shape_center.isEqual(expected_center, 1e-5), msg=f"For '{align_mode}' align, wall center {shape_center} does not match expected {expected_center}", ) def test_baseless_wall_stretch_api(self): """ Tests the proxy methods for graphically editing baseless walls: calc_endpoints() and set_from_endpoints(). """ self.printTestMessage("Checking baseless wall stretch API...") # 1. Arrange: Create a baseless wall and then set its placement. initial_placement = App.Placement( App.Vector(1000, 1000, 0), App.Rotation(App.Vector(0, 0, 1), 45) ) # Create wall first, then set its placement. wall = Arch.makeWall(length=2000) wall.Placement = initial_placement self.document.recompute() # 2. Test calc_endpoints() endpoints = wall.Proxy.calc_endpoints(wall) self.assertEqual(len(endpoints), 2, "calc_endpoints should return two points.") # Verify the calculated endpoints against manual calculation half_len_vec_x = App.Vector(1000, 0, 0) rotated_half_vec = initial_placement.Rotation.multVec(half_len_vec_x) expected_p1 = initial_placement.Base - rotated_half_vec expected_p2 = initial_placement.Base + rotated_half_vec self.assertTrue(endpoints[0].isEqual(expected_p1, 1e-6), "Start point is incorrect.") self.assertTrue(endpoints[1].isEqual(expected_p2, 1e-6), "End point is incorrect.") # 3. Test set_from_endpoints() new_p1 = App.Vector(0, 0, 0) new_p2 = App.Vector(4000, 0, 0) wall.Proxy.set_from_endpoints(wall, [new_p1, new_p2]) self.document.recompute() # Assert that the wall's properties have been updated correctly self.assertAlmostEqual( wall.Length.Value, 4000.0, delta=1e-6, msg="Length was not updated correctly." ) expected_center = App.Vector(2000, 0, 0) self.assertTrue( wall.Placement.Base.isEqual(expected_center, 1e-6), "Placement.Base (center) was not updated correctly.", ) # Check rotation (should now be zero as the new points are on the X-axis) self.assertAlmostEqual( wall.Placement.Rotation.Angle, 0.0, delta=1e-6, msg="Placement.Rotation was not updated correctly.", )