diff --git a/src/Mod/BIM/ArchAxis.py b/src/Mod/BIM/ArchAxis.py index e5918bfa8a..d110f749df 100644 --- a/src/Mod/BIM/ArchAxis.py +++ b/src/Mod/BIM/ArchAxis.py @@ -95,15 +95,21 @@ class _Axis: pl = obj.Placement geoms = [] dist = 0 - if obj.Distances and obj.Length.Value: - if len(obj.Distances) == len(obj.Angles): - for i in range(len(obj.Distances)): + distances = [0] + angles = [0] + if hasattr(obj, "Distances"): + distances = obj.Distances + if hasattr(obj, "Angles"): + angles = obj.Angles + if distances and obj.Length.Value: + if angles and len(distances) == len(angles): + for i in range(len(distances)): if hasattr(obj.Length,"Value"): l = obj.Length.Value else: l = obj.Length - dist += obj.Distances[i] - ang = math.radians(obj.Angles[i]) + dist += distances[i] + ang = math.radians(angles[i]) p1 = Vector(dist,0,0) p2 = Vector(dist+(l/math.cos(ang))*math.sin(ang),l,0) if hasattr(obj,"Limit") and obj.Limit.Value: @@ -274,8 +280,29 @@ class _ViewProviderAxis: self.linecoords.point.setValues(verts) self.lineset.coordIndex.setValues(0,len(vset),vset) self.lineset.coordIndex.setNum(len(vset)) - self.onChanged(obj.ViewObject,"BubbleSize") - self.onChanged(obj.ViewObject,"ShowLabel") + elif prop in ["Placement", "Length"] and not hasattr(obj, "Distances"): + # copy values from FlatLines/Wireframe nodes + rn = obj.ViewObject.RootNode + if rn.getNumChildren() < 3: + return + coords = rn.getChild(1) + pts = coords.point.getValues() + self.linecoords.point.setValues(pts) + #self.linecoords.point.setNum(len(pts)) + sw = rn.getChild(2) + if sw.getNumChildren() < 4: + return + edges = sw.getChild(sw.getNumChildren()-2) + if not edges.getNumChildren(): + return + if edges.getChild(0).getNumChildren() < 4: + return + eset = edges.getChild(0).getChild(3) + vset = eset.coordIndex.getValues() + self.lineset.coordIndex.setValues(0,len(vset),vset) + self.lineset.coordIndex.setNum(len(vset)) + self.onChanged(obj.ViewObject,"BubbleSize") + self.onChanged(obj.ViewObject,"ShowLabel") def onChanged(self, vobj, prop): @@ -317,7 +344,10 @@ class _ViewProviderAxis: pos = [] else: pos = [vobj.BubblePosition] - for i in range(len(vobj.Object.Distances)): + n = 0 + if hasattr(vobj.Object, "Distances"): + n = len(vobj.Object.Distances) + for i in range(n): for p in pos: if hasattr(vobj.Object,"Limit") and vobj.Object.Limit.Value: verts = [vobj.Object.Placement.inverse().multVec(vobj.Object.Shape.Edges[i].Vertexes[0].Point), @@ -679,7 +709,7 @@ class _AxisTaskPanel: 'fills the treewidget' self.updating = True self.tree.clear() - if self.obj: + if self.obj and hasattr(self.obj, "Distances"): for i in range(len(self.obj.Distances)): item = QtGui.QTreeWidgetItem(self.tree) item.setText(0,str(i+1)) diff --git a/src/Mod/BIM/importers/exportIFC.py b/src/Mod/BIM/importers/exportIFC.py index 7e09064fa3..e710208a64 100644 --- a/src/Mod/BIM/importers/exportIFC.py +++ b/src/Mod/BIM/importers/exportIFC.py @@ -2474,6 +2474,29 @@ def create_annotation(anno, ifcfile, context, history, preferences): objectType = "LEADER" elif anno.Shape.Faces: objectType = "AREA" + elif Draft.getType(anno) == "Axis": + axdata = anno.Proxy.getAxisData(anno) + axes = [] + for ax in axdata: + p1 = ifcbin.createIfcCartesianPoint(tuple(FreeCAD.Vector(ax[0]).multiply(preferences['SCALE_FACTOR'])[:2])) + p2 = ifcbin.createIfcCartesianPoint(tuple(FreeCAD.Vector(ax[1]).multiply(preferences['SCALE_FACTOR'])[:2])) + pol = ifcbin.createIfcPolyline([p1,p2]) + axis = ifcfile.createIfcGridAxis(ax[2],pol,True) + axes.append(axis) + if axes: + if len(axes) > 1: + xvc = ifcbin.createIfcDirection((1.0,0.0,0.0)) + zvc = ifcbin.createIfcDirection((0.0,0.0,1.0)) + ovc = ifcbin.createIfcCartesianPoint((0.0,0.0,0.0)) + gpl = ifcbin.createIfcAxis2Placement3D(ovc,zvc,xvc) + plac = ifcbin.createIfcLocalPlacement(gpl) + grid = ifcfile.createIfcGrid(uid,history,name,description,None,plac,None,axes,None,None) + return grid + else: + return axes[0] + else: + print("Unable to handle object",anno.Label) + return None else: objectType = "LINEWORK" sh = anno.Shape.copy() diff --git a/src/Mod/BIM/nativeifc/ifc_export.py b/src/Mod/BIM/nativeifc/ifc_export.py index ca2d3d10cd..d55e159c9c 100644 --- a/src/Mod/BIM/nativeifc/ifc_export.py +++ b/src/Mod/BIM/nativeifc/ifc_export.py @@ -179,13 +179,15 @@ def get_object_type(ifcentity, objecttype=None): objecttype = "dimension" elif get_text(ifcentity): objecttype = "text" + elif ifcentity.is_a("IfcGridAxis"): + objecttype = "axis" return objecttype def is_annotation(obj): """Determines if the given FreeCAD object should be saved as an IfcAnnotation""" - if getattr(obj, "IfcClass", None) == "IfcAnnotation": + if getattr(obj, "IfcClass", None) in ["IfcAnnotation", "IfcGridAxis"]: return True if getattr(obj, "IfcType", None) == "Annotation": return True @@ -270,6 +272,30 @@ def get_sectionplane(annotation): return None +def get_axis(obj): + """Determines if a given IFC entity is an IfcGridAxis. Returns a tuple + containing a Placement, a length value in millimeters, and a tag""" + + if obj.is_a("IfcGridAxis"): + tag = obj.AxisTag + s = ifcopenshell.util.unit.calculate_unit_scale(obj.file) * 1000 + shape = importIFCHelper.get2DShape(obj.AxisCurve, s, notext=True) + if shape: + edge = shape[0].Edges[0] # we suppose here the axis shape is a single straight line + if obj.SameSense: + p0 = edge.Vertexes[0].Point + p1 = edge.Vertexes[-1].Point + else: + p0 = edge.Vertexes[-1].Point + p1 = edge.Vertexes[0].Point + length = edge.Length + placement = FreeCAD.Placement() + placement.Base = p0 + placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0,1,0), p1.sub(p0)) + return (placement, length, tag) + return None + + def create_annotation(obj, ifcfile): """Adds an IfcAnnotation from the given object to the given IFC file""" diff --git a/src/Mod/BIM/nativeifc/ifc_generator.py b/src/Mod/BIM/nativeifc/ifc_generator.py index 9c9ea9289b..578fe3c368 100644 --- a/src/Mod/BIM/nativeifc/ifc_generator.py +++ b/src/Mod/BIM/nativeifc/ifc_generator.py @@ -465,21 +465,39 @@ def set_representation(vobj, node): """Sets the correct coin nodes for the given Part object""" # node = [colors, verts, faces, edges, parts] + if not vobj.RootNode: + return + if vobj.RootNode.getNumChildren() < 3: + return coords = vobj.RootNode.getChild(1) # SoCoordinate3 - fset = vobj.RootNode.getChild(2).getChild(1).getChild(6) # SoBrepFaceSet - eset = ( - vobj.RootNode.getChild(2).getChild(2).getChild(0).getChild(3) - ) # SoBrepEdgeSet + switch = vobj.RootNode.getChild(2) + num_modes = switch.getNumChildren() + if num_modes < 3: + return + # the number of display modes under switch can vary. + # the last 4 ones are the ones that are defined for + # Part features + faces = switch.getChild(num_modes-3) + edges = switch.getChild(num_modes-2) + fset = None + if faces.getNumChildren() >= 7: + fset = faces.getChild(6) # SoBrepFaceSet + eset = None + if edges.getNumChildren() >= 1: + if edges.getChild(0).getNumChildren() >= 4: + eset = edges.getChild(0).getChild(3) # SoBrepEdgeSet # reset faces and edges - fset.coordIndex.deleteValues(0) - eset.coordIndex.deleteValues(0) + if fset: + fset.coordIndex.deleteValues(0) + if eset: + eset.coordIndex.deleteValues(0) coords.point.deleteValues(0) if not node: return - if node[1] and node[3]: + if node[1] and node[3] and eset: coords.point.setValues(node[1]) eset.coordIndex.setValues(node[3]) - if node[2] and node[4]: + if node[2] and node[4] and fset: fset.coordIndex.setValues(node[2]) fset.partIndex.setValues(node[4]) @@ -553,7 +571,9 @@ def delete_ghost(document): def get_annotation_shape(annotation, ifcfile, coin=False): - """Returns a shape or a coin node form an IFC annotation""" + """Returns a shape or a coin node form an IFC annotation. + Returns [colors, verts, faces, edges], colors and faces + being normally None for 2D shapes.""" import Part from importers import importIFCHelper @@ -562,14 +582,21 @@ def get_annotation_shape(annotation, ifcfile, coin=False): placement = None ifcscale = importIFCHelper.getScaling(ifcfile) shapes2d = [] - for rep in annotation.Representation.Representations: - if rep.RepresentationIdentifier in ["Annotation", "FootPrint", "Axis"]: - sh = importIFCHelper.get2DShape(rep, ifcscale, notext=True) - if sh: - shapes2d.extend(sh) + if hasattr(annotation, "Representation"): + for rep in annotation.Representation.Representations: + if rep.RepresentationIdentifier in ["Annotation", "FootPrint", "Axis"]: + sh = importIFCHelper.get2DShape(rep, ifcscale, notext=True) + if sh: + shapes2d.extend(sh) + elif hasattr(annotation, "AxisCurve"): + sh = importIFCHelper.get2DShape(annotation.AxisCurve, ifcscale, notext=True) + shapes2d.extend(sh) if shapes2d: shape = Part.makeCompound(shapes2d) - placement = importIFCHelper.getPlacement(annotation.ObjectPlacement, ifcscale) + if hasattr(annotation, "ObjectPlacement"): + placement = importIFCHelper.getPlacement(annotation.ObjectPlacement, ifcscale) + else: + placement = None if coin: iv = shape.writeInventor() iv = iv.replace("\n", "") diff --git a/src/Mod/BIM/nativeifc/ifc_objects.py b/src/Mod/BIM/nativeifc/ifc_objects.py index f7a890ec31..26e3c96749 100644 --- a/src/Mod/BIM/nativeifc/ifc_objects.py +++ b/src/Mod/BIM/nativeifc/ifc_objects.py @@ -26,7 +26,8 @@ import FreeCAD translate = FreeCAD.Qt.translate # the property groups below should not be treated as psets -NON_PSETS = ["Base", "IFC", "", "Geometry", "Dimension", "Linear/radial dimension", "SectionPlane", "PhysicalProperties"] +NON_PSETS = ["Base", "IFC", "", "Geometry", "Dimension", "Linear/radial dimension", + "SectionPlane", "Axis", "PhysicalProperties"] class ifc_object: """Base class for all IFC-based objects""" diff --git a/src/Mod/BIM/nativeifc/ifc_tools.py b/src/Mod/BIM/nativeifc/ifc_tools.py index d96783e771..2b98c240a1 100644 --- a/src/Mod/BIM/nativeifc/ifc_tools.py +++ b/src/Mod/BIM/nativeifc/ifc_tools.py @@ -253,7 +253,7 @@ def create_object(ifcentity, document, ifcfile, shapemode=0, objecttype=None): if exobj: return exobj s = "IFC: Created #{}: {}, '{}'\n".format( - ifcentity.id(), ifcentity.is_a(), ifcentity.Name + ifcentity.id(), ifcentity.is_a(), getattr(ifcentity, "Name", "") ) objecttype = ifc_export.get_object_type(ifcentity, objecttype) FreeCAD.Console.PrintLog(s) @@ -494,6 +494,7 @@ def add_object(document, otype=None, oname="IfcObject"): 'text', 'dimension', 'sectionplane', + 'axis', or anything else for a standard IFC object""" if not document: @@ -501,6 +502,15 @@ def add_object(document, otype=None, oname="IfcObject"): if otype == "sectionplane": obj = Arch.makeSectionPlane() obj.Proxy = ifc_objects.ifc_object(otype) + elif otype == "axis": + obj = Arch.makeAxis() + obj.Proxy = ifc_objects.ifc_object(otype) + obj.removeProperty("Angles") + obj.removeProperty("Distances") + obj.removeProperty("Labels") + obj.removeProperty("Limit") + if obj.ViewObject: + obj.ViewObject.DisplayMode = "Flat Lines" elif otype == "dimension": obj = Draft.make_dimension(FreeCAD.Vector(), FreeCAD.Vector(1,0,0)) obj.Proxy = ifc_objects.ifc_object(otype) @@ -666,7 +676,18 @@ def add_properties( if value is not None: setattr(obj, attr, str(value)) # annotation properties - if ifcentity.is_a("IfcAnnotation"): + if ifcentity.is_a("IfcGridAxis"): + axisdata = ifc_export.get_axis(ifcentity) + if axisdata: + if "Placement" not in obj.PropertiesList: + obj.addProperty("App::PropertyPlacement", "Placement", "Base") + if "CustomText" in obj.PropertiesList: + obj.setPropertyStatus("CustomText", "Hidden") + obj.setExpression("CustomText", "AxisTag") + if "Length" not in obj.PropertiesList: + obj.addProperty("App::PropertyLength","Length","Axis") + obj.Length = axisdata[1] + elif ifcentity.is_a("IfcAnnotation"): sectionplane = ifc_export.get_sectionplane(ifcentity) if sectionplane: if "Placement" not in obj.PropertiesList: @@ -1028,6 +1049,13 @@ def set_placement(obj): if obj.Class in ["IfcProject", "IfcProjectLibrary"]: return element = get_ifc_element(obj) + if not hasattr(element, "ObjectPlacement"): + # special case: this is a grid axis, it has no placement + if element.is_a("IfcGridAxis"): + return set_axis_points(obj, element, ifcfile) + # other cases of objects without ObjectPlacement? + print("DEBUG: object without ObjectPlacement",element) + return False placement = FreeCAD.Placement(obj.Placement) placement.Base = FreeCAD.Vector(placement.Base).multiply(get_scale(ifcfile)) new_matrix = get_ios_matrix(placement) @@ -1053,6 +1081,29 @@ def set_placement(obj): return False +def set_axis_points(obj, element, ifcfile): + """Sets the points of an axis from placement and length""" + + if element.AxisCurve.is_a("IfcPolyline"): + p1 = obj.Placement.Base + p2 = obj.Placement.multVec(FreeCAD.Vector(0, obj.Length.Value, 0)) + api_run( + "attribute.edit_attributes", + ifcfile, + product=element.AxisCurve.Points[0], + attributes={"Coordinates": tuple(p1)}, + ) + api_run( + "attribute.edit_attributes", + ifcfile, + product=element.AxisCurve.Points[-1], + attributes={"Coordinates": tuple(p2)}, + ) + return True + print("DEBUG: unhandled axis type:",element.AxisCurve.is_a()) + return False + + def save_ifc(obj, filepath=None): """Saves the linked IFC file of a project, but does not mark it as saved""" @@ -1218,6 +1269,7 @@ def create_relationship(old_obj, obj, parent, element, ifcfile, mode=None): parent_element = get_ifc_element(parent) else: parent_element = parent + uprel = None # case 4: anything inside group if parent_element.is_a("IfcGroup"): # special case: adding a section plane to a grouo turns it into a drawing @@ -1355,7 +1407,7 @@ def create_relationship(old_obj, obj, parent, element, ifcfile, mode=None): "void.add_opening", ifcfile, opening=element, element=parent_element ) # case 3: element aggregated inside other element - else: + elif element.is_a("IfcProduct"): try: api_run("aggregate.unassign_object", ifcfile, products=[element]) except: