# 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/>.                                      *
# *                                                                         *
# ***************************************************************************

__title__ = "FreeCAD Panel"
__author__ = "Yorik van Havre"
__url__ = "https://www.freecad.org"

## @package ArchPanel
#  \ingroup ARCH
#  \brief The Panel object and tools
#
#  This module provides tools to build Panel objects.
#  Panels consist of a closed shape that gets extruded to
#  produce a flat object.

import math

import FreeCAD
import ArchCommands
import ArchComponent
import Draft
import DraftVecUtils
import Part

from FreeCAD import Vector
from draftutils import params

if FreeCAD.GuiUp:
    from PySide import QtCore, QtGui
    from PySide.QtCore import QT_TRANSLATE_NOOP
    import FreeCADGui
    from draftutils.translate import translate
else:
    # \cond
    def translate(ctxt, txt):
        return txt

    def QT_TRANSLATE_NOOP(ctxt, txt):
        return txt

    # \endcond


class _Panel(ArchComponent.Component):
    "The Panel object"

    def __init__(self, obj):

        ArchComponent.Component.__init__(self, obj)
        self.Type = "Panel"
        self.setProperties(obj)
        obj.IfcType = "Plate"

    def setProperties(self, obj):

        pl = obj.PropertiesList
        if not "Length" in pl:
            obj.addProperty(
                "App::PropertyLength",
                "Length",
                "Panel",
                QT_TRANSLATE_NOOP(
                    "App::Property", "The length of this element, if not based on a profile"
                ),
                locked=True,
            )
        if not "Width" in pl:
            obj.addProperty(
                "App::PropertyLength",
                "Width",
                "Panel",
                QT_TRANSLATE_NOOP(
                    "App::Property", "The width of this element, if not based on a profile"
                ),
                locked=True,
            )
        if not "Thickness" in pl:
            obj.addProperty(
                "App::PropertyLength",
                "Thickness",
                "Panel",
                QT_TRANSLATE_NOOP(
                    "App::Property", "The thickness or extrusion depth of this element"
                ),
                locked=True,
            )
        if not "Sheets" in pl:
            obj.addProperty(
                "App::PropertyInteger",
                "Sheets",
                "Panel",
                QT_TRANSLATE_NOOP("App::Property", "The number of sheets to use"),
                locked=True,
            )
            obj.Sheets = 1
        if not "Offset" in pl:
            obj.addProperty(
                "App::PropertyDistance",
                "Offset",
                "Panel",
                QT_TRANSLATE_NOOP(
                    "App::Property", "The offset between this panel and its baseline"
                ),
                locked=True,
            )
        if not "WaveLength" in pl:
            obj.addProperty(
                "App::PropertyLength",
                "WaveLength",
                "Panel",
                QT_TRANSLATE_NOOP("App::Property", "The length of waves for corrugated elements"),
                locked=True,
            )
        if not "WaveHeight" in pl:
            obj.addProperty(
                "App::PropertyLength",
                "WaveHeight",
                "Panel",
                QT_TRANSLATE_NOOP("App::Property", "The height of waves for corrugated elements"),
                locked=True,
            )
        if not "WaveOffset" in pl:
            obj.addProperty(
                "App::PropertyDistance",
                "WaveOffset",
                "Panel",
                QT_TRANSLATE_NOOP(
                    "App::Property", "The horizontal offset of waves for corrugated elements"
                ),
                locked=True,
            )
        if not "WaveDirection" in pl:
            obj.addProperty(
                "App::PropertyAngle",
                "WaveDirection",
                "Panel",
                QT_TRANSLATE_NOOP(
                    "App::Property", "The direction of waves for corrugated elements"
                ),
                locked=True,
            )
        if not "WaveType" in pl:
            obj.addProperty(
                "App::PropertyEnumeration",
                "WaveType",
                "Panel",
                QT_TRANSLATE_NOOP("App::Property", "The type of waves for corrugated elements"),
                locked=True,
            )
            obj.WaveType = ["Curved", "Trapezoidal", "Spikes"]
        if not "WaveBottom" in pl:
            obj.addProperty(
                "App::PropertyBool",
                "WaveBottom",
                "Panel",
                QT_TRANSLATE_NOOP(
                    "App::Property", "If the wave also affects the bottom side or not"
                ),
                locked=True,
            )
        if not "Area" in pl:
            obj.addProperty(
                "App::PropertyArea",
                "Area",
                "Panel",
                QT_TRANSLATE_NOOP("App::Property", "The area of this panel"),
                locked=True,
            )
        if not "FaceMaker" in pl:
            obj.addProperty(
                "App::PropertyEnumeration",
                "FaceMaker",
                "Panel",
                QT_TRANSLATE_NOOP(
                    "App::Property", "The facemaker type to use to build the profile of this object"
                ),
                locked=True,
            )
            obj.FaceMaker = ["None", "Simple", "Cheese", "Bullseye"]
        if not "Normal" in pl:
            obj.addProperty(
                "App::PropertyVector",
                "Normal",
                "Panel",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "The normal extrusion direction of this object (keep (0,0,0) for automatic normal)",
                ),
                locked=True,
            )
        obj.setEditorMode("VerticalArea", 2)
        obj.setEditorMode("HorizontalArea", 2)

    def onDocumentRestored(self, obj):

        ArchComponent.Component.onDocumentRestored(self, obj)
        self.setProperties(obj)

    def loads(self, state):

        self.Type = "Panel"

    def execute(self, obj):
        "creates the panel shape"

        if self.clone(obj):
            return

        layers = []
        length = 0
        width = 0
        thickness = 0

        # base tests
        if obj.Base:
            if hasattr(obj.Base, "Shape"):
                if obj.Base.Shape.isNull():
                    return
            elif obj.Base.isDerivedFrom("Mesh::Feature"):
                if not obj.Base.Mesh.isSolid():
                    return
        else:
            if obj.Length.Value:
                length = obj.Length.Value
            else:
                return
            if obj.Width.Value:
                width = obj.Width.Value
            else:
                return
        if obj.Thickness.Value:
            thickness = obj.Thickness.Value
        else:
            if not obj.Base:
                return
            elif hasattr(obj.Base, "Shape"):
                if not obj.Base.Shape.Solids:
                    return
        if hasattr(obj, "Material"):
            if obj.Material:
                if hasattr(obj.Material, "Materials"):
                    varwidth = 0
                    thicknesses = [t for t in obj.Material.Thicknesses if t >= 0]
                    restwidth = thickness - sum(thicknesses)
                    if restwidth > 0:
                        varwidth = [t for t in thicknesses if t == 0]
                        if varwidth:
                            varwidth = restwidth / len(varwidth)
                    for t in obj.Material.Thicknesses:
                        if t:
                            layers.append(t)
                        elif varwidth:
                            layers.append(varwidth)
        # creating base shape
        pl = obj.Placement
        base = None
        normal = None
        if hasattr(obj, "Normal"):
            if obj.Normal.Length > 0:
                normal = Vector(obj.Normal)
                normal.normalize()
                normal.multiply(thickness)
        baseprofile = None
        if obj.Base:
            base = obj.Base.Shape.copy()
            if not base.Solids:
                # p = FreeCAD.Placement(obj.Base.Placement)
                if base.Faces:
                    baseprofile = base
                    if not normal:
                        normal = baseprofile.Faces[0].normalAt(0, 0).multiply(thickness)
                    if layers:
                        layeroffset = 0
                        shps = []
                        for l in layers:
                            if l >= 0:
                                n = Vector(normal).normalize().multiply(abs(l))
                                b = base.extrude(n)
                                if layeroffset:
                                    o = Vector(normal).normalize().multiply(layeroffset)
                                    b.translate(o)
                                shps.append(b)
                            layeroffset += abs(l)
                        base = Part.makeCompound(shps)
                    else:
                        base = base.extrude(normal)
                elif base.Wires:
                    fm = False
                    if hasattr(obj, "FaceMaker"):
                        if obj.FaceMaker != "None":
                            try:
                                baseprofile = Part.makeFace(
                                    base.Wires, "Part::FaceMaker" + str(obj.FaceMaker)
                                )
                                fm = True
                            except Exception:
                                FreeCAD.Console.PrintError(
                                    translate("Arch", "Facemaker returned an error") + "\n"
                                )
                                return
                    if not fm:
                        closed = True
                        for w in base.Wires:
                            if not w.isClosed():
                                closed = False
                        if closed:
                            baseprofile = ArchCommands.makeFace(base.Wires)
                    if not normal:
                        normal = baseprofile.normalAt(0, 0).multiply(thickness)
                    if layers:
                        layeroffset = 0
                        shps = []
                        for l in layers:
                            if l >= 0:
                                n = Vector(normal).normalize().multiply(abs(l))
                                b = baseprofile.extrude(n)
                                if layeroffset:
                                    o = Vector(normal).normalize().multiply(layeroffset)
                                    b.translate(o)
                                shps.append(b)
                            layeroffset += abs(l)
                        base = Part.makeCompound(shps)
                    else:
                        base = baseprofile.extrude(normal)
                elif obj.Base.isDerivedFrom("Mesh::Feature"):
                    if obj.Base.Mesh.isSolid():
                        if obj.Base.Mesh.countComponents() == 1:
                            sh = ArchCommands.getShapeFromMesh(obj.Base.Mesh)
                            if sh.isClosed() and sh.isValid() and sh.Solids:
                                base = sh
        else:
            if layers:
                shps = []
                layeroffset = 0
                for l in layers:
                    if l >= 0:
                        if normal:
                            n = Vector(normal).normalize().multiply(l)
                        else:
                            n = Vector(0, 0, 1).multiply(abs(l))
                        l2 = length / 2 or 0.5
                        w2 = width / 2 or 0.5
                        v1 = Vector(-l2, -w2, layeroffset)
                        v2 = Vector(l2, -w2, layeroffset)
                        v3 = Vector(l2, w2, layeroffset)
                        v4 = Vector(-l2, w2, layeroffset)
                        base = Part.makePolygon([v1, v2, v3, v4, v1])
                        baseprofile = Part.Face(base)
                        base = baseprofile.extrude(n)
                        shps.append(base)
                    layeroffset += abs(l)
                base = Part.makeCompound(shps)
            else:
                if not normal:
                    normal = Vector(0, 0, 1).multiply(thickness)
                l2 = length / 2 or 0.5
                w2 = width / 2 or 0.5
                v1 = Vector(-l2, -w2, 0)
                v2 = Vector(l2, -w2, 0)
                v3 = Vector(l2, w2, 0)
                v4 = Vector(-l2, w2, 0)
                base = Part.makePolygon([v1, v2, v3, v4, v1])
                baseprofile = Part.Face(base)
                base = baseprofile.extrude(normal)

        if hasattr(obj, "Area"):
            if baseprofile:
                obj.Area = baseprofile.Area

        if hasattr(obj, "WaveLength"):
            if baseprofile and obj.WaveLength.Value and obj.WaveHeight.Value:
                # corrugated element
                bb = baseprofile.BoundBox
                bb.enlarge(bb.DiagonalLength)
                downsegment = None
                if hasattr(obj, "WaveBottom"):
                    if not obj.WaveBottom:
                        if obj.WaveType == "Curved":
                            if obj.Thickness.Value > obj.WaveHeight.Value:
                                downsegment = obj.Thickness.Value
                            else:
                                downsegment = obj.WaveHeight.Value + obj.Thickness.Value
                        else:
                            downsegment = obj.Thickness.Value
                p1 = Vector(0, 0, 0)
                p5 = Vector(obj.WaveLength.Value * 2, 0, 0)
                if obj.WaveType == "Curved":
                    p2 = Vector(obj.WaveLength.Value / 2, 0, obj.WaveHeight.Value)
                    p3 = Vector(obj.WaveLength.Value, 0, 0)
                    e1 = Part.Arc(p1, p2, p3).toShape()
                    p4 = Vector(obj.WaveLength.Value * 1.5, 0, -obj.WaveHeight.Value)
                    e2 = Part.Arc(p3, p4, p5).toShape()
                    upsegment = Part.Wire([e1, e2])
                    if not downsegment:
                        if obj.Thickness.Value < e1.Curve.Radius:
                            c3 = e1.Curve.copy()
                            c3.Radius = e1.Curve.Radius - obj.Thickness.Value
                            e3 = Part.Arc(c3, e1.FirstParameter, e1.LastParameter).toShape()
                            c4 = e2.Curve.copy()
                            c4.Radius = e2.Curve.Radius + obj.Thickness.Value
                            e4 = Part.Arc(c4, e2.FirstParameter, e2.LastParameter).toShape()
                            downsegment = Part.Wire([e3, e4])
                        else:
                            r = e2.Curve.Radius + obj.Thickness.Value
                            z = math.sqrt(r**2 - obj.WaveLength.Value**2)
                            p6 = e2.Curve.Center.add(Vector(-obj.WaveLength, 0, -z))
                            p7 = e2.Curve.Center.add(Vector(0, 0, -r))
                            p8 = e2.Curve.Center.add(Vector(obj.WaveLength, 0, -z))
                            downsegment = Part.Arc(p6, p7, p8).toShape()

                elif obj.WaveType == "Trapezoidal":
                    p2 = Vector(obj.WaveLength.Value / 4, 0, obj.WaveHeight.Value)
                    p3 = Vector(obj.WaveLength.Value, 0, obj.WaveHeight.Value)
                    p4 = Vector(obj.WaveLength.Value * 1.25, 0, 0)
                    upsegment = Part.makePolygon([p1, p2, p3, p4, p5])
                    if not downsegment:
                        a = ((p1.sub(p2)).getAngle(p3.sub(p2))) / 2
                        tx = obj.Thickness.Value / math.tan(a)
                        d1 = Vector(tx, 0, -obj.Thickness.Value)
                        d2 = Vector(-tx, 0, -obj.Thickness.Value)
                        p6 = p1.add(d1)
                        if tx >= p3.sub(p2).Length / 2:
                            d3 = p2.sub(p1)
                            d3.normalize()
                            d3.multiply((0.625 * obj.WaveLength.Value) / d3.x)
                            d4 = Vector(d3.x, 0, -d3.z)
                            p7 = p6.add(d3)
                            p8 = p7.add(d4)
                            p9 = p5.add(d1)
                            downsegment = Part.makePolygon([p6, p7, p8, p9])
                        elif tx <= 0.625 * obj.WaveLength.Value:
                            p7 = p2.add(d1)
                            p8 = p3.add(d2)
                            p9 = p4.add(d2)
                            p10 = p5.add(d1)
                            downsegment = Part.makePolygon([p6, p7, p8, p9, p10])
                        else:
                            downsegment = obj.Thickness.Value

                else:  # spike
                    p2 = Vector(obj.WaveHeight.Value, 0, obj.WaveHeight.Value)
                    p3 = Vector(obj.WaveHeight.Value * 2, 0, 0)
                    upsegment = Part.makePolygon([p1, p2, p3, p5])
                    if not downsegment:
                        downsegment = obj.Thickness.Value

                upsegment.translate(Vector(bb.getPoint(0).x, bb.getPoint(0).y, bb.Center.z))
                if isinstance(downsegment, Part.Shape):
                    downsegment.translate(Vector(bb.getPoint(0).x, bb.getPoint(0).y, bb.Center.z))
                if hasattr(obj, "WaveOffset"):
                    if obj.WaveOffset.Value:
                        upsegment.translate(Vector(obj.WaveOffset.Value, 0, 0))
                        if isinstance(downsegment, Part.Shape):
                            downsegment.translate(Vector(obj.WaveOffset.Value, 0, 0))

                upedges = []
                downedges = []
                for i in range(int(bb.XLength / (obj.WaveLength.Value * 2))):
                    w1 = upsegment.copy()
                    w1.translate(Vector(obj.WaveLength.Value * 2 * i, 0, 0))
                    upedges.extend(w1.Edges)
                    if isinstance(downsegment, Part.Shape):
                        w2 = downsegment.copy()
                        w2.translate(Vector(obj.WaveLength.Value * 2 * i, 0, 0))
                        downedges.extend(w2.Edges)
                upwire = Part.Wire(upedges)
                FreeCAD.upwire = upwire  # REMOVE
                if isinstance(downsegment, Part.Shape):
                    downwire = Part.Wire(downedges)
                    FreeCAD.downwire = downwire  # REMOVE
                    e1 = Part.LineSegment(
                        upwire.Vertexes[0].Point, downwire.Vertexes[0].Point
                    ).toShape()
                    e2 = Part.LineSegment(
                        upwire.Vertexes[-1].Point, downwire.Vertexes[-1].Point
                    ).toShape()
                    basewire = Part.Wire(upwire.Edges + [e1, e2] + downwire.Edges)
                else:
                    z = obj.Thickness.Value
                    if obj.WaveType == "Curved":
                        z += obj.WaveHeight.Value
                    p1 = upwire.Vertexes[0].Point
                    p2 = p1.add(Vector(0, 0, -z))
                    p3 = Vector(upwire.Vertexes[-1].Point.x, upwire.Vertexes[-1].Point.y, p2.z)
                    p4 = upwire.Vertexes[-1].Point
                    w = Part.makePolygon([p1, p2, p3, p4])
                    basewire = Part.Wire(upwire.Edges + w.Edges)

                FreeCAD.basewire = basewire
                if not basewire.isClosed():
                    print("Error closing base wire - check FreeCAD.basewire")
                    return

                baseface = Part.Face(basewire)
                base = baseface.extrude(Vector(0, bb.YLength, 0))
                if normal.cross(FreeCAD.Vector(0, 0, 1)).Length > 1e-6:
                    rot = FreeCAD.Rotation(FreeCAD.Vector(0, -1, 0), FreeCAD.Vector(*normal[:2], 0))
                    base.rotate(bb.Center, rot.Axis, math.degrees(rot.Angle))
                rot = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), normal)
                base.rotate(bb.Center, rot.Axis, math.degrees(rot.Angle))
                if obj.WaveDirection.Value:
                    base.rotate(bb.Center, normal, obj.WaveDirection.Value)
                n1 = normal.negative().normalize().multiply(obj.WaveHeight.Value * 2)
                self.vol = baseprofile.copy()
                self.vol.translate(n1)
                self.vol = self.vol.extrude(n1.negative().multiply(2))
                base = self.vol.common(base)
                base = base.removeSplitter()
                if not base:
                    FreeCAD.Console.PrintError(
                        translate("Arch", "Error computing shape of") + " " + obj.Label + "\n"
                    )
                    return False

        if base and (obj.Sheets > 1) and normal and thickness:
            bases = [base]
            for i in range(1, obj.Sheets):
                n = FreeCAD.Vector(normal).normalize().multiply(i * thickness)
                b = base.copy()
                b.translate(n)
                bases.append(b)
            base = Part.makeCompound(bases)

        if base and normal and hasattr(obj, "Offset"):
            if obj.Offset.Value:
                v = DraftVecUtils.scaleTo(normal, obj.Offset.Value)
                base.translate(v)

        # process subshapes
        base = self.processSubShapes(obj, base, pl)

        # applying
        if base:
            if not base.isNull():
                if base.isValid() and base.Solids:
                    if len(base.Solids) == 1:
                        if base.Volume < 0:
                            base.reverse()
                        if base.Volume < 0:
                            FreeCAD.Console.PrintError(
                                translate("Arch", "Could not compute a shape")
                            )
                            return
                        base = base.removeSplitter()
                    obj.Shape = base
                    if not pl.isNull():
                        obj.Placement = pl


