# SPDX-License-Identifier: LGPL-2.1-or-later # *************************************************************************** # * * # * Copyright (c) 2011 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 * # * . * # * * # *************************************************************************** ## @package ArchSectionPlane # \ingroup ARCH # \brief The Section plane object and tools # # This module provides tools to build Section plane objects. # It also contains functionality to produce SVG rendering of # section planes, to be used in the TechDraw module import math import re import tempfile import time import uuid import FreeCAD import ArchCommands import ArchComponent import Draft import DraftVecUtils from FreeCAD import Vector from draftutils import params if FreeCAD.GuiUp: from pivy import coin 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 ISRENDERING = False # flag to prevent concurrent runs of the coin renderer def getSectionData(source): """Returns some common data from section planes and building parts""" if hasattr(source, "Objects"): objs = source.Objects cutplane = source.Shape elif hasattr(source, "Group"): import Part objs = source.Group cutplane = Part.makePlane(1000, 1000, FreeCAD.Vector(-500, -500, 0)) m = 1 if source.ViewObject and hasattr(source.ViewObject, "CutMargin"): m = source.ViewObject.CutMargin.Value cutplane.translate(FreeCAD.Vector(0, 0, m)) cutplane.Placement = cutplane.Placement.multiply(source.Placement) onlySolids = True if hasattr(source, "OnlySolids"): onlySolids = source.OnlySolids clip = False if hasattr(source, "Clip"): clip = source.Clip p = FreeCAD.Placement(source.Placement) direction = p.Rotation.multVec(FreeCAD.Vector(0, 0, 1)) if objs: objs = Draft.get_group_contents(objs, walls=True, addgroups=True) return objs, cutplane, onlySolids, clip, direction def getCutShapes( objs, cutplane, onlySolids, clip, joinArch, showHidden, groupSshapesByObject=False ): """ returns a list of shapes (visible, hidden, cut lines...) obtained from performing a series of booleans against the given cut plane """ import Part import DraftGeomUtils shapes = [] hshapes = [] sshapes = [] objectShapes = [] objectSshapes = [] if joinArch: shtypes = {} for o in objs: if Draft.getType(o) in ["Wall", "Structure"]: if o.Shape.isNull(): pass elif onlySolids: shtypes.setdefault( o.Material.Name if (hasattr(o, "Material") and o.Material) else "None", [] ).extend(o.Shape.Solids) else: shtypes.setdefault( o.Material.Name if (hasattr(o, "Material") and o.Material) else "None", [] ).append(o.Shape.copy()) elif hasattr(o, "Shape"): if o.Shape.isNull(): pass elif onlySolids: shapes.extend(o.Shape.Solids) objectShapes.append((o, o.Shape.Solids)) else: shapes.append(o.Shape.copy()) objectShapes.append((o, [o.Shape.copy()])) for k, v in shtypes.items(): v1 = v.pop() if v: v1 = v1.multiFuse(v) v1 = v1.removeSplitter() if v1.Solids: shapes.extend(v1.Solids) objectShapes.append((k, v1.Solids)) else: print("ArchSectionPlane: Fusing Arch objects produced non-solid results") shapes.append(v1) objectShapes.append((k, [v1])) else: for o in objs: if hasattr(o, "Shape"): if o.Shape.isNull(): pass elif onlySolids: if o.Shape.isValid(): shapes.extend(o.Shape.Solids) objectShapes.append((o, o.Shape.Solids)) else: shapes.append(o.Shape) objectShapes.append((o, [o.Shape])) cutface, cutvolume, invcutvolume = ArchCommands.getCutVolume(cutplane, shapes, clip) shapes = [] for o, shapeList in objectShapes: tmpSshapes = [] for sh in shapeList: for sub in (sh.SubShapes if sh.ShapeType == "Compound" else [sh]): if cutvolume: if sub.Volume < 0: sub = sub.reversed() # Use reversed as sub is immutable. c = sub.cut(cutvolume) s = sub.section(cutface) try: wires = DraftGeomUtils.findWires(s.Edges) for w in wires: f = Part.Face(w) tmpSshapes.append(f) except Part.OCCError: # print "ArchView: unable to get a face" tmpSshapes.append(s) shapes.extend(c.SubShapes if c.ShapeType == "Compound" else [c]) if showHidden: c = sub.cut(invcutvolume) hshapes.extend(c.SubShapes if c.ShapeType == "Compound" else [c]) else: shapes.append(sub) if len(tmpSshapes) > 0: sshapes.extend(tmpSshapes) if groupSshapesByObject: objectSshapes.append((o, tmpSshapes)) if groupSshapesByObject: return shapes, hshapes, sshapes, cutface, cutvolume, invcutvolume, objectSshapes else: return shapes, hshapes, sshapes, cutface, cutvolume, invcutvolume def getFillForObject(o, defaultFill, source): """returns a color tuple from an object's material""" if hasattr(source, "UseMaterialColorForFill") and source.UseMaterialColorForFill: material = None if hasattr(o, "Material") and o.Material: material = o.Material elif isinstance(o, str): material = FreeCAD.ActiveDocument.getObject(o) if material: if hasattr(material, "SectionColor") and material.SectionColor: return material.SectionColor elif hasattr(material, "Color") and material.Color: return material.Color elif hasattr(o, "ViewObject") and hasattr(o.ViewObject, "ShapeColor"): return o.ViewObject.ShapeColor return defaultFill def isOriented(obj, plane): """determines if an annotation is facing the cutplane or not""" norm1 = plane.normalAt(0, 0) if hasattr(obj, "Placement"): norm2 = obj.Placement.Rotation.multVec(FreeCAD.Vector(0, 0, 1)) elif hasattr(obj, "Normal"): norm2 = obj.Normal if norm2.Length < 0.01: return True else: return True a = norm1.getAngle(norm2) if (a < 0.01) or (abs(a - math.pi) < 0.01): return True return False def update_svg_cache(source, renderMode, showHidden, showFill, fillSpaces, joinArch, allOn, objs): """ Returns None or cached SVG, clears shape cache if required """ svgcache = None if hasattr(source, "Proxy"): if hasattr(source.Proxy, "svgcache") and source.Proxy.svgcache: # TODO check array bounds svgcache = source.Proxy.svgcache[0] # empty caches if we want to force-recalculate for certain properties if ( source.Proxy.svgcache[1] != renderMode or source.Proxy.svgcache[2] != showHidden or source.Proxy.svgcache[3] != showFill or source.Proxy.svgcache[4] != fillSpaces or source.Proxy.svgcache[5] != joinArch or source.Proxy.svgcache[6] != allOn or source.Proxy.svgcache[7] != set(objs) ): svgcache = None if ( source.Proxy.svgcache[4] != fillSpaces or source.Proxy.svgcache[5] != joinArch or source.Proxy.svgcache[6] != allOn or source.Proxy.svgcache[7] != set(objs) ): source.Proxy.shapecache = None return svgcache def getSVG( source, renderMode="Wireframe", allOn=False, showHidden=False, scale=1, rotation=0, linewidth=1, lineColor=(0.0, 0.0, 0.0), fontsize=1, linespacing=None, showFill=False, fillColor=(1.0, 1.0, 1.0), techdraw=False, fillSpaces=False, cutlinewidth=0, joinArch=False, ): """ Return an SVG fragment from an Arch SectionPlane or BuildingPart. allOn If it is `True`, all cut objects are shown, regardless of if they are visible or not. renderMode Can be `'Wireframe'` (default) or `'Solid'` to use the Arch solid renderer. showHidden If it is `True`, the hidden geometry above the section plane is shown in dashed line. showFill If it is `True`, the cut areas get filled with a pattern. lineColor Color of lines for the `renderMode` is `'Wireframe'`. fillColor If `showFill` is `True` and `renderMode` is `'Wireframe'`, the cut areas are filled with `fillColor`. fillSpaces If `True`, shows space objects as filled surfaces. """ import Part objs, cutplane, onlySolids, clip, direction = getSectionData(source) if not objs: return "" if not allOn: objs = Draft.removeHidden(objs) # separate spaces and Draft objects spaces = [] nonspaces = [] drafts = [] # Only used for annotations. windows = [] cutface = None for o in objs: if Draft.getType(o) == "Space": spaces.append(o) elif Draft.getType(o) in [ "Dimension", "AngularDimension", "LinearDimension", "Annotation", "Label", "Text", "DraftText", "Axis", ]: if isOriented(o, cutplane): drafts.append(o) elif o.isDerivedFrom("App::DocumentObjectGroup"): # These will have been expanded by getSectionData already pass else: nonspaces.append(o) if Draft.getType(o.getLinkedObject()) == "Window": # To support Link of Windows(Doors) windows.append(o) objs = nonspaces scaledLineWidth = linewidth / scale if renderMode in ["Coin", 2, "Coin mono", 3]: # don't scale linewidths in coin mode svgLineWidth = str(linewidth) + "px" else: svgLineWidth = str(scaledLineWidth) + "px" if cutlinewidth: scaledCutLineWidth = cutlinewidth / scale svgCutLineWidth = str(scaledCutLineWidth) + "px" else: st = params.get_param_arch("CutLineThickness") svgCutLineWidth = str(scaledLineWidth * st) + "px" yt = params.get_param_arch("SymbolLineThickness") svgSymbolLineWidth = str(linewidth * yt) hiddenPattern = params.get_param_arch("archHiddenPattern") svgHiddenPattern = hiddenPattern.replace(" ", "") # fillpattern = ' bool: """Return true if boundBox has a non-zero volume""" return boundBox.XLength > 0 and boundBox.YLength > 0 and boundBox.ZLength > 0 def getDXF(obj): """Return a DXF representation from a TechDraw view.""" allOn = getattr(obj, "AllOn", True) showHidden = getattr(obj, "ShowHidden", False) result = [] import TechDraw import Part if not obj.Source: return result source = obj.Source objs, cutplane, onlySolids, clip, direction = getSectionData(source) if not objs: return result if not allOn: objs = Draft.removeHidden(objs) objs = [ obj for obj in objs if ( not obj.isDerivedFrom("Part::Part2DObject") and Draft.getType(obj) not in ["BezCurve", "BSpline", "Wire", "Annotation", "Dimension", "Space"] ) ] vshapes, hshapes, sshapes, cutface, cutvolume, invcutvolume = getCutShapes( objs, cutplane, onlySolids, clip, False, showHidden ) if vshapes: result.append(TechDraw.projectToDXF(Part.makeCompound(vshapes), direction)) if sshapes: result.append(TechDraw.projectToDXF(Part.makeCompound(sshapes), direction)) if hshapes: result.append(TechDraw.projectToDXF(Part.makeCompound(hshapes), direction)) return result def getCameraData(floatlist): """reconstructs a valid camera data string from stored values""" c = "" if len(floatlist) >= 12: d = floatlist camtype = "orthographic" if len(floatlist) == 13: if d[12] == 1: camtype = "perspective" if camtype == "orthographic": c = "#Inventor V2.1 ascii\n\n\nOrthographicCamera {\n viewportMapping ADJUST_CAMERA\n " else: c = "#Inventor V2.1 ascii\n\n\nPerspectiveCamera {\n viewportMapping ADJUST_CAMERA\n " c += "position " + str(d[0]) + " " + str(d[1]) + " " + str(d[2]) + "\n " c += ( "orientation " + str(d[3]) + " " + str(d[4]) + " " + str(d[5]) + " " + str(d[6]) + "\n " ) c += "aspectRatio " + str(d[9]) + "\n " c += "focalDistance " + str(d[10]) + "\n " if camtype == "orthographic": c += "height " + str(d[11]) + "\n\n}\n" else: c += "heightAngle " + str(d[11]) + "\n\n}\n" return c def getCoinSVG(cutplane, objs, cameradata=None, linewidth=0.2, singleface=False, facecolor=None): """Returns an SVG fragment generated from a coin view""" if not FreeCAD.GuiUp: return "" # do not allow concurrent runs # wait until the other rendering has finished global ISRENDERING while ISRENDERING: time.sleep(0.1) ISRENDERING = True # a name to save a temp file svgfile = tempfile.mkstemp(suffix=".svg")[1] # set object lighting to single face to get black fills # but this creates artifacts in svg output, triangulation gets visible... ldict = {} if singleface: for obj in objs: if hasattr(obj, "ViewObject") and hasattr(obj.ViewObject, "Lighting"): ldict[obj.Name] = obj.ViewObject.Lighting obj.ViewObject.Lighting = "One side" # get nodes to render root_node = coin.SoSeparator() boundbox = FreeCAD.BoundBox() for obj in objs: if hasattr(obj.ViewObject, "RootNode") and obj.ViewObject.RootNode: old_visibility = obj.ViewObject.isVisible() # ignore visibility as only visible objects are passed here obj.ViewObject.show() node_copy = obj.ViewObject.RootNode.copy() root_node.addChild(node_copy) if old_visibility: obj.ViewObject.show() else: obj.ViewObject.hide() if hasattr(obj, "Shape") and hasattr(obj.Shape, "BoundBox"): boundbox.add(obj.Shape.BoundBox) # reset lighting of objects if ldict: for obj in objs: if obj.Name in ldict: obj.ViewObject.Lighting = ldict[obj.Name] # create viewer view_window = FreeCADGui.createViewer() view_window_name = "Temp" + str(uuid.uuid4().hex[:8]) view_window.setName(view_window_name) # disable animations to prevent a crash: # https://github.com/FreeCAD/FreeCAD/issues/24929 view_window.setAnimationEnabled(False) inventor_view = view_window.getViewer() inventor_view.setBackgroundColor(1, 1, 1) view_window.redraw() # set clip plane clip = coin.SoClipPlane() norm = cutplane.normalAt(0, 0).negative() proj = DraftVecUtils.project(cutplane.CenterOfMass, norm) dist = proj.Length if proj.getAngle(norm) > 1: dist = -dist clip.on = True plane = coin.SbPlane(coin.SbVec3f(norm.x, norm.y, norm.z), dist) # dir, position on dir clip.plane.setValue(plane) root_node.insertChild(clip, 0) # add white marker at scene bound box corner markervec = FreeCAD.Vector(10, 10, 10) a = cutplane.normalAt(0, 0).getAngle(markervec) if (a < 0.01) or (abs(a - math.pi) < 0.01): markervec = FreeCAD.Vector(10, -10, 10) boundbox.enlarge(10) # so the marker don't overlap the objects sep = coin.SoSeparator() mat = coin.SoMaterial() mat.diffuseColor.setValue([1, 1, 1]) sep.addChild(mat) coords = coin.SoCoordinate3() coords.point.setValues( [ [boundbox.XMin, boundbox.YMin, boundbox.ZMin], [boundbox.XMin + markervec.x, boundbox.YMin + markervec.y, boundbox.ZMin + markervec.z], ] ) sep.addChild(coords) lset = coin.SoIndexedLineSet() lset.coordIndex.setValues(0, 2, [0, 1]) sep.addChild(lset) root_node.insertChild(sep, 0) # set scenegraph inventor_view.setSceneGraph(root_node) # set camera if cameradata: view_window.setCamera(cameradata) else: view_window.setCameraType("Orthographic") # rot = FreeCAD.Rotation(FreeCAD.Vector(0,0,1),cutplane.normalAt(0,0)) vx = cutplane.Placement.Rotation.multVec(FreeCAD.Vector(1, 0, 0)) vy = cutplane.Placement.Rotation.multVec(FreeCAD.Vector(0, 1, 0)) vz = cutplane.Placement.Rotation.multVec(FreeCAD.Vector(0, 0, 1)) rot = FreeCAD.Rotation(vx, vy, vz, "ZXY") view_window.setCameraOrientation(rot.Q) # this is needed to set correct focal depth, otherwise saving doesn't work properly view_window.fitAll() # save view # print("saving to",svgfile) view_window.saveVectorGraphic(svgfile, 1) # number is pixel size # set linewidth placeholder f = open(svgfile, "r") svg = f.read() f.close() svg = svg.replace("stroke-width:1.0;", "stroke-width:" + str(linewidth) + ";") svg = svg.replace('stroke-width="1px', 'stroke-width="' + str(linewidth)) # find marker and calculate scale factor and translation # factor = None trans = None import WorkingPlane wp = WorkingPlane.PlaneBase() wp.align_to_point_and_axis_svg(Vector(0, 0, 0), cutplane.normalAt(0, 0), 0) p = wp.get_local_coords(markervec) orlength = FreeCAD.Vector(p.x, p.y, 0).Length marker = re.findall(r"", svg) if marker: marker = marker[0].split('"') x1 = float(marker[1]) y1 = float(marker[3]) x2 = float(marker[5]) y2 = float(marker[7]) p1 = FreeCAD.Vector(x1, y1, 0) p2 = FreeCAD.Vector(x2, y2, 0) factor = orlength / p2.sub(p1).Length if factor: orig = wp.get_local_coords(FreeCAD.Vector(boundbox.XMin, boundbox.YMin, boundbox.ZMin)) orig = FreeCAD.Vector(orig.x, -orig.y, 0) scaledp1 = FreeCAD.Vector(p1.x * factor, p1.y * factor, 0) trans = orig.sub(scaledp1) # remove marker svg = re.sub(r"", "", svg, count=1) # remove background rectangle svg = re.sub(r"", "", svg, count=1, flags=re.MULTILINE | re.DOTALL) # set face color to white if facecolor: res = re.findall(r"fill:(.*?); stroke:(.*?);", svg) pairs = [] for pair in res: if (pair not in pairs) and (pair[0] == pair[1]) and (pair[0] not in ["#0a0a0a"]): # coin seems to be rendering a lot of lines as thin triangles with the #0a0a0a color... pairs.append(pair) for pair in pairs: svg = re.sub( r"fill:" + pair[0] + "; stroke:" + pair[1] + ";", "fill:" + facecolor + "; stroke:" + facecolor + ";", svg, ) # embed everything in a scale group and scale the viewport if factor: if trans: svg = svg.replace( "", '\n', 1, ) else: svg = svg.replace( "", '\n', 1 ) svg = svg.replace("", "\n") # trigger viewer close QtCore.QTimer.singleShot(1, lambda: closeViewer(view_window_name)) # strip svg tags (needed for TD Arch view) svg = re.sub(r"<\?xml.*?>", "", svg, flags=re.MULTILINE | re.DOTALL) svg = re.sub(r"", "", svg, flags=re.MULTILINE | re.DOTALL) svg = re.sub(r"<\/svg>", "", svg, flags=re.MULTILINE | re.DOTALL) ISRENDERING = False return svg def closeViewer(name): """Close temporary viewers""" mw = FreeCADGui.getMainWindow() for sw in mw.findChildren(QtGui.QMdiSubWindow): if sw.windowTitle() == name: sw.close() class _SectionPlane: "A section plane object" def __init__(self, obj): obj.Proxy = self self.Type = "SectionPlane" self.setProperties(obj) def setProperties(self, obj): pl = obj.PropertiesList if not "Placement" in pl: obj.addProperty( "App::PropertyPlacement", "Placement", "SectionPlane", QT_TRANSLATE_NOOP("App::Property", "The placement of this object"), locked=True, ) if not "Shape" in pl: obj.addProperty( "Part::PropertyPartShape", "Shape", "SectionPlane", QT_TRANSLATE_NOOP("App::Property", "The shape of this object"), locked=True, ) if not "Objects" in pl: obj.addProperty( "App::PropertyLinkList", "Objects", "SectionPlane", QT_TRANSLATE_NOOP( "App::Property", "The objects that must be considered by this section plane. Empty means the whole document.", ), locked=True, ) if not "OnlySolids" in pl: obj.addProperty( "App::PropertyBool", "OnlySolids", "SectionPlane", QT_TRANSLATE_NOOP( "App::Property", "If false, non-solids will be cut too, with possible wrong results.", ), locked=True, ) obj.OnlySolids = True if not "Clip" in pl: obj.addProperty( "App::PropertyBool", "Clip", "SectionPlane", QT_TRANSLATE_NOOP( "App::Property", "If True, resulting views will be clipped to the section plane area.", ), locked=True, ) if not "UseMaterialColorForFill" in pl: obj.addProperty( "App::PropertyBool", "UseMaterialColorForFill", "SectionPlane", QT_TRANSLATE_NOOP( "App::Property", "If true, the color of the objects material will be used to fill cut areas.", ), locked=True, ) obj.UseMaterialColorForFill = False if not "Depth" in pl: obj.addProperty( "App::PropertyLength", "Depth", "SectionPlane", QT_TRANSLATE_NOOP( "App::Property", "Geometry further than this value will be cut off. Keep zero for unlimited.", ), locked=True, ) def onDocumentRestored(self, obj): self.setProperties(obj) def execute(self, obj): import math import Part l = 1 h = 1 if obj.ViewObject: if hasattr(obj.ViewObject, "DisplayLength"): l = obj.ViewObject.DisplayLength.Value h = obj.ViewObject.DisplayHeight.Value elif hasattr(obj.ViewObject, "DisplaySize"): # old objects l = obj.ViewObject.DisplaySize.Value h = obj.ViewObject.DisplaySize.Value if not l: l = 1 if not h: h = 1 p = Part.makePlane(l, h, Vector(l / 2, -h / 2, 0), Vector(0, 0, -1)) # make sure the normal direction is pointing outwards, you never know what OCC will decide... # Apply the object's placement to the new plane first. p.Placement = obj.Placement # Now, check if the resulting plane's normal matches the placement's intended direction. # This robustly handles all rotation angles and potential OCC inconsistencies. target_normal = obj.Placement.Rotation.multVec(FreeCAD.Vector(0, 0, 1)) if p.normalAt(0, 0).getAngle(target_normal) > math.pi / 2: p.reverse() obj.Shape = p self.svgcache = None self.shapecache = None def getNormal(self, obj): return obj.Shape.Faces[0].normalAt(0, 0) def dumps(self): return None def loads(self, state): self.Type = "SectionPlane" class _ViewProviderSectionPlane: "A View Provider for Section Planes" def __init__(self, vobj): vobj.Proxy = self self.setProperties(vobj) def setProperties(self, vobj): pl = vobj.PropertiesList d = 0 if "DisplaySize" in pl: d = vobj.DisplaySize.Value vobj.removeProperty("DisplaySize") if not "DisplayLength" in pl: vobj.addProperty( "App::PropertyLength", "DisplayLength", "SectionPlane", QT_TRANSLATE_NOOP("App::Property", "The display length of this section plane"), locked=True, ) if d: vobj.DisplayLength = d else: vobj.DisplayLength = 1000 if not "DisplayHeight" in pl: vobj.addProperty( "App::PropertyLength", "DisplayHeight", "SectionPlane", QT_TRANSLATE_NOOP("App::Property", "The display height of this section plane"), locked=True, ) if d: vobj.DisplayHeight = d else: vobj.DisplayHeight = 1000 if not "ArrowSize" in pl: vobj.addProperty( "App::PropertyLength", "ArrowSize", "SectionPlane", QT_TRANSLATE_NOOP("App::Property", "The size of the arrows of this section plane"), locked=True, ) vobj.ArrowSize = 50 if not "Transparency" in pl: vobj.addProperty( "App::PropertyPercent", "Transparency", "SectionPlane", QT_TRANSLATE_NOOP("App::Property", "The transparency of this object"), locked=True, ) vobj.Transparency = 85 if not "LineWidth" in pl: vobj.addProperty( "App::PropertyFloat", "LineWidth", "SectionPlane", QT_TRANSLATE_NOOP("App::Property", "The line width of this object"), locked=True, ) vobj.LineWidth = 1 if not "CutDistance" in pl: vobj.addProperty( "App::PropertyLength", "CutDistance", "SectionPlane", QT_TRANSLATE_NOOP("App::Property", "Show the cut in the 3D view"), locked=True, ) if not "LineColor" in pl: vobj.addProperty( "App::PropertyColor", "LineColor", "SectionPlane", QT_TRANSLATE_NOOP("App::Property", "The color of this object"), locked=True, ) vobj.LineColor = ArchCommands.getDefaultColor("Helpers") if not "CutView" in pl: vobj.addProperty( "App::PropertyBool", "CutView", "SectionPlane", QT_TRANSLATE_NOOP("App::Property", "Show the cut in the 3D view"), locked=True, ) if not "CutMargin" in pl: vobj.addProperty( "App::PropertyLength", "CutMargin", "SectionPlane", QT_TRANSLATE_NOOP( "App::Property", "The distance between the cut plane and the actual view cut (keep this a very small value but not zero)", ), locked=True, ) vobj.CutMargin = 1 if not "ShowLabel" in pl: vobj.addProperty( "App::PropertyBool", "ShowLabel", "SectionPlane", QT_TRANSLATE_NOOP("App::Property", "Show the label in the 3D view"), locked=True, ) if not "FontName" in pl: vobj.addProperty( "App::PropertyFont", "FontName", "SectionPlane", QT_TRANSLATE_NOOP("App::Property", "The name of the font"), locked=True, ) vobj.FontName = params.get_param("textfont") if not "FontSize" in pl: vobj.addProperty( "App::PropertyLength", "FontSize", "SectionPlane", QT_TRANSLATE_NOOP("App::Property", "The size of the text font"), locked=True, ) vobj.FontSize = params.get_param("textheight") * params.get_param( "DefaultAnnoScaleMultiplier" ) def onDocumentRestored(self, vobj): self.setProperties(vobj) def getIcon(self): import Arch_rc return ":/icons/Arch_SectionPlane_Tree.svg" def claimChildren(self): # buggy at the moment so it's disabled - it will for ex. swallow a building object directly at the root of the document... # if hasattr(self,"Object") and hasattr(self.Object,"Objects"): # return self.Object.Objects return [] def attach(self, vobj): self.Object = vobj.Object self.clip = None self.mat1 = coin.SoMaterial() self.mat2 = coin.SoMaterial() self.fcoords = coin.SoCoordinate3() # fs = coin.SoType.fromName("SoBrepFaceSet").createInstance() # this causes a FreeCAD freeze for me fs = coin.SoIndexedFaceSet() fs.coordIndex.setValues(0, 7, [0, 1, 2, -1, 0, 2, 3]) self.drawstyle = coin.SoDrawStyle() self.drawstyle.style = coin.SoDrawStyle.LINES self.lcoords = coin.SoCoordinate3() import PartGui # Required for "SoBrepEdgeSet" (because a SectionPlane is not a Part::FeaturePython object). ls = coin.SoType.fromName("SoBrepEdgeSet").createInstance() ls.coordIndex.setValues( 0, 57, [ 0, 1, -1, 2, 3, 4, 5, -1, 6, 7, 8, 9, -1, 10, 11, -1, 12, 13, 14, 15, -1, 16, 17, 18, 19, -1, 20, 21, -1, 22, 23, 24, 25, -1, 26, 27, 28, 29, -1, 30, 31, -1, 32, 33, 34, 35, -1, 36, 37, 38, 39, -1, 40, 41, 42, 43, 44, ], ) self.txtcoords = coin.SoTransform() self.txtfont = coin.SoFont() self.txtfont.name = "" self.txt = coin.SoAsciiText() self.txt.justification = coin.SoText2.LEFT self.txt.string.setValue(" ") sep = coin.SoSeparator() psep = coin.SoSeparator() fsep = coin.SoSeparator() tsep = coin.SoSeparator() fsep.addChild(self.mat2) fsep.addChild(self.fcoords) fsep.addChild(fs) psep.addChild(self.mat1) psep.addChild(self.drawstyle) psep.addChild(self.lcoords) psep.addChild(ls) tsep.addChild(self.mat1) tsep.addChild(self.txtcoords) tsep.addChild(self.txtfont) tsep.addChild(self.txt) sep.addChild(fsep) sep.addChild(psep) sep.addChild(tsep) vobj.addDisplayMode(sep, "Default") self.onChanged(vobj, "DisplayLength") self.onChanged(vobj, "LineColor") self.onChanged(vobj, "Transparency") self.onChanged(vobj, "CutView") def getDisplayModes(self, vobj): return ["Default"] def getDefaultDisplayMode(self): return "Default" def setDisplayMode(self, mode): return mode def updateData(self, obj, prop): vobj = obj.ViewObject if prop in ["Placement"]: # for some reason the text doesn't rotate with the host placement?? self.txtcoords.rotation.setValue(obj.Placement.Rotation.Q) self.onChanged(vobj, "DisplayLength") # Defer the clipping plane update until after the current event # loop finishes. This ensures the scene graph has been updated with the # new placement before we try to recalculate the clip plane. if vobj and hasattr(vobj, "CutView") and vobj.CutView: from PySide import QtCore # We use a lambda to pass the vobj argument to the delayed function. QtCore.QTimer.singleShot(0, lambda: self.refreshCutView(vobj)) elif prop == "Label": if hasattr(obj.ViewObject, "ShowLabel") and obj.ViewObject.ShowLabel: self.txt.string = obj.Label return def refreshCutView(self, vobj): """ Forces a refresh of the SoClipPlane by toggling the CutView property. This is called with a delay to ensure the object's placement is up-to-date. """ if vobj and hasattr(vobj, "CutView") and vobj.CutView: vobj.CutView = False vobj.CutView = True def onChanged(self, vobj, prop): if prop == "LineColor": if hasattr(vobj, "LineColor"): l = vobj.LineColor self.mat1.diffuseColor.setValue([l[0], l[1], l[2]]) self.mat2.diffuseColor.setValue([l[0], l[1], l[2]]) elif prop == "Transparency": if hasattr(vobj, "Transparency"): self.mat2.transparency.setValue(vobj.Transparency / 100.0) elif prop in ["DisplayLength", "DisplayHeight", "ArrowSize"]: # for IFC objects: propagate to the object if prop in ["DisplayLength", "DisplayHeight"]: if hasattr(vobj.Object.Proxy, "onChanged"): vobj.Object.Proxy.onChanged(vobj.Object, prop) if hasattr(vobj, "DisplayLength") and hasattr(vobj, "DisplayHeight"): ld = vobj.DisplayLength.Value / 2 hd = vobj.DisplayHeight.Value / 2 elif hasattr(vobj, "DisplaySize"): # old objects ld = vobj.DisplaySize.Value / 2 hd = vobj.DisplaySize.Value / 2 else: ld = 1 hd = 1 verts = [] fverts = [] pl = FreeCAD.Placement(vobj.Object.Placement) if hasattr(vobj, "ArrowSize"): l1 = vobj.ArrowSize.Value if vobj.ArrowSize.Value > 0 else 0.1 else: l1 = 0.1 l2 = l1 / 3 for v in [[-ld, -hd], [ld, -hd], [ld, hd], [-ld, hd]]: p1 = pl.multVec(Vector(v[0], v[1], 0)) p2 = pl.multVec(Vector(v[0], v[1], -l1)) p3 = pl.multVec(Vector(v[0] - l2, v[1], -l1 + l2)) p4 = pl.multVec(Vector(v[0] + l2, v[1], -l1 + l2)) p5 = pl.multVec(Vector(v[0], v[1] - l2, -l1 + l2)) p6 = pl.multVec(Vector(v[0], v[1] + l2, -l1 + l2)) verts.extend([[p1.x, p1.y, p1.z], [p2.x, p2.y, p2.z]]) fverts.append([p1.x, p1.y, p1.z]) verts.extend( [[p2.x, p2.y, p2.z], [p3.x, p3.y, p3.z], [p4.x, p4.y, p4.z], [p2.x, p2.y, p2.z]] ) verts.extend( [[p2.x, p2.y, p2.z], [p5.x, p5.y, p5.z], [p6.x, p6.y, p6.z], [p2.x, p2.y, p2.z]] ) p7 = pl.multVec(Vector(-ld + l2, -hd + l2, 0)) # text pos verts.extend(fverts + [fverts[0]]) self.lcoords.point.setValues(verts) self.fcoords.point.setValues(fverts) self.txtcoords.translation.setValue([p7.x, p7.y, p7.z]) # self.txtfont.size = l1 elif prop == "LineWidth": self.drawstyle.lineWidth = vobj.LineWidth elif prop in ["CutView", "CutMargin"]: if ( hasattr(vobj, "CutView") and FreeCADGui.ActiveDocument.ActiveView and hasattr(FreeCADGui.ActiveDocument.ActiveView, "getSceneGraph") ): sg = FreeCADGui.ActiveDocument.ActiveView.getSceneGraph() if vobj.CutView: if self.clip: sg.removeChild(self.clip) self.clip = None for o in Draft.get_group_contents(vobj.Object.Objects, walls=True): if hasattr(o.ViewObject, "Lighting"): o.ViewObject.Lighting = "One side" self.clip = coin.SoClipPlane() self.clip.on.setValue(True) norm = vobj.Object.Proxy.getNormal(vobj.Object) mp = vobj.Object.Shape.CenterOfMass mp = DraftVecUtils.project(mp, norm) dist = mp.Length # - 0.1 # to not clip exactly on the section object norm = norm.negative() marg = 1 if hasattr(vobj, "CutMargin"): marg = vobj.CutMargin.Value if mp.getAngle(norm) > 1: dist += marg dist = -dist else: dist -= marg plane = coin.SbPlane(coin.SbVec3f(norm.x, norm.y, norm.z), dist) self.clip.plane.setValue(plane) sg.insertChild(self.clip, 0) else: if self.clip: sg.removeChild(self.clip) self.clip = None elif prop == "ShowLabel": if vobj.ShowLabel: self.txt.string = vobj.Object.Label or " " else: self.txt.string = " " elif prop == "FontName": if hasattr(self, "txtfont") and hasattr(vobj, "FontName"): if vobj.FontName: self.txtfont.name = vobj.FontName else: self.txtfont.name = "" elif prop == "FontSize": if hasattr(self, "txtfont") and hasattr(vobj, "FontSize"): self.txtfont.size = vobj.FontSize.Value return def dumps(self): return None def loads(self, state): return None def setEdit(self, vobj, mode): if mode != 0: return None taskd = SectionPlaneTaskPanel() taskd.obj = vobj.Object taskd.update() FreeCADGui.Control.showDialog(taskd) return True def unsetEdit(self, vobj, mode): if mode != 0: return None FreeCADGui.Control.closeDialog() return True def doubleClicked(self, vobj): self.edit() return True def setupContextMenu(self, vobj, menu): if FreeCADGui.activeWorkbench().name() != "BIMWorkbench": return actionEdit = QtGui.QAction(translate("Arch", "Edit"), menu) QtCore.QObject.connect(actionEdit, QtCore.SIGNAL("triggered()"), self.edit) menu.addAction(actionEdit) actionToggleCutview = QtGui.QAction( QtGui.QIcon(":/icons/Draft_Edit.svg"), translate("Arch", "Toggle Cutview"), menu ) actionToggleCutview.triggered.connect(lambda: self.toggleCutview(vobj)) menu.addAction(actionToggleCutview) def edit(self): FreeCADGui.ActiveDocument.setEdit(self.Object, 0) def toggleCutview(self, vobj): vobj.CutView = not vobj.CutView class SectionPlaneTaskPanel: """A TaskPanel for all the section plane object""" def __init__(self): # the panel has a tree widget that contains categories # for the subcomponents, such as additions, subtractions. # the categories are shown only if they are not empty. self.obj = None # Create the first box for object scope self.scope_widget = QtGui.QWidget() scope_layout = QtGui.QGridLayout(self.scope_widget) self.title = QtGui.QLabel(self.scope_widget) scope_layout.addWidget(self.title, 0, 0, 1, 2) # tree self.tree = QtGui.QTreeWidget(self.scope_widget) scope_layout.addWidget(self.tree, 1, 0, 1, 2) self.tree.setColumnCount(1) self.tree.header().hide() # add / remove buttons self.addButton = QtGui.QPushButton(self.scope_widget) self.addButton.setIcon(QtGui.QIcon(":/icons/Arch_Add.svg")) scope_layout.addWidget(self.addButton, 2, 0, 1, 1) self.delButton = QtGui.QPushButton(self.scope_widget) self.delButton.setIcon(QtGui.QIcon(":/icons/Arch_Remove.svg")) scope_layout.addWidget(self.delButton, 2, 1, 1, 1) self.delButton.setEnabled(False) # Create the second box for tools self.tools_widget = QtGui.QWidget() tools_layout = QtGui.QVBoxLayout(self.tools_widget) # Cut View toggle button self.cutViewButton = QtGui.QPushButton(self.tools_widget) self.cutViewButton.setIcon(QtGui.QIcon(":/icons/Arch_CutPlane.svg")) self.cutViewButton.setObjectName("cutViewButton") self.cutViewButton.setCheckable(True) QtCore.QObject.connect( self.cutViewButton, QtCore.SIGNAL("toggled(bool)"), self.toggleCutView ) tools_layout.addWidget(self.cutViewButton) # rotate / resize buttons self.rotation_label = QtGui.QLabel(self.tools_widget) tools_layout.addWidget(self.rotation_label) rotation_layout = QtGui.QHBoxLayout() self.rotateXButton = QtGui.QPushButton(self.tools_widget) self.rotateYButton = QtGui.QPushButton(self.tools_widget) self.rotateZButton = QtGui.QPushButton(self.tools_widget) rotation_layout.addWidget(self.rotateXButton) rotation_layout.addWidget(self.rotateYButton) rotation_layout.addWidget(self.rotateZButton) tools_layout.addLayout(rotation_layout) size_pos_layout = QtGui.QHBoxLayout() self.resizeButton = QtGui.QPushButton(self.tools_widget) self.recenterButton = QtGui.QPushButton(self.tools_widget) size_pos_layout.addWidget(self.resizeButton) size_pos_layout.addWidget(self.recenterButton) tools_layout.addLayout(size_pos_layout) QtCore.QObject.connect(self.addButton, QtCore.SIGNAL("clicked()"), self.addElement) QtCore.QObject.connect(self.delButton, QtCore.SIGNAL("clicked()"), self.removeElement) QtCore.QObject.connect(self.rotateXButton, QtCore.SIGNAL("clicked()"), self.rotateX) QtCore.QObject.connect(self.rotateYButton, QtCore.SIGNAL("clicked()"), self.rotateY) QtCore.QObject.connect(self.rotateZButton, QtCore.SIGNAL("clicked()"), self.rotateZ) QtCore.QObject.connect(self.resizeButton, QtCore.SIGNAL("clicked()"), self.resize) QtCore.QObject.connect(self.recenterButton, QtCore.SIGNAL("clicked()"), self.recenter) QtCore.QObject.connect(self.tree, QtCore.SIGNAL("itemSelectionChanged()"), self.onTreeClick) self.form = [self.scope_widget, self.tools_widget] self.update() def isAllowedAlterSelection(self): return True def isAllowedAlterView(self): return True def getStandardButtons(self): return QtGui.QDialogButtonBox.Close def getIcon(self, obj): if hasattr(obj.ViewObject, "Proxy"): return QtGui.QIcon(obj.ViewObject.Proxy.getIcon()) elif obj.isDerivedFrom("Sketcher::SketchObject"): return QtGui.QIcon(":/icons/Sketcher_Sketch.svg") elif obj.isDerivedFrom("App::DocumentObjectGroup"): return QtGui.QApplication.style().standardIcon(QtGui.QStyle.SP_DirIcon) elif hasattr(obj.ViewObject, "Icon"): return QtGui.QIcon(obj.ViewObject.Icon) return QtGui.QIcon(":/icons/Part_3D_object.svg") def update(self): "fills the treewidget" self.tree.clear() if self.obj: for o in self.obj.Objects: item = QtGui.QTreeWidgetItem(self.tree) item.setText(0, o.Label) item.setToolTip(0, o.Name) item.setIcon(0, self.getIcon(o)) if self.obj.ViewObject and hasattr(self.obj.ViewObject, "CutView"): self.cutViewButton.setChecked(self.obj.ViewObject.CutView) self.retranslateUi() def addElement(self): if self.obj: added = False for o in FreeCADGui.Selection.getSelection(): if o != self.obj: ArchComponent.addToComponent(self.obj, o, "Objects") added = True if added: self.update() else: FreeCAD.Console.PrintWarning( "Select objects in the 3D view or in the model tree before pressing the button\n" ) def removeElement(self): if self.obj: it = self.tree.currentItem() if it: comp = FreeCAD.ActiveDocument.getObject(str(it.toolTip(0))) ArchComponent.removeFromComponent(self.obj, comp) self.update() def rotate(self, axis): if self.obj and self.obj.Shape and self.obj.Shape.Faces: face = self.obj.Shape.copy() import Part local_axis = self.obj.Placement.Rotation.multVec(axis) face.rotate(self.obj.Placement.Base, local_axis, 90) self.obj.Placement = face.Placement self.obj.Proxy.execute(self.obj) def rotateX(self): self.rotate(FreeCAD.Vector(1, 0, 0)) def rotateY(self): self.rotate(FreeCAD.Vector(0, 1, 0)) def rotateZ(self): self.rotate(FreeCAD.Vector(0, 0, 1)) def getBB(self): bb = FreeCAD.BoundBox() if self.obj: for o in Draft.get_group_contents(self.obj.Objects): if hasattr(o, "Shape") and hasattr(o.Shape, "BoundBox"): bb.add(o.Shape.BoundBox) return bb def resize(self): if self.obj and self.obj.ViewObject: bb = self.getBB() n = self.obj.Proxy.getNormal(self.obj) margin = bb.XLength * 0.1 if (n.getAngle(FreeCAD.Vector(1, 0, 0)) < 0.1) or ( n.getAngle(FreeCAD.Vector(-1, 0, 0)) < 0.1 ): self.obj.ViewObject.DisplayLength = bb.YLength + margin self.obj.ViewObject.DisplayHeight = bb.ZLength + margin elif (n.getAngle(FreeCAD.Vector(0, 1, 0)) < 0.1) or ( n.getAngle(FreeCAD.Vector(0, -1, 0)) < 0.1 ): self.obj.ViewObject.DisplayLength = bb.XLength + margin self.obj.ViewObject.DisplayHeight = bb.ZLength + margin elif (n.getAngle(FreeCAD.Vector(0, 0, 1)) < 0.1) or ( n.getAngle(FreeCAD.Vector(0, 0, -1)) < 0.1 ): self.obj.ViewObject.DisplayLength = bb.XLength + margin self.obj.ViewObject.DisplayHeight = bb.YLength + margin self.obj.Proxy.execute(self.obj) def recenter(self): if self.obj: self.obj.Placement.Base = self.getBB().Center def onTreeClick(self): if self.tree.selectedItems(): self.delButton.setEnabled(True) else: self.delButton.setEnabled(False) def accept(self): FreeCAD.ActiveDocument.recompute() FreeCADGui.ActiveDocument.resetEdit() return True def reject(self): FreeCAD.ActiveDocument.recompute() FreeCADGui.ActiveDocument.resetEdit() return True def toggleCutView(self, checked): if self.obj and self.obj.ViewObject and hasattr(self.obj.ViewObject, "CutView"): self.obj.ViewObject.CutView = checked def retranslateUi(self): self.scope_widget.setWindowTitle(QtGui.QApplication.translate("Arch", "Scope", None)) self.tools_widget.setWindowTitle( QtGui.QApplication.translate("Arch", "Placement and Visuals", None) ) self.title.setText( QtGui.QApplication.translate("Arch", "Objects seen by this section plane", None) ) self.delButton.setText(QtGui.QApplication.translate("Arch", "Remove", None)) self.delButton.setToolTip( QtGui.QApplication.translate( "Arch", "Removes highlighted objects from the list above", None ) ) self.addButton.setText(QtGui.QApplication.translate("Arch", "Add Selected", None)) self.addButton.setToolTip( QtGui.QApplication.translate( "Arch", "Adds selected objects to the scope of this section plane", None ) ) self.cutViewButton.setText(QtGui.QApplication.translate("Arch", "Cut View", None)) self.cutViewButton.setToolTip( QtGui.QApplication.translate( "Arch", "Creates a live cut in the 3D view, hiding geometry on one side of the plane to see inside your model", None, ) ) self.rotation_label.setText(QtGui.QApplication.translate("Arch", "Rotate by 90°", None)) self.rotateXButton.setText(QtGui.QApplication.translate("Arch", "Rotate X", None)) self.rotateXButton.setToolTip( QtGui.QApplication.translate("Arch", "Rotates the plane around its local X-axis", None) ) self.rotateYButton.setText(QtGui.QApplication.translate("Arch", "Rotate Y", None)) self.rotateYButton.setToolTip( QtGui.QApplication.translate("Arch", "Rotates the plane around its local Y-axis", None) ) self.rotateZButton.setText(QtGui.QApplication.translate("Arch", "Rotate Z", None)) self.rotateZButton.setToolTip( QtGui.QApplication.translate("Arch", "Rotates the plane around its local Z-axis", None) ) self.resizeButton.setText(QtGui.QApplication.translate("Arch", "Resize to Fit", None)) self.resizeButton.setToolTip( QtGui.QApplication.translate( "Arch", "Resizes the plane to fit the objects in the list above", None ) ) self.recenterButton.setText(QtGui.QApplication.translate("Arch", "Recenter Plane", None)) self.recenterButton.setToolTip( QtGui.QApplication.translate( "Arch", "Centers the plane on the objects in the list above", None ) )