527 lines
20 KiB
Python
527 lines
20 KiB
Python
# SPDX-License-Identifier: LGPL-2.1-or-later
|
|
|
|
# ***************************************************************************
|
|
# * *
|
|
# * Copyright (c) 2024 Yorik van Havre <yorik@uncreated.net> *
|
|
# * *
|
|
# * This file is part of FreeCAD. *
|
|
# * *
|
|
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
|
# * under the terms of the GNU Lesser General Public License as *
|
|
# * published by the Free Software Foundation, either version 2.1 of the *
|
|
# * License, or (at your option) any later version. *
|
|
# * *
|
|
# * FreeCAD is distributed in the hope that it will be useful, but *
|
|
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
|
# * Lesser General Public License for more details. *
|
|
# * *
|
|
# * You should have received a copy of the GNU Lesser General Public *
|
|
# * License along with FreeCAD. If not, see *
|
|
# * <https://www.gnu.org/licenses/>. *
|
|
# * *
|
|
# ***************************************************************************
|
|
|
|
"""This contains nativeifc status widgets and functionality"""
|
|
|
|
import csv
|
|
import os
|
|
|
|
import FreeCAD
|
|
import FreeCADGui
|
|
|
|
translate = FreeCAD.Qt.translate
|
|
params = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/NativeIFC")
|
|
text_on = translate("BIM", "Strict IFC mode is ON (all objects are IFC)")
|
|
text_off = translate("BIM", "Strict IFC mode is OFF (IFC and non-IFC objects allowed)")
|
|
|
|
|
|
def set_status_widget(statuswidget):
|
|
"""Adds the needed controls to the status bar"""
|
|
|
|
from PySide import QtGui # lazy import
|
|
import Arch_rc
|
|
|
|
# lock button
|
|
lock_button = QtGui.QAction()
|
|
icon = QtGui.QIcon(":/icons/IFC.svg")
|
|
lock_button.setIcon(icon)
|
|
lock_button.setCheckable(True)
|
|
doc = FreeCAD.ActiveDocument
|
|
statuswidget.addAction(lock_button)
|
|
statuswidget.lock_button = lock_button
|
|
if doc and "IfcFilePath" in doc.PropertiesList:
|
|
checked = True
|
|
else:
|
|
checked = False
|
|
# set the button first, without converting the document
|
|
lock_button.setChecked(checked)
|
|
on_toggle_lock(checked, noconvert=True)
|
|
lock_button.triggered.connect(on_toggle_lock)
|
|
set_properties_editor(statuswidget)
|
|
|
|
|
|
def set_properties_editor(statuswidget):
|
|
"""Adds additional buttons to the properties editor"""
|
|
|
|
if hasattr(statuswidget, "propertybuttons"):
|
|
statuswidget.propertybuttons.show()
|
|
else:
|
|
from PySide import QtCore, QtGui # lazy loading
|
|
|
|
mw = FreeCADGui.getMainWindow()
|
|
editor = mw.findChild(QtGui.QTabWidget,"propertyTab")
|
|
if editor:
|
|
pTabCornerWidget = QtGui.QWidget()
|
|
pButton1 = QtGui.QToolButton(pTabCornerWidget)
|
|
pButton1.setText("")
|
|
pButton1.setToolTip(translate("BIM","Add IFC property..."))
|
|
pButton1.setIcon(QtGui.QIcon(":/icons/IFC.svg"))
|
|
pButton1.clicked.connect(on_add_property)
|
|
pButton2 = QtGui.QToolButton(pTabCornerWidget)
|
|
pButton2.setText("")
|
|
pButton2.setToolTip(translate("BIM","Add standard IFC Property Set..."))
|
|
pButton2.setIcon(QtGui.QIcon(":/icons/BIM_IfcProperties.svg"))
|
|
pButton2.clicked.connect(on_add_pset)
|
|
pHLayout = QtGui.QHBoxLayout(pTabCornerWidget)
|
|
pHLayout.addWidget(pButton1)
|
|
pHLayout.addWidget(pButton2)
|
|
pHLayout.setSpacing(2)
|
|
pHLayout.setContentsMargins(2, 2, 0, 0)
|
|
pHLayout.insertStretch(0)
|
|
editor.setCornerWidget(pTabCornerWidget, QtCore.Qt.BottomRightCorner)
|
|
statuswidget.propertybuttons = pTabCornerWidget
|
|
QtCore.QTimer.singleShot(0,pTabCornerWidget.show)
|
|
|
|
|
|
def on_add_property():
|
|
"""When the 'add property' button is clicked"""
|
|
|
|
sel = FreeCADGui.Selection.getSelection()
|
|
if not sel:
|
|
return
|
|
from PySide import QtGui # lazy loading
|
|
from . import ifc_psets
|
|
obj = sel[0]
|
|
psets = list(set([obj.getGroupOfProperty(p) for p in obj.PropertiesList]))
|
|
psets = [p for p in psets if p]
|
|
psets = [p for p in psets if p not in ["Base", "IFC", "Geometry"]]
|
|
mw = FreeCADGui.getMainWindow()
|
|
editor = mw.findChild(QtGui.QTabWidget,"propertyTab")
|
|
pset = None
|
|
if editor:
|
|
wid = editor.currentWidget()
|
|
if wid and wid.objectName() == "propertyEditorData":
|
|
if wid.currentIndex().parent():
|
|
pset = wid.currentIndex().parent().data()
|
|
else:
|
|
pset = wid.currentIndex().data()
|
|
form = FreeCADGui.PySideUic.loadUi(":/ui/dialogAddProperty.ui")
|
|
# center the dialog over FreeCAD window
|
|
form.move(mw.frameGeometry().topLeft() + mw.rect().center() - form.rect().center())
|
|
form.field_pset.clear()
|
|
form.field_pset.addItems(psets)
|
|
if pset and (pset in psets):
|
|
form.field_pset.setCurrentIndex(psets.index(pset))
|
|
# TODO check for name duplicates while typing
|
|
# execute
|
|
result = form.exec_()
|
|
if not result:
|
|
return
|
|
pname = form.field_name.text()
|
|
if pname in obj.PropertiesList:
|
|
print("DEBUG: property already exists",pname)
|
|
return
|
|
pset = form.field_pset.currentText()
|
|
if not pset:
|
|
# TODO disable the OK button if empty
|
|
t = translate("BIM","No Property set provided")
|
|
FreeCAD.Console.PrintError(t+"\n")
|
|
ptype = form.field_type.currentIndex()
|
|
ptype = ["IfcLabel", "IfcBoolean",
|
|
"IfcInteger", "IfcReal",
|
|
"IfcLengthMeasure", "IfcAreaMeasure"][ptype]
|
|
fctype = ifc_psets.get_freecad_type(ptype)
|
|
FreeCAD.ActiveDocument.openTransaction(translate("BIM","add property"))
|
|
for obj in sel:
|
|
obj.addProperty(fctype, pname, pset, ptype+":"+pname)
|
|
ifc_psets.edit_pset(obj, pname, force=True)
|
|
FreeCAD.ActiveDocument.commitTransaction()
|
|
|
|
|
|
def on_add_pset():
|
|
"""When the 'add pset' button is pressed"""
|
|
|
|
def read_csv(csvfile):
|
|
result = {}
|
|
if os.path.exists(csvfile):
|
|
with open(csvfile, "r") as f:
|
|
reader = csv.reader(f, delimiter=";")
|
|
for row in reader:
|
|
result[row[0]] = row[1:]
|
|
return result
|
|
|
|
def get_fcprop(ifcprop):
|
|
if ifcprop == "IfcLengthMeasure":
|
|
return "App::PropertyDistance"
|
|
elif ifcprop == "IfcPositiveLengthMeasure":
|
|
return "App::PropertyLength"
|
|
elif ifcprop in ["IfcBoolean", "IfcLogical"]:
|
|
return "App::PropertyBool"
|
|
elif ifcprop == "IfcInteger":
|
|
return "App::PropertyInteger"
|
|
elif ifcprop == "IfcReal":
|
|
return "App::PropertyFloat"
|
|
elif ifcprop == "IfcAreaMeasure":
|
|
return "App::PropertyArea"
|
|
return "App::PropertyString"
|
|
|
|
sel = FreeCADGui.Selection.getSelection()
|
|
if not sel:
|
|
return
|
|
from . import ifc_psets
|
|
obj = sel[0]
|
|
mw = FreeCADGui.getMainWindow()
|
|
# read standard psets
|
|
psetpath = os.path.join(
|
|
FreeCAD.getResourceDir(), "Mod", "BIM", "Presets", "pset_definitions.csv"
|
|
)
|
|
custompath = os.path.join(FreeCAD.getUserAppDataDir(), "BIM", "CustomPsets.csv")
|
|
psetdefs = read_csv(psetpath)
|
|
psetdefs.update(read_csv(custompath))
|
|
psetkeys = list(psetdefs.keys())
|
|
psetkeys.sort()
|
|
form = FreeCADGui.PySideUic.loadUi(":/ui/dialogAddPSet.ui")
|
|
# center the dialog over FreeCAD window
|
|
form.move(mw.frameGeometry().topLeft() + mw.rect().center() - form.rect().center())
|
|
form.field_pset.clear()
|
|
form.field_pset.addItems(psetkeys)
|
|
# execute
|
|
result = form.exec_()
|
|
if not result:
|
|
return
|
|
pset = form.field_pset.currentText()
|
|
existing_psets = list(set([obj.getGroupOfProperty(p) for p in obj.PropertiesList]))
|
|
if pset in existing_psets:
|
|
t = translate("BIM","Property set already exists")
|
|
FreeCAD.Console.PrintError(t+": "+pset+"\n")
|
|
return
|
|
props = [psetdefs[pset][i:i+2] for i in range(0, len(psetdefs[pset]), 2)]
|
|
props = [[p[0], p[1]] for p in props]
|
|
FreeCAD.ActiveDocument.openTransaction(translate("BIM","add property set"))
|
|
for obj in sel:
|
|
existing_psets = list(set([obj.getGroupOfProperty(p) for p in obj.PropertiesList]))
|
|
if pset not in existing_psets:
|
|
ifc_psets.add_pset(obj, pset)
|
|
for prop in props:
|
|
if prop[0] in obj.PropertiesList:
|
|
t = translate("BIM","Property already exists")
|
|
FreeCAD.Console.PrintWarning(t+": "+obj.Label+","+prop[0]+"\n")
|
|
else:
|
|
obj.addProperty(get_fcprop(prop[1]),prop[0],pset,prop[1]+":"+prop[0])
|
|
FreeCAD.ActiveDocument.commitTransaction()
|
|
|
|
|
|
def on_toggle_lock(checked=None, noconvert=False, setchecked=False):
|
|
"""When the toolbar button is pressed"""
|
|
|
|
if checked is None:
|
|
checked = get_lock_status()
|
|
set_menu(checked)
|
|
set_button(checked, setchecked)
|
|
if not noconvert:
|
|
if checked:
|
|
lock_document()
|
|
else:
|
|
unlock_document()
|
|
|
|
|
|
def on_open():
|
|
"""What happens when opening an existing document"""
|
|
|
|
pass # TODO implement
|
|
|
|
|
|
def on_activate():
|
|
"""What happens when activating a document"""
|
|
|
|
from PySide import QtGui # lazy import
|
|
|
|
# always reset the menu to normal first
|
|
set_menu(False)
|
|
if FreeCADGui.activeWorkbench().name() != "BIMWorkbench":
|
|
return
|
|
doc = FreeCAD.ActiveDocument
|
|
if doc and "IfcFilePath" in doc.PropertiesList:
|
|
checked = True
|
|
else:
|
|
checked = False
|
|
mw = FreeCADGui.getMainWindow()
|
|
statuswidget = mw.findChild(QtGui.QToolBar, "BIMStatusWidget")
|
|
if hasattr(statuswidget, "lock_button"):
|
|
statuswidget.lock_button.setChecked(checked)
|
|
on_toggle_lock(checked, noconvert=True)
|
|
|
|
|
|
def on_new():
|
|
"""What happens when creating a new document"""
|
|
|
|
pass # TODO implement
|
|
|
|
|
|
def set_menu(locked=False):
|
|
"""Sets the File menu items"""
|
|
|
|
from PySide import QtGui # lazy loading
|
|
|
|
# switch Std_Save and IFC_Save
|
|
mw = FreeCADGui.getMainWindow()
|
|
wb = FreeCADGui.activeWorkbench()
|
|
save_action = mw.findChild(QtGui.QAction, "Std_Save")
|
|
if locked and "IFC_Save" in FreeCADGui.listCommands():
|
|
if not hasattr(FreeCADGui,"IFC_WBManipulator"):
|
|
FreeCADGui.IFC_WBManipulator = IFC_WBManipulator()
|
|
# we need to void the shortcut otherwise it keeps active
|
|
# even if the command is not shown
|
|
FreeCADGui.IFC_saveshortcut = save_action.shortcut()
|
|
save_action.setShortcut("")
|
|
FreeCADGui.addWorkbenchManipulator(FreeCADGui.IFC_WBManipulator)
|
|
wb.reloadActive()
|
|
else:
|
|
if hasattr(FreeCADGui,"IFC_saveshortcut"):
|
|
save_action.setShortcut(FreeCADGui.IFC_saveshortcut)
|
|
del FreeCADGui.IFC_saveshortcut
|
|
if hasattr(FreeCADGui,"IFC_WBManipulator"):
|
|
FreeCADGui.removeWorkbenchManipulator(FreeCADGui.IFC_WBManipulator)
|
|
del FreeCADGui.IFC_WBManipulator
|
|
wb.reloadActive()
|
|
|
|
|
|
def set_button(checked=False, setchecked=False):
|
|
"""Sets the lock button"""
|
|
|
|
from PySide import QtGui # lazy loading
|
|
|
|
mw = FreeCADGui.getMainWindow()
|
|
statuswidget = mw.findChild(QtGui.QToolBar, "BIMStatusWidget")
|
|
if hasattr(statuswidget, "lock_button"):
|
|
lock_button = statuswidget.lock_button
|
|
if checked:
|
|
lock_button.setToolTip(text_on)
|
|
icon = QtGui.QIcon(":/icons/IFC.svg")
|
|
lock_button.setIcon(icon)
|
|
if setchecked:
|
|
lock_button.setChecked(True)
|
|
else:
|
|
lock_button.setToolTip(text_off)
|
|
image = QtGui.QImage(":/icons/IFC.svg")
|
|
grayscale = image.convertToFormat(QtGui.QImage.Format_Grayscale8)
|
|
grayscale = grayscale.convertToFormat(image.format())
|
|
grayscale.setAlphaChannel(image)
|
|
icon = QtGui.QIcon(QtGui.QPixmap.fromImage(grayscale))
|
|
lock_button.setIcon(icon)
|
|
if setchecked:
|
|
lock_button.setChecked(False)
|
|
|
|
|
|
def unlock_document():
|
|
"""Unlocks the active document"""
|
|
|
|
from . import ifc_tools # lazy loading
|
|
|
|
doc = FreeCAD.ActiveDocument
|
|
if not doc:
|
|
return
|
|
if "IfcFilePath" in doc.PropertiesList:
|
|
# this is a locked document
|
|
doc.openTransaction("Unlock document")
|
|
children = [o for o in doc.Objects if not o.InList]
|
|
project = None
|
|
if children:
|
|
project = ifc_tools.create_document_object(
|
|
doc, filename=doc.IfcFilePath, silent=True
|
|
)
|
|
project.Group = children
|
|
props = ["IfcFilePath", "Modified", "Proxy", "Schema"]
|
|
props += [p for p in doc.PropertiesList if doc.getGroupOfProperty(p) == "IFC"]
|
|
for prop in props:
|
|
doc.setPropertyStatus(prop, "-LockDynamic")
|
|
doc.removeProperty(prop)
|
|
if project:
|
|
project.Modified = True
|
|
doc.commitTransaction()
|
|
doc.recompute()
|
|
|
|
|
|
def lock_document():
|
|
"""Locks the active document"""
|
|
|
|
from . import ifc_tools # lazy loading
|
|
from importers import exportIFC
|
|
from . import ifc_geometry
|
|
from . import ifc_export
|
|
from PySide import QtCore
|
|
|
|
doc = FreeCAD.ActiveDocument
|
|
if not doc:
|
|
return
|
|
products = []
|
|
spatial = []
|
|
ifcfile = None
|
|
if "IfcFilePath" not in doc.PropertiesList:
|
|
# this is not a locked document
|
|
projects = [o for o in doc.Objects if getattr(o, "Class", None) == "IfcProject"]
|
|
if len(projects) == 1:
|
|
# 1 there is a project already
|
|
project = projects[0]
|
|
children = project.OutListRecursive
|
|
rest = [o for o in doc.Objects if o not in children and o != project]
|
|
doc.openTransaction("Lock document")
|
|
ifc_tools.convert_document(
|
|
doc, filename=project.IfcFilePath, strategy=3, silent=True
|
|
)
|
|
ifcfile = doc.Proxy.ifcfile
|
|
if rest:
|
|
# 1b some objects are outside
|
|
objs = find_toplevel(rest)
|
|
prefs, context = ifc_export.get_export_preferences(ifcfile)
|
|
products = exportIFC.export(objs, ifcfile, preferences=prefs)
|
|
for product in products.values():
|
|
if not getattr(product, "ContainedInStructure", None):
|
|
if not getattr(product, "FillsVoids", None):
|
|
if not getattr(product, "VoidsElements", None):
|
|
if not getattr(product, "Decomposes", None):
|
|
new = ifc_tools.create_object(product, doc, ifcfile)
|
|
children = ifc_tools.create_children(
|
|
new, ifcfile, recursive=True
|
|
)
|
|
for o in [new] + children:
|
|
ifc_geometry.add_geom_properties(o)
|
|
for n in [o.Name for o in rest]:
|
|
doc.removeObject(n)
|
|
else:
|
|
# 1a all objects are already inside a project
|
|
pass
|
|
doc.removeObject(project.Name)
|
|
doc.Modified = True
|
|
# all objects have been deleted, we need to show at least something
|
|
if not doc.Objects:
|
|
ifc_tools.create_children(doc, ifcfile, recursive=True)
|
|
doc.commitTransaction()
|
|
doc.recompute()
|
|
elif len(projects) > 1:
|
|
# 2 there is more than one project
|
|
FreeCAD.Console.PrintError(
|
|
"Unable to lock this document because it contains several IFC documents\n"
|
|
)
|
|
QtCore.QTimer.singleShot(100, on_toggle_lock)
|
|
elif doc.Objects:
|
|
# 3 there is no project but objects
|
|
doc.openTransaction("Lock document")
|
|
objs = find_toplevel(doc.Objects)
|
|
deletelist = [o.Name for o in doc.Objects]
|
|
#ifc_export.export_and_convert(objs, doc)
|
|
ifc_export.direct_conversion(objs, doc)
|
|
for n in deletelist:
|
|
if doc.getObject(n):
|
|
doc.removeObject(n)
|
|
doc.IfcFilePath = ""
|
|
doc.Modified = True
|
|
doc.commitTransaction()
|
|
doc.recompute()
|
|
else:
|
|
# 4 this is an empty document
|
|
doc.openTransaction("Create IFC document")
|
|
ifc_tools.convert_document(doc)
|
|
doc.commitTransaction()
|
|
doc.recompute()
|
|
# reveal file contents if needed
|
|
if "IfcFilePath" in doc.PropertiesList:
|
|
create = True
|
|
for o in doc.Objects:
|
|
# scan for site or building
|
|
if getattr(o, "IfcClass", "") in ("IfcSite", "IfcBuilding"):
|
|
create = False
|
|
break
|
|
if create:
|
|
if not ifcfile:
|
|
ifcfile = doc.Proxy.ifcfile
|
|
ifc_tools.create_children(doc, ifcfile, recursive=False)
|
|
|
|
|
|
def find_toplevel(objs):
|
|
"""Finds the top-level objects from the list"""
|
|
|
|
# filter out any object that depend on another from the list
|
|
nobjs = []
|
|
for obj in objs:
|
|
for parent in obj.InListRecursive:
|
|
if parent in objs:
|
|
# exception: The object is hosting another
|
|
if hasattr(parent,"Host") and parent.Host == obj:
|
|
nobjs.append(obj)
|
|
elif hasattr(parent,"Hosts") and obj in parent.Hosts:
|
|
nobjs.append(obj)
|
|
break
|
|
else:
|
|
nobjs.append(obj)
|
|
# filter out non-convertible objects
|
|
objs = filter_out(nobjs)
|
|
return objs
|
|
|
|
|
|
def filter_out(objs):
|
|
"""Filter out objects that should not be converted to IFC"""
|
|
|
|
import Draft
|
|
|
|
nobjs = []
|
|
for obj in objs:
|
|
if obj.isDerivedFrom("Part::Feature"):
|
|
nobjs.append(obj)
|
|
elif obj.isDerivedFrom("Mesh::Feature"):
|
|
nobjs.append(obj)
|
|
elif Draft.is_group(obj):
|
|
if filter_out(obj.Group):
|
|
# only append groups that contain exportable objects
|
|
nobjs.append(obj)
|
|
else:
|
|
print("DEBUG: Filtering out",obj.Label)
|
|
elif obj.isDerivedFrom("App::Feature"):
|
|
if Draft.get_type(obj) in ("Dimension","LinearDimension","Layer","Text","DraftText"):
|
|
nobjs.append(obj)
|
|
else:
|
|
print("DEBUG: Filtering out",obj.Label)
|
|
else:
|
|
print("DEBUG: Filtering out",obj.Label)
|
|
return nobjs
|
|
|
|
|
|
def get_lock_status():
|
|
"""Returns the status of the IFC lock button"""
|
|
|
|
if not FreeCAD.GuiUp:
|
|
return PARAMS.GetBool("SingleDoc")
|
|
from PySide import QtGui
|
|
mw = FreeCADGui.getMainWindow()
|
|
statuswidget = mw.findChild(QtGui.QToolBar, "BIMStatusWidget")
|
|
if hasattr(statuswidget, "lock_button"):
|
|
return statuswidget.lock_button.isChecked()
|
|
else:
|
|
return False
|
|
|
|
|
|
# add entry to File menu
|
|
# https://github.com/FreeCAD/FreeCAD/pull/10933
|
|
class IFC_WBManipulator:
|
|
def modifyMenuBar(self):
|
|
return [{"remove":"Std_Save"},
|
|
{"remove":"Std_SaveAs"},
|
|
{"insert":"IFC_Save", "menuItem":"Std_SaveCopy"},
|
|
{"insert":"IFC_SaveAs", "menuItem":"Std_SaveCopy"},
|
|
]
|
|
def modifyToolBars(self):
|
|
return [{"remove" : "Std_Save"},
|
|
{"append" : "IFC_Save", "toolBar" : "File"},
|
|
]
|