class _ViewProviderPanel(ArchComponent.ViewProviderComponent):
    "A View Provider for the Panel object"

    def __init__(self, vobj):

        ArchComponent.ViewProviderComponent.__init__(self, vobj)
        vobj.ShapeColor = ArchCommands.getDefaultColor("Panel")

    def getIcon(self):

        # import Arch_rc
        if hasattr(self, "Object"):
            if hasattr(self.Object, "CloneOf"):
                if self.Object.CloneOf:
                    return ":/icons/Arch_Panel_Clone.svg"
        return ":/icons/Arch_Panel_Tree.svg"

    def updateData(self, obj, prop):

        if prop in ["Placement", "Shape", "Material"]:
            if hasattr(obj, "Material"):
                if obj.Material:
                    if hasattr(obj.Material, "Materials"):
                        activematerials = [
                            obj.Material.Materials[i]
                            for i in range(len(obj.Material.Materials))
                            if obj.Material.Thicknesses[i] >= 0
                        ]
                        if len(activematerials) == len(obj.Shape.Solids):
                            cols = []
                            for i, mat in enumerate(activematerials):
                                c = obj.ViewObject.ShapeColor
                                c = (c[0], c[1], c[2], 1.0 - obj.ViewObject.Transparency / 100.0)
                                if "DiffuseColor" in mat.Material:
                                    if "(" in mat.Material["DiffuseColor"]:
                                        c = tuple(
                                            [
                                                float(f)
                                                for f in mat.Material["DiffuseColor"]
                                                .strip("()")
                                                .split(",")
                                            ]
                                        )
                                if "Transparency" in mat.Material:
                                    c = (
                                        c[0],
                                        c[1],
                                        c[2],
                                        1.0 - float(mat.Material["Transparency"]),
                                    )
                                cols.extend([c for j in range(len(obj.Shape.Solids[i].Faces))])
                            if obj.ViewObject.DiffuseColor != cols:
                                obj.ViewObject.DiffuseColor = cols
        ArchComponent.ViewProviderComponent.updateData(self, obj, prop)


