# SPDX-License-Identifier: LGPL-2.1-or-later # *************************************************************************** # * * # * Copyright (c) 2022 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 * # * . * # * * # *************************************************************************** """This module contains IFC object definitions""" import FreeCAD import FreeCADGui translate = FreeCAD.Qt.translate # the property groups below should not be treated as psets NON_PSETS = [ "Base", "IFC", "", "Geometry", "Dimension", "Linear/radial dimension", "SectionPlane", "Axis", "PhysicalProperties", "BuildingPart", "IFC Attributes", ] class ifc_object: """Base class for all IFC-based objects""" def __init__(self, otype=None): self.cached = True # this marks that the object is freshly created and its shape should be taken from cache self.virgin_placement = True # this allows one to set the initial placement without triggering any placement change if otype: self.Type = otype[0].upper() + otype[1:] # capitalize to match Draft standard else: self.Type = "IfcObject" def onBeforeChange(self, obj, prop): if prop == "Schema": self.old_schema = obj.Schema elif prop == "Placement": self.old_placement = obj.Placement def onChanged(self, obj, prop): # link class property to its hidder IfcClass counterpart if prop == "IfcClass" and hasattr(obj, "Class") and obj.Class != obj.IfcClass: obj.Class = obj.IfcClass self.rebuild_classlist(obj, setprops=True) elif prop == "Class" and hasattr(obj, "IfcClass") and obj.Class != obj.IfcClass: obj.IfcClass = obj.Class self.rebuild_classlist(obj, setprops=True) elif prop == "Schema": self.edit_schema(obj, obj.Schema) elif prop == "Type": self.edit_type(obj) self.assign_classification(obj) elif prop == "Classification": self.edit_classification(obj) elif prop == "Group": self.edit_group(obj) elif hasattr(obj, prop) and obj.getGroupOfProperty(prop) == "IFC": if prop not in ["StepId"]: self.edit_attribute(obj, prop) elif prop == "Label": self.edit_attribute(obj, "Name", obj.Label) elif prop == "Text": self.edit_annotation(obj, "Text", "\n".join(obj.Text)) elif prop in ["Start", "End"]: self.edit_annotation(obj, prop) elif prop in ["DisplayLength", "DisplayHeight", "Depth"]: self.edit_annotation(obj, prop) elif prop == "Placement": if getattr(self, "virgin_placement", False): self.virgin_placement = False elif obj.Placement != getattr(self, "old_placement", None): # print("placement changed for",obj.Label,"to",obj.Placement) self.edit_placement(obj) elif prop == "Modified": if obj.ViewObject: obj.ViewObject.signalChangeIcon() elif hasattr(obj, prop) and obj.getGroupOfProperty(prop) == "Geometry": self.edit_geometry(obj, prop) elif hasattr(obj, prop) and obj.getGroupOfProperty(prop) == "Quantities": self.edit_quantity(obj, prop) elif hasattr(obj, prop) and obj.getGroupOfProperty(prop) not in NON_PSETS: # Treat all property groups outside the default ones as Psets # print("DEBUG: editinog pset prop",prop) self.edit_pset(obj, prop) def onDocumentRestored(self, obj): self.rebuild_classlist(obj) if hasattr(obj, "IfcFilePath"): # once we have loaded the project, recalculate child coin nodes from PySide import QtCore # lazy loading if obj.OutListRecursive: for child in obj.OutListRecursive: if getattr(child, "ShapeMode", None) == "Coin": child.Proxy.cached = True child.touch() else: obj.Proxy.cached = True QtCore.QTimer.singleShot(100, obj.touch) QtCore.QTimer.singleShot(100, obj.Document.recompute) QtCore.QTimer.singleShot(100, self.fit_all) def assign_classification(self, obj): """ Assigns Classification to an IFC object in a case where the object references a Type that has a Classification property, so we move copy the Type's property to our actual object. """ if not getattr(obj, "Type", None): return type_obj = obj.Type if getattr(type_obj, "Classification", None): # Check if there is Classification already, since user can just change # the IFC type, but there could be one previously assigned which had # Classification if getattr(obj, "Classification", None) is None: obj.addProperty("App::PropertyString", "Classification", "IFC") obj.Classification = type_obj.Classification obj.recompute() elif getattr(obj, "Classification", None): # This means user has assigned type that has no classification, so clear # the one that they have currently selected obj.Classification = "" obj.recompute() def fit_all(self): """Fits the view""" if FreeCAD.GuiUp: FreeCADGui.SendMsgToActiveView("ViewFit") def rebuild_classlist(self, obj, setprops=False): """rebuilds the list of Class enum property according to current class""" from . import ifc_tools # lazy import obj.Class = [obj.IfcClass] obj.Class = ifc_tools.get_ifc_classes(obj, obj.IfcClass) obj.Class = obj.IfcClass if setprops: ifc_tools.remove_unused_properties(obj) ifc_tools.add_properties(obj) def __getstate__(self): return getattr(self, "Type", None) def __setstate__(self, state): self.loads(state) def dumps(self): return getattr(self, "Type", None) def loads(self, state): if state: self.Type = state def execute(self, obj): from . import ifc_generator # lazy import if obj.isDerivedFrom("Part::Feature"): cached = getattr(self, "cached", False) ifc_generator.generate_geometry(obj, cached=cached) self.cached = False self.rebuild_classlist(obj) def addObject(self, obj, child): if child not in obj.Group: g = obj.Group g.append(child) obj.Group = g def removeObject(self, obj, child): if child in obj.Group: g = obj.Group g.remove(child) obj.Group = g def edit_attribute(self, obj, attribute, value=None): """Edits an attribute of an underlying IFC object""" from . import ifc_tools # lazy import if not value: value = obj.getPropertyByName(attribute) ifcfile = ifc_tools.get_ifcfile(obj) elt = ifc_tools.get_ifc_element(obj, ifcfile) if elt: result = ifc_tools.set_attribute(ifcfile, elt, attribute, value) if result: if hasattr(result, "id") and (result.id() != obj.StepId): obj.StepId = result.id() def edit_annotation(self, obj, attribute, value=None): """Edits an attribute of an underlying IFC annotation""" from . import ifc_tools # lazy import from . import ifc_export if not value: if hasattr(obj, attribute): value = obj.getPropertyByName(attribute) ifcfile = ifc_tools.get_ifcfile(obj) elt = ifc_tools.get_ifc_element(obj, ifcfile) if elt: if attribute == "Text": text = ifc_export.get_text(elt) if text: ifc_tools.set_attribute(ifcfile, text, "Literal", value) elif attribute in ["Start", "End"]: dim = ifc_export.get_dimension(elt) if dim: rep = dim[0] for curve in rep.Items: if not hasattr(curve, "Elements"): # this is a TextLiteral for the dimension text - skip it continue for sub in curve.Elements: if sub.is_a("IfcIndexedPolyCurve"): points = sub.Points value = list(points.CoordList) is2d = "2D" in points.is_a() if attribute == "Start": value[0] = ifc_export.get_scaled_point(obj.Start, ifcfile, is2d) else: value[-1] = ifc_export.get_scaled_point(obj.End, ifcfile, is2d) ifc_tools.set_attribute(ifcfile, points, "CoordList", value) else: print("DEBUG: unknown dimension curve type:", sub) elif attribute in ["DisplayLength", "DisplayHeight", "Depth"]: l = w = h = 1000.0 if obj.ViewObject: if obj.ViewObject.DisplayLength.Value: l = ifc_export.get_scaled_value(obj.ViewObject.DisplayLength.Value, ifcfile) if obj.ViewObject.DisplayHeight.Value: w = ifc_export.get_scaled_value(obj.ViewObject.DisplayHeight.Value, ifcfile) if obj.Depth.Value: h = ifc_export.get_scaled_value(obj.Depth.Value, ifcfile) if elt.Representation.Representations: for rep in elt.Representation.Representations: for item in rep.Items: if item.is_a("IfcCsgSolid"): if item.TreeRootExpression.is_a("IfcBlock"): block = item.TreeRootExpression loc = block.Position.Location ifc_tools.set_attribute(ifcfile, block, "XLength", l) ifc_tools.set_attribute(ifcfile, block, "YLength", w) ifc_tools.set_attribute(ifcfile, block, "ZLength", h) ifc_tools.set_attribute( ifcfile, loc, "Coordinates", (-l / 2, -h / 2, -h) ) def edit_geometry(self, obj, prop): """Edits a geometry property of an object""" from . import ifc_geometry # lazy loading from . import ifc_tools # lazy import result = ifc_geometry.set_geom_property(obj, prop) if result: obj.touch() def edit_schema(self, obj, schema): """Changes the schema of an IFC document""" from . import ifc_tools # lazy import ifcfile = ifc_tools.get_ifcfile(obj) if not ifcfile: return if not getattr(self, "old_schema", None): return if schema != ifcfile.wrapped_data.schema_name(): # set obj.Proxy.silent = True to disable the schema change warning if obj.ViewObject and not getattr(self, "silent", False): if not obj.ViewObject.Proxy.schema_warning(): return ifcfile, migration_table = ifc_tools.migrate_schema(ifcfile, schema) self.ifcfile = ifcfile for old_id, new_id in migration_table.items(): child = [o for o in obj.OutListRecursive if getattr(o, "StepId", None) == old_id] if len(child) == 1: child[0].StepId = new_id def edit_placement(self, obj): """Syncs the internal IFC placement""" from . import ifc_tools # lazy import ifc_tools.set_placement(obj) def edit_pset(self, obj, prop): """Edits a Pset value""" from . import ifc_psets # lazy import ifc_psets.edit_pset(obj, prop) def edit_group(self, obj): """Edits the children list""" from . import ifc_tools # lazy import from . import ifc_layers if obj.Class in [ "IfcPresentationLayerAssignment", "IfcPresentationLayerWithStyle", ]: ifcfile = ifc_tools.get_ifcfile(obj) if not ifcfile: return newlist = [] for child in obj.Group: if not getattr(child, "StepId", None) or ifc_tools.get_ifcfile(child) != ifcfile: print( "DEBUG: Not an IFC object. Removing", child.Label, "from layer", obj.Label, ) else: # print("DEBUG: adding", child.Label, "to layer", obj.Label) newlist.append(child) ifc_layers.add_to_layer(child, obj) if newlist != obj.Group: obj.Group = newlist def edit_type(self, obj): """Edits the type of this object""" from . import ifc_types # lazy import ifc_types.edit_type(obj) def edit_quantity(self, obj, prop): """Edits the given quantity""" pass # TODO implement def get_section_data(self, obj): """Returns two things: a list of objects and a cut plane""" from . import ifc_tools # lazy import import Part if not obj.IfcClass == "IfcAnnotation": return [], None if obj.ObjectType != "DRAWING": return [], None objs = getattr(obj, "Objects", []) if not objs: # no object defined, we automatically use the project objs = [] proj = ifc_tools.get_project(obj) if isinstance(proj, FreeCAD.DocumentObject): objs.append(proj) objs.extend(ifc_tools.get_freecad_children(proj)) if objs: s = [] for o in objs: # TODO print a better message if o.ShapeMode != "Shape": s.append(o) if s: FreeCAD.Console.PrintLog("DEBUG: Generating shapes. This might take some time...\n") for o in s: o.ShapeMode = "Shape" o.recompute() l = 1 h = 1 if obj.ViewObject: if hasattr(obj.ViewObject, "DisplayLength"): l = obj.ViewObject.DisplayLength.Value h = obj.ViewObject.DisplayHeight.Value plane = Part.makePlane(l, h, FreeCAD.Vector(l / 2, -h / 2, 0), FreeCAD.Vector(0, 0, 1)) plane.Placement = obj.Placement return objs, plane else: print("DEBUG: Section plane returned no objects") return [], None def edit_classification(self, obj): """Edits the classification of this object""" from . import ifc_classification # lazy loading ifc_classification.edit_classification(obj) class document_object: """Holder for the document's IFC objects""" def __init__(self): pass