diff --git a/src/Mod/BIM/Arch.py b/src/Mod/BIM/Arch.py
index 061ba0f9b4..c8b50ac2c2 100644
--- a/src/Mod/BIM/Arch.py
+++ b/src/Mod/BIM/Arch.py
@@ -1814,32 +1814,230 @@ def joinWalls(walls, delete=False, deletebase=False):
return base
-def makeWindow(baseobj=None, width=None, height=None, parts=None, name=None):
+def makeWindow(
+ baseobj: Optional[FreeCAD.DocumentObject] = None,
+ width: Optional[float] = None,
+ height: Optional[float] = None,
+ parts: Optional[list[str]] = None,
+ name: Optional[str] = None,
+) -> FreeCAD.DocumentObject:
"""
- Creates a window object based on the given base object.
+ Creates an Arch Window object, which can represent either a window or a door.
+
+ The created object can be based on a 2D profile (e.g., a Sketch), have its
+ dimensions set directly, or be defined by custom components. It can be
+ inserted into host objects like Walls, creating openings. The IfcType of
+ the object can be set to "Window" or "Door" accordingly (presets often
+ handle this automatically).
Parameters
----------
- baseobj : Draft.Wire or Sketcher.Sketch, optional
- The base object for the window. It should be a well-formed, closed
- Draft.Wire or Sketcher.Sketch object. Defaults to None.
+ baseobj : FreeCAD.DocumentObject, optional
+ The base object for the window/door.
+ If `baseobj` is an existing `Arch.Window` (or Door), it will be cloned.
+ If `baseobj` is a 2D object with wires (e.g., `Sketcher::SketchObject`,
+ `Draft.Wire`), these wires are used to define the geometry.
+ If `parts` is None, default components are generated from `baseobj.Shape.Wires`:
+ - If one closed wire: `["Default", "Frame", "Wire0", "1", "0"]` (or "Solid panel").
+ - If multiple closed wires (e.g., Wire0 outer, Wire1 inner):
+ `["Default", "Frame", "Wire0,Wire1", "1", "0"]` (Wire1 cuts Wire0).
+ The `Normal` direction is derived from `baseobj.Placement`.
+ Defaults to None.
width : float, optional
- The width of the window. Defaults to None.
+ The total width of the window/door.
+ If `baseobj` is None, this value is used by `ensureBase()` on first
+ recompute to create a default sketch with a "Width" constraint.
+ If `baseobj` is a sketch with a "Width" named constraint, setting
+ `window_or_door.Width` will drive this sketch constraint. `makeWindow` itself
+ does not initially set the object's `Width` *from* a sketch's constraint.
+ Defaults to None (or an Arch preference value if `baseobj` is None).
height : float, optional
- The height of the window. Defaults to None.
- parts : list, optional
- The parts of the window. Defaults to None.
+ The total height of the window/door.
+ If `baseobj` is None, this value is used by `ensureBase()` on first
+ recompute to create a default sketch with a "Height" constraint.
+ If `baseobj` is a sketch with a "Height" named constraint, setting
+ `window_or_door.Height` will drive this sketch constraint. `makeWindow` itself
+ does not initially set the object's `Height` *from* a sketch's constraint.
+ Defaults to None (or an Arch preference value if `baseobj` is None).
+ parts : list[str], optional
+ A list defining custom components for the window/door. The list is flat, with
+ every 5 elements describing one component:
+ `["Name1", "Type1", "WiresStr1", "ThickStr1", "OffsetStr1", ...]`
+ - `Name`: User-defined name (e.g., "OuterFrame").
+ - `Type`: Component type (e.g., "Frame", "Glass panel", "Solid panel").
+ See `ArchWindow.WindowPartTypes`.
+ - `WiresStr`: Comma-separated string defining wire usage from `baseobj.Shape.Wires`
+ (0-indexed) and optionally hinge/opening from `baseobj.Shape.Edges` (1-indexed).
+ Example: `"Wire0,Wire1,Edge8,Mode1"`.
+ - `"WireN"`: Uses Nth wire for the base face.
+ - `"WireN,WireM"`: WireN is base, WireM is cutout.
+ - `"EdgeK"`: Kth edge is hinge.
+ - `"ModeL"`: Lth opening mode from `ArchWindow.WindowOpeningModes`.
+ - `ThickStr`: Thickness as string (e.g., `"50.0"`). Appending `"+V"`
+ adds the object's `Frame` property value.
+ - `OffsetStr`: Offset along normal as string (e.g., `"25.0"`). Appending `"+V"`
+ adds the object's `Offset` property value.
+ Defaults to None. If None and `baseobj` is a sketch, default parts
+ are generated as described under `baseobj`.
name : str, optional
- The name to assign to the created window. Defaults to None.
+ The name (label) for the created window/door. If None, a default localized
+ name ("Window" or "Door", depending on context or subsequent changes) is used.
+ Defaults to None.
Returns
-------
- Part::FeaturePython
- The created window object.
+ FreeCAD.DocumentObject
+ The created Arch Window object (which is a `Part::FeaturePython` instance,
+ configurable to represent a window or a door).
+
+ See Also
+ --------
+ ArchWindowPresets.makeWindowPreset : Create window/door from predefined types.
+ ArchWall.addComponents : Add a window/door to a wall (creates opening).
Notes
-----
- 1. If baseobj is not a closed shape, the tool may not create a proper solid figure.
+ - **Dual purpose (window/door)**: despite its name, this function is the primary
+ way to programmatically create both windows and doors in the BIM workbench.
+ The distinction is often made by setting the `IfcType` property of the
+ created object to "Window" or "Door", and by the chosen components or preset.
+ - **Sketch-based dimensions**: If `baseobj` is a `Sketcher::SketchObject`
+ with named constraints "Width" and "Height", these sketch constraints will be
+ parametrically driven by the created object's `Width` and `Height` properties
+ respectively *after* the object is created and its properties are changed.
+ `makeWindow` itself does not initially populate the object's `Width`/`Height` from
+ these sketch constraints if `width`/`height` arguments are not passed to it.
+ The object's internal `Width` and `Height` properties are the drivers.
+ - **Object from dimensions (No `baseobj` initially)**: if `baseobj` is `None` but
+ `width` and `height` are provided, `makeWindow` creates an Arch Window object.
+ Upon the first `doc.recompute()`, the `ensureBase()` mechanism generates
+ an internal sketch (`obj.Base`) with "Width" and "Height" constraints
+ driven by `obj.Width` and `obj.Height`. However, `obj.WindowParts`
+ will remain undefined, resulting in a shapeless object until `WindowParts`
+ are manually set.
+ - **`obj.Frame` and `obj.Offset` properties**: these main properties of the
+ created object (e.g., `my_window.Frame = 50.0`) provide the values used when
+ `"+V"` is specified in the `ThicknessString` or `OffsetString` of a component
+ within the `parts` list.
+ - **Hosting and openings**: to create an opening in a host object (e.g., `Arch.Wall`),
+ set `obj.Hosts = [my_wall]`. The opening's shape is typically derived
+ from `obj.HoleWire` (defaulting to the largest wire of `obj.Base`) and
+ extruded by `obj.HoleDepth` (if 0, tries to match host thickness).
+ A custom `obj.Subvolume` can also define the opening shape.
+ - **Component management**: components and their geometry are primarily
+ managed by the `_Window` class and its methods in `ArchWindow.py`.
+ - **Initialization from sketch `baseobj`**: when `baseobj` is a sketch
+ (e.g., `Sketcher::SketchObject`) and `parts` is `None` or provided:
+ - The `window.Shape` (geometric representation) is correctly generated
+ at the global position and orientation defined by `baseobj.Placement`.
+ - However, the created window object's own `window.Placement` property is
+ **not** automatically initialized from `baseobj.Placement` and typically
+ remains at the identity placement (origin, no rotation).
+ - Similarly, the `window.Width` and `window.Height` properties are **not**
+ automatically populated from the dimensions of the `baseobj` sketch.
+ These properties will default to 0.0 or values from Arch preferences
+ (if `width`/`height` arguments to `makeWindow` are also `None`).
+ - If you need the `window` object's `Placement`, `Width`, or `Height`
+ properties to reflect the `baseobj` sketch for subsequent operations
+ (e.g., if other systems query these specific window properties, or if
+ you intend to parametrically drive the sketch via these window properties),
+ you may need to set them manually after `makeWindow` is called:
+ - The `ArchWindow._Window.execute()` method, when recomputing the window,
+ *does* use `window.Base.Shape` (the sketch's shape in its global position)
+ to generate the window's geometry. The `ArchWindow._Window.getSubVolume()`
+ method also correctly uses `window.Base.Shape` and the window object's
+ (identity) `Placement` for creating the cutting volume.
+
+ Examples
+ --------
+ >>> import FreeCAD as App
+ >>> import Draft, Arch, Sketcher, Part
+ >>> doc = App.newDocument("ArchWindowDoorExamples")
+
+ >>> # Ex1: Basic window from sketch and parts definition, oriented to XZ (vertical) plane
+ >>> sketch_ex1 = doc.addObject('Sketcher::SketchObject', 'WindowSketchEx1_Vertical')
+ >>> # Define geometry in sketch's local XY plane (width along local X, height along local Y)
+ >>> sketch_ex1.addGeometry(Part.LineSegment(App.Vector(0,0,0), App.Vector(1000,0,0))) # Wire0 - Outer
+ >>> sketch_ex1.addGeometry(Part.LineSegment(App.Vector(1000,0,0), App.Vector(1000,1200,0)))
+ >>> sketch_ex1.addGeometry(Part.LineSegment(App.Vector(1000,1200,0), App.Vector(0,1200,0)))
+ >>> sketch_ex1.addGeometry(Part.LineSegment(App.Vector(0,1200,0), App.Vector(0,0,0)))
+ >>> sketch_ex1.addGeometry(Part.LineSegment(App.Vector(100,100,0), App.Vector(900,100,0))) # Wire1 - Inner
+ >>> sketch_ex1.addGeometry(Part.LineSegment(App.Vector(900,100,0), App.Vector(900,1100,0)))
+ >>> sketch_ex1.addGeometry(Part.LineSegment(App.Vector(900,1100,0), App.Vector(100,1100,0)))
+ >>> sketch_ex1.addGeometry(Part.LineSegment(App.Vector(100,1100,0), App.Vector(100,100,0)))
+ >>> doc.recompute() # Update sketch Wires
+ >>> # Orient sketch: Rotate +90 deg around X-axis to place sketch's XY onto global XZ.
+ >>> # Sketch's local Y (height) now aligns with global Z. Sketch normal is global -Y.
+ >>> sketch_ex1.Placement.Rotation = App.Rotation(App.Vector(1,0,0), 90)
+ >>> doc.recompute() # Apply sketch placement
+ >>> window_ex1 = Arch.makeWindow(baseobj=sketch_ex1, name="MyWindowEx1_Vertical")
+ >>> # Window Normal will be derived as global +Y, extrusion along +Y.
+ >>> window_ex1.WindowParts = [
+ ... "Frame", "Frame", "Wire0,Wire1", "60", "0", # Frame from Wire0-Wire1
+ ... "Glass", "Glass panel", "Wire1", "10", "25" # Glass from Wire1, offset in Normal dir
+ ... ]
+ >>> doc.recompute()
+
+ >>> # Ex2: Window from sketch with named "Width"/"Height" constraints (on default XY plane)
+ >>> sketch_ex2 = doc.addObject('Sketcher::SketchObject', 'WindowSketchEx2_Named')
+ >>> sketch_ex2.addGeometry(Part.LineSegment(App.Vector(0,0,0), App.Vector(800,0,0))) # Edge 0
+ >>> sketch_ex2.addGeometry(Part.LineSegment(App.Vector(800,0,0), App.Vector(800,600,0))) # Edge 1
+ >>> sketch_ex2.addGeometry(Part.LineSegment(App.Vector(800,600,0), App.Vector(0,600,0))) # Complete Wire0
+ >>> sketch_ex2.addGeometry(Part.LineSegment(App.Vector(0,600,0), App.Vector(0,0,0)))
+ >>> sketch_ex2.addConstraint(Sketcher.Constraint('DistanceX',0,1,0,2, 800))
+ >>> sketch_ex2.renameConstraint(sketch_ex2.ConstraintCount-1, "Width")
+ >>> sketch_ex2.addConstraint(Sketcher.Constraint('DistanceY',1,1,1,2, 600))
+ >>> sketch_ex2.renameConstraint(sketch_ex2.ConstraintCount-1, "Height")
+ >>> doc.recompute()
+ >>> window_ex2 = Arch.makeWindow(baseobj=sketch_ex2, name="MyWindowEx2_Parametric")
+ >>> window_ex2.WindowParts = ["Frame", "Frame", "Wire0", "50", "0"]
+ >>> doc.recompute()
+ >>> print(f"Ex2 Initial - Sketch Width: {sketch_ex2.getDatum('Width')}, Window Width: {window_ex2.Width.Value}")
+ >>> window_ex2.Width = 950 # This drives the sketch constraint
+ >>> doc.recompute()
+ >>> print(f"Ex2 Updated - Sketch Width: {sketch_ex2.getDatum('Width')}, Window Width: {window_ex2.Width.Value}")
+
+ >>> # Ex3: Window from dimensions only (initially shapeless, sketch on XY plane)
+ >>> window_ex3 = Arch.makeWindow(width=700, height=900, name="MyWindowEx3_Dims")
+ >>> print(f"Ex3 Initial - Base: {window_ex3.Base}, Shape isNull: {window_ex3.Shape.isNull()}")
+ >>> doc.recompute() # ensureBase creates the sketch on XY plane
+ >>> print(f"Ex3 After Recompute - Base: {window_ex3.Base.Name if window_ex3.Base else 'None'}, Shape isNull: {window_ex3.Shape.isNull()}")
+ >>> window_ex3.WindowParts = ["SimpleFrame", "Frame", "Wire0", "40", "0"] # Wire0 from auto-generated sketch
+ >>> doc.recompute()
+ >>> print(f"Ex3 After Parts - Shape isNull: {window_ex3.Shape.isNull()}")
+
+ >>> # Ex4: Door created using an ArchWindowPresets function
+ >>> # Note: Arch.makeWindowPreset calls Arch.makeWindow internally
+ >>> door_ex4_preset = makeWindowPreset(
+ ... "Simple door", width=900, height=2100,
+ ... h1=50, h2=0, h3=0, w1=70, w2=40, o1=0, o2=0 # Preset-specific params
+ ... )
+ >>> if door_ex4_preset:
+ ... door_ex4_preset.Label = "MyDoorEx4_Preset"
+ ... doc.recompute()
+
+ >>> # Ex5: Door created from a sketch, with IfcType manually set (sketch on XY plane)
+ >>> sketch_ex5_door = doc.addObject('Sketcher::SketchObject', 'DoorSketchEx5')
+ >>> sketch_ex5_door.addGeometry(Part.LineSegment(App.Vector(0,0,0), App.Vector(850,0,0))) # Wire0
+ >>> sketch_ex5_door.addGeometry(Part.LineSegment(App.Vector(850,0,0), App.Vector(850,2050,0)))
+ >>> sketch_ex5_door.addGeometry(Part.LineSegment(App.Vector(850,2050,0), App.Vector(0,2050,0)))
+ >>> sketch_ex5_door.addGeometry(Part.LineSegment(App.Vector(0,2050,0), App.Vector(0,0,0)))
+ >>> doc.recompute()
+ >>> door_ex5_manual = Arch.makeWindow(baseobj=sketch_ex5_door, name="MyDoorEx5_Manual")
+ >>> door_ex5_manual.WindowParts = ["DoorPanel", "Solid panel", "Wire0", "40", "0"]
+ >>> door_ex5_manual.IfcType = "Door" # Explicitly define as a Door
+ >>> doc.recompute()
+
+ >>> # Ex6: Hosting the vertical window from Ex1 in an Arch.Wall
+ >>> wall_ex6 = Arch.makeWall(None, length=4000, width=200, height=2400)
+ >>> wall_ex6.Label = "WallForOpening_Ex6"
+ >>> # Window_ex1 is already oriented (its sketch placement was set in Ex1).
+ >>> # Now, just position the window object itself.
+ >>> window_ex1.Placement.Base = App.Vector(1500, wall_ex6.Width.Value / 2, 900) # X, Y (center of wall), Z (sill)
+ >>> window_ex1.HoleDepth = 0 # Use wall's thickness for the opening depth
+ >>> doc.recompute() # Apply window placement and HoleDepth
+ >>> window_ex1.Hosts = [wall_ex6]
+ >>> doc.recompute() # Wall recomputes to create the opening
"""
import Draft
import DraftGeomUtils
@@ -1899,7 +2097,7 @@ def makeWindow(baseobj=None, width=None, height=None, parts=None, name=None):
part_name, part_type, wires_str, part_frame_thickness, part_offset
]
else:
- # bind properties from base obj if existing
+ # Bind properties from base obj if they exist
for prop in ["Height", "Width", "Subvolume", "Tag", "Description", "Material"]:
for baseobj_prop in baseobj.PropertiesList:
if (baseobj_prop == prop) or baseobj_prop.endswith(f"_{prop}"):
diff --git a/src/Mod/BIM/CMakeLists.txt b/src/Mod/BIM/CMakeLists.txt
index 338ab06294..e018875b81 100644
--- a/src/Mod/BIM/CMakeLists.txt
+++ b/src/Mod/BIM/CMakeLists.txt
@@ -202,9 +202,9 @@ SET(nativeifc_SRCS
)
SET(bimtests_SRCS
- bimtests/TestArch.py
bimtests/TestArchAxis.py
bimtests/TestArchBase.py
+ bimtests/TestArchBaseGui.py
bimtests/TestArchComponent.py
bimtests/TestArchBuildingPart.py
bimtests/TestArchRoof.py
@@ -214,6 +214,7 @@ SET(bimtests_SRCS
bimtests/TestArchPanel.py
bimtests/TestArchWindow.py
bimtests/TestArchStairs.py
+ bimtests/TestArchStructure.py
bimtests/TestArchPipe.py
bimtests/TestArchCurtainWall.py
bimtests/TestArchProfile.py
@@ -229,6 +230,8 @@ SET(bimtests_SRCS
bimtests/TestArchTruss.py
bimtests/TestWebGLExport.py
bimtests/TestWebGLExportGui.py
+ bimtests/TestArchImportersGui.py
+ bimtests/TestArchBuildingPartGui.py
)
SOURCE_GROUP("" FILES ${Arch_SRCS})
diff --git a/src/Mod/BIM/TestArch.py b/src/Mod/BIM/TestArch.py
index 03288827c6..9e2f4cc038 100644
--- a/src/Mod/BIM/TestArch.py
+++ b/src/Mod/BIM/TestArch.py
@@ -28,7 +28,7 @@ from bimtests.TestArchSpace import TestArchSpace
from bimtests.TestArchWall import TestArchWall
from bimtests.TestArchBuildingPart import TestArchBuildingPart
from bimtests.TestArchAxis import TestArchAxis
-from bimtests.TestArch import TestArch
+from bimtests.TestArchStructure import TestArchStructure
from bimtests.TestArchMaterial import TestArchMaterial
from bimtests.TestArchPanel import TestArchPanel
from bimtests.TestArchWindow import TestArchWindow
diff --git a/src/Mod/BIM/TestArchGui.py b/src/Mod/BIM/TestArchGui.py
index d08f0b7fe9..97292491e1 100644
--- a/src/Mod/BIM/TestArchGui.py
+++ b/src/Mod/BIM/TestArchGui.py
@@ -22,208 +22,8 @@
# * *
# ***************************************************************************
-# Unit test for the Arch module
-
-import os
-import unittest
-
-import FreeCAD as App
-from FreeCAD import Units
-
-import Arch
-import Draft
-import Part
-import Sketcher
-import TechDraw
-import WorkingPlane
-
-from draftutils.messages import _msg
-
-if App.GuiUp:
- import FreeCADGui
+"""Import all Arch module unit tests in GUI mode."""
+#from bimtests.TestArchImportersGui import TestArchImportersGui
+#from bimtests.TestArchBuildingPartGui import TestArchBuildingPartGui
from bimtests.TestWebGLExportGui import TestWebGLExportGui
-
-class ArchTest(unittest.TestCase):
-
- def setUp(self):
- # setting a new document to hold the tests
- if App.ActiveDocument:
- if App.ActiveDocument.Name != "ArchTest":
- App.newDocument("ArchTest")
- else:
- App.newDocument("ArchTest")
- App.setActiveDocument("ArchTest")
-
- def testRebar(self):
- App.Console.PrintLog ('Checking Arch Rebar…\n')
- s = Arch.makeStructure(length=2,width=3,height=5)
- sk = App.ActiveDocument.addObject('Sketcher::SketchObject','Sketch')
- sk.AttachmentSupport = (s,["Face6"])
- sk.addGeometry(Part.LineSegment(App.Vector(-0.85,1.25,0),App.Vector(0.75,1.25,0)))
- sk.addGeometry(Part.LineSegment(App.Vector(0.75,1.25,0),App.Vector(0.75,-1.20,0)))
- sk.addGeometry(Part.LineSegment(App.Vector(0.75,-1.20,0),App.Vector(-0.85,-1.20,0)))
- sk.addGeometry(Part.LineSegment(App.Vector(-0.85,-1.20,0),App.Vector(-0.85,1.25,0)))
- sk.addConstraint(Sketcher.Constraint('Coincident',0,2,1,1))
- sk.addConstraint(Sketcher.Constraint('Coincident',1,2,2,1))
- sk.addConstraint(Sketcher.Constraint('Coincident',2,2,3,1))
- sk.addConstraint(Sketcher.Constraint('Coincident',3,2,0,1))
- r = Arch.makeRebar(s,sk,diameter=.1,amount=2)
- self.assertTrue(r,"Arch Rebar failed")
-
- def testBuildingPart(self):
- """Create a BuildingPart from a wall with a window and check its shape.
- """
- # Also regression test for:
- # https://github.com/FreeCAD/FreeCAD/issues/6178
- operation = "Arch BuildingPart"
- _msg(" Test '{}'".format(operation))
- # Most of the code below taken from testWindow function.
- line = Draft.makeLine(App.Vector(0, 0, 0), App.Vector(3000, 0, 0))
- wall = Arch.makeWall(line)
- sk = App.ActiveDocument.addObject("Sketcher::SketchObject", "Sketch001")
- sk.Placement.Rotation = App.Rotation(App.Vector(1, 0, 0), 90)
- sk.addGeometry(Part.LineSegment(App.Vector( 500, 800, 0), App.Vector(1500, 800, 0)))
- sk.addGeometry(Part.LineSegment(App.Vector(1500, 800, 0), App.Vector(1500, 2000, 0)))
- sk.addGeometry(Part.LineSegment(App.Vector(1500, 2000, 0), App.Vector( 500, 2000, 0)))
- sk.addGeometry(Part.LineSegment(App.Vector( 500, 2000, 0), App.Vector( 500, 800, 0)))
- sk.addConstraint(Sketcher.Constraint('Coincident', 0, 2, 1, 1))
- sk.addConstraint(Sketcher.Constraint('Coincident', 1, 2, 2, 1))
- sk.addConstraint(Sketcher.Constraint('Coincident', 2, 2, 3, 1))
- sk.addConstraint(Sketcher.Constraint('Coincident', 3, 2, 0, 1))
- App.ActiveDocument.recompute()
- win = Arch.makeWindow(sk)
- Arch.removeComponents(win, host=wall)
- App.ActiveDocument.recompute()
- bp = Arch.makeBuildingPart()
-
- # Wall visibility works when standalone
- FreeCADGui.Selection.clearSelection()
- FreeCADGui.Selection.addSelection('ArchTest',wall.Name)
- assert wall.Visibility
- FreeCADGui.runCommand('Std_ToggleVisibility',0)
- App.ActiveDocument.recompute()
- assert not wall.Visibility
- FreeCADGui.runCommand('Std_ToggleVisibility',0)
- assert wall.Visibility
-
- bp.Group = [wall]
- App.ActiveDocument.recompute()
- # Fails with OCC 7.5
- # self.assertTrue(len(bp.Shape.Faces) == 16, "'{}' failed".format(operation))
-
- # Wall visibility works when inside a BuildingPart
- FreeCADGui.runCommand('Std_ToggleVisibility',0)
- App.ActiveDocument.recompute()
- assert not wall.Visibility
- FreeCADGui.runCommand('Std_ToggleVisibility',0)
- assert wall.Visibility
-
- # Wall visibility works when BuildingPart Toggled
- FreeCADGui.Selection.clearSelection()
- FreeCADGui.Selection.addSelection('ArchTest',bp.Name)
- FreeCADGui.runCommand('Std_ToggleVisibility',0)
- assert not wall.Visibility
- FreeCADGui.runCommand('Std_ToggleVisibility',0)
- assert wall.Visibility
-
- # Wall visibiity works inside group inside BuildingPart Toggled
- grp = App.ActiveDocument.addObject("App::DocumentObjectGroup","Group")
- grp.Label="Group"
- grp.Group = [wall]
- bp.Group = [grp]
- App.ActiveDocument.recompute()
- assert wall.Visibility
- FreeCADGui.runCommand('Std_ToggleVisibility',0)
- App.ActiveDocument.recompute()
- assert not wall.Visibility
- FreeCADGui.runCommand('Std_ToggleVisibility',0)
- App.ActiveDocument.recompute()
- assert wall.Visibility
-
- def testImportSH3D(self):
- """Import a SweetHome 3D file
- """
- operation = "importers.importSH3D"
- _msg(" Test '{}'".format(operation))
- import BIM.importers.importSH3DHelper
- importer = BIM.importers.importSH3DHelper.SH3DImporter(None)
- importer.import_sh3d_from_string(SH3D_HOME)
- assert App.ActiveDocument.Site
- assert App.ActiveDocument.BuildingPart.Label == "Building"
- assert App.ActiveDocument.BuildingPart001.Label == "Level"
- assert App.ActiveDocument.Wall
-
- def tearDown(self):
- App.closeDocument("ArchTest")
- pass
-
-
-SH3D_HOME = """
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-"""
diff --git a/src/Mod/BIM/bimtests/TestArch.py b/src/Mod/BIM/bimtests/TestArch.py
deleted file mode 100644
index d06542ff4f..0000000000
--- a/src/Mod/BIM/bimtests/TestArch.py
+++ /dev/null
@@ -1,171 +0,0 @@
-# SPDX-License-Identifier: LGPL-2.1-or-later
-
-# ***************************************************************************
-# * *
-# * Copyright (c) 2013 Yorik van Havre *
-# * 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 *
-# * . *
-# * *
-# ***************************************************************************
-
-import os
-import FreeCAD as App
-import Arch
-import Draft
-import Part
-import Sketcher
-import Arch
-from bimtests import TestArchBase
-from draftutils.messages import _msg
-
-# TODO: move these tests to their relevant modules, remove this file
-class TestArch(TestArchBase.TestArchBase):
-
- def testStructure(self):
- App.Console.PrintLog ('Checking BIM Structure...\n')
- s = Arch.makeStructure(length=2,width=3,height=5)
- self.assertTrue(s,"BIM Structure failed")
-
- def testWindow(self):
- operation = "Arch Window"
- _msg(" Test '{}'".format(operation))
- line = Draft.makeLine(App.Vector(0, 0, 0), App.Vector(3000, 0, 0))
- wall = Arch.makeWall(line)
- sk = App.ActiveDocument.addObject("Sketcher::SketchObject", "Sketch001")
- sk.Placement.Rotation = App.Rotation(App.Vector(1, 0, 0), 90)
- sk.addGeometry(Part.LineSegment(App.Vector( 500, 800, 0), App.Vector(1500, 800, 0)))
- sk.addGeometry(Part.LineSegment(App.Vector(1500, 800, 0), App.Vector(1500, 2000, 0)))
- sk.addGeometry(Part.LineSegment(App.Vector(1500, 2000, 0), App.Vector( 500, 2000, 0)))
- sk.addGeometry(Part.LineSegment(App.Vector( 500, 2000, 0), App.Vector( 500, 800, 0)))
- sk.addConstraint(Sketcher.Constraint('Coincident', 0, 2, 1, 1))
- sk.addConstraint(Sketcher.Constraint('Coincident', 1, 2, 2, 1))
- sk.addConstraint(Sketcher.Constraint('Coincident', 2, 2, 3, 1))
- sk.addConstraint(Sketcher.Constraint('Coincident', 3, 2, 0, 1))
- App.ActiveDocument.recompute()
- win = Arch.makeWindow(sk)
- Arch.removeComponents(win, host=wall)
- App.ActiveDocument.recompute()
- self.assertTrue(win, "'{}' failed".format(operation))
-
- def testAxis(self):
- App.Console.PrintLog ('Checking Arch Axis...\n')
- a = Arch.makeAxis()
- self.assertTrue(a,"Arch Axis failed")
-
- def testSection(self):
- App.Console.PrintLog ('Checking Arch Section...\n')
- s = Arch.makeSectionPlane([])
- self.assertTrue(s,"Arch Section failed")
-
- def testStairs(self):
- App.Console.PrintLog ('Checking Arch Stairs...\n')
- s = Arch.makeStairs()
- self.assertTrue(s,"Arch Stairs failed")
-
- def testFrame(self):
- App.Console.PrintLog ('Checking Arch Frame...\n')
- l=Draft.makeLine(App.Vector(0,0,0),App.Vector(-2,0,0))
- p = Draft.makeRectangle(length=.5,height=.5)
- f = Arch.makeFrame(l,p)
- self.assertTrue(f,"Arch Frame failed")
-
- def testEquipment(self):
- App.Console.PrintLog ('Checking Arch Equipment...\n')
- box = App.ActiveDocument.addObject("Part::Box", "Box")
- box.Length = 500
- box.Width = 2000
- box.Height = 600
- equip = Arch.makeEquipment(box)
- self.assertTrue(equip,"Arch Equipment failed")
-
- def testPipe(self):
- App.Console.PrintLog ('Checking Arch Pipe...\n')
- pipe = Arch.makePipe(diameter=120, length=3000)
- self.assertTrue(pipe,"Arch Pipe failed")
-
- def testAdd(self):
- App.Console.PrintLog ('Checking Arch Add...\n')
- l=Draft.makeLine(App.Vector(0,0,0),App.Vector(2,0,0))
- w = Arch.makeWall(l,width=0.2,height=2)
- sb = Part.makeBox(1,1,1)
- b = App.ActiveDocument.addObject('Part::Feature','Box')
- b.Shape = sb
- App.ActiveDocument.recompute()
- Arch.addComponents(b,w)
- App.ActiveDocument.recompute()
- r = (w.Shape.Volume > 1.5)
- self.assertTrue(r,"Arch Add failed")
-
- def testRemove(self):
- App.Console.PrintLog ('Checking Arch Remove...\n')
- l=Draft.makeLine(App.Vector(0,0,0),App.Vector(2,0,0))
- w = Arch.makeWall(l,width=0.2,height=2,align="Right")
- sb = Part.makeBox(1,1,1)
- b = App.ActiveDocument.addObject('Part::Feature','Box')
- b.Shape = sb
- App.ActiveDocument.recompute()
- Arch.removeComponents(b,w)
- App.ActiveDocument.recompute()
- r = (w.Shape.Volume < 0.75)
- self.assertTrue(r,"Arch Remove failed")
-
- def testViewGeneration(self):
- """Tests the whole TD view generation workflow"""
-
- operation = "View generation"
- _msg(" Test '{}'".format(operation))
-
- # Create a few objects
- points = [App.Vector(0.0, 0.0, 0.0), App.Vector(2000.0, 0.0, 0.0)]
- line = Draft.make_wire(points)
- wall = Arch.makeWall(line, height=2000)
- wpl = App.Placement(App.Vector(500,0,1500), App.Vector(1,0,0),-90)
- win = Arch.makeWindowPreset('Fixed', width=1000.0, height=1000.0, h1=50.0, h2=50.0, h3=50.0, w1=100.0, w2=50.0, o1=0.0, o2=50.0, placement=wpl)
- win.Hosts = [wall]
- profile = Arch.makeProfile([169, 'HEA', 'HEA100', 'H', 100.0, 96.0, 5.0, 8.0])
- column = Arch.makeStructure(profile, height=2000.0)
- column.Profile = "HEA100"
- column.Placement.Base = App.Vector(500.0, 600.0, 0.0)
- level = Arch.makeFloor()
- level.addObjects([wall, column])
- App.ActiveDocument.recompute()
-
- # Create a drawing view
- section = Arch.makeSectionPlane(level)
- drawing = Arch.make2DDrawing()
- view = Draft.make_shape2dview(section)
- cut = Draft.make_shape2dview(section)
- cut.InPlace = False
- cut.ProjectionMode = "Cutfaces"
- drawing.addObjects([view, cut])
- App.ActiveDocument.recompute()
-
- # Create a TD page
- tpath = os.path.join(App.getResourceDir(),"Mod","TechDraw","Templates","ISO","A3_Landscape_blank.svg")
- page = App.ActiveDocument.addObject("TechDraw::DrawPage", "Page")
- template = App.ActiveDocument.addObject("TechDraw::DrawSVGTemplate", "Template")
- template.Template = tpath
- page.Template = template
- view = App.ActiveDocument.addObject("TechDraw::DrawViewDraft", "DraftView")
- view.Source = drawing
- page.addView(view)
- view.Scale = 1.0
- view.X = "20cm"
- view.Y = "15cm"
- App.ActiveDocument.recompute()
- assert True
diff --git a/src/Mod/BIM/bimtests/TestArchBase.py b/src/Mod/BIM/bimtests/TestArchBase.py
index 72292dbe59..db997cdde5 100644
--- a/src/Mod/BIM/bimtests/TestArchBase.py
+++ b/src/Mod/BIM/bimtests/TestArchBase.py
@@ -30,11 +30,35 @@ import FreeCAD
class TestArchBase(unittest.TestCase):
def setUp(self):
- print(f"Initializing: {self.__class__.__name__}")
- self.document = FreeCAD.newDocument(self.__class__.__name__)
+ """
+ Set up a new, clean document for the test. Ensure test isolation by creating a
+ uniquely-named document and cleaning up any potential leftovers from a previously failed
+ run.
+ """
+ self.doc_name = self.__class__.__name__
+
+ # Close any document of the same name that might have been left over from a crashed or
+ # aborted test run. FreeCAD.getDocument() raises a NameError if the document is not found,
+ # so we wrap this check in a try...except block.
+ try:
+ FreeCAD.getDocument(self.doc_name)
+ # If getDocument() succeeds, the document exists and must be closed.
+ FreeCAD.closeDocument(self.doc_name)
+ except NameError:
+ # This is the expected path on a clean run; do nothing.
+ pass
+
+ # Create a fresh document for the current test.
+ self.document = FreeCAD.newDocument(self.doc_name)
+ self.assertEqual(self.document.Name, self.doc_name)
def tearDown(self):
- FreeCAD.closeDocument(self.document.Name)
+ """Close the test document after all tests in the class are complete."""
+ if hasattr(self, 'document') and self.document:
+ try:
+ FreeCAD.closeDocument(self.document.Name)
+ except Exception as e:
+ FreeCAD.Console.PrintError(f"Error during tearDown in {self.__class__.__name__}: {e}\n")
def printTestMessage(self, text, prepend_text="Test ", end="\n"):
"""Write messages to the console including the line ending.
@@ -43,3 +67,4 @@ class TestArchBase(unittest.TestCase):
passed as the prepend_text argument
"""
FreeCAD.Console.PrintMessage(prepend_text + text + end)
+
diff --git a/src/Mod/BIM/bimtests/TestArchBaseGui.py b/src/Mod/BIM/bimtests/TestArchBaseGui.py
new file mode 100644
index 0000000000..488a3e1ade
--- /dev/null
+++ b/src/Mod/BIM/bimtests/TestArchBaseGui.py
@@ -0,0 +1,55 @@
+# 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 *
+# * . *
+# * *
+# ***************************************************************************
+
+import unittest
+import FreeCAD
+import FreeCADGui
+from bimtests.TestArchBase import TestArchBase
+
+class TestArchBaseGui(TestArchBase):
+ """
+ The base class for all Arch/BIM GUI unit tests.
+ It inherits from TestArchBase to handle document setup and adds
+ GUI-specific initialization by activating the BIM workbench.
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ """
+ Ensure the GUI is available and activate the BIM workbench once
+ before any tests in the inheriting class are run.
+ """
+ if not FreeCAD.GuiUp:
+ raise unittest.SkipTest("Cannot run GUI tests in a CLI environment.")
+
+ # Activating the workbench ensures all GUI commands are loaded and ready.
+ FreeCADGui.activateWorkbench("BIMWorkbench")
+
+ def setUp(self):
+ """
+ Run the parent's setup to create the uniquely named document.
+ The workbench is already activated by setUpClass.
+ """
+ super().setUp()
+
diff --git a/src/Mod/BIM/bimtests/TestArchBuildingPartGui.py b/src/Mod/BIM/bimtests/TestArchBuildingPartGui.py
new file mode 100644
index 0000000000..fb978c5766
--- /dev/null
+++ b/src/Mod/BIM/bimtests/TestArchBuildingPartGui.py
@@ -0,0 +1,79 @@
+import FreeCAD as App
+import FreeCADGui
+import Arch
+import Draft
+import Part
+import Sketcher
+from bimtests.TestArchBaseGui import TestArchBaseGui
+
+class TestArchBuildingPartGui(TestArchBaseGui):
+
+ def testBuildingPart(self):
+ """Create a BuildingPart from a wall with a window and check its shape.
+ """
+ # Also regression test for:
+ # https://github.com/FreeCAD/FreeCAD/issues/6178
+ #operation = "Arch BuildingPart"
+ #_msg(" Test '{}'".format(operation))
+ # Most of the code below taken from testWindow function.
+ line = Draft.makeLine(App.Vector(0, 0, 0), App.Vector(3000, 0, 0))
+ wall = Arch.makeWall(line)
+ sk = App.ActiveDocument.addObject("Sketcher::SketchObject", "Sketch001")
+ sk.Placement.Rotation = App.Rotation(App.Vector(1, 0, 0), 90)
+ sk.addGeometry(Part.LineSegment(App.Vector( 500, 800, 0), App.Vector(1500, 800, 0)))
+ sk.addGeometry(Part.LineSegment(App.Vector(1500, 800, 0), App.Vector(1500, 2000, 0)))
+ sk.addGeometry(Part.LineSegment(App.Vector(1500, 2000, 0), App.Vector( 500, 2000, 0)))
+ sk.addGeometry(Part.LineSegment(App.Vector( 500, 2000, 0), App.Vector( 500, 800, 0)))
+ sk.addConstraint(Sketcher.Constraint('Coincident', 0, 2, 1, 1))
+ sk.addConstraint(Sketcher.Constraint('Coincident', 1, 2, 2, 1))
+ sk.addConstraint(Sketcher.Constraint('Coincident', 2, 2, 3, 1))
+ sk.addConstraint(Sketcher.Constraint('Coincident', 3, 2, 0, 1))
+ App.ActiveDocument.recompute()
+ win = Arch.makeWindow(sk)
+ Arch.removeComponents(win, host=wall)
+ App.ActiveDocument.recompute()
+ bp = Arch.makeBuildingPart()
+
+ # Wall visibility works when standalone
+ FreeCADGui.Selection.clearSelection()
+ FreeCADGui.Selection.addSelection(self.doc_name,wall.Name)
+ assert wall.Visibility
+ FreeCADGui.runCommand('Std_ToggleVisibility',0)
+ App.ActiveDocument.recompute()
+ assert not wall.Visibility
+ FreeCADGui.runCommand('Std_ToggleVisibility',0)
+ assert wall.Visibility
+
+ bp.Group = [wall]
+ App.ActiveDocument.recompute()
+ # Fails with OCC 7.5
+ # self.assertTrue(len(bp.Shape.Faces) == 16, "'{}' failed".format(operation))
+
+ # Wall visibility works when inside a BuildingPart
+ FreeCADGui.runCommand('Std_ToggleVisibility',0)
+ App.ActiveDocument.recompute()
+ assert not wall.Visibility
+ FreeCADGui.runCommand('Std_ToggleVisibility',0)
+ assert wall.Visibility
+
+ # Wall visibility works when BuildingPart Toggled
+ FreeCADGui.Selection.clearSelection()
+ FreeCADGui.Selection.addSelection(self.doc_name,bp.Name)
+ FreeCADGui.runCommand('Std_ToggleVisibility',0)
+ assert not wall.Visibility
+ FreeCADGui.runCommand('Std_ToggleVisibility',0)
+ assert wall.Visibility
+
+ # Wall visibiity works inside group inside BuildingPart Toggled
+ grp = App.ActiveDocument.addObject("App::DocumentObjectGroup","Group")
+ grp.Label="Group"
+ grp.Group = [wall]
+ bp.Group = [grp]
+ App.ActiveDocument.recompute()
+ assert wall.Visibility
+ FreeCADGui.runCommand('Std_ToggleVisibility',0)
+ App.ActiveDocument.recompute()
+ assert not wall.Visibility
+ FreeCADGui.runCommand('Std_ToggleVisibility',0)
+ App.ActiveDocument.recompute()
+ assert wall.Visibility
\ No newline at end of file
diff --git a/src/Mod/BIM/bimtests/TestArchComponent.py b/src/Mod/BIM/bimtests/TestArchComponent.py
index cbb82b0188..81b50812bf 100644
--- a/src/Mod/BIM/bimtests/TestArchComponent.py
+++ b/src/Mod/BIM/bimtests/TestArchComponent.py
@@ -26,6 +26,7 @@
import Arch
import Draft
+import Part
import FreeCAD as App
from bimtests import TestArchBase
from draftutils.messages import _msg
@@ -34,6 +35,32 @@ from math import pi, cos, sin, radians
class TestArchComponent(TestArchBase.TestArchBase):
+ def testAdd(self):
+ App.Console.PrintLog ('Checking Arch Add...\n')
+ l=Draft.makeLine(App.Vector(0,0,0),App.Vector(2,0,0))
+ w = Arch.makeWall(l,width=0.2,height=2)
+ sb = Part.makeBox(1,1,1)
+ b = App.ActiveDocument.addObject('Part::Feature','Box')
+ b.Shape = sb
+ App.ActiveDocument.recompute()
+ Arch.addComponents(b,w)
+ App.ActiveDocument.recompute()
+ r = (w.Shape.Volume > 1.5)
+ self.assertTrue(r,"Arch Add failed")
+
+ def testRemove(self):
+ App.Console.PrintLog ('Checking Arch Remove...\n')
+ l=Draft.makeLine(App.Vector(0,0,0),App.Vector(2,0,0))
+ w = Arch.makeWall(l,width=0.2,height=2,align="Right")
+ sb = Part.makeBox(1,1,1)
+ b = App.ActiveDocument.addObject('Part::Feature','Box')
+ b.Shape = sb
+ App.ActiveDocument.recompute()
+ Arch.removeComponents(b,w)
+ App.ActiveDocument.recompute()
+ r = (w.Shape.Volume < 0.75)
+ self.assertTrue(r,"Arch Remove failed")
+
def testBsplineSlabAreas(self):
"""Test the HorizontalArea and VerticalArea properties of a Bspline-based slab.
diff --git a/src/Mod/BIM/bimtests/TestArchEquipment.py b/src/Mod/BIM/bimtests/TestArchEquipment.py
index fde7d625ba..22d5f57f0f 100644
--- a/src/Mod/BIM/bimtests/TestArchEquipment.py
+++ b/src/Mod/BIM/bimtests/TestArchEquipment.py
@@ -23,15 +23,22 @@
# ***************************************************************************
import Arch
+import FreeCAD as App
from bimtests import TestArchBase
class TestArchEquipment(TestArchBase.TestArchBase):
def test_makeEquipment(self):
"""Test the makeEquipment function."""
- operation = "Testing makeEquipment..."
- self.printTestMessage(operation)
obj = Arch.makeEquipment()
self.assertIsNotNone(obj, "makeEquipment failed to create an object")
- self.assertEqual(obj.Label, "Equipment", "Incorrect default label for Equipment")
\ No newline at end of file
+ self.assertEqual(obj.Label, "Equipment", "Incorrect default label for Equipment")
+
+ def testEquipment(self):
+ box = App.ActiveDocument.addObject("Part::Box", "Box")
+ box.Length = 500
+ box.Width = 2000
+ box.Height = 600
+ equip = Arch.makeEquipment(box)
+ self.assertTrue(equip,"Arch Equipment failed")
\ No newline at end of file
diff --git a/src/Mod/BIM/bimtests/TestArchFrame.py b/src/Mod/BIM/bimtests/TestArchFrame.py
index cc12facba1..ea83420334 100644
--- a/src/Mod/BIM/bimtests/TestArchFrame.py
+++ b/src/Mod/BIM/bimtests/TestArchFrame.py
@@ -23,15 +23,21 @@
# ***************************************************************************
import Arch
+import Draft
+import FreeCAD as App
from bimtests import TestArchBase
class TestArchFrame(TestArchBase.TestArchBase):
def test_makeFrame(self):
"""Test the makeFrame function."""
- operation = "Testing makeFrame..."
- self.printTestMessage(operation)
obj = Arch.makeFrame(None, None)
self.assertIsNotNone(obj, "makeFrame failed to create an object")
- self.assertEqual(obj.Label, "Frame", "Incorrect default label for Frame")
\ No newline at end of file
+ self.assertEqual(obj.Label, "Frame", "Incorrect default label for Frame")
+
+ def testFrame(self):
+ l=Draft.makeLine(App.Vector(0,0,0),App.Vector(-2,0,0))
+ p = Draft.makeRectangle(length=.5,height=.5)
+ f = Arch.makeFrame(l,p)
+ self.assertTrue(f,"Arch Frame failed")
\ No newline at end of file
diff --git a/src/Mod/BIM/bimtests/TestArchImportersGui.py b/src/Mod/BIM/bimtests/TestArchImportersGui.py
new file mode 100644
index 0000000000..7337972151
--- /dev/null
+++ b/src/Mod/BIM/bimtests/TestArchImportersGui.py
@@ -0,0 +1,88 @@
+
+import FreeCAD as App
+from bimtests.TestArchBaseGui import TestArchBaseGui
+
+class TestArchImportersGui(TestArchBaseGui):
+
+ def testImportSH3D(self):
+ """Import a SweetHome 3D file
+ """
+ import BIM.importers.importSH3DHelper
+
+ importer = BIM.importers.importSH3DHelper.SH3DImporter(None)
+ importer.import_sh3d_from_string(SH3D_HOME)
+
+ assert App.ActiveDocument.Site
+ assert App.ActiveDocument.BuildingPart.Label == "Building"
+ assert App.ActiveDocument.BuildingPart001.Label == "Level"
+ assert App.ActiveDocument.Wall
+
+
+SH3D_HOME = """
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"""
diff --git a/src/Mod/BIM/bimtests/TestArchRebar.py b/src/Mod/BIM/bimtests/TestArchRebar.py
index d981207cc5..5253804f25 100644
--- a/src/Mod/BIM/bimtests/TestArchRebar.py
+++ b/src/Mod/BIM/bimtests/TestArchRebar.py
@@ -170,4 +170,27 @@ class TestArchRebar(TestArchBase.TestArchBase):
# The Shape should be the default null/empty shape.
self.assertIsNotNone(rebar.Shape, "Rebar object should always have a Shape attribute.")
self.assertTrue(rebar.Shape.isNull(),
- "Rebar.Shape should be a null shape if no Base is provided.")
\ No newline at end of file
+ "Rebar.Shape should be a null shape if no Base is provided.")
+
+ def test_RebarMe(self):
+ """
+ Tests the creation of an Arch Rebar
+ """
+ import Sketcher
+ self.printTestMessage('Checking Arch Rebar...\n')
+ s = Arch.makeStructure(length=2, width=3, height=5)
+ sk = self.document.addObject('Sketcher::SketchObject', 'Sketch')
+ sk.AttachmentSupport = (s, ["Face6"])
+ sk.addGeometry(Part.LineSegment(FreeCAD.Vector(-0.85, 1.25, 0), FreeCAD.Vector(0.75, 1.25, 0)))
+ sk.addGeometry(Part.LineSegment(FreeCAD.Vector(0.75, 1.25, 0), FreeCAD.Vector(0.75, -1.20, 0)))
+ sk.addGeometry(Part.LineSegment(FreeCAD.Vector(0.75, -1.20, 0), FreeCAD.Vector(-0.85, -1.20, 0)))
+ sk.addGeometry(Part.LineSegment(FreeCAD.Vector(-0.85, -1.20, 0), FreeCAD.Vector(-0.85, 1.25, 0)))
+ sk.addConstraint(Sketcher.Constraint('Coincident', 0, 2, 1, 1))
+ sk.addConstraint(Sketcher.Constraint('Coincident', 1, 2, 2, 1))
+ sk.addConstraint(Sketcher.Constraint('Coincident', 2, 2, 3, 1))
+ sk.addConstraint(Sketcher.Constraint('Coincident', 3, 2, 0, 1))
+ self.document.recompute()
+ r = Arch.makeRebar(s, sk, diameter=.1, amount=2)
+ self.document.recompute()
+ self.assertTrue(r, "Arch Rebar creation failed")
+ self.assertFalse(r.Shape.isNull(), "Rebar shape is null")
\ No newline at end of file
diff --git a/src/Mod/BIM/bimtests/TestArchSectionPlane.py b/src/Mod/BIM/bimtests/TestArchSectionPlane.py
index 07871f3524..d40aa5c2c3 100644
--- a/src/Mod/BIM/bimtests/TestArchSectionPlane.py
+++ b/src/Mod/BIM/bimtests/TestArchSectionPlane.py
@@ -23,6 +23,9 @@
# ***************************************************************************
import Arch
+import Draft
+import os
+import FreeCAD as App
from bimtests import TestArchBase
class TestArchSectionPlane(TestArchBase.TestArchBase):
@@ -34,4 +37,47 @@ class TestArchSectionPlane(TestArchBase.TestArchBase):
section_plane = Arch.makeSectionPlane(name="TestSectionPlane")
self.assertIsNotNone(section_plane, "makeSectionPlane failed to create a section plane object.")
- self.assertEqual(section_plane.Label, "TestSectionPlane", "Section plane label is incorrect.")
\ No newline at end of file
+ self.assertEqual(section_plane.Label, "TestSectionPlane", "Section plane label is incorrect.")
+
+ def testViewGeneration(self):
+ """Tests the whole TD view generation workflow"""
+
+ # Create a few objects
+ points = [App.Vector(0.0, 0.0, 0.0), App.Vector(2000.0, 0.0, 0.0)]
+ line = Draft.make_wire(points)
+ wall = Arch.makeWall(line, height=2000)
+ wpl = App.Placement(App.Vector(500,0,1500), App.Vector(1,0,0),-90)
+ win = Arch.makeWindowPreset('Fixed', width=1000.0, height=1000.0, h1=50.0, h2=50.0, h3=50.0, w1=100.0, w2=50.0, o1=0.0, o2=50.0, placement=wpl)
+ win.Hosts = [wall]
+ profile = Arch.makeProfile([169, 'HEA', 'HEA100', 'H', 100.0, 96.0, 5.0, 8.0])
+ column = Arch.makeStructure(profile, height=2000.0)
+ column.Profile = "HEA100"
+ column.Placement.Base = App.Vector(500.0, 600.0, 0.0)
+ level = Arch.makeFloor()
+ level.addObjects([wall, column])
+ App.ActiveDocument.recompute()
+
+ # Create a drawing view
+ section = Arch.makeSectionPlane(level)
+ drawing = Arch.make2DDrawing()
+ view = Draft.make_shape2dview(section)
+ cut = Draft.make_shape2dview(section)
+ cut.InPlace = False
+ cut.ProjectionMode = "Cutfaces"
+ drawing.addObjects([view, cut])
+ App.ActiveDocument.recompute()
+
+ # Create a TD page
+ tpath = os.path.join(App.getResourceDir(),"Mod","TechDraw","Templates","ISO","A3_Landscape_blank.svg")
+ page = App.ActiveDocument.addObject("TechDraw::DrawPage", "Page")
+ template = App.ActiveDocument.addObject("TechDraw::DrawSVGTemplate", "Template")
+ template.Template = tpath
+ page.Template = template
+ view = App.ActiveDocument.addObject("TechDraw::DrawViewDraft", "DraftView")
+ view.Source = drawing
+ page.addView(view)
+ view.Scale = 1.0
+ view.X = "20cm"
+ view.Y = "15cm"
+ App.ActiveDocument.recompute()
+ assert True
\ No newline at end of file
diff --git a/src/Mod/BIM/bimtests/TestArchStructure.py b/src/Mod/BIM/bimtests/TestArchStructure.py
new file mode 100644
index 0000000000..488b3fb973
--- /dev/null
+++ b/src/Mod/BIM/bimtests/TestArchStructure.py
@@ -0,0 +1,34 @@
+# 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 *
+# * . *
+# * *
+# ***************************************************************************
+
+import FreeCAD as App
+import Arch
+from bimtests import TestArchBase
+
+class TestArchStructure(TestArchBase.TestArchBase):
+
+ def testStructure(self):
+ App.Console.PrintLog ('Checking BIM Structure...\n')
+ s = Arch.makeStructure(length=2,width=3,height=5)
+ self.assertTrue(s,"BIM Structure failed")
\ No newline at end of file
diff --git a/src/Mod/BIM/bimtests/TestArchWindow.py b/src/Mod/BIM/bimtests/TestArchWindow.py
index db5c020797..1a6f3ab96a 100644
--- a/src/Mod/BIM/bimtests/TestArchWindow.py
+++ b/src/Mod/BIM/bimtests/TestArchWindow.py
@@ -22,16 +22,634 @@
# * *
# ***************************************************************************
-import Arch
+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_makeWindow(self):
- """Test the makeWindow function."""
- operation = "Testing makeWindow function"
- self.printTestMessage(operation)
+ 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())
- window = Arch.makeWindow(width=1200, height=1000)
- self.assertIsNotNone(window, "makeWindow failed to create a window object.")
- self.assertEqual(window.Label, "Window", "Window label is incorrect.")
\ No newline at end of file
+ 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, Sill 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
+ original_window.Sill = 100.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