# SPDX-License-Identifier: LGPL-2.1-or-later

# ***************************************************************************
# *                                                                         *
# *   Copyright (c) 2011 Yorik van Havre <yorik@uncreated.net>              *
# *                                                                         *
# *   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                               *
# *   <https://www.gnu.org/licenses/>.                                      *
# *                                                                         *
# ***************************************************************************

## @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 gui_utils
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 = '<pattern id="sectionfill" patternUnits="userSpaceOnUse" patternTransform="matrix(5,0,0,5,0,0)"'
    # fillpattern += ' x="0" y="0" width="10" height="10">'
    # fillpattern += '<g>'
    # fillpattern += '<rect width="10" height="10" style="stroke:none; fill:#ffffff" /><path style="stroke:#000000; stroke-width:1" d="M0,0 l10,10" /></g></pattern>'
    svgLineColor = Draft.getrgb(lineColor)
    svg = ""
    # reading cached version
    svgcache = update_svg_cache(
        source, renderMode, showHidden, showFill, fillSpaces, joinArch, allOn, objs
    )
    should_update_svg_cache = False
    if showFill or not svgcache:
        should_update_svg_cache = True

    # generating SVG
    if renderMode in ["Coin", 2, "Coin mono", 3]:
        # render using a coin viewer
        if hasattr(source.ViewObject, "ViewData") and source.ViewObject.ViewData:
            cameradata = None  # getCameraData(source.ViewObject.ViewData)
        else:
            cameradata = None
        if should_update_svg_cache:
            if renderMode in ["Coin mono", 3]:
                svgcache = getCoinSVG(
                    cutplane, objs, cameradata, linewidth="SVGLINEWIDTH", facecolor="#ffffff"
                )
            else:
                svgcache = getCoinSVG(cutplane, objs, cameradata, linewidth="SVGLINEWIDTH")
    elif renderMode in ["Solid", 1]:
        if should_update_svg_cache:
            svgcache = ""
            # render using the Arch Vector Renderer
            import ArchVRM
            import WorkingPlane

            wp = WorkingPlane.PlaneBase()
            pl = FreeCAD.Placement(source.Placement)
            if source.ViewObject and hasattr(source.ViewObject, "CutMargin"):
                mv = pl.multVec(FreeCAD.Vector(0, 0, 1))
                mv.multiply(source.ViewObject.CutMargin)
                pl.move(mv)
            wp.align_to_rotation(pl.Rotation)
            # wp.inverse()
            render = ArchVRM.Renderer()
            render.setWorkingPlane(wp)
            render.addObjects(objs)
            if showHidden:
                render.cut(cutplane, showHidden)
            else:
                render.cut(cutplane)
            g = '<g transform="scale(1,-1)">\n'
            if hasattr(source.ViewObject, "RotateSolidRender"):
                if source.ViewObject.RotateSolidRender.Value != 0:
                    g = '<g transform="scale(1,-1) rotate('
                    g += str(source.ViewObject.RotateSolidRender.Value)
                    g += ')">\n'
            svgcache += g
            svgcache += render.getViewSVG(linewidth="SVGLINEWIDTH")
            # svgcache += fillpattern
            svgcache += render.getSectionSVG(linewidth="SVGCUTLINEWIDTH", fillpattern="#ffffff")
            if showHidden:
                svgcache += render.getHiddenSVG(linewidth="SVGLINEWIDTH")
            svgcache += "</g>\n"
            # print(render.info())
    else:
        # Wireframe (0) mode

        if (
            hasattr(source, "Proxy")
            and hasattr(source.Proxy, "shapecache")
            and source.Proxy.shapecache
        ):
            vshapes = source.Proxy.shapecache[0]
            hshapes = source.Proxy.shapecache[1]
            sshapes = source.Proxy.shapecache[2]
            cutface = source.Proxy.shapecache[3]
            # cutvolume = source.Proxy.shapecache[4] # Unused
            # invcutvolume = source.Proxy.shapecache[5] # Unused
            objectSshapes = source.Proxy.shapecache[6]
        else:
            if showFill:
                vshapes, hshapes, sshapes, cutface, cutvolume, invcutvolume, objectSshapes = (
                    getCutShapes(objs, cutplane, onlySolids, clip, joinArch, showHidden, True)
                )
            else:
                vshapes, hshapes, sshapes, cutface, cutvolume, invcutvolume = getCutShapes(
                    objs, cutplane, onlySolids, clip, joinArch, showHidden
                )
                objectSshapes = []
            source.Proxy.shapecache = [
                vshapes,
                hshapes,
                sshapes,
                cutface,
                cutvolume,
                invcutvolume,
                objectSshapes,
            ]

        if should_update_svg_cache:
            svgcache = ""
            # render using the TechDraw module
            import TechDraw
            import Part

            if vshapes:
                baseshape = Part.makeCompound(vshapes)
                style = {"stroke": "SVGLINECOLOR", "stroke-width": "SVGLINEWIDTH"}
                svgcache += TechDraw.projectToSVG(
                    baseshape,
                    direction,
                    hStyle=style,
                    h0Style=style,
                    h1Style=style,
                    vStyle=style,
                    v0Style=style,
                    v1Style=style,
                )
            if hshapes:
                hshapes = Part.makeCompound(hshapes)
                style = {
                    "stroke": "SVGLINECOLOR",
                    "stroke-width": "SVGLINEWIDTH",
                    "stroke-dasharray": "SVGHIDDENPATTERN",
                }
                svgcache += TechDraw.projectToSVG(
                    hshapes,
                    direction,
                    hStyle=style,
                    h0Style=style,
                    h1Style=style,
                    vStyle=style,
                    v0Style=style,
                    v1Style=style,
                )
            if sshapes:
                if showFill:
                    # svgcache += fillpattern
                    svgcache += '<g transform="rotate(180)">\n'
                    for o, shapes in objectSshapes:
                        for s in shapes:
                            if s.Edges:
                                objectFill = getFillForObject(o, fillColor, source)
                                # svg += Draft.get_svg(s,
                                #                      direction=direction.negative(),
                                #                      linewidth=0,
                                #                      fillstyle="sectionfill",
                                #                      color=(0,0,0))
                                # temporarily disabling fill patterns
                                svgcache += Draft.get_svg(
                                    s,
                                    linewidth=0,
                                    fillstyle=Draft.getrgb(objectFill, testbw=False),
                                    direction=direction.negative(),
                                    color=lineColor,
                                )
                    svgcache += "</g>\n"
                sshapes = Part.makeCompound(sshapes)
                style = {"stroke": "SVGLINECOLOR", "stroke-width": "SVGCUTLINEWIDTH"}
                svgcache += TechDraw.projectToSVG(
                    sshapes,
                    direction,
                    hStyle=style,
                    h0Style=style,
                    h1Style=style,
                    vStyle=style,
                    v0Style=style,
                    v1Style=style,
                )
    if should_update_svg_cache:
        if hasattr(source, "Proxy"):
            source.Proxy.svgcache = [
                svgcache,
                renderMode,
                showHidden,
                showFill,
                fillSpaces,
                joinArch,
                allOn,
                set(objs),
            ]

    svgcache = svgcache.replace("SVGLINECOLOR", svgLineColor)
    svgcache = svgcache.replace("SVGLINEWIDTH", svgLineWidth)
    svgcache = svgcache.replace("SVGHIDDENPATTERN", svgHiddenPattern)
    svgcache = svgcache.replace("SVGCUTLINEWIDTH", svgCutLineWidth)
    svg += svgcache

    if drafts:
        if not techdraw:
            svg += '<g transform="scale(1,-1)">'
        for d in drafts:
            svg += Draft.get_svg(
                d,
                scale=scale,
                linewidth=svgSymbolLineWidth,
                fontsize=fontsize,
                linespacing=linespacing,
                direction=direction,
                color=lineColor,
                techdraw=techdraw,
                rotation=rotation,
                override=False,
            )
        if not techdraw:
            svg += "</g>"

    if not cutface:
        # if we didn't calculate anything better, use the cutplane...
        cutface = cutplane

    # filter out spaces not cut by the source plane
    if cutface and spaces:
        spaces = [s for s in spaces if s.Shape.BoundBox.intersect(cutface.BoundBox)]
    if spaces:
        if not techdraw:
            svg += '<g transform="scale(1,-1)">'
        for s in spaces:
            svg += Draft.get_svg(
                s,
                scale=scale,
                linewidth=svgSymbolLineWidth,
                fontsize=fontsize,
                linespacing=linespacing,
                direction=direction,
                color=lineColor,
                techdraw=techdraw,
                rotation=rotation,
                fillspaces=fillSpaces,
            )
        if not techdraw:
            svg += "</g>"

    # add additional edge symbols from windows
    cutwindows = []
    if cutface and windows and BoundBoxValid(cutface.BoundBox):
        cutwindows = [
            w.Name
            for w in windows
            if BoundBoxValid(w.Shape.BoundBox) and w.Shape.BoundBox.intersect(cutface.BoundBox)
        ]
    if windows:
        sh = []
        for w in windows:
            if w.Name in cutwindows:
                wlo = w.getLinkedObject()  # To support Link of Windows(Doors)
                if hasattr(wlo, "SymbolPlan") and wlo.SymbolPlan:
                    if not hasattr(wlo.Proxy, "sshapes"):
                        wlo.Proxy.execute(wlo)
                    if hasattr(wlo.Proxy, "sshapes") and wlo.Proxy.sshapes:
                        c = Part.makeCompound(wlo.Proxy.sshapes)
                        c.Placement = w.Placement
                        sh.append(c)
        if sh:
            if not techdraw:
                svg += '<g transform="scale(1,-1)">'
            for s in sh:
                svg += Draft.get_svg(
                    s,
                    scale=scale,
                    linewidth=svgSymbolLineWidth,
                    fontsize=fontsize,
                    linespacing=linespacing,
                    fillstyle="none",
                    direction=direction,
                    color=lineColor,
                    techdraw=techdraw,
                    rotation=rotation,
                )
            if not techdraw:
                svg += "</g>"

    return svg


