BIM: refactor link processing, add BIM_Link wrapper command (#28104)
* BIM: refactor, introduce link properties override helper * BIM: add Link support to Arch_Add, Arch_Remove Fixes: https://github.com/FreeCAD/FreeCAD/issues/26255 Fixes: https://github.com/FreeCAD/FreeCAD/issues/28139 Co-authored-by: Roy-043 <70520633+Roy-043@users.noreply.github.com> * BIM: add link command wrapper * BIM: add link addition and removal tests --------- Co-authored-by: Roy-043 <70520633+Roy-043@users.noreply.github.com>
This commit is contained in:
+99
-13
@@ -57,6 +57,24 @@ else:
|
||||
# module functions ###############################################
|
||||
|
||||
|
||||
def is_type_or_link(obj, type_name):
|
||||
"""Checks if the given object or its linked object is of the specified type.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj: <App::DocumentObject>
|
||||
The object to check.
|
||||
type_name: str
|
||||
The type to check against (e.g., 'Window', 'Wall').
|
||||
"""
|
||||
if not obj:
|
||||
return False
|
||||
|
||||
target = obj.getLinkedObject() if hasattr(obj, "getLinkedObject") else obj
|
||||
|
||||
return Draft.getType(target) == type_name
|
||||
|
||||
|
||||
def getStringList(objects):
|
||||
"""getStringList(objects): returns a string defining a list
|
||||
of objects"""
|
||||
@@ -136,12 +154,10 @@ def addComponents(objectsList, host):
|
||||
x = getattr(host, "Axes", [])
|
||||
for o in objectsList:
|
||||
if hasattr(o, "Shape"):
|
||||
if Draft.getType(o) == "Window":
|
||||
if hasattr(o, "Hosts"):
|
||||
if not host in o.Hosts:
|
||||
g = o.Hosts
|
||||
g.append(host)
|
||||
o.Hosts = g
|
||||
if is_type_or_link(o, "Window"):
|
||||
ensure_link_overrides(o)
|
||||
if hasattr(o, "Hosts") and host not in o.Hosts:
|
||||
o.Hosts += [host]
|
||||
elif o in outList:
|
||||
FreeCAD.Console.PrintWarning(
|
||||
translate(
|
||||
@@ -206,12 +222,10 @@ def removeComponents(objectsList, host=None):
|
||||
host.Axes = a
|
||||
s = host.Subtractions
|
||||
for o in objectsList:
|
||||
if Draft.getType(o) == "Window":
|
||||
if hasattr(o, "Hosts"):
|
||||
if not host in o.Hosts:
|
||||
g = o.Hosts
|
||||
g.append(host)
|
||||
o.Hosts = g
|
||||
if is_type_or_link(o, "Window"):
|
||||
ensure_link_overrides(o)
|
||||
if hasattr(o, "Hosts") and host not in o.Hosts:
|
||||
o.Hosts += [host]
|
||||
elif not o in s:
|
||||
s.append(o)
|
||||
if FreeCAD.GuiUp:
|
||||
@@ -281,8 +295,13 @@ def removeComponents(objectsList, host=None):
|
||||
if o in a:
|
||||
a.remove(o)
|
||||
h.Objects = a
|
||||
if hasattr(o, "Hosts") and Draft.getType(o) == "Window":
|
||||
if hasattr(o, "Hosts") and is_type_or_link(o, "Window"):
|
||||
ensure_link_overrides(o)
|
||||
# Ensure the hosts are recomputed upon window removal
|
||||
old_hosts = o.Hosts[:]
|
||||
o.Hosts = []
|
||||
for old_host in old_hosts:
|
||||
old_host.touch()
|
||||
|
||||
|
||||
def makeComponent(baseobj=None, name=None, delete=False):
|
||||
@@ -1727,3 +1746,70 @@ def makeIfcSpreadsheet(archobj=None):
|
||||
FreeCAD.ActiveDocument.removeObject(ifc_spreadsheet)
|
||||
else:
|
||||
return ifc_spreadsheet
|
||||
|
||||
|
||||
def override_link_properties(link_obj, prop_names):
|
||||
"""
|
||||
Injects local properties into a Link object to 'shadow' its source's properties.
|
||||
|
||||
By creating a local property, the Link can then to hold instance-specific data (like a Window's
|
||||
'Hosts' list) without modifying the source and without triggering the heavy CopyOnChange
|
||||
deep-copy mechanism of App::Link.
|
||||
|
||||
This function must be called before the Link's first recompute, which is when the core merges
|
||||
the source's properties into the Link. This is typically achieved by calling it from within the
|
||||
`appLinkExecute` method of the source object, or by forcefully calling it via
|
||||
`ensure_link_overrides` if the user interacts with an unrecomputed Link.
|
||||
|
||||
See: https://wiki.freecad.org/Std_LinkMake#Copy_on_Change
|
||||
|
||||
Parameters
|
||||
----------
|
||||
link_obj : <App::DocumentObject>
|
||||
The newly created Link object (`App::Link`) that will receive the overridden properties.
|
||||
prop_names : list of str
|
||||
A list of property names (e.g., `["Hosts"]`) to extract from the source object and
|
||||
recreate locally on the Link object.
|
||||
"""
|
||||
if not link_obj or not prop_names:
|
||||
return
|
||||
|
||||
# Check if we have a linked object
|
||||
source_obj = link_obj.LinkedObject
|
||||
if not source_obj:
|
||||
return
|
||||
|
||||
for prop_name in prop_names:
|
||||
# If the property is not in PropertiesList, the Link has not yet merged the source's
|
||||
# properties (i.e. we're running inside the `appLinkExecute` hook), or the property
|
||||
# genuinely doesn't exist.
|
||||
if prop_name not in link_obj.PropertiesList:
|
||||
|
||||
prop_type = source_obj.getTypeIdOfProperty(prop_name)
|
||||
if not prop_type: # Property doesn't exist on the source
|
||||
continue
|
||||
|
||||
prop_group = source_obj.getGroupOfProperty(prop_name)
|
||||
prop_doc = source_obj.getDocumentationOfProperty(prop_name)
|
||||
|
||||
# Inject the shadow property locally on the Link
|
||||
link_obj.addProperty(prop_type, prop_name, prop_group, prop_doc, locked=True)
|
||||
|
||||
# Initialize with the source's current value when the link is created. Afterwards, the
|
||||
# user can change the Link's property independently, and it won't affect the source or
|
||||
# trigger a deep copy.
|
||||
prop_value = getattr(source_obj, prop_name)
|
||||
setattr(link_obj, prop_name, prop_value)
|
||||
|
||||
|
||||
def ensure_link_overrides(obj):
|
||||
"""
|
||||
Ensures that a Link object has its required override properties set up. This acts as a safeguard
|
||||
if a user modifies a Link before it has been recomputed for the first time (which normally
|
||||
triggers appLinkExecute).
|
||||
"""
|
||||
if obj.isDerivedFrom("App::Link"):
|
||||
source = obj.LinkedObject
|
||||
if source and hasattr(source, "Proxy") and hasattr(source.Proxy, "LinkOverrideProperties"):
|
||||
# Force the override injection on the spot
|
||||
override_link_properties(obj, source.Proxy.LinkOverrideProperties)
|
||||
|
||||
@@ -201,6 +201,10 @@ class Component(ArchIFC.IfcProduct):
|
||||
The object to turn into an Arch Component
|
||||
"""
|
||||
|
||||
# List of properties to override in App::Link.
|
||||
# Subclasses which require overrides (e.g. Window, Rebar) should populate the overrides list.
|
||||
LinkOverrideProperties = []
|
||||
|
||||
def __init__(self, obj):
|
||||
obj.Proxy = self
|
||||
self.Type = "Component"
|
||||
@@ -1326,6 +1330,20 @@ class Component(ArchIFC.IfcProduct):
|
||||
return True
|
||||
return False
|
||||
|
||||
def appLinkExecute(self, obj, linkObj, index, linkElement):
|
||||
"""
|
||||
App::Link hook: called when a link to a BIM object is created.
|
||||
Used to setup shadow properties for lightweight instancing.
|
||||
"""
|
||||
# Shadow the given property so multiple links can have independent values without triggering
|
||||
# a deep copy of the BIM object geometry.
|
||||
if self.LinkOverrideProperties:
|
||||
ArchCommands.override_link_properties(linkObj, self.LinkOverrideProperties)
|
||||
|
||||
# Execute features in the SketchArch External Add-on, if present
|
||||
if hasattr(self, "executeSketchArchFeatures"):
|
||||
self.executeSketchArchFeatures(obj, linkObj, index, linkElement)
|
||||
|
||||
|
||||
class AreaCalculator:
|
||||
"""Helper class to compute vertical area, horizontal area, and perimeter length.
|
||||
|
||||
@@ -79,6 +79,11 @@ ANGLETOLERANCE = 0.67 # vectors with angles below this are considered going in
|
||||
class CurtainWall(ArchComponent.Component):
|
||||
"The curtain wall object"
|
||||
|
||||
# Configure App::Link shadowing, so that linked curtain walls can have independent Host
|
||||
# properties without triggering a deep copy of the geometry. See
|
||||
# ArchComponent.Component.appLinkExecute()
|
||||
LinkOverrideProperties = ["Host"]
|
||||
|
||||
def __init__(self, obj):
|
||||
|
||||
ArchComponent.Component.__init__(self, obj)
|
||||
|
||||
@@ -85,25 +85,6 @@ class _Equipment(ArchComponent.Component):
|
||||
obj.IfcType = "Furnishing Element"
|
||||
else:
|
||||
obj.IfcType = "Building Element Proxy"
|
||||
# Add features in the SketchArch External Add-on, if present
|
||||
self.addSketchArchFeatures(obj)
|
||||
|
||||
def addSketchArchFeatures(self, obj, linkObj=None, mode=None):
|
||||
"""
|
||||
To add features in the SketchArch External Add-on, if present (https://github.com/paullee0/FreeCAD_SketchArch)
|
||||
- import ArchSketchObject module, and
|
||||
- set properties that are common to ArchObjects (including Links) and ArchSketch
|
||||
to support the additional features
|
||||
|
||||
To install SketchArch External Add-on, see https://github.com/paullee0/FreeCAD_SketchArch#iv-install
|
||||
"""
|
||||
|
||||
try:
|
||||
import ArchSketchObject
|
||||
|
||||
ArchSketchObject.ArchSketch.setPropertiesLinkCommon(self, obj, linkObj, mode)
|
||||
except:
|
||||
pass
|
||||
|
||||
def setProperties(self, obj):
|
||||
|
||||
@@ -159,9 +140,6 @@ class _Equipment(ArchComponent.Component):
|
||||
ArchComponent.Component.onDocumentRestored(self, obj)
|
||||
self.setProperties(obj)
|
||||
|
||||
# Add features in the SketchArch External Add-on, if present
|
||||
self.addSketchArchFeatures(obj)
|
||||
|
||||
def loads(self, state):
|
||||
|
||||
self.Type = "Equipment"
|
||||
@@ -208,19 +186,6 @@ class _Equipment(ArchComponent.Component):
|
||||
except:
|
||||
pass
|
||||
|
||||
def appLinkExecute(self, obj, linkObj, index, linkElement):
|
||||
"""
|
||||
Default Link Execute method() -
|
||||
See https://forum.freecad.org/viewtopic.php?f=22&t=42184&start=10#p361124
|
||||
@realthunder added support to Links to run Linked Scripted Object's methods()
|
||||
"""
|
||||
|
||||
# Add features in the SketchArch External Add-on, if present
|
||||
self.addSketchArchFeatures(obj, linkObj)
|
||||
|
||||
# Execute features in the SketchArch External Add-on, if present
|
||||
self.executeSketchArchFeatures(obj, linkObj)
|
||||
|
||||
def computeAreas(self, obj):
|
||||
return
|
||||
|
||||
|
||||
@@ -70,6 +70,10 @@ else:
|
||||
class _Rebar(ArchComponent.Component):
|
||||
"A parametric reinforcement bar (rebar) object"
|
||||
|
||||
# Configure App::Link shadowing, so that linked rebars can have independent Host properties
|
||||
# without triggering a deep copy of the geometry. See ArchComponent.Component.appLinkExecute()
|
||||
LinkOverrideProperties = ["Host"]
|
||||
|
||||
def __init__(self, obj):
|
||||
|
||||
ArchComponent.Component.__init__(self, obj)
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import ArchWindow
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
|
||||
@@ -36,28 +35,7 @@ class ArchSketch(ArchSketchObject):
|
||||
pass
|
||||
|
||||
def setPropertiesLinkCommon(self, orgFp, linkFp=None, mode=None):
|
||||
if linkFp:
|
||||
fp = linkFp
|
||||
else:
|
||||
fp = orgFp
|
||||
prop = fp.PropertiesList
|
||||
if not isinstance(fp.getLinkedObject().Proxy, ArchWindow._Window):
|
||||
pass
|
||||
else:
|
||||
if "Hosts" not in prop:
|
||||
# inherited properties of Link are not in PropertiesList:
|
||||
old_hosts = getattr(fp, "Hosts", [])
|
||||
fp.addProperty(
|
||||
"App::PropertyLinkList",
|
||||
"Hosts",
|
||||
"Window",
|
||||
QT_TRANSLATE_NOOP("App::Property", "The objects that host this window"),
|
||||
locked=True,
|
||||
)
|
||||
fp.Hosts = old_hosts
|
||||
for host in old_hosts:
|
||||
host.touch()
|
||||
# Arch Window's code
|
||||
pass
|
||||
|
||||
|
||||
# from ArchSketchObjectExt import ArchSketch # Doesn't work
|
||||
|
||||
@@ -105,6 +105,10 @@ def recolorize(attr): # names is [docname,objname]
|
||||
class _Window(ArchComponent.Component):
|
||||
"The Window object"
|
||||
|
||||
# Configure App::Link shadowing, so that linked windows can have independent Hosts properties
|
||||
# without triggering a deep copy of the geometry. See ArchComponent.Component.appLinkExecute()
|
||||
LinkOverrideProperties = ["Hosts"]
|
||||
|
||||
def __init__(self, obj):
|
||||
|
||||
ArchComponent.Component.__init__(self, obj)
|
||||
@@ -113,26 +117,6 @@ class _Window(ArchComponent.Component):
|
||||
obj.IfcType = "Window"
|
||||
obj.MoveWithHost = True
|
||||
|
||||
# Add features in the SketchArch External Add-on
|
||||
self.addSketchArchFeatures(obj)
|
||||
|
||||
def addSketchArchFeatures(self, obj, linkObj=None, mode=None):
|
||||
"""
|
||||
To add features in the SketchArch External Add-on (https://github.com/paullee0/FreeCAD_SketchArch)
|
||||
- import ArchSketchObject module, and
|
||||
- set properties that are common to ArchObjects (including Links) and ArchSketch
|
||||
to support the additional features
|
||||
|
||||
To install SketchArch External Add-on, see https://github.com/paullee0/FreeCAD_SketchArch#iv-install
|
||||
"""
|
||||
|
||||
try:
|
||||
import ArchSketchObject
|
||||
|
||||
ArchSketchObject.ArchSketch.setPropertiesLinkCommon(self, obj, linkObj, mode)
|
||||
except:
|
||||
pass
|
||||
|
||||
def setProperties(self, obj, mode=None):
|
||||
|
||||
lp = obj.PropertiesList
|
||||
@@ -313,9 +297,6 @@ class _Window(ArchComponent.Component):
|
||||
ArchComponent.Component.onDocumentRestored(self, obj)
|
||||
self.setProperties(obj, mode="ODR")
|
||||
|
||||
# Add features in the SketchArch External Add-on
|
||||
self.addSketchArchFeatures(obj, mode="ODR")
|
||||
|
||||
# During the v1.1 dev cycle an experiment with a new SillHeight handling was
|
||||
# undertaken. This did not work out as intended and was therefore reverted.
|
||||
# Related PRs:
|
||||
@@ -752,19 +733,6 @@ class _Window(ArchComponent.Component):
|
||||
except:
|
||||
pass
|
||||
|
||||
def appLinkExecute(self, obj, linkObj, index, linkElement):
|
||||
"""
|
||||
Default Link Execute method() -
|
||||
See https://forum.freecad.org/viewtopic.php?f=22&t=42184&start=10#p361124
|
||||
@realthunder added support to Links to run Linked Scripted Object's methods()
|
||||
"""
|
||||
|
||||
# Add features in the SketchArch External Add-on
|
||||
self.addSketchArchFeatures(obj, linkObj)
|
||||
|
||||
# Execute features in the SketchArch External Add-on
|
||||
self.executeSketchArchFeatures(obj, linkObj)
|
||||
|
||||
def getSubFace(self):
|
||||
"returns a subface for creation of subvolume for cutting in a base object"
|
||||
# creation of subface from HoleWire (getSubWire)
|
||||
|
||||
@@ -146,6 +146,7 @@ SET(bimcommands_SRCS
|
||||
bimcommands/BimLayers.py
|
||||
bimcommands/BimLeader.py
|
||||
bimcommands/BimLibrary.py
|
||||
bimcommands/BimLink.py
|
||||
bimcommands/BimMaterial.py
|
||||
bimcommands/BimMoveView.py
|
||||
bimcommands/BimNudge.py
|
||||
|
||||
+14
-1
@@ -150,7 +150,7 @@ class BIMWorkbench(Workbench):
|
||||
"Draft_Rotate",
|
||||
"Draft_Scale",
|
||||
"Draft_Mirror",
|
||||
"BIM_Clone",
|
||||
"BIM_CloneTools",
|
||||
"BIM_Copy",
|
||||
"BIM_SimpleCopy",
|
||||
"BIM_Compound",
|
||||
@@ -358,6 +358,18 @@ class BIMWorkbench(Workbench):
|
||||
def IsActive(self):
|
||||
return hasattr(FreeCADGui.getMainWindow().getActiveWindow(), "getSceneGraph")
|
||||
|
||||
class BIM_CloneTools:
|
||||
def GetCommands(self):
|
||||
return ("BIM_Clone", "BIM_LinkMake")
|
||||
|
||||
def GetResources(self):
|
||||
label = QT_TRANSLATE_NOOP("BIM_CloneTools", "Cloning Tools")
|
||||
tooltip = label
|
||||
return {"MenuText": label, "ToolTip": tooltip, "Icon": "BIM_Clone"}
|
||||
|
||||
def IsActive(self):
|
||||
return hasattr(FreeCADGui.getMainWindow().getActiveWindow(), "getSceneGraph")
|
||||
|
||||
# create generic tools command
|
||||
class BIM_GenericTools:
|
||||
def __init__(self, tools):
|
||||
@@ -401,6 +413,7 @@ class BIMWorkbench(Workbench):
|
||||
FreeCADGui.addCommand("BIM_ReportTools", BIM_ReportTools())
|
||||
FreeCADGui.addCommand("BIM_GenericTools", BIM_GenericTools(self.generictools))
|
||||
FreeCADGui.addCommand("BIM_Create2DViews", BIM_Create2DViews(self.create_2dviews))
|
||||
FreeCADGui.addCommand("BIM_CloneTools", BIM_CloneTools())
|
||||
|
||||
# workaround for issue #26539: create draftingtools list without grouped commands
|
||||
# https://github.com/FreeCAD/FreeCAD/issues/26539
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
|
||||
QT_TRANSLATE_NOOP = FreeCAD.Qt.QT_TRANSLATE_NOOP
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
|
||||
class BIM_LinkMake:
|
||||
def GetResources(self):
|
||||
return {
|
||||
"Pixmap": "Link",
|
||||
"MenuText": QT_TRANSLATE_NOOP("BIM_LinkMake", "Make Link"),
|
||||
"ToolTip": QT_TRANSLATE_NOOP(
|
||||
"BIM_LinkMake",
|
||||
"Creates a Link to the selected object and immediately enables moving it",
|
||||
),
|
||||
"Accel": "L, K",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
return not FreeCAD.ActiveDocument is None
|
||||
|
||||
def Activated(self):
|
||||
from draftutils.todo import ToDo
|
||||
|
||||
sel = FreeCADGui.Selection.getSelection()
|
||||
if not sel:
|
||||
FreeCAD.Console.PrintError(translate("BIM", "Select an object to link") + "\n")
|
||||
return
|
||||
|
||||
doc = FreeCAD.ActiveDocument
|
||||
doc.openTransaction("Create BIM Link")
|
||||
|
||||
new_links = []
|
||||
|
||||
try:
|
||||
for obj in sel:
|
||||
# Create the native Link
|
||||
lnk = doc.addObject("App::Link", obj.Label + "_Link")
|
||||
lnk.LinkedObject = obj
|
||||
|
||||
# We do not manipulate LinkCopyOnChange here.
|
||||
# The 'appLinkExecute' hook in the object's proxy will handle the injection of
|
||||
# shadow properties (like Hosts) to ensure the link remains lightweight.
|
||||
|
||||
new_links.append(lnk)
|
||||
|
||||
doc.commitTransaction()
|
||||
doc.recompute()
|
||||
|
||||
# Enter Move mode
|
||||
if new_links:
|
||||
FreeCADGui.Selection.clearSelection()
|
||||
for lnk in new_links:
|
||||
FreeCADGui.Selection.addSelection(lnk)
|
||||
|
||||
# Defer the Move command to ensure the document is stable
|
||||
ToDo.delay(FreeCADGui.runCommand, "Draft_Move")
|
||||
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintError(f"BIM Link creation failed: {e}\n")
|
||||
doc.abortTransaction()
|
||||
|
||||
|
||||
FreeCADGui.addCommand("BIM_LinkMake", BIM_LinkMake())
|
||||
@@ -718,3 +718,129 @@ class TestArchComponent(TestArchBase.TestArchBase):
|
||||
self.assertTrue(
|
||||
comp.Shape.isNull(), "Component retained a stale shape after its Base was removed."
|
||||
)
|
||||
|
||||
def test_add_window_link_standard(self):
|
||||
"""Test adding a Window Link to a Wall using standard lifecycle (recompute before add).
|
||||
This verifies the 'appLinkExecute' hook mechanism.
|
||||
"""
|
||||
operation = "Arch Link addition (Standard Lifecycle)"
|
||||
self.printTestMessage(operation)
|
||||
|
||||
# Create the host wall
|
||||
line = Draft.makeLine(App.Vector(0, 0, 0), App.Vector(4000, 0, 0))
|
||||
wall = Arch.makeWall(line, width=200, height=3000, align="Center")
|
||||
self.document.recompute()
|
||||
initial_volume = wall.Shape.Volume
|
||||
|
||||
# Create a prototype window
|
||||
rect = Draft.makeRectangle(length=1000, height=1500)
|
||||
rect.Placement.Rotation = App.Rotation(App.Vector(1, 0, 0), 90)
|
||||
rect.Placement.Base = App.Vector(0, 0, 0)
|
||||
self.document.recompute()
|
||||
|
||||
win_proto = Arch.makeWindow(baseobj=rect, name="Window_Prototype")
|
||||
win_proto.Width = 1000
|
||||
win_proto.Height = 1500
|
||||
win_proto.Frame = 100
|
||||
self.document.recompute()
|
||||
|
||||
# Create a link to the prototype window and recompute
|
||||
# This is the standard lifecycle
|
||||
link1 = self.document.addObject("App::Link", "Link_Standard")
|
||||
link1.LinkedObject = win_proto
|
||||
link1.Placement.Base = App.Vector(1000, 0, 500)
|
||||
self.document.recompute() # Trigger appLinkExecute -> shadow_link_properties
|
||||
|
||||
# Add the window link to the wall
|
||||
Arch.addComponents(link1, wall)
|
||||
self.document.recompute()
|
||||
|
||||
# Assert
|
||||
self.assertIn(wall, link1.Hosts, "Link should host the wall")
|
||||
self.assertNotIn(wall, win_proto.Hosts, "Prototype should not host the wall")
|
||||
self.assertLess(wall.Shape.Volume, initial_volume, "Wall volume should decrease (hole cut)")
|
||||
|
||||
def test_add_window_link_immediate(self):
|
||||
"""Test adding a Window Link to a Wall immediately without recomputing.
|
||||
This verifies the 'ensure_link_overrides' safeguard mechanism.
|
||||
"""
|
||||
operation = "Arch Link addition (Immediate Lifecycle)"
|
||||
self.printTestMessage(operation)
|
||||
|
||||
# Create the host wall
|
||||
line = Draft.makeLine(App.Vector(0, 0, 0), App.Vector(4000, 0, 0))
|
||||
wall = Arch.makeWall(line, width=200, height=3000, align="Center")
|
||||
self.document.recompute()
|
||||
initial_volume = wall.Shape.Volume
|
||||
|
||||
# Create a prototype window
|
||||
rect = Draft.makeRectangle(length=1000, height=1500)
|
||||
rect.Placement.Rotation = App.Rotation(App.Vector(1, 0, 0), 90)
|
||||
rect.Placement.Base = App.Vector(0, 0, 0)
|
||||
self.document.recompute()
|
||||
|
||||
win_proto = Arch.makeWindow(baseobj=rect, name="Window_Prototype")
|
||||
win_proto.Width = 1000
|
||||
win_proto.Height = 1500
|
||||
win_proto.Frame = 100
|
||||
self.document.recompute()
|
||||
|
||||
# Create a link to the prototype window without recomputing
|
||||
link2 = self.document.addObject("App::Link", "Link_Immediate")
|
||||
link2.LinkedObject = win_proto
|
||||
link2.Placement.Base = App.Vector(3000, 0, 500)
|
||||
|
||||
# Add the window link to the wall immediately
|
||||
# This triggers ensure_link_overrides -> shadow_link_properties
|
||||
Arch.addComponents(link2, wall)
|
||||
self.document.recompute()
|
||||
|
||||
# Assert
|
||||
self.assertIn(wall, link2.Hosts, "Link should host the wall")
|
||||
self.assertNotIn(wall, win_proto.Hosts, "Prototype should NOT host the wall")
|
||||
self.assertLess(wall.Shape.Volume, initial_volume, "Wall volume should decrease (hole cut)")
|
||||
|
||||
def test_remove_window_link(self):
|
||||
"""Test removing a Window Link from a Wall."""
|
||||
operation = "Arch Link removal"
|
||||
self.printTestMessage(operation)
|
||||
|
||||
# Create the host wall
|
||||
line = Draft.makeLine(App.Vector(0, 0, 0), App.Vector(4000, 0, 0))
|
||||
wall = Arch.makeWall(line, width=200, height=3000, align="Center")
|
||||
self.document.recompute()
|
||||
initial_volume = wall.Shape.Volume
|
||||
|
||||
# Create a prototype window
|
||||
rect = Draft.makeRectangle(length=1000, height=1500)
|
||||
rect.Placement.Rotation = App.Rotation(App.Vector(1, 0, 0), 90)
|
||||
rect.Placement.Base = App.Vector(0, 0, 0)
|
||||
self.document.recompute()
|
||||
|
||||
win_proto = Arch.makeWindow(baseobj=rect)
|
||||
win_proto.Width = 1000
|
||||
win_proto.Height = 1500
|
||||
win_proto.Frame = 100
|
||||
self.document.recompute()
|
||||
|
||||
# Create a link to the prototype window
|
||||
link = self.document.addObject("App::Link", "Link_Remove")
|
||||
link.LinkedObject = win_proto
|
||||
link.Placement.Base = App.Vector(2000, 0, 500)
|
||||
self.document.recompute()
|
||||
|
||||
# Add the window link to the wall, ensure it cuts the hole
|
||||
Arch.addComponents(link, wall)
|
||||
self.document.recompute()
|
||||
cut_volume = wall.Shape.Volume
|
||||
self.assertLess(cut_volume, initial_volume, "Setup failed: Wall not cut")
|
||||
|
||||
# Remove the window link from the wall
|
||||
Arch.removeComponents([link])
|
||||
self.document.recompute()
|
||||
|
||||
# Assert
|
||||
self.assertNotIn(wall, link.Hosts, "Link should no longer host the wall")
|
||||
self.assertAlmostEqual(
|
||||
wall.Shape.Volume, initial_volume, 3, "Wall volume should be restored"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user