#*************************************************************************** #* Copyright (c) 2018 Yorik van Havre * #* * #* This program is free software; you can redistribute it and/or modify * #* it under the terms of the GNU Lesser General Public License (LGPL) * #* as published by the Free Software Foundation; either version 2 of * #* the License, or (at your option) any later version. * #* for detail see the LICENCE text file. * #* * #* This program 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 Library General Public License for more details. * #* * #* You should have received a copy of the GNU Library General Public * #* License along with this program; if not, write to the Free Software * #* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * #* USA * #* * #*************************************************************************** __title__ = "FreeCAD Arch External Reference" __author__ = "Yorik van Havre" __url__ = "https://www.freecad.org" import FreeCAD import os import zipfile import re from draftutils import params if FreeCAD.GuiUp: import FreeCADGui from PySide import QtCore, QtGui from draftutils.translate import translate from PySide.QtCore import QT_TRANSLATE_NOOP else: # \cond def translate(ctxt,txt): return txt def QT_TRANSLATE_NOOP(ctxt,txt): return txt # \endcond ## @package ArchReference # \ingroup ARCH # \brief The Reference object and tools # # This module provides tools to build Reference objects. # References can take a shape from a Part-based object in # another file. class ArchReference: """The Arch Reference object""" def __init__(self, obj): obj.Proxy = self ArchReference.setProperties(self, obj) self.Type = "Reference" self.reload = True def setProperties(self, obj): pl = obj.PropertiesList if not "File" in pl: t = QT_TRANSLATE_NOOP("App::Property","The base file this component is built upon") obj.addProperty("App::PropertyFile","File","Reference",t) if not "Part" in pl: t = QT_TRANSLATE_NOOP("App::Property","The part to use from the base file") obj.addProperty("App::PropertyString","Part","Reference",t) if not "ReferenceMode" in pl: t = QT_TRANSLATE_NOOP("App::Property","The way the referenced objects are included in the current document. 'Normal' includes the shape, 'Transient' discards the shape when the object is switched off (smaller filesize), 'Lightweight' does not import the shape but only the OpenInventor representation") obj.addProperty("App::PropertyEnumeration","ReferenceMode","Reference",t) obj.ReferenceMode = ["Normal","Transient","Lightweight"] if "TransientReference" in pl: if obj.TransientReference: obj.ReferenceMode = "Transient" obj.removeProperty("TransientReference") t = translate("Arch", "TransientReference property to ReferenceMode") FreeCAD.Console.PrintMessage(translate("Arch","Upgrading")+" "+obj.Label+" "+t+"\n") if not "FuseArch" in pl: t = QT_TRANSLATE_NOOP("App::Property","Fuse objects of same material") obj.addProperty("App::PropertyBool","FuseArch", "Reference", t) self.Type = "Reference" def onDocumentRestored(self, obj): ArchReference.setProperties(self, obj) self.reload = False if obj.ReferenceMode == "Lightweight": if obj.ViewObject and obj.ViewObject.Proxy: obj.ViewObject.Proxy.loadInventor(obj) def dumps(self): return None def loads(self, state): return None def onChanged(self, obj, prop): if prop in ["File","Part"]: self.reload = True elif prop == "ReferenceMode": if obj.ReferenceMode == "Normal": if obj.ViewObject and obj.ViewObject.Proxy: obj.ViewObject.Proxy.unloadInventor(obj) if (not obj.Shape) or obj.Shape.isNull(): self.reload = True obj.touch() elif obj.ReferenceMode == "Transient": if obj.ViewObject and obj.ViewObject.Proxy: obj.ViewObject.Proxy.unloadInventor(obj) self.reload = False elif obj.ReferenceMode == "Lightweight": self.reload = False import Part pl = obj.Placement obj.Shape = Part.Shape() obj.Placement = pl if obj.ViewObject and obj.ViewObject.Proxy: obj.ViewObject.Proxy.loadInventor(obj) def execute(self, obj): import Part pl = obj.Placement filename = self.getFile(obj) if filename and self.reload and obj.ReferenceMode in ["Normal","Transient"]: self.parts = self.getPartsList(obj) if self.parts: if filename.lower().endswith(".fcstd"): zdoc = zipfile.ZipFile(filename) if zdoc: if obj.Part: if obj.Part in self.parts: if self.parts[obj.Part][1] in zdoc.namelist(): f = zdoc.open(self.parts[obj.Part][1]) shapedata = f.read() f.close() shapedata = shapedata.decode("utf8") shape = self.cleanShape(shapedata,obj,self.parts[obj.Part][2]) obj.Shape = shape if not pl.isIdentity(): obj.Placement = pl else: t = translate("Arch","Part not found in file") FreeCAD.Console.PrintError(t+"\n") else: shapes = [] for part in self.parts.values(): f = zdoc.open(part[1]) shapedata = f.read() f.close() shapedata = shapedata.decode("utf8") shape = self.cleanShape(shapedata,obj) shapes.append(shape) if shapes: obj.Shape = Part.makeCompound(shapes) elif filename.lower().endswith(".ifc"): ifcfile = self.getIfcFile(filename) if not ifcfile: return try: from nativeifc import ifc_tools from nativeifc import ifc_generator except: t = translate("Arch","NativeIFC not available - unable to process IFC files") FreeCAD.Console.PrintError(t+"\n") return elements = self.getIFCElements(obj, ifcfile) shape, colors = ifc_generator.generate_shape(ifcfile, elements, cached=True) if shape: placement = shape.Placement obj.Shape = shape obj.Placement = placement if colors: ifc_tools.set_colors(obj, colors) elif filename.lower().endswith(".dxf"): # create a special parameter set to control the DXF importer loc = "User parameter:BaseApp/Preferences/Mod/Arch" hGrp = FreeCAD.ParamGet(loc).GetGroup("RefDxfImport") hGrp.SetBool("dxfUseDraftVisGroups", False) hGrp.SetBool("dxfGetOriginalColors", False) hGrp.SetBool("groupLayers", True) hGrp.SetFloat("dxfScaling", 1.0) hGrp.SetBool("dxftext", False) hGrp.SetBool("dxfImportPoints", True) hGrp.SetBool("dxflayout", False) hGrp.SetBool("dxfstarblocks", False) doc = obj.Document oldobjs = list(doc.Objects) import Import Import.readDXF(filename, doc.Name, True, loc + "/RefDxfImport") newobjs = [o for o in doc.Objects if o not in oldobjs] shapes = [o.Shape for o in newobjs if o.isDerivedFrom("Part::Feature")] if len(shapes) == 1: obj.Shape = shapes[0] elif len(shapes) > 1: obj.Shape = Part.makeCompound(shapes) names = [o.Name for o in newobjs] for n in names: doc.removeObject(n) self.reload = False def getIFCElements(self, obj, ifcfile): """returns IFC elements for this object""" try: from nativeifc import ifc_generator except: t = translate("Arch","NativeIFC not available - unable to process IFC files") FreeCAD.Console.PrintError(t+"\n") return if obj.Part: element = ifcfile[int(obj.Part)] else: element = ifcfile.by_type("IfcProject")[0] elements = ifc_generator.get_decomposed_elements(element) elements = ifc_generator.filter_types(elements) return elements def cleanShape(self, shapedata, obj, materials=None): """cleans the imported shape""" import Part shape = Part.Shape() shape.importBrepFromString(shapedata) if obj.FuseArch and materials: # separate lone edges shapes = [] for edge in shape.Edges: found = False for solid in shape.Solids: for soledge in solid.Edges: if edge.hashCode() == soledge.hashCode(): found = True break if found: break if found: break else: shapes.append(edge) #print("solids:",len(shape.Solids),"mattable:",materials) for key,solindexes in materials.items(): if key == "Undefined": # do not join objects with no defined material for solindex in [int(i) for i in solindexes.split(",")]: shapes.append(shape.Solids[solindex]) else: fusion = None for solindex in [int(i) for i in solindexes.split(",")]: if not fusion: fusion = shape.Solids[solindex] else: fusion = fusion.fuse(shape.Solids[solindex]) if fusion: shapes.append(fusion) shape = Part.makeCompound(shapes) try: shape = shape.removeSplitter() except Exception: t = translate("Arch","Error removing splitter") FreeCAD.Console.PrintError(obj.Label+": "+t+"\n") return shape def exists(self,filepath): """case-insensitive version of os.path.exists. Returns the actual file path or None""" if os.path.exists(filepath): return filepath # check for uppercase/lowercase extensions p, e = os.path.splitext(filepath) if os.path.exists(p + e.lower()): return p + e.lower() if os.path.exists(p + e.upper()): return p + e.upper() return None def getFile(self,obj,filename=None): """gets a valid file, if possible""" if not filename: filename = obj.File if not filename: return None if not filename.lower().endswith(".fcstd"): if not filename.lower().endswith(".ifc"): if not filename.lower().endswith(".dxf"): return None if not self.exists(filename): # search for the file in the current directory if not found basename = os.path.basename(filename) currentdir = os.path.dirname(obj.Document.FileName) altfile = os.path.join(currentdir,basename) if altfile == obj.Document.FileName: return None elif self.exists(altfile): return self.exists(altfile) else: # search for subpaths in current folder altfile = None subdirs = self.splitall(os.path.dirname(filename)) for i in range(len(subdirs)): subpath = [currentdir]+subdirs[-i:]+[basename] altfile = os.path.join(*subpath) if self.exists(altfile): return self.exists(altfile) return None return self.exists(filename) def getPartsList(self, obj, filename=None): """returns a list of Part-based objects in a file""" filename = self.getFile(obj, filename) if not filename: return None if filename.lower().endswith(".fcstd"): return self.getPartsListFCSTD(obj, filename) elif filename.lower().endswith(".ifc"): return self.getPartsListIFC(obj, filename) elif filename.lower().endswith(".dxf"): return self.getPartsListDXF(obj, filename) def getPartsListDXF(self, obj, filename): """returns a list of Part-based objects in a DXF file""" # support layers #with open(filename) as f: # txt = f.read() return {} def getPartsListIFC(self, obj, filename): """returns a list of Part-based objects in a IFC file""" ifcfile = self.getIfcFile(filename) if not ifcfile: return None structs = ifcfile.by_type("IfcSpatialElement") res = {} for s in structs: n = s.Name if not n: n = "" name = "#" + str(s.id()) + " " + n + "(" + s.is_a() + ")" res[str(s.id())] = [name, s, None] return res def getPartsListFCSTD(self, obj, filename): """returns a list of Part-based objects in a FCStd file""" parts = {} materials = {} zdoc = zipfile.ZipFile(filename) with zdoc.open("Document.xml") as docf: name = None label = None part = None materials = {} writemode = False for line in docf: line = line.decode("utf8") if "" in line: writemode = False elif "" in line: if name and label and part: parts[name] = [label,part,materials] name = None label = None part = None materials = {} writemode = False return parts def getIfcFile(self, filename): """Gets an IfcOpenShell object""" try: import ifcopenshell except: t = translate("Arch","NativeIFC not available - unable to process IFC files") FreeCAD.Console.PrintError(t+"\n") return None if not getattr(self, "ifcfile", None): self.ifcfile = ifcopenshell.open(filename) return self.ifcfile def getColors(self, obj): """returns the DiffuseColor of the referenced object""" filename = self.getFile(obj) if not filename: return None part = obj.Part if not obj.Part: return None colors = None if filename.lower().endswith(".fcstd"): zdoc = zipfile.ZipFile(filename) if not "GuiDocument.xml" in zdoc.namelist(): return None colorfile = None with zdoc.open("GuiDocument.xml") as docf: writemode1 = False writemode2 = False for line in docf: line = line.decode("utf8") if ("= 1: if self.partCombo.itemData(i) != self.obj.Part: self.obj.Part = self.partCombo.itemData(i) else: self.obj.Part = "" QtCore.QTimer.singleShot(0,FreeCAD.ActiveDocument.recompute) if self.filename and self.obj.Label == "External Reference": self.obj.Label = os.path.basename(self.filename) FreeCADGui.ActiveDocument.resetEdit() return True def reject(self): FreeCAD.ActiveDocument.recompute() FreeCADGui.ActiveDocument.resetEdit() return True def chooseFile(self): loc = QtCore.QDir.homePath() if self.obj.File: loc = os.path.dirname(self.obj.File) filters = "*.FCStd *.dxf" # enable IFC support if NativeIFC is present try: from nativeifc import ifc_tools except: pass else: filters += " *.ifc" filters = translate("Arch","Reference files")+" ("+filters+")" f = QtGui.QFileDialog.getOpenFileName(self.form, translate("Arch","Choose reference file"), loc, filters) if f: self.filename = f[0] self.fileButton.setText(os.path.basename(self.filename)) parts = self.obj.Proxy.getPartsList(self.obj,self.filename) self.partCombo.clear() if parts: self.partCombo.setEnabled(True) sortedkeys = sorted(parts) self.partCombo.addItem(translate("Arch","None (Use whole object)"),"") for k in sortedkeys: self.partCombo.addItem(parts[k][0],k) if self.obj.Part: if self.obj.Part in sortedkeys: self.partCombo.setCurrentIndex(sortedkeys.index(self.obj.Part)) else: self.partCombo.setEnabled(False) def openFile(self): if self.obj.File: if self.obj.File.lower().endswith(".fcstd"): FreeCAD.openDocument(self.obj.File) else: FreeCAD.loadFile(self.obj.File) FreeCADGui.Control.closeDialog() FreeCADGui.ActiveDocument.resetEdit()