BIM: allow boundaries to be defined from a single object (e.g. wall) (#20158)

* BIM: Add test for space from single wall boundaries

* BIM: Arch_Space, enable creation of spaces from single objects with
boundaries

* BIM: update and expand docstring
This commit is contained in:
Furgo
2025-03-19 10:45:26 +01:00
committed by GitHub
parent 21c07cabc5
commit e8c4d7ea2f
3 changed files with 127 additions and 18 deletions

View File

@@ -768,33 +768,93 @@ def makeSite(objectslist=None,baseobj=None,name=None):
def makeSpace(objects=None,baseobj=None,name=None):
"""Creates a space object from the given objects.
"""makeSpace([objects],[baseobj],[name]): Creates a space object from the given objects.
Objects can be one document object, in which case it becomes the base shape of the space
object, or a list of selection objects as got from getSelectionEx(), or a list of tuples
(object, subobjectname)"""
Parameters
----------
objects : object or List(<SelectionObject>) or App::PropertyLinkSubList, optional
The object or selection set that defines the space. If a single object is given,
it becomes the base shape for the object. If the object or selection set contains
subelements, these will be used as the boundaries to create the space. By default None.
baseobj : object or List(<SelectionObject>) or App::PropertyLinkSubList, optional
Currently unimplemented, it replaces and behaves in the same way as the objects parameter
if defined. By default None.
name : str, optional
The user-facing name to assign to the space object's label. By default None, in
which case the label is set to "Space".
Notes
-----
The objects parameter can be passed using either of these different formats:
1. Single object (e.g. a Part::Feature document object). Will be used as the space's base
shape.
::
objects = <Part::Feature>
2. List of selection objects, as provided by ``Gui.Selection.getSelectionEx()``. This
requires the GUI to be active. The `SubObjects` property of each selection object in the
list defines the space's boundaries. If the list contains a single selection object without
subobjects, or with only one subobject, the object in its ``Object`` property is used as
the base shape.
::
objects = [<SelectionObject>, ...]
3. A list of tuples that can be assigned to an ``App::PropertyLinkSubList`` property. Each
tuple contains a document object and a nested tuple of subobjects that define boundaries. If
the list contains a single tuple without a nested subobjects tuple, or a subobjects tuple
with only one subobject, the object in the tuple is used as the base shape.
::
objects = [(obj1, ("Face1")), (obj2, ("Face1")), ...]
objects = [(obj, ("Face1", "Face2", "Face3", "Face4"))]
"""
import ArchSpace
if not FreeCAD.ActiveDocument:
FreeCAD.Console.PrintError("No active document. Aborting\n")
return
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython","Space")
obj.Label = name if name else translate("Arch","Space")
ArchSpace._Space(obj)
space = FreeCAD.ActiveDocument.addObject("Part::FeaturePython","Space")
space.Label = name if name else translate("Arch","Space")
ArchSpace._Space(space)
if FreeCAD.GuiUp:
ArchSpace._ViewProviderSpace(obj.ViewObject)
ArchSpace._ViewProviderSpace(space.ViewObject)
if baseobj:
objects = baseobj
if objects:
if not isinstance(objects,list):
objects = [objects]
if len(objects) == 1:
obj.Base = objects[0]
if FreeCAD.GuiUp:
objects[0].ViewObject.hide()
isSingleObject = lambda objs: len(objs) == 1
# We assume that the objects list is not a mixed set. The type of the first
# object will determine the type of the set.
# Input to this function can come into three different formats. First convert it
# to a common format: [ (<Part::PartFeature>, ["Face1", ...]), ... ]
if (hasattr(objects[0], "isDerivedFrom") and
objects[0].isDerivedFrom("Gui::SelectionObject")):
# Selection set: convert to common format
# [<SelectionObject>, ...]
objects = [(obj.Object, obj.SubElementNames) for obj in objects]
elif (isinstance(objects[0], tuple) or isinstance(objects[0], list)):
# Tuple or list of object with subobjects: pass unmodified
# [ (<Part::PartFeature>, ["Face1", ...]), ... ]
pass
else:
obj.Proxy.addSubobjects(obj,objects)
return obj
# Single object: assume anything else passed is a single object with no
# boundaries.
# [ <Part::PartFeature> ]
objects = [(objects[0], [])]
if isSingleObject(objects):
# For a single object, having boundaries is determined by them being defined
# as more than one subelement (e.g. two faces)
boundaries = [obj for obj in objects if len(obj[1]) > 1]
else:
boundaries = [obj for obj in objects if obj[1]]
if isSingleObject(objects) and not boundaries:
space.Base = objects[0][0]
if FreeCAD.GuiUp:
objects[0][0].ViewObject.hide()
else:
space.Proxy.addSubobjects(space, boundaries)
return space
def makeStairs(baseobj=None,length=None,width=None,height=None,steps=None,name=None):

View File

@@ -818,6 +818,58 @@ class ArchTest(unittest.TestCase):
App.ActiveDocument.recompute()
assert True
def test_SpaceFromSingleWall(self):
"""Create a space from boundaries of a single wall.
"""
from FreeCAD import Units
operation = "Arch Space from single wall"
_msg(f"\n Test '{operation}'")
# Create a wall
wallInnerLength = 4000.0
wallHeight = 3000.0
wallInnerFaceArea = wallInnerLength * wallHeight
pl = App.Placement()
pl.Rotation.Q = (0.0, 0.0, 0.0, 1.0)
pl.Base = App.Vector(0.0, 0.0, 0.0)
rectangleBase = Draft.make_rectangle(
length=wallInnerLength, height=wallInnerLength, placement=pl, face=True, support=None)
App.ActiveDocument.recompute() # To calculate rectangle area
rectangleArea = rectangleBase.Area
App.ActiveDocument.getObject(rectangleBase.Name).MakeFace = False
wall = Arch.makeWall(baseobj=rectangleBase, height=wallHeight, align="Left")
App.ActiveDocument.recompute() # To calculate face areas
# Create a space from the wall's inner faces
boundaries = [f"Face{ind+1}" for ind, face in enumerate(wall.Shape.Faces)
if round(face.Area) == round(wallInnerFaceArea)]
if App.GuiUp:
FreeCADGui.Selection.clearSelection()
FreeCADGui.Selection.addSelection(wall, boundaries)
space = Arch.makeSpace(FreeCADGui.Selection.getSelectionEx())
# Alternative, but test takes longer to run (~10x)
# FreeCADGui.activateWorkbench("BIMWorkbench")
# FreeCADGui.runCommand('Arch_Space', 0)
# space = App.ActiveDocument.Space
else:
# Also tests the alternative way of specifying the boundaries
# [ (<Part::PartFeature>, ["Face1", ...]), ... ]
space = Arch.makeSpace([(wall, boundaries)])
App.ActiveDocument.recompute() # To calculate space area
# Assert if area is as expected
expectedArea = Units.parseQuantity(str(rectangleArea))
actualArea = Units.parseQuantity(str(space.Area))
self.assertAlmostEqual(
expectedArea.Value,
actualArea.Value,
msg = f"Invalid area value. Expected: {expectedArea.UserString}, actual: {actualArea.UserString}")
def tearDown(self):
App.closeDocument("ArchTest")
pass

View File

@@ -56,10 +56,7 @@ class Arch_Space:
sel = FreeCADGui.Selection.getSelection()
if sel:
FreeCADGui.Control.closeDialog()
if len(sel) == 1:
FreeCADGui.doCommand("obj = Arch.makeSpace(FreeCADGui.Selection.getSelection())")
else:
FreeCADGui.doCommand("obj = Arch.makeSpace(FreeCADGui.Selection.getSelectionEx())")
FreeCADGui.doCommand("obj = Arch.makeSpace(FreeCADGui.Selection.getSelectionEx())")
FreeCADGui.addModule("Draft")
FreeCADGui.doCommand("Draft.autogroup(obj)")
FreeCAD.ActiveDocument.commitTransaction()