def BoundBoxValid(boundBox) -> 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
    # <line x1="284.986" y1="356.166" x2="285.038" y2="356.166" stroke="#ffffff" stroke-width="1px" />
    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"<line x1=.*?stroke=\"\#ffffff\".*?\/>", 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"<line x1=.*?stroke=\"\#ffffff\".*?\/>", "", svg, count=1)

    # remove background rectangle
    svg = re.sub(r"<path.*?>", "", 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(
                "<g>",
                '<g transform="translate('
                + str(trans.x)
                + " "
                + str(trans.y)
                + ") scale("
                + str(factor)
                + ","
                + str(factor)
                + ')">\n<g>',
                1,
            )
        else:
            svg = svg.replace(
                "<g>", '<g transform="scale(' + str(factor) + "," + str(factor) + ')">\n<g>', 1
            )
        svg = svg.replace("</svg>", "</g>\n</svg>")

    # 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.*?>", "", 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.main_transform = gui_utils.find_coin_node(vobj.RootNode, coin.SoTransform)
        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"]:
            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
            if hasattr(vobj, "ArrowSize"):
                l1 = vobj.ArrowSize.Value if vobj.ArrowSize.Value > 0 else 0.1
            else:
                l1 = 0.1
            l2 = l1 / 3
            pl = vobj.Object.Placement
            self.main_transform.translation.setValue(pl.Base)
            self.main_transform.rotation = coin.SbRotation(pl.Rotation.Q)
            verts = []
            fverts = []
            for v in [[-ld, -hd], [ld, -hd], [ld, hd], [-ld, hd]]:
                p1 = Vector(v[0], v[1], 0)
                p2 = Vector(v[0], v[1], -l1)
                p3 = Vector(v[0] - l2, v[1], -l1 + l2)
                p4 = Vector(v[0] + l2, v[1], -l1 + l2)
                p5 = Vector(v[0], v[1] - l2, -l1 + l2)
                p6 = Vector(v[0], v[1] + l2, -l1 + l2)
                fverts.append(p1)
                verts.extend([p1, p2])
                verts.extend([p2, p3, p4, p2])
                verts.extend([p2, p5, p6, p2])
            verts.extend(fverts + [fverts[0]])
            p7 = Vector(-ld + l2, -hd + l2, 0)  # text pos
            self.lcoords.point.setValues(verts)
            self.fcoords.point.setValues(fverts)
            self.txtcoords.translation.setValue(p7)
            # 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
            )
        )