class PanelCut(Draft.DraftObject):
    "A flat, 2D view of an Arch Panel"

    def __init__(self, obj):
        Draft.DraftObject.__init__(self, obj)
        obj.Proxy = self

        # setProperties of ArchComponent will be overwritten
        # thus setProperties from ArchComponent will be explicit called to get the properties
        ArchComponent.ViewProviderComponent.setProperties(self, obj)

        self.setProperties(obj)

    def setProperties(self, obj):

        pl = obj.PropertiesList
        if not "Source" in pl:
            obj.addProperty(
                "App::PropertyLink",
                "Source",
                "PanelCut",
                QT_TRANSLATE_NOOP("App::Property", "The linked object"),
                locked=True,
            )
        if not "TagText" in pl:
            obj.addProperty(
                "App::PropertyString",
                "TagText",
                "PanelCut",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "The text to display. Can be %tag%, %label% or %description% to display the panel tag or label",
                ),
                locked=True,
            )
            obj.TagText = "%tag%"
        if not "TagSize" in pl:
            obj.addProperty(
                "App::PropertyLength",
                "TagSize",
                "PanelCut",
                QT_TRANSLATE_NOOP("App::Property", "The size of the tag text"),
                locked=True,
            )
            obj.TagSize = 10
        if not "TagPosition" in pl:
            obj.addProperty(
                "App::PropertyVector",
                "TagPosition",
                "PanelCut",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "The position of the tag text. Keep (0,0,0) for center position",
                ),
                locked=True,
            )
        if not "TagRotation" in pl:
            obj.addProperty(
                "App::PropertyAngle",
                "TagRotation",
                "PanelCut",
                QT_TRANSLATE_NOOP("App::Property", "The rotation of the tag text"),
                locked=True,
            )
        if not "FontFile" in pl:
            obj.addProperty(
                "App::PropertyFile",
                "FontFile",
                "PanelCut",
                QT_TRANSLATE_NOOP("App::Property", "The font of the tag text"),
                locked=True,
            )
            obj.FontFile = params.get_param("ShapeStringFontFile")
        if not "MakeFace" in pl:
            obj.addProperty(
                "App::PropertyBool",
                "MakeFace",
                "PanelCut",
                QT_TRANSLATE_NOOP(
                    "App::Property", "If True, the object is rendered as a face, if possible."
                ),
                locked=True,
            )
        if not "AllowedAngles" in pl:
            obj.addProperty(
                "App::PropertyFloatList",
                "AllowedAngles",
                "PanelCut",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "The allowed angles this object can be rotated to when placed on sheets",
                ),
                locked=True,
            )
        self.Type = "PanelCut"
        if not "CutOffset" in pl:
            obj.addProperty(
                "App::PropertyDistance",
                "CutOffset",
                "PanelCut",
                QT_TRANSLATE_NOOP(
                    "App::Property", "An offset value to move the cut plane from the center point"
                ),
                locked=True,
            )

    def onDocumentRestored(self, obj):

        self.setProperties(obj)

    def execute(self, obj):

        pl = obj.Placement
        if obj.Source:
            base = None
            n = None
            if Draft.getType(obj.Source) == "Panel":
                import DraftGeomUtils
                import Part

                baseobj = None
                if obj.Source.CloneOf:
                    baseobj = obj.Source.CloneOf.Base
                if obj.Source.Base:
                    baseobj = obj.Source.Base
                if baseobj:
                    if hasattr(baseobj, "Shape"):
                        if baseobj.Shape.Solids:
                            center = baseobj.Shape.BoundBox.Center
                            diag = baseobj.Shape.BoundBox.DiagonalLength
                            if obj.Source.Normal.Length:
                                n = obj.Source.Normal
                            elif baseobj.isDerivedFrom("Part::Extrusion"):
                                n = baseobj.Dir
                            if not n:
                                n = Vector(0, 0, 1)
                            if hasattr(obj, "CutOffset") and obj.CutOffset.Value:
                                l = obj.CutOffset.Value
                                d = Vector(n)
                                d.multiply(l)
                                center = center.add(d)
                            plane = Part.makePlane(diag, diag, center, n)
                            plane.translate(center.sub(plane.BoundBox.Center))
                            wires = []
                            for sol in baseobj.Shape.Solids:
                                s = sol.section(plane)
                                wires.extend(DraftGeomUtils.findWires(s.Edges))
                            if wires:
                                base = self.buildCut(obj, wires)
                        else:
                            base = self.buildCut(obj, baseobj.Shape.Wires)
                            for w in base.Wires:
                                n = DraftGeomUtils.getNormal(w)
                                if n:
                                    break
                            if not n:
                                n = Vector(0, 0, 1)
                        if base and n:
                            base.translate(base.BoundBox.Center.negative())
                            r = FreeCAD.Rotation(n, Vector(0, 0, 1))
                            base.rotate(Vector(0, 0, 0), r.Axis, math.degrees(r.Angle))
                    elif baseobj.isDerivedFrom("Mesh::Feature"):
                        return
                else:
                    l2 = obj.Source.Length / 2
                    w2 = obj.Source.Width / 2
                    v1 = Vector(-l2, -w2, 0)
                    v2 = Vector(l2, -w2, 0)
                    v3 = Vector(l2, w2, 0)
                    v4 = Vector(-l2, w2, 0)
                    base = Part.makePolygon([v1, v2, v3, v4, v1])
                if base:
                    self.outline = base
                    if obj.FontFile and obj.TagText and obj.TagSize.Value:
                        if obj.TagPosition.Length == 0:
                            pos = base.BoundBox.Center
                        else:
                            pos = obj.TagPosition
                        if obj.TagText == "%tag%":
                            string = obj.Source.Tag
                        elif obj.TagText == "%label%":
                            string = obj.Source.Label
                        elif obj.TagText == "%description%":
                            string = obj.Source.Description
                        else:
                            string = obj.TagText
                        chars = []
                        for char in Part.makeWireString(string, obj.FontFile, obj.TagSize.Value, 0):
                            chars.extend(char)
                        textshape = Part.Compound(chars)
                        textshape.translate(pos.sub(textshape.BoundBox.Center))
                        textshape.rotate(
                            textshape.BoundBox.Center, Vector(0, 0, 1), obj.TagRotation.Value
                        )
                        self.tag = textshape
                        base = Part.Compound([base, textshape])
                    else:
                        base = Part.Compound([base])
                    obj.Shape = base
                    obj.Placement = pl

    def buildCut(self, obj, wires):
        """buildCut(obj,wires): builds the object shape"""

        import Part

        if hasattr(obj, "MakeFace"):
            if obj.MakeFace:
                face = None
                if len(wires) > 1:
                    d = 0
                    ow = None
                    for w in wires:
                        if w.BoundBox.DiagonalLength > d:
                            d = w.BoundBox.DiagonalLength
                            ow = w
                    if ow:
                        face = Part.Face(ow)
                        for w in wires:
                            if w.hashCode() != ow.hashCode():
                                wface = Part.Face(w)
                                face = face.cut(wface)
                else:
                    face = Part.Face(wires[0])
                if face:
                    return face
        return Part.makeCompound(wires)

    def getWires(self, obj):
        """getWires(obj): returns a tuple containing 3 shapes
        that define the panel outline, the panel holes, and
        tags (engravings): (outline,holes,tags). Any of these can
        be None if nonexistent"""

        tag = None
        outl = None
        inl = None
        if not hasattr(self, "outline"):
            self.execute(obj)
        if not hasattr(self, "outline"):
            return None
        outl = self.outline.copy()
        if hasattr(self, "tag"):
            tag = self.tag.copy()
        if tag:
            tag.Placement = obj.Placement.multiply(tag.Placement)

        outl = self.outline.copy()
        outl.Placement = obj.Placement.multiply(outl.Placement)
        if len(outl.Wires) > 1:
            # separate outline
            d = 0
            ow = None
            for w in outl.Wires:
                if w.BoundBox.DiagonalLength > d:
                    d = w.BoundBox.DiagonalLength
                    ow = w
            if ow:
                inl = Part.Compound([w for w in outl.Wires if w.hashCode() != ow.hashCode()])
                outl = Part.Compound([ow])
        else:
            inl = None
            outl = Part.Compound([outl.Wires[0]])
        return (outl, inl, tag)


