Download src/Mod/BIM/bimtests/TestArchWindow.py from AbdulElahGwaith/FreeCAD: direct link, hf CLI and curl.
- Browser
- Download file 35.3 kB
-
https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/BIM/bimtests/TestArchWindow.py
- Command line
-
hf download hf://AbdulElahGwaith/FreeCAD/src/Mod/BIM/bimtests/TestArchWindow.py
-
curl -L -o TestArchWindow.py https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/BIM/bimtests/TestArchWindow.py
35.3 kB
| # 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 * | |
| # * <https://www.gnu.org/licenses/>. * | |
| # * * | |
| # *************************************************************************** | |
| import FreeCAD | |
| from bimtests import TestArchBase | |
| import Arch | |
| import ArchWindow # For ArchWindow._Window proxy class | |
| import Part | |
| import Draft | |
| import Sketcher | |
| class TestArchWindow(TestArchBase.TestArchBase): | |
| def test_create_no_args(self): | |
| """Test creating a window with no arguments.""" | |
| window = Arch.makeWindow(name="Window_NoArgs") | |
| self.assertIsNotNone(window) | |
| self.assertTrue( | |
| hasattr(window, "Proxy") and isinstance(window.Proxy, ArchWindow._Window), | |
| "Window proxy is not of expected ArchWindow._Window type", | |
| ) | |
| self.assertEqual(window.Label, "Window_NoArgs") | |
| self.assertEqual(window.IfcType, "Window") | |
| self.assertIsNone(window.Base) | |
| self.assertEqual(len(window.WindowParts), 0) | |
| self.assertTrue(window.Shape.isNull()) | |
| self.document.recompute() | |
| self.assertTrue(window.Shape.isNull()) | |
| def test_create_from_sketch_single_wire_default_parts(self): | |
| """Test creating a window from a single-wire sketch, relying on default parts.""" | |
| sketch = self._create_sketch_with_wires("SketchSingle", [(0, 0, 1000, 1200)]) | |
| window = Arch.makeWindow(baseobj=sketch, name="Window_SketchSingle_Default") | |
| self.document.recompute() | |
| self.assertEqual(window.Base, sketch) | |
| self.assertTrue( | |
| len(window.WindowParts) >= 5, "Should have at least one default part (5 elements)." | |
| ) | |
| self.assertEqual(window.WindowParts[0], "Default") | |
| self.assertEqual( | |
| window.WindowParts[1], | |
| "Solid panel", | |
| "Default part type incorrect for single-wire sketch.", | |
| ) | |
| self.assertIn("Wire0", window.WindowParts[2]) | |
| self.assertFalse(window.Shape.isNull()) | |
| self.assertGreater(len(window.Shape.Solids), 0) | |
| def test_create_from_sketch_two_wires_default_parts(self): | |
| """Test creating a window from two-wire sketch (concentric), relying on default parts.""" | |
| sketch = self._create_sketch_with_wires( | |
| "SketchTwoWires", [(0, 0, 1000, 1200), (100, 100, 800, 1000)] | |
| ) | |
| window = Arch.makeWindow(baseobj=sketch, name="Window_SketchTwo_Default") | |
| self.document.recompute() | |
| self.assertEqual(window.Base, sketch) | |
| self.assertGreaterEqual(len(window.WindowParts), 5) | |
| self.assertEqual(window.WindowParts[0], "Default") | |
| self.assertEqual( | |
| window.WindowParts[1], "Frame", "Default type for multi-wire should be Frame." | |
| ) | |
| self.assertIn("Wire0", window.WindowParts[2]) | |
| self.assertIn("Wire1", window.WindowParts[2]) | |
| self.assertFalse(window.Shape.isNull()) | |
| self.assertGreater(len(window.Shape.Solids), 0) | |
| def test_sketch_named_constraints_driven_by_window_props(self): | |
| """Test that window Width/Height properties drive sketch's named constraints.""" | |
| sketch_width, sketch_height = 800.0, 1000.0 | |
| sketch = self._create_sketch_with_named_constraints( | |
| "SketchNamed", sketch_width, sketch_height | |
| ) | |
| window = Arch.makeWindow(baseobj=sketch, name="Window_NamedSketch") | |
| # Set window Width/Height to ensure they become the drivers if sketch also has the constraints. | |
| # They need to be set explicitly after creating the window, as they are not automatically initialized | |
| # from the sketch. | |
| window.Width = sketch_width | |
| window.Height = sketch_height | |
| self.document.recompute() | |
| self.assertEqual(sketch.getDatum("Width").Value, sketch_width) | |
| self.assertEqual(sketch.getDatum("Height").Value, sketch_height) | |
| new_win_width = 1200.0 | |
| window.Width = new_win_width | |
| self.document.recompute() | |
| self.assertEqual( | |
| sketch.getDatum("Width").Value, | |
| new_win_width, | |
| "Window.Width should drive sketch 'Width' constraint.", | |
| ) | |
| new_win_height = 1500.0 | |
| window.Height = new_win_height | |
| self.document.recompute() | |
| self.assertEqual( | |
| sketch.getDatum("Height").Value, | |
| new_win_height, | |
| "Window.Height should drive sketch 'Height' constraint.", | |
| ) | |
| def test_create_from_sketch_with_custom_parts(self): | |
| """Test creating a window from sketch with explicit custom parts.""" | |
| sketch = self._create_sketch_with_wires( | |
| "SketchCustom", [(0, 0, 1200, 1000), (100, 100, 1000, 800)] | |
| ) | |
| custom_parts = [ | |
| "OuterFrame", | |
| "Frame", | |
| "Wire0,Wire1", | |
| "70", | |
| "0", | |
| "GlassPane", | |
| "Glass panel", | |
| "Wire1", | |
| "20", | |
| "25", | |
| ] | |
| window = Arch.makeWindow(baseobj=sketch, parts=custom_parts, name="Window_Custom") | |
| self.document.recompute() | |
| self.assertEqual(window.Base, sketch) | |
| self.assertEqual(list(window.WindowParts), custom_parts) | |
| self.assertFalse(window.Shape.isNull()) | |
| self.assertEqual(len(window.Shape.Solids), 2, "Expected two solids for frame and glass.") | |
| def test_custom_parts_with_plus_v_references(self): | |
| """Test creating a window with custom parts using '+V' to reference window.Frame and window.Offset.""" | |
| sketch = self._create_sketch_with_wires("SketchPlusV", [(0, 0, 1000, 800)]) | |
| frame_val = 60.0 | |
| offset_val = 10.0 | |
| custom_parts_plus_v = ["MainFrame", "Frame", "Wire0", "50+V", "5+V"] | |
| window = Arch.makeWindow(baseobj=sketch, parts=custom_parts_plus_v, name="Window_PlusV") | |
| window.Frame = frame_val | |
| window.Offset = offset_val | |
| self.document.recompute() | |
| self.assertFalse(window.Shape.isNull()) | |
| self.assertGreater(len(window.Shape.Solids), 0) | |
| def test_create_with_width_height_no_baseobj_initially(self): | |
| """ | |
| Test Arch.makeWindow(width, height) current behavior regarding window.Base. | |
| It verifies that window.Base is not automatically created and remains None, | |
| and that Width/Height properties are correctly set on the window object. | |
| """ | |
| win_w, win_h = 1000.0, 1200.0 | |
| window_name = "Window_W_H_NoAutoBase" | |
| window = Arch.makeWindow(width=win_w, height=win_h, name=window_name) | |
| # 1. Check initial state after makeWindow call | |
| self.assertEqual(window.Label, window_name) | |
| self.assertIsNone( | |
| window.Base, "Immediately after makeWindow(W,H), window.Base should be None." | |
| ) | |
| self.assertTrue( | |
| window.Shape.isNull(), "Initially, window.Shape should be null as no Base or Parts yet." | |
| ) | |
| self.assertAlmostEqual( | |
| window.Width.Value, | |
| win_w, | |
| places=5, | |
| msg="Window.Width property not correctly set by makeWindow.", | |
| ) | |
| self.assertAlmostEqual( | |
| window.Height.Value, | |
| win_h, | |
| places=5, | |
| msg="Window.Height property not correctly set by makeWindow.", | |
| ) | |
| # 2. Perform a document recompute | |
| # This should trigger window.execute() -> ArchComponent.ensureBase() | |
| self.document.recompute() | |
| # 3. Assert the CURRENT BEHAVIOR: window.Base remains None | |
| # The original ArchComponent.ensureBase does not create a sketch if Base is None | |
| # and only Width/Height are provided to the object. | |
| self.assertIsNone( | |
| window.Base, | |
| "Current Behavior: window.Base should remain None even after recomputes for makeWindow(W,H).", | |
| ) | |
| # 4. Consequently, window.Shape should still be null. | |
| self.assertTrue( | |
| window.Shape.isNull(), "window.Shape should remain null if window.Base was not created." | |
| ) | |
| # 5. Attempting to set parts that rely on a Base sketch (e.g., "Wire0") | |
| # should not produce a shape if Base is None. | |
| window.WindowParts = ["Default", "Frame", "Wire0", "50", "0"] | |
| self.document.recompute() | |
| self.assertTrue( | |
| window.Shape.isNull(), | |
| "window.Shape should still be null if WindowParts reference Wire0 from a non-existent Base.", | |
| ) | |
| def test_window_in_wall_creates_opening(self): | |
| """ | |
| Test if a window hosted in a wall creates a geometric opening, | |
| verifying changes in Volume and VerticalArea. | |
| Manually sets win.Width and win.Height after Arch.makeWindow(sk) | |
| as current Arch.makeWindow(sk) behavior does not initialize these | |
| properties from the sketch. It expects win.Placement to remain identity. | |
| """ | |
| # 1. Create Wall | |
| wall_length = 3000.0 | |
| wall_thickness = 200.0 | |
| wall_height = 2400.0 | |
| line = Draft.makeLine(FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(wall_length, 0, 0)) | |
| self.document.recompute() | |
| wall = Arch.makeWall( | |
| line, | |
| width=wall_thickness, | |
| height=wall_height, | |
| align="Left", | |
| name="TestWall_ForOpening_Args", | |
| ) | |
| self.document.recompute() | |
| initial_wall_volume = wall.Shape.Volume | |
| initial_wall_vertical_area = wall.VerticalArea.Value | |
| # 2. Create Sketch for Window profile | |
| sketch_profile_width = 800.0 | |
| sketch_profile_height = 1000.0 | |
| sk = self._create_sketch_with_wires( | |
| "WindowSketch_Args", [(0, 0, sketch_profile_width, sketch_profile_height)] | |
| ) | |
| # Position and orient the sketch in 3D space | |
| sk.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), 90) | |
| win_global_x_start = (wall.Length.Value - sketch_profile_width) / 2 | |
| win_global_z_start = 900.0 | |
| sk.Placement.Base = FreeCAD.Vector(win_global_x_start, 0, win_global_z_start) | |
| self.document.recompute() | |
| # 3. Create Window from sketch | |
| win = Arch.makeWindow(sk, name="WindowInWall_Args") | |
| # Manually set Width and Height to match the sketch profile dimensions | |
| win.Width = sketch_profile_width | |
| win.Height = sketch_profile_height | |
| # win.Placement remains identity | |
| win.HoleDepth = 0 # Use "smart" hole depth calculation | |
| win.WindowParts = ["DefaultFrame", "Frame", "Wire0", "60", "0"] | |
| win.recompute() | |
| self.document.recompute() | |
| self.assertFalse(win.Shape.isNull(), "Window must have a valid shape.") | |
| self.assertEqual( | |
| win.Placement, | |
| FreeCAD.Placement(), | |
| f"Window.Placement should be identity; got {win.Placement}", | |
| ) | |
| self.assertAlmostEqual(win.Width.Value, sketch_profile_width, 5) | |
| self.assertAlmostEqual(win.Height.Value, sketch_profile_height, 5) | |
| # 4. Add Window to Wall | |
| Arch.addComponents(win, host=wall) | |
| self.document.recompute() | |
| # 5. Assertions | |
| # Wall.Subtractions PropertyLinkList check (informational, expected to be empty) | |
| wall_subtractions_prop = wall.Subtractions if hasattr(wall, "Subtractions") else [] | |
| self.assertEqual( | |
| len(wall_subtractions_prop), | |
| 0, | |
| f"Wall.Subtractions property list expected to be empty, but: " | |
| f"{wall_subtractions_prop}", | |
| ) | |
| # Geometric checks | |
| current_wall_volume = wall.Shape.Volume | |
| self.assertLess( | |
| current_wall_volume, | |
| initial_wall_volume, | |
| f"Wall volume should decrease. Before: {initial_wall_volume}, " | |
| f"After: {current_wall_volume}", | |
| ) | |
| ideal_removed_volume = win.Width.Value * win.Height.Value * wall.Width.Value | |
| self.assertAlmostEqual( | |
| current_wall_volume, | |
| initial_wall_volume - ideal_removed_volume, | |
| places=3, | |
| msg=f"Wall volume cut not as expected. Initial: {initial_wall_volume}, " | |
| f"Current: {current_wall_volume}, IdealRemoved: {ideal_removed_volume}", | |
| ) | |
| # Check VerticalArea | |
| current_wall_vertical_area = wall.VerticalArea.Value | |
| window_opening_area_on_one_face = win.Width.Value * win.Height.Value | |
| area_of_side_reveals = 2 * win.Height.Value * wall.Width.Value | |
| expected_wall_vertical_area_after = ( | |
| initial_wall_vertical_area | |
| - (2 * window_opening_area_on_one_face) | |
| + area_of_side_reveals | |
| ) | |
| self.assertAlmostEqual( | |
| current_wall_vertical_area, | |
| expected_wall_vertical_area_after, | |
| places=3, | |
| msg=f"Wall VerticalArea change not as expected. Initial: " | |
| f"{initial_wall_vertical_area}, Current: {current_wall_vertical_area}, " | |
| f"ExpectedAfter: {expected_wall_vertical_area_after}", | |
| ) | |
| def test_clone_window(self): | |
| """Test cloning an Arch.Window object. | |
| Notes: | |
| - The clone's name is automatically generated, the `name` argument is ignored. | |
| - The clone's WindowParts and other properties are always empty, despite the original having them. | |
| """ | |
| sketch = self._create_sketch_with_wires("OriginalSketch", [(0, 0, 600, 800)]) | |
| original_parts = ["MainFrame", "Frame", "Wire0", "50", "10"] | |
| original_window = Arch.makeWindow( | |
| baseobj=sketch, parts=original_parts, name="OriginalWindow" | |
| ) | |
| original_window.Frame = 60.0 | |
| self.document.recompute() | |
| self.assertEqual(original_window.Label, "OriginalWindow") | |
| cloned_window = Arch.makeWindow(baseobj=original_window) | |
| self.document.recompute() | |
| self.assertIsNotNone(cloned_window) | |
| self.assertIsNotNone(cloned_window.CloneOf) | |
| self.assertEqual(cloned_window.CloneOf, original_window) | |
| self.assertEqual(cloned_window.IfcType, original_window.IfcType) | |
| self.assertFalse(cloned_window.Shape.isNull()) | |
| self.assertAlmostEqual( | |
| cloned_window.Shape.Volume, | |
| original_window.Shape.Volume, | |
| delta=1e-5, | |
| msg="Cloned window volume should match original.", | |
| ) | |
| def test_create_window_on_xz_plane(self): | |
| """Test creating a window oriented on the XZ (vertical) plane.""" | |
| # Create a a frame-like profile. | |
| sketch = self._create_sketch_with_wires( | |
| "Sketch_XZ_Plane", [(0, 0, 1000, 1200), (100, 100, 800, 1000)] | |
| ) | |
| # Orient the sketch to the XZ plane. | |
| sketch.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), 90) | |
| self.document.recompute() | |
| # Create the window using explicit parts. | |
| window = Arch.makeWindow(baseobj=sketch, name="Window_XZ_Plane") | |
| window.WindowParts = [ | |
| "Frame", | |
| "Frame", | |
| "Wire0,Wire1", | |
| "60", | |
| "0", # A 60mm thick frame | |
| "Glass", | |
| "Glass panel", | |
| "Wire1", | |
| "10", | |
| "25", # A 10mm thick glass pane, offset by 25mm | |
| ] | |
| self.document.recompute() | |
| # Check the resulting geometry's orientation and dimensions. | |
| self.assertFalse(window.Shape.isNull(), "Window shape should not be null.") | |
| self.assertEqual( | |
| len(window.Shape.Solids), 2, "Window should contain two solids (frame and glass)." | |
| ) | |
| bb = window.Shape.BoundBox | |
| # The window's overall "thickness" is determined by the frame (60mm). | |
| # Its "width" (X) and "height" (Z) should be larger. | |
| self.assertGreater( | |
| bb.XLength, | |
| bb.YLength, | |
| "Window XLength (width) should be greater than YLength (thickness).", | |
| ) | |
| self.assertGreater( | |
| bb.ZLength, | |
| bb.YLength, | |
| "Window ZLength (height) should be greater than YLength (thickness).", | |
| ) | |
| # Verify the overall thickness is correct (60mm, from the Frame component). | |
| self.assertAlmostEqual( | |
| bb.YLength, 60.0, places=5, msg="Window thickness (YLength) is incorrect." | |
| ) | |
| def test_opening_property_rotates_component(self): | |
| """Test that setting the Opening property rotates a hinged component.""" | |
| # Create a window from a preset with a hinge | |
| window = Arch.makeWindowPreset( | |
| "Open 1-pane", width=1000, height=1200, h1=50, h2=50, h3=0, w1=100, w2=50, o1=0, o2=50 | |
| ) | |
| self.document.recompute() | |
| # The preset creates an outer frame, an inner opening frame, and glass. | |
| self.assertEqual(len(window.Shape.Solids), 3, "Preset should create three solids.") | |
| # Solid[1] is the inner, opening frame. | |
| opening_pane = window.Shape.Solids[1] | |
| # Get initial position | |
| initial_center = opening_pane.CenterOfMass | |
| # Set opening to 50%. The 'Opening' property is App::PropertyPercent, which expects an integer. | |
| window.Opening = 50 | |
| self.document.recompute() | |
| # Get new position of the same component | |
| new_opening_pane = window.Shape.Solids[1] | |
| new_center = new_opening_pane.CenterOfMass | |
| # Assert that the Z-coordinate has changed, indicating rotation around a horizontal hinge. | |
| self.assertNotAlmostEqual( | |
| initial_center.z, | |
| new_center.z, | |
| places=3, | |
| msg="The Z-coordinate of the opening pane's center should change upon rotation.", | |
| ) | |
| # The Y-coordinate should remain largely unchanged for a bottom-hinged (awning-style) window. | |
| self.assertAlmostEqual(initial_center.y, new_center.y, places=3) | |
| def test_symbol_plan_creates_wire_geometry(self): | |
| """Test that enabling SymbolPlan adds 2D wire geometry to the window's shape.""" | |
| # Create a window with an opening mode | |
| window = Arch.makeWindowPreset( | |
| "Open 1-pane", width=1000, height=1200, h1=50, h2=50, h3=0, w1=100, w2=50, o1=0, o2=50 | |
| ) | |
| # SymbolPlan is True by default for presets with hinges, but we set it explicitly | |
| window.SymbolPlan = True | |
| self.document.recompute() | |
| # Assert that the shape contains both solids and the 2D symbol edges. | |
| self.assertIsInstance(window.Shape, Part.Compound, "Window shape should be a compound.") | |
| # Find symbol edges by finding edges that do not belong to any face in the shape | |
| face_edge_hashes = {e.hashCode() for face in window.Shape.Faces for e in face.Edges} | |
| all_edge_hashes = {e.hashCode() for e in window.Shape.Edges} | |
| symbol_edge_hashes = all_edge_hashes - face_edge_hashes | |
| self.assertEqual(len(window.Shape.Solids), 3, "Window shape should contain 3 solids.") | |
| self.assertEqual( | |
| len(symbol_edge_hashes), 2, "Expected 2 edges for the plan symbol (arc and line)." | |
| ) | |
| def test_custom_subvolume_creates_opening(self): | |
| """Test that a custom Subvolume shape correctly creates an opening in a host wall.""" | |
| # Create a wall and store its initial state | |
| wall_base = Draft.makeLine(FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(4000, 0, 0)) | |
| wall = Arch.makeWall(wall_base, width=200, height=3000, align="Left") | |
| self.document.recompute() | |
| initial_wall_volume = wall.Shape.Volume | |
| initial_wall_shape = ( | |
| wall.Shape.copy() | |
| ) # Store initial shape for accurate intersection calculation | |
| # Create a simple window | |
| window_sketch = self._create_sketch_with_wires("WinSketch", [(0, 0, 10, 10)]) | |
| window = Arch.makeWindow(window_sketch) | |
| # Create a custom cutting box as the window's Subvolume | |
| cutting_box = self.document.addObject("Part::Box", "CuttingBox") | |
| cutting_box.Length = 800 | |
| cutting_box.Width = 300 # Wider than wall | |
| cutting_box.Height = 1000 | |
| cutting_box.Placement.Base = FreeCAD.Vector(1600, -50, 800) | |
| self.document.recompute() | |
| window.Subvolume = cutting_box | |
| # Add window to wall to create opening using Arch.addComponents | |
| Arch.addComponents(window, host=wall) | |
| self.document.recompute() | |
| # Assert volume has decreased by the intersecting part of the box's volume | |
| expected_volume_change = cutting_box.Shape.common(initial_wall_shape).Volume | |
| final_wall_volume = wall.Shape.Volume | |
| self.assertAlmostEqual( | |
| final_wall_volume, | |
| initial_wall_volume - expected_volume_change, | |
| places=3, | |
| msg="Wall volume did not decrease correctly after applying custom Subvolume.", | |
| ) | |
| def test_door_preset_sets_ifctype_door(self): | |
| """Test that creating a window from a 'door' preset correctly sets its IfcType to 'Door'.""" | |
| # Create a door using a preset with non-zero values for all required parameters | |
| door = Arch.makeWindowPreset( | |
| "Simple door", width=900, height=2100, h1=50, h2=50, h3=0, w1=100, w2=40, o1=0, o2=0 | |
| ) | |
| self.assertIsNotNone(door, "makeWindowPreset for a door should create an object.") | |
| self.document.recompute() | |
| # Assert IfcType is "Door" | |
| self.assertEqual(door.IfcType, "Door", "IfcType should be 'Door' for a door preset.") | |
| # Assert component count | |
| # A simple door preset has an outer frame and a solid door panel | |
| self.assertEqual( | |
| len(door.Shape.Solids), | |
| 2, | |
| "A simple door should have 2 solid components (frame and panel).", | |
| ) | |
| def test_cloned_window_in_wall_creates_opening(self): | |
| """Tests if a cloned Arch.Window, when hosted in a wall, creates a geometric opening.""" | |
| # Create the host wall | |
| wall_length = 3000.0 | |
| wall_thickness = 200.0 | |
| wall_height = 2500.0 | |
| wall_base = Draft.makeLine(FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(wall_length, 0, 0)) | |
| wall = Arch.makeWall( | |
| wall_base, width=wall_thickness, height=wall_height, name="WallForClonedWindow" | |
| ) | |
| self.document.recompute() | |
| initial_wall_volume = wall.Shape.Volume | |
| self.assertGreater(initial_wall_volume, 0, "Wall should have a positive volume initially.") | |
| # Create the original window's sketch | |
| window_width = 800.0 | |
| window_height = 1200.0 | |
| original_sketch = self._create_sketch_with_wires( | |
| "OriginalWinSketch", [(0, 0, window_width, window_height)] | |
| ) | |
| # Orient the sketch to be vertical before creating the window | |
| # This mimics the behavior of the interactive Arch_Window tool. | |
| original_sketch.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), 90) | |
| self.document.recompute() | |
| # Create the original window from the now-oriented sketch | |
| original_window = Arch.makeWindow(baseobj=original_sketch, name="OriginalWindow") | |
| original_window.Width = window_width # Explicitly set properties | |
| original_window.Height = window_height | |
| self.document.recompute() | |
| self.assertFalse( | |
| original_window.Shape.isNull(), "Original window shape should not be null." | |
| ) | |
| # Create the clone | |
| cloned_window = Draft.clone(original_window) | |
| cloned_window.Label = "ClonedWindow" | |
| # Position the clone inside the wall | |
| clone_x = 1100.0 # Center of the clone in the wall's length | |
| clone_y = wall_thickness / 2 # Center of the clone in the wall's thickness | |
| clone_z = 800.0 # Sill height | |
| cloned_window.Placement.Base = FreeCAD.Vector(clone_x, clone_y, clone_z) | |
| self.document.recompute() | |
| self.assertIsNotNone( | |
| cloned_window.CloneOf, "Cloned window should have CloneOf property set." | |
| ) | |
| self.assertEqual( | |
| cloned_window.CloneOf, original_window, "CloneOf should point to the original window." | |
| ) | |
| self.assertAlmostEqual(cloned_window.Shape.Volume, original_window.Shape.Volume, delta=1e-5) | |
| # Add the clone to the wall's hosts | |
| Arch.addComponents(cloned_window, host=wall) | |
| self.document.recompute() | |
| self.assertIn( | |
| wall, cloned_window.Hosts, "Wall should be in the cloned window's Hosts list." | |
| ) | |
| final_wall_volume = wall.Shape.Volume | |
| self.assertLess( | |
| final_wall_volume, | |
| initial_wall_volume, | |
| "Wall volume should have decreased after hosting the cloned window.", | |
| ) | |
| expected_removed_volume = window_width * window_height * wall.Width.Value | |
| self.assertAlmostEqual( | |
| initial_wall_volume - final_wall_volume, | |
| expected_removed_volume, | |
| delta=1e-5, | |
| msg="The volume removed from the wall by the cloned window is incorrect.", | |
| ) | |
| def test_addComponents_window_to_wall(self): | |
| """ | |
| Tests the Arch.addComponents function for adding a window to a wall. | |
| Verifies that the Hosts, InList, and OutList properties are correctly | |
| updated, and that the geometric opening is created. | |
| """ | |
| self.printTestMessage("Testing Arch.addComponents for window-wall hosting...") | |
| # Create the wall and window | |
| wall_base = Draft.makeLine(FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(3000, 0, 0)) | |
| wall = Arch.makeWall(wall_base, width=200, height=2500, name="HostWall") | |
| self.document.recompute() | |
| initial_wall_volume = wall.Shape.Volume | |
| window_width = 800.0 | |
| window_height = 1200.0 | |
| window_sketch = self._create_sketch_with_wires( | |
| "WindowSketchForAdd", [(0, 0, window_width, window_height)] | |
| ) | |
| window_sketch.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), 90) | |
| window_sketch.Placement.Base = FreeCAD.Vector(1100, 100, 800) # Position it within the wall | |
| self.document.recompute() | |
| window = Arch.makeWindow(baseobj=window_sketch, name="HostedWindow") | |
| window.Width = window_width | |
| window.Height = window_height | |
| self.document.recompute() | |
| # Initial state verification | |
| self.assertEqual(len(window.Hosts), 0, "Window.Hosts should be empty initially.") | |
| self.assertNotIn(window, wall.InList, "Window should not be in wall's InList initially.") | |
| # Add the window to the wall | |
| Arch.addComponents(window, wall) | |
| self.document.recompute() | |
| # Check all relationships and the final geometry | |
| # Property assertions | |
| self.assertIn(wall, window.Hosts, "Wall should be in window.Hosts after addComponents.") | |
| self.assertEqual(len(window.Hosts), 1, "Window.Hosts should contain exactly one host.") | |
| # Dependency graph assertions | |
| self.assertIn(window, wall.InList, "Window should be in wall's InList after hosting.") | |
| self.assertIn(wall, window.OutList, "Wall should be in window's OutList after hosting.") | |
| # Negative assertion | |
| self.assertNotIn( | |
| window, | |
| wall.Subtractions, | |
| "Window should not be in wall's Subtractions list for this hosting mechanism.", | |
| ) | |
| # Geometric assertion | |
| final_wall_volume = wall.Shape.Volume | |
| self.assertLess( | |
| final_wall_volume, | |
| initial_wall_volume, | |
| "Wall volume should decrease after hosting the window.", | |
| ) | |
| expected_removed_volume = window_width * window_height * wall.Width.Value | |
| self.assertAlmostEqual( | |
| initial_wall_volume - final_wall_volume, | |
| expected_removed_volume, | |
| delta=1e-5, | |
| msg="The volume removed from the wall is incorrect.", | |
| ) | |
| def _create_sketch_with_wires( | |
| self, name: str, wire_definitions: list[tuple[float, float, float, float]] | |
| ) -> "FreeCAD.DocumentObject": | |
| """ | |
| Helper to create a sketch with one or more specified rectangular wires. | |
| Each rectangle is defined in the sketch's local XY plane. The sketch | |
| is created in the current document (`self.doc`). After adding all | |
| geometry and basic coincident constraints for each rectangle, the | |
| document is recomputed. | |
| Parameters | |
| ---------- | |
| name : str | |
| The name for the new Sketcher.SketchObject. | |
| wire_definitions : list[tuple[float, float, float, float]] | |
| A list where each element defines one rectangular wire. | |
| Each tuple within the list should be in the format `(x, y, w, h)`: | |
| - `x` (float): The x-coordinate of the bottom-left corner of the rectangle | |
| in the sketch's local coordinate system. | |
| - `y` (float): The y-coordinate of the bottom-left corner of the rectangle | |
| in the sketch's local coordinate system. | |
| - `w` (float): The width of the rectangle (along the sketch's local X-axis). | |
| - `h` (float): The height of the rectangle (along the sketch's local Y-axis). | |
| For example, `[(0, 0, 100, 200)]` creates one 100x200 rectangle at the | |
| sketch origin. `[(0,0,100,100), (10,10,80,80)]` creates two concentric | |
| rectangles if used as outer and inner boundaries. The order of wires | |
| in this list corresponds to `Wire0`, `Wire1`, etc., when used by | |
| Arch objects. The sketch is recomputed and returned. | |
| Returns | |
| ------- | |
| FreeCAD.DocumentObject | |
| The created Sketcher object (specifically, an object of type "Sketcher::SketchObject"). | |
| """ | |
| sketch: FreeCAD.DocumentObject = self.document.addObject("Sketcher::SketchObject", name) | |
| for i, (x, y, w, h) in enumerate(wire_definitions): | |
| sketch.addGeometry( | |
| Part.LineSegment(FreeCAD.Vector(x, y, 0), FreeCAD.Vector(x + w, y, 0)) | |
| ) | |
| sketch.addGeometry( | |
| Part.LineSegment(FreeCAD.Vector(x + w, y, 0), FreeCAD.Vector(x + w, y + h, 0)) | |
| ) | |
| sketch.addGeometry( | |
| Part.LineSegment(FreeCAD.Vector(x + w, y + h, 0), FreeCAD.Vector(x, y + h, 0)) | |
| ) | |
| sketch.addGeometry( | |
| Part.LineSegment(FreeCAD.Vector(x, y + h, 0), FreeCAD.Vector(x, y, 0)) | |
| ) | |
| base_idx = i * 4 | |
| sketch.addConstraint(Sketcher.Constraint("Coincident", base_idx, 2, base_idx + 1, 1)) | |
| sketch.addConstraint( | |
| Sketcher.Constraint("Coincident", base_idx + 1, 2, base_idx + 2, 1) | |
| ) | |
| sketch.addConstraint( | |
| Sketcher.Constraint("Coincident", base_idx + 2, 2, base_idx + 3, 1) | |
| ) | |
| sketch.addConstraint(Sketcher.Constraint("Coincident", base_idx + 3, 2, base_idx, 1)) | |
| self.document.recompute() | |
| return sketch | |
| def _create_sketch_with_named_constraints( | |
| self, name: str, initial_width: float, initial_height: float | |
| ) -> "FreeCAD.DocumentObject": | |
| """ | |
| Helper to create a rectangular sketch with "Width" and "Height" named constraints. | |
| The sketch is created in the current document (`self.doc`) on the | |
| default XY plane. It consists of a single rectangle defined by four | |
| line segments, with its bottom-left corner at (0,0,0) in the | |
| sketch's local coordinates. | |
| Parameters | |
| ---------- | |
| name : str | |
| The name for the new Sketcher.SketchObject. | |
| initial_width : float | |
| The initial value for the "Width" constraint (along sketch's X-axis). | |
| initial_height : float | |
| The initial value for the "Height" constraint (along sketch's Y-axis). | |
| Returns | |
| ------- | |
| FreeCAD.DocumentObject | |
| The created Sketcher object (specifically, an object of type "Sketcher::SketchObject"). | |
| """ | |
| sketch: FreeCAD.DocumentObject = self.document.addObject("Sketcher::SketchObject", name) | |
| sketch.addGeometry( | |
| Part.LineSegment(FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(initial_width, 0, 0)), False | |
| ) | |
| sketch.addGeometry( | |
| Part.LineSegment( | |
| FreeCAD.Vector(initial_width, 0, 0), | |
| FreeCAD.Vector(initial_width, initial_height, 0), | |
| ), | |
| False, | |
| ) | |
| sketch.addGeometry( | |
| Part.LineSegment( | |
| FreeCAD.Vector(initial_width, initial_height, 0), | |
| FreeCAD.Vector(0, initial_height, 0), | |
| ), | |
| False, | |
| ) | |
| sketch.addGeometry( | |
| Part.LineSegment(FreeCAD.Vector(0, initial_height, 0), FreeCAD.Vector(0, 0, 0)), False | |
| ) | |
| sketch.addConstraint(Sketcher.Constraint("Coincident", 0, 2, 1, 1)) | |
| sketch.addConstraint(Sketcher.Constraint("Coincident", 1, 2, 2, 1)) | |
| sketch.addConstraint(Sketcher.Constraint("Coincident", 2, 2, 3, 1)) | |
| sketch.addConstraint(Sketcher.Constraint("Coincident", 3, 2, 0, 1)) | |
| sketch.addConstraint(Sketcher.Constraint("Horizontal", 0)) | |
| sketch.addConstraint(Sketcher.Constraint("Vertical", 1)) | |
| sketch.addConstraint(Sketcher.Constraint("Horizontal", 2)) | |
| sketch.addConstraint(Sketcher.Constraint("Vertical", 3)) | |
| sketch.addConstraint(Sketcher.Constraint("DistanceX", 0, 1, 0, 2, initial_width)) | |
| sketch.renameConstraint(sketch.ConstraintCount - 1, "Width") | |
| sketch.addConstraint(Sketcher.Constraint("DistanceY", 1, 1, 1, 2, initial_height)) | |
| sketch.renameConstraint(sketch.ConstraintCount - 1, "Height") | |
| self.document.recompute() | |
| return sketch | |