# SPDX-License-Identifier: LGPL-2.1-or-later # *************************************************************************** # * * # * Copyright (c) 2018 Yorik van Havre * # * * # * 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 * # * . * # * * # *************************************************************************** __title__ = "FreeCAD Arch External Reference" __author__ = "Yorik van Havre" __url__ = "https://www.freecad.org" ## @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. import os import re import struct import zipfile import FreeCAD from draftutils import params if FreeCAD.GuiUp: from PySide import QtCore, QtGui from PySide.QtCore import QT_TRANSLATE_NOOP import FreeCADGui from draftutils.translate import translate else: # \cond def translate(ctxt, txt): return txt def QT_TRANSLATE_NOOP(ctxt, txt): return txt # \endcond class ArchReference: """The Arch Reference object""" def __init__(self, obj): obj.Proxy = self self.Type = "Reference" ArchReference.setProperties(self, obj) 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, locked=True) 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, locked=True) 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, locked=True ) 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, locked=True) def onDocumentRestored(self, obj): ArchReference.setProperties(self, obj) if obj.ReferenceMode == "Lightweight": self.reload = False if obj.ViewObject and obj.ViewObject.Proxy: obj.ViewObject.Proxy.loadInventor(obj) else: self.reload = True self.execute(obj) # sets self.reload to False again def dumps(self): return None def loads(self, state): self.Type = "Reference" 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: self.shapes = [] 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]) self.shapes.append(shape) 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: 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) self.shapes.append(shape) if self.shapes: obj.Shape = Part.makeCompound(self.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 Shape Appearance of the referenced object(s)""" filename = self.getFile(obj) if not filename: return [] if not filename.lower().endswith(".fcstd"): return [] if not getattr(self, "parts", {}): return [] if not getattr(self, "shapes", []): return [] totalcolors = [] parts = [obj.Part] if obj.Part else self.parts.keys() lenparts = len(parts) for i, part in enumerate(parts): lenfaces = len(self.shapes[i].Faces) if lenfaces: colors = self._getColorsPart(filename, part) if len(colors) == lenfaces: totalcolors.extend(colors) elif lenparts == 1: totalcolors.append(colors[0]) else: totalcolors.extend([colors[0]] * lenfaces) return totalcolors def _getColorsPart(self, filename, part): zdoc = zipfile.ZipFile(filename) if not "GuiDocument.xml" in zdoc.namelist(): return [] colors = [] colorfile = None with zdoc.open("GuiDocument.xml") as docf: writemode1 = False writemode2 = False writemode3 = 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) + 1) 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()