class ViewProviderPanelCut(Draft.ViewProviderDraft):
    "a view provider for the panel cut object"

    def __init__(self, vobj):

        Draft.ViewProviderDraft.__init__(self, vobj)
        self.setProperties(vobj)

    def setProperties(self, vobj):

        pl = vobj.PropertiesList
        if not "Margin" in pl:
            vobj.addProperty(
                "App::PropertyLength",
                "Margin",
                "Arch",
                QT_TRANSLATE_NOOP("App::Property", "A margin inside the boundary"),
                locked=True,
            )
        if not "ShowMargin" in pl:
            vobj.addProperty(
                "App::PropertyBool",
                "ShowMargin",
                "Arch",
                QT_TRANSLATE_NOOP("App::Property", "Turns the display of the margin on/off"),
                locked=True,
            )

    def onDocumentRestored(self, vobj):

        self.setProperties(vobj)

    def attach(self, vobj):

        Draft.ViewProviderDraft.attach(self, vobj)
        from pivy import coin

        self.coords = coin.SoCoordinate3()
        self.lineset = coin.SoLineSet()
        self.lineset.numVertices.setValue(-1)
        lineStyle = coin.SoDrawStyle()
        lineStyle.linePattern = 0x0F0F
        self.color = coin.SoBaseColor()
        self.switch = coin.SoSwitch()
        sep = coin.SoSeparator()
        self.switch.whichChild = -1
        sep.addChild(self.color)
        sep.addChild(lineStyle)
        sep.addChild(self.coords)
        sep.addChild(self.lineset)
        self.switch.addChild(sep)
        vobj.Annotation.addChild(self.switch)
        self.onChanged(vobj, "ShowMargin")
        self.onChanged(vobj, "LineColor")

    def onChanged(self, vobj, prop):

        if prop in ["Margin", "ShowMargin"]:
            if hasattr(vobj, "Margin") and hasattr(vobj, "ShowMargin"):
                if (vobj.Margin.Value > 0) and vobj.Object.Shape and vobj.ShowMargin:
                    self.lineset.numVertices.setValue(-1)
                    if vobj.Object.Shape.Wires:
                        d = 0
                        dw = None
                        for w in vobj.Object.Shape.Wires:
                            if w.BoundBox.DiagonalLength > d:
                                d = w.BoundBox.DiagonalLength
                                dw = w
                        if dw:
                            ow = dw.makeOffset2D(vobj.Margin.Value)
                            verts = []
                            for v in ow.OrderedVertexes:
                                v = vobj.Object.Placement.inverse().multVec(v.Point)
                                verts.append((v.x, v.y, v.z))
                            if dw.isClosed():
                                verts.append(verts[0])
                        self.coords.point.setValues(verts)
                        self.lineset.numVertices.setValue(len(verts))
                        self.switch.whichChild = 0
                else:
                    self.switch.whichChild = -1
        elif prop == "LineColor":
            if hasattr(vobj, "LineColor"):
                c = vobj.LineColor
                self.color.rgb.setValue(c[0], c[1], c[2])
        Draft.ViewProviderDraft.onChanged(self, vobj, prop)

    def updateData(self, obj, prop):

        if prop in ["Shape"]:
            self.onChanged(obj.ViewObject, "Margin")
        Draft.ViewProviderDraft.updateData(self, obj, prop)

    def doubleClicked(self, vobj):

        # See setEdit in ViewProviderDraft.
        FreeCADGui.runCommand("Std_TransformManip")
        return True


class PanelSheet(Draft.DraftObject):
    "A collection of Panel cuts under a sheet"

    def __init__(self, obj):

        Draft.DraftObject.__init__(self, obj)
        obj.Proxy = self
        self.setProperties(obj)

    def setProperties(self, obj):

        pl = obj.PropertiesList
        if not "Group" in pl:
            obj.addProperty(
                "App::PropertyLinkList",
                "Group",
                "PanelSheet",
                QT_TRANSLATE_NOOP("App::Property", "The linked Panel cuts"),
                locked=True,
            )
        if not "TagText" in pl:
            obj.addProperty(
                "App::PropertyString",
                "TagText",
                "PanelSheet",
                QT_TRANSLATE_NOOP("App::Property", "The tag text to display"),
                locked=True,
            )
        if not "TagSize" in pl:
            obj.addProperty(
                "App::PropertyLength",
                "TagSize",
                "PanelSheet",
                QT_TRANSLATE_NOOP("App::Property", "The size of the tag text"),
                locked=True,
            )
            obj.TagSize = 10
        if not "TagPosition" in pl:
            obj.addProperty(
                "App::PropertyVector",
                "TagPosition",
                "PanelSheet",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "The position of the tag text. Keep (0,0,0) for center position",
                ),
                locked=True,
            )
        if not "TagRotation" in pl:
            obj.addProperty(
                "App::PropertyAngle",
                "TagRotation",
                "PanelSheet",
                QT_TRANSLATE_NOOP("App::Property", "The rotation of the tag text"),
                locked=True,
            )
        if not "FontFile" in pl:
            obj.addProperty(
                "App::PropertyFile",
                "FontFile",
                "PanelSheet",
                QT_TRANSLATE_NOOP("App::Property", "The font of the tag text"),
                locked=True,
            )
            obj.FontFile = params.get_param("ShapeStringFontFile")
        if not "Width" in pl:
            obj.addProperty(
                "App::PropertyLength",
                "Width",
                "PanelSheet",
                QT_TRANSLATE_NOOP("App::Property", "The width of the sheet"),
                locked=True,
            )
            obj.Width = params.get_param_arch("PanelLength")
        if not "Height" in pl:
            obj.addProperty(
                "App::PropertyLength",
                "Height",
                "PanelSheet",
                QT_TRANSLATE_NOOP("App::Property", "The height of the sheet"),
                locked=True,
            )
            obj.Height = params.get_param_arch("PanelWidth")
        if not "FillRatio" in pl:
            obj.addProperty(
                "App::PropertyPercent",
                "FillRatio",
                "PanelSheet",
                QT_TRANSLATE_NOOP("App::Property", "The fill ratio of this sheet"),
                locked=True,
            )
            obj.setEditorMode("FillRatio", 2)
        if not "MakeFace" in pl:
            obj.addProperty(
                "App::PropertyBool",
                "MakeFace",
                "PanelSheet",
                QT_TRANSLATE_NOOP(
                    "App::Property", "If True, the object is rendered as a face, if possible."
                ),
                locked=True,
            )
        if not "GrainDirection" in pl:
            obj.addProperty(
                "App::PropertyAngle",
                "GrainDirection",
                "PanelSheet",
                QT_TRANSLATE_NOOP(
                    "App::Property", "Specifies an angle for the wood grain (Clockwise, 0 is North)"
                ),
                locked=True,
            )
        if not "Scale" in pl:
            obj.addProperty(
                "App::PropertyFloat",
                "Scale",
                "PanelSheet",
                QT_TRANSLATE_NOOP(
                    "App::Property", "Specifies the scale applied to each panel view."
                ),
                locked=True,
            )
            obj.Scale = 1.0
        if not "Rotations" in pl:
            obj.addProperty(
                "App::PropertyFloatList",
                "Rotations",
                "PanelSheet",
                QT_TRANSLATE_NOOP("App::Property", "A list of possible rotations for the nester"),
                locked=True,
            )
        self.Type = "PanelSheet"

    def onDocumentRestored(self, obj):

        self.setProperties(obj)

    def execute(self, obj):

        import Part

        self.sheettag = None
        self.sheetborder = None
        pl = obj.Placement
        if obj.Width.Value and obj.Height.Value:
            l2 = obj.Width.Value / 2
            w2 = obj.Height.Value / 2
            v1 = Vector(-l2, -w2, 0)
            v2 = Vector(l2, -w2, 0)
            v3 = Vector(l2, w2, 0)
            v4 = Vector(-l2, w2, 0)
            base = Part.makePolygon([v1, v2, v3, v4, v1])
            if hasattr(obj, "MakeFace"):
                if obj.MakeFace:
                    base = Part.Face(base)
            self.sheetborder = base
            wires = []
            area = obj.Width.Value * obj.Height.Value
            subarea = 0
            for v in obj.Group:
                if hasattr(v, "Shape"):
                    wires.extend(v.Shape.Wires)
                    if Draft.getType(v) == "PanelCut":
                        if v.Source:
                            subarea += v.Source.Area.Value
                    else:
                        for w in v.Shape.Wires:
                            if w.isClosed():
                                f = Part.Face(w)
                                subarea += f.Area
            if wires:
                base = Part.Compound([base] + wires)
            if obj.FontFile and obj.TagText and obj.TagSize.Value:
                chars = []
                for char in Part.makeWireString(obj.TagText, obj.FontFile, obj.TagSize.Value, 0):
                    chars.extend(char)
                textshape = Part.Compound(chars)
                textshape.translate(obj.TagPosition)
                textshape.rotate(textshape.BoundBox.Center, Vector(0, 0, 1), obj.TagRotation.Value)
                self.sheettag = textshape
                base = Part.Compound([base, textshape])
            base.scale(obj.Scale, FreeCAD.Vector())
            obj.Shape = base
            obj.Placement = pl
            obj.FillRatio = int((subarea / area) * 100)

    def getOutlines(self, obj, transform=False):
        """getOutlines(obj,transform=False): returns a list of compounds whose wires define the
        outlines of the panels in this sheet. If transform is True, the placement of
        the sheet will be added to each wire"""

        outp = []
        for p in obj.Group:
            ispanel = False
            if hasattr(p, "Proxy"):
                if hasattr(p.Proxy, "getWires"):
                    ispanel = True
                    w = p.Proxy.getWires(p)
                    if w[0]:
                        w = w[0]
                        w.scale(obj.Scale, FreeCAD.Vector())
                        if transform:
                            w.Placement = obj.Placement.multiply(w.Placement)
                        outp.append(w)
            if not ispanel:
                if hasattr(p, "Shape"):
                    for w in p.Shape.Wires:
                        w.scale(obj.Scale, FreeCAD.Vector())
                        if transform:
                            w.Placement = obj.Placement.multiply(w.Placement)
                        outp.append(w)
        return outp

    def getHoles(self, obj, transform=False):
        """getHoles(obj,transform=False): returns a list of compound whose wires define the
        holes contained in the panels in this sheet. If transform is True, the placement of
        the sheet will be added to each wire"""

        outp = []
        for p in obj.Group:
            if hasattr(p, "Proxy"):
                if hasattr(p.Proxy, "getWires"):
                    w = p.Proxy.getWires(p)
                    if w[1]:
                        w = w[1]
                        w.scale(obj.Scale, FreeCAD.Vector())
                        if transform:
                            w.Placement = obj.Placement.multiply(w.Placement)
                        outp.append(w)
        return outp

    def getTags(self, obj, transform=False):
        """getTags(obj,transform=False): returns a list of compounds whose wires define the
        tags (engravings) contained in the panels in this sheet and the sheet intself.
        If transform is True, the placement of the sheet will be added to each wire.
        Warning, the wires returned by this function may not be closed,
        depending on the font"""

        outp = []
        for p in obj.Group:
            if hasattr(p, "Proxy"):
                if hasattr(p.Proxy, "getWires"):
                    w = p.Proxy.getWires(p)
                    if w[2]:
                        w = w[2]
                        w.scale(obj.Scale, FreeCAD.Vector())
                        if transform:
                            w.Placement = obj.Placement.multiply(w.Placement)
                        outp.append(w)
        if self.sheettag is not None:
            w = self.sheettag.copy()
            w.scale(obj.Scale, FreeCAD.Vector())
            if transform:
                w.Placement = obj.Placement.multiply(w.Placement)
            outp.append(w)

        return outp


class ViewProviderPanelSheet(Draft.ViewProviderDraft):
    "a view provider for the panel sheet object"

    def __init__(self, vobj):

        Draft.ViewProviderDraft.__init__(self, vobj)
        self.setProperties(vobj)
        vobj.PatternSize = 0.0035

    def setProperties(self, vobj):

        pl = vobj.PropertiesList
        if not "Margin" in pl:
            vobj.addProperty(
                "App::PropertyLength",
                "Margin",
                "PanelSheet",
                QT_TRANSLATE_NOOP("App::Property", "A margin inside the boundary"),
                locked=True,
            )
        if not "ShowMargin" in pl:
            vobj.addProperty(
                "App::PropertyBool",
                "ShowMargin",
                "PanelSheet",
                QT_TRANSLATE_NOOP("App::Property", "Turns the display of the margin on/off"),
                locked=True,
            )
        if not "ShowGrain" in pl:
            vobj.addProperty(
                "App::PropertyBool",
                "ShowGrain",
                "PanelSheet",
                QT_TRANSLATE_NOOP(
                    "App::Property", "Turns the display of the wood grain texture on/off"
                ),
                locked=True,
            )

    def onDocumentRestored(self, vobj):

        self.setProperties(vobj)

    def getIcon(self):

        return ":/icons/Arch_Panel_Sheet_Tree.svg"

    def setEdit(self, vobj, mode):
        if mode == 1 or mode == 2:
            return None

        taskd = SheetTaskPanel(vobj.Object)
        taskd.update()
        FreeCADGui.Control.showDialog(taskd)
        return True

    def unsetEdit(self, vobj, mode):
        if mode == 1 or mode == 2:
            return None

        FreeCADGui.Control.closeDialog()
        return True

    def attach(self, vobj):

        Draft.ViewProviderDraft.attach(self, vobj)
        from pivy import coin

        self.coords = coin.SoCoordinate3()
        self.lineset = coin.SoLineSet()
        self.lineset.numVertices.setValue(-1)
        lineStyle = coin.SoDrawStyle()
        lineStyle.linePattern = 0x0F0F
        self.color = coin.SoBaseColor()
        self.switch = coin.SoSwitch()
        sep = coin.SoSeparator()
        self.switch.whichChild = -1
        sep.addChild(self.color)
        sep.addChild(lineStyle)
        sep.addChild(self.coords)
        sep.addChild(self.lineset)
        self.switch.addChild(sep)
        vobj.Annotation.addChild(self.switch)
        self.onChanged(vobj, "ShowMargin")
        self.onChanged(vobj, "LineColor")

    def onChanged(self, vobj, prop):

        if prop in ["Margin", "ShowMargin"]:
            if hasattr(vobj, "Margin") and hasattr(vobj, "ShowMargin"):
                if (
                    (vobj.Margin.Value > 0)
                    and (vobj.Margin.Value < vobj.Object.Width.Value / 2)
                    and (vobj.Margin.Value < vobj.Object.Height.Value / 2)
                ):
                    l2 = vobj.Object.Width.Value / 2
                    w2 = vobj.Object.Height.Value / 2
                    v = vobj.Margin.Value
                    v1 = (-l2 + v, -w2 + v, 0)
                    v2 = (l2 - v, -w2 + v, 0)
                    v3 = (l2 - v, w2 - v, 0)
                    v4 = (-l2 + v, w2 - v, 0)
                    self.coords.point.setValues([v1, v2, v3, v4, v1])
                    self.lineset.numVertices.setValue(5)
                if vobj.ShowMargin:
                    self.switch.whichChild = 0
                else:
                    self.switch.whichChild = -1
        elif prop == "LineColor":
            if hasattr(vobj, "LineColor"):
                c = vobj.LineColor
                self.color.rgb.setValue(c[0], c[1], c[2])
        elif prop == "ShowGrain":
            if hasattr(vobj, "ShowGrain"):
                if vobj.ShowGrain:
                    vobj.Pattern = "woodgrain"
                else:
                    vobj.Pattern = "None"
        Draft.ViewProviderDraft.onChanged(self, vobj, prop)

    def updateData(self, obj, prop):

        if prop in ["Width", "Height"]:
            self.onChanged(obj.ViewObject, "Margin")
        elif prop == "GrainDirection":
            if hasattr(self, "texcoords"):
                if self.texcoords:
                    s = FreeCAD.Vector(self.texcoords.directionS.getValue().getValue()).Length
                    vS = DraftVecUtils.rotate(
                        FreeCAD.Vector(s, 0, 0), -math.radians(obj.GrainDirection.Value)
                    )
                    vT = DraftVecUtils.rotate(
                        FreeCAD.Vector(0, s, 0), -math.radians(obj.GrainDirection.Value)
                    )
                    self.texcoords.directionS.setValue(vS.x, vS.y, vS.z)
                    self.texcoords.directionT.setValue(vT.x, vT.y, vT.z)
        Draft.ViewProviderDraft.updateData(self, obj, prop)


class SheetTaskPanel(ArchComponent.ComponentTaskPanel):

    def __init__(self, obj):

        ArchComponent.ComponentTaskPanel.__init__(self)
        self.obj = obj
        self.optwid = QtGui.QWidget()
        self.optwid.setWindowTitle(QtGui.QApplication.translate("Arch", "Tools", None))
        lay = QtGui.QVBoxLayout(self.optwid)
        self.editButton = QtGui.QPushButton(self.optwid)
        self.editButton.setIcon(QtGui.QIcon(":/icons/Draft_Edit.svg"))
        self.editButton.setText(QtGui.QApplication.translate("Arch", "Edit views positions", None))
        lay.addWidget(self.editButton)
        QtCore.QObject.connect(self.editButton, QtCore.SIGNAL("clicked()"), self.editNodes)
        self.form = [self.form, self.optwid]

    def editNodes(self):

        FreeCADGui.Control.closeDialog()
        FreeCADGui.runCommand("Draft_Edit")
