Files
PVPlant/Project/Area/PVPlantArea.py
2025-07-31 09:58:38 +02:00

986 lines
30 KiB
Python

# /**********************************************************************
# * *
# * Copyright (c) 2021 Javier Braña <javier.branagutierrez@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify*
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program 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 Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307*
# * USA *
# * *
# ***********************************************************************
import FreeCAD
import Part
import PVPlantSite
import Utils.PVPlantUtils as utils
import MeshPart as mp
if FreeCAD.GuiUp:
import FreeCADGui
from DraftTools import translate
else:
# \cond
def translate(ctxt,txt):
return txt
def QT_TRANSLATE_NOOP(ctxt,txt):
return txt
# \endcond
import os
from PVPlantResources import DirIcons as DirIcons
__title__ = "PVPlant Areas"
__author__ = "Javier Braña"
__url__ = "http://www.sogos-solar.com"
import PVPlantResources
from PVPlantResources import DirIcons as DirIcons
Dir3dObjects = os.path.join(PVPlantResources.DirResources, "3dObjects")
''' Default Area: '''
def makeArea(points = None, type = 0):
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Area")
if type == 0:
_Area(obj)
_ViewProviderArea(obj.ViewObject)
elif type == 1:
_ForbiddenArea(obj)
_ViewProviderForbiddenArea(obj.ViewObject)
if points:
obj.Points = points
return obj
class _Area:
def __init__(self, obj):
''' Initialize the Area object '''
self.Type = None
self.obj = None
self.setProperties(obj)
def setProperties(self, obj):
pl = obj.PropertiesList
if not ("Base" in pl):
obj.addProperty("App::PropertyLink",
"Base",
"Area",
"Base wire"
).Base = None
if not ("Type" in pl):
obj.addProperty("App::PropertyString",
"Type",
"Area",
"Points that define the area"
).Type = "Area"
obj.setEditorMode("Type", 1)
self.Type = obj.Type
obj.Proxy = self
def onDocumentRestored(self, obj):
""" Method run when the document is restored """
self.setProperties(obj)
def __getstate__(self):
return None # No necesitamos guardar estado adicional
def __setstate__(self, state):
pass
def execute(self, obj):
''' Execute the area object '''
pass
class _ViewProviderArea:
def __init__(self, vobj):
vobj.Proxy = self
def attach(self, vobj):
''' Create Object visuals in 3D view. '''
self.ViewObject = vobj
def getIcon(self):
'''
Return object treeview icon.
'''
return str(os.path.join(DirIcons, "area.svg"))
'''
def claimChildren(self):
"""
Provides object grouping
"""
return self.Object.Group
'''
def setEdit(self, vobj, mode=0):
"""
Enable edit
"""
return True
def unsetEdit(self, vobj, mode=0):
"""
Disable edit
"""
return False
def doubleClicked(self, vobj):
"""
Detect double click
"""
pass
def setupContextMenu(self, obj, menu):
"""
Context menu construction
"""
pass
def edit(self):
"""
Edit callback
"""
pass
def __getstate__(self):
return None
def __setstate__(self, state):
pass
''' Frame Area '''
def makeFramedArea(base = None, select = None):
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "FrameArea")
FrameArea(obj)
ViewProviderFrameArea(obj.ViewObject)
if base:
obj.Base = base
if select:
frames = []
for o in select:
if hasattr(o, "Proxy") and (o.Proxy.Type == "Tracker"):
if not (o in frames):
frames.append(o)
if len(frames) > 0:
print(frames)
obj.Frames = frames
try:
group = FreeCAD.ActiveDocument.FrameZones
except:
group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'FrameZones')
group.Label = "FrameZones"
group.addObject(obj)
return obj
class FrameArea(_Area):
def __init__(self, obj):
_Area.__init__(self, obj)
self.setProperties(obj)
self.obj = None
def setProperties(self, obj):
_Area.setProperties(self, obj)
pl = obj.PropertiesList
if not ("Frames" in pl):
obj.addProperty("App::PropertyLinkList",
"Frames",
"Area",
"All the frames inside this area."
)
if not ("FrameNumber" in pl):
obj.addProperty("App::PropertyInteger",
"FrameNumber",
"Area",
"The number of frames inside this area."
)
obj.setEditorMode("FrameNumber", 1)
if not ("Type" in pl):
obj.addProperty("App::PropertyString",
"Type",
"Base",
"The facemaker type to use to build the profile of this object"
).Type = "FrameArea"
obj.setEditorMode("Type", 1)
self.Type = "FrameArea"
obj.Proxy = self
self.obj = obj
def onDocumentRestored(self, obj):
"""Method run when the document is restored."""
self.setProperties(obj)
def onBeforeChange(self, obj, prop):
''' '''
pass
def onChanged(self, obj, prop):
if prop == "Base":
if obj.Base.Shape is None:
obj.Shape = Part.Shape()
return
import Utils.PVPlantUtils as utils
base = obj.Base.Shape
land = PVPlantSite.get().Terrain.Mesh
vec = FreeCAD.Vector(0,0,1)
wire = utils.getProjected(base, vec)
tmp = mp.projectShapeOnMesh(wire, land, vec)
shape = Part.makeCompound([])
for section in tmp:
shape.add(Part.makePolygon(section))
obj.Shape = shape
if prop == "Frames":
lf = []
for o in obj.Frames:
if not hasattr(o, "Proxy"):
continue
if o.Proxy.Type == "Tracker":
lf.append(o)
obj.Frames = lf
obj.FramesNumber = len(obj.Frames)
def addFrame(self, frame):
list = self.obj.Frames.copy()
list.append(frame)
self.obj.Frames = sorted(list, key=lambda x: x.Name)
def execute(self, obj):
''' execute '''
if not hasattr(obj, "Frames"):
return
obj.FrameNumber = len(obj.Frames)
def __getstate__(self):
return None
def __setstate__(self, state):
pass
class ViewProviderFrameArea(_ViewProviderArea):
def __init__(self, vobj):
''' Set view properties. '''
self.Object = vobj.Object
vobj.Proxy = self
def getIcon(self):
''' Return object treeview icon '''
return str(os.path.join(DirIcons, "FrameArea.svg"))
def claimChildren(self):
""" Provides object grouping """
children = []
if self.Object.Base:
children.append(self.Object.Base)
return children
''' offsets '''
def makeOffsetArea(base = None, val=None):
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "OffsetArea")
OffsetArea(obj)
obj.Base = base
ViewProviderOffsetArea(obj.ViewObject)
if val:
obj.OffsetDistance = val
try:
offsetsgroup = FreeCAD.ActiveDocument.Offsets
except:
offsetsgroup = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'Offsets')
offsetsgroup.Label = "Offsets"
offsetsgroup.addObject(obj)
return obj
class OffsetArea(_Area):
def __init__(self, obj):
'''_Area.__init__(self, obj)
self.setProperties(obj)'''
super().__init__(obj) # Llama al constructor de _Area
def setProperties(self, obj):
super().setProperties(obj) # Propiedades de la clase base
pl = obj.PropertiesList
if not ("OffsetDistance" in pl):
obj.addProperty("App::PropertyDistance",
"OffsetDistance",
"OffsetArea",
"Base wire"
)
self.Type = obj.Type = "Area_Offset"
def onDocumentRestored(self, obj):
"""Method run when the document is restored."""
self.setProperties(obj)
def execute(self, obj):
# Comprobar dependencias críticas
if not hasattr(obj, "Base") or not obj.Base or not obj.Base.Shape:
return
if not hasattr(PVPlantSite, "get") or not PVPlantSite.get().Terrain:
return
base = obj.Base.Shape
land = PVPlantSite.get().Terrain.Mesh
vec = FreeCAD.Vector(0, 0, 1)
wire = utils.getProjected(base, vec)
wire = wire.makeOffset2D(obj.OffsetDistance.Value, 2, False, False, True)
sections = mp.projectShapeOnMesh(wire, land, vec)
pts = []
for section in sections:
pts.extend(section)
# Crear forma solo si hay resultados
if sections:
obj.Shape = Part.makePolygon(pts)
else:
obj.Shape = Part.Shape() # Forma vacía si falla
class ViewProviderOffsetArea(_ViewProviderArea):
def getIcon(self):
''' Return object treeview icon. '''
return str(os.path.join(DirIcons, "offset.svg"))
def claimChildren(self):
""" Provides object grouping """
children = []
if self.ViewObject and self.ViewObject.Object.Base:
children.append(self.ViewObject.Object.Base)
return children
''' Forbidden Area: '''
def makeProhibitedArea(base = None):
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "ExclusionArea")
ProhibitedArea(obj)
ViewProviderForbiddenArea(obj.ViewObject)
if base:
obj.Base = base
try:
group = FreeCAD.ActiveDocument.getObject("Exclusions")
except:
group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'Exclusions')
group.Label = "Exclusions"
group.addObject(obj)
return obj
class ProhibitedArea(OffsetArea):
def __init__(self, obj):
OffsetArea.__init__(self, obj)
self.setProperties(obj)
def setProperties(self, obj):
OffsetArea.setProperties(self, obj)
self.Type = obj.Type = "ProhibitedArea"
obj.Proxy = self
'''# Propiedades de color
if not hasattr(obj, "OriginalColor"):
obj.addProperty("App::PropertyColor",
"OriginalColor",
"Display",
"Color for original wire")
obj.OriginalColor = (1.0, 0.0, 0.0) # Rojo
if not hasattr(obj, "OffsetColor"):
obj.addProperty("App::PropertyColor",
"OffsetColor",
"Display",
"Color for offset wire")
obj.OffsetColor = (1.0, 0.5, 0.0) # Naranja
# Propiedades de grosor
if not hasattr(obj, "OriginalWidth"):
obj.addProperty("App::PropertyFloat",
"OriginalWidth",
"Display",
"Line width for original wire")
obj.OriginalWidth = 4.0
if not hasattr(obj, "OffsetWidth"):
obj.addProperty("App::PropertyFloat",
"OffsetWidth",
"Display",
"Line width for offset wire")
obj.OffsetWidth = 4.0'''
def execute(self, obj):
# Comprobar dependencias
if not hasattr(obj, "Base") or not obj.Base or not obj.Base.Shape:
return
if not hasattr(PVPlantSite, "get") or not PVPlantSite.get().Terrain:
return
base = obj.Base.Shape
land = PVPlantSite.get().Terrain.Mesh
vec = FreeCAD.Vector(0, 0, 1)
# 1. Crear wire original
original_wire = utils.getProjected(base, vec)
sections_original = mp.projectShapeOnMesh(original_wire, land, vec)
# 2. Crear wire offset
offset_wire = original_wire.makeOffset2D(obj.OffsetDistance.Value, 2, False, False, True)
sections_offset = mp.projectShapeOnMesh(offset_wire, land, vec)
# Crear formas compuestas
def make_polygon(sections):
if not sections:
return Part.Shape()
pts = []
for section in sections:
pts.extend(section)
return Part.makePolygon(pts)
compounds = []
if sections_original:
compounds.append(make_polygon(sections_original))
if sections_offset:
compounds.append(make_polygon(sections_offset))
if compounds:
obj.Shape = Part.makeCompound(compounds)
else:
obj.Shape = Part.Shape()
# Actualizar colores en la vista
if FreeCAD.GuiUp and obj.ViewObject:
obj.ViewObject.Proxy.updateVisual()
class ViewProviderForbiddenArea(_ViewProviderArea):
def __init__(self, vobj):
super().__init__(vobj)
# Valores por defecto
self.original_color = (1.0, 0.0, 0.0) # Rojo
self.offset_color = (1.0, 0.5, 0.0) # Naranja
self.original_width = 4.0
self.offset_width = 4.0
self.line_widths = [] # Almacenará los grosores por arista
vobj.LineColor = (1.0, 0.0, 0.0)
vobj.LineWidth = 4
vobj.PointColor = (1.0, 0.0, 0.0)
vobj.PointSize = 4
def getIcon(self):
''' Return object treeview icon. '''
return str(os.path.join(DirIcons, "area_forbidden.svg"))
def claimChildren(self):
""" Provides object grouping """
children = []
if self.ViewObject and self.ViewObject.Object.Base:
children.append(self.ViewObject.Object.Base)
return children
def attach(self, vobj):
super().attach(vobj)
# Inicializar visualización
self.updateVisual()
def updateVisual(self):
"""Actualiza colores y grosores de línea"""
if not hasattr(self, 'ViewObject') or not self.ViewObject or not self.ViewObject.Object:
return
obj = self.ViewObject.Object
# Obtener propiedades de color y grosor
try:
self.original_color = obj.OriginalColor
self.offset_color = obj.OffsetColor
self.original_width = obj.OriginalWidth
self.offset_width = obj.OffsetWidth
except:
pass
# Actualizar colores si hay forma
if hasattr(obj, 'Shape') and obj.Shape and not obj.Shape.isNull():
if len(obj.Shape.SubShapes) >= 2:
# Asignar colores
colors = []
colors.append(self.original_color) # Primer wire (original)
colors.append(self.offset_color) # Segundo wire (offset)
self.ViewObject.DiffuseColor = colors
# Preparar grosores por arista
#self.prepareLineWidths()
# Asignar grosores usando LineWidthArray
'''if self.line_widths:
self.ViewObject.LineWidthArray = self.line_widths'''
# Establecer grosor global como respaldo
#self.ViewObject.LineWidth = max(self.original_width, self.offset_width)
def prepareLineWidths(self):
"""Prepara la lista de grosores para cada arista"""
self.line_widths = []
obj = self.ViewObject.Object
if hasattr(obj, 'Shape') and obj.Shape and not obj.Shape.isNull():
# Contar aristas en cada subforma
for i, subshape in enumerate(obj.Shape.SubShapes):
edge_count = len(subshape.Edges) if hasattr(subshape, 'Edges') else 1
# Determinar grosor según tipo de wire
width = self.original_width if i == 0 else self.offset_width
# Asignar el mismo grosor a todas las aristas de este wire
self.line_widths.extend([width] * edge_count)
def onChanged(self, vobj, prop):
"""Maneja cambios en propiedades de visualización"""
if prop in ["LineColor", "PointColor", "ShapeColor", "LineWidth"]:
self.updateVisual()
def updateData(self, obj, prop):
"""Actualiza cuando cambian los datos del objeto"""
if prop == "Shape":
self.updateVisual()
'''def __getstate__(self):
return {
"original_color": self.original_color,
"offset_color": self.offset_color,
"original_width": self.original_width,
"offset_width": self.offset_width
}
def __setstate__(self, state):
if "original_color" in state:
self.original_color = state["original_color"]
if "offset_color" in state:
self.offset_color = state["offset_color"]
if "original_width" in state:
self.original_width = state.get("original_width", 4.0)
if "offset_width" in state:
self.offset_width = state.get("offset_width", 4.0)'''
''' PV Area: '''
def makePVSubplant():
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "PVSubplant")
PVSubplant(obj)
ViewProviderPVSubplant(obj.ViewObject)
return obj
class PVSubplant:
def __init__(self, obj):
self.setProperties(obj)
self.Type = None
self.obj = None
def setProperties(self, obj):
pl = obj.PropertiesList
if not "Setup" in pl:
obj.addProperty("App::PropertyEnumeration",
"Setup",
"PVSubplant",
"The facemaker type to use to build the profile of this object"
).Setup = ["String Inverter", "Central Inverter"]
if not ("Frames" in pl):
obj.addProperty("App::PropertyLinkList",
"Frames",
"PVSubplant",
"List of frames"
)
if not ("Inverters" in pl):
obj.addProperty("App::PropertyLinkList",
"Inverters",
"PVSubplant",
"List of Inverters"
)
if not ("Cables" in pl):
obj.addProperty("App::PropertyLinkList",
"Cables",
"PVSubplant",
"List of Cables"
)
if not "TotalPVModules" in pl:
obj.addProperty("App::PropertyQuantity",
"TotalPVModules",
"PVSubplant",
"The facemaker type to use to build the profile of this object"
)
if not "TotalPowerDC" in pl:
obj.addProperty("App::PropertyQuantity",
"TotalPowerDC",
"PVSubplant",
"The facemaker type to use to build the profile of this object"
)
if not "Color" in pl:
obj.addProperty("App::PropertyColor",
"Color",
"PVSubplant",
"Color"
)
if not "Type" in pl:
obj.addProperty("App::PropertyString",
"Type",
"Base",
"The facemaker type to use to build the profile of this object"
).Type = "PVSubplant"
obj.setEditorMode("Type", 1)
self.Type = obj.Type
self.obj = obj
obj.Proxy = self
def onDocumentRestored(self, obj):
"""Method run when the document is restored."""
self.setProperties(obj)
def onChanged(self, obj, prop):
if prop == "Color":
obj.ViewObject.LineColor = obj.Color
obj.ViewObject.PointColor = obj.Color
if prop == "Setup":
if obj.Setup == "Central Inverter":
if not ("StringBoxes" in obj.PropertiesList):
obj.addProperty("App::PropertyLinkList",
"StringBoxes",
"PVSubplant",
"List of String-Boxes"
)
else:
if hasattr(obj, "StringBoxes"):
obj.removeProperty("StringBoxes")
if prop == "Frames":
import numpy as np
# 1. Dibujar contorno:
maxdist = 6000
if len(obj.Frames) > 0:
onlyframes = []
for object in obj.Frames:
if object.Name.startswith("Tracker"):
onlyframes.append(object)
obj.Frames = onlyframes
pts = []
for frame in obj.Frames:
for panel in frame.Shape.SubShapes[0].SubShapes[0].SubShapes:
zm = panel.BoundBox.ZMax
for i in range(8):
pt = panel.BoundBox.getPoint(i)
if pt.z == zm:
pts.append(pt)
import MeshTools.Triangulation as Triangulation
import MeshTools.MeshGetBoundary as mgb
m = Triangulation.Triangulate(np.array(pts), MaxlengthLE=maxdist, use3d=False)
b = mgb.get_boundary(m)
obj.Shape = b
# 2. rellenar información
power = 0
modulenum = 0
for frame in obj.Frames:
modules = frame.Setup.ModuleColumns * frame.Setup.ModuleRows
power += frame.Setup.ModulePower.Value * modules
modulenum += modules
obj.TotalPowerDC = power
obj.TotalPVModules = modulenum
obj.ViewObject.LineWidth = 5
def execute(self, obj):
''' '''
pass
def __getstate__(self):
return None
def __setstate__(self, state):
pass
class ViewProviderPVSubplant:
def __init__(self, vobj):
''' Set view properties. '''
self.Object = None
vobj.Proxy = self
def attach(self, vobj):
''' Create Object visuals in 3D view. '''
self.Object = vobj.Object
return
def getIcon(self):
''' Return object treeview icon. '''
return str(os.path.join(DirIcons, "subplant.svg"))
def claimChildren(self):
""" Provides object grouping """
children = []
if self.Object.Frames:
children.extend(self.Object.Frames)
return children
def __getstate__(self):
return None
'''def onDelete(self, feature, subelements):
try:
for obj in self.claimChildren():
obj.ViewObject.show()
except Exception as err:
FreeCAD.Console.PrintError("Error in onDelete: " + str(err))
return True
def canDragObjects(self):
return True
def canDropObjects(self):
return True
def canDragObject(self, dragged_object):
return True
def canDropObject(self, incoming_object):
return hasattr(incoming_object, 'Shape')
def dragObject(self, selfvp, dragged_object):
objs = self.Object.Objects
objs.remove(dragged_object)
self.Object.Objects = objs
def dropObject(self, selfvp, incoming_object):
self.Object.Objects = self.Object.Objects + [incoming_object]
def setEdit(self, vobj, mode=0):
"""
Enable edit
"""
return True
def unsetEdit(self, vobj, mode=0):
"""
Disable edit
"""
return False
def doubleClicked(self, vobj):
"""
Detect double click
"""
pass
def setupContextMenu(self, obj, menu):
"""
Context menu construction
"""
pass
def edit(self):
"""
Edit callback
"""
pass
def __getstate__(self):
"""
Save variables to file.
"""
return None
def __setstate__(self,state):
"""
Get variables from file.
"""
return None'''
# Comandos: -----------------------------------------------------------------------------------------------------------
class CommandDivideArea:
def GetResources(self):
return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "area.svg")),
'Accel': "A, D",
'MenuText': "Divide Area",
'ToolTip': "Allowed Area"}
def Activated(self):
sel = FreeCADGui.Selection.getSelection()[0]
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False
class CommandBoundary:
def GetResources(self):
return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "area.svg")),
'Accel': "A, B",
'MenuText': "Area",
'ToolTip': "Allowed Area"}
def Activated(self):
sel = FreeCADGui.Selection.getSelection()[0]
obj = makeArea([ver.Point for ver in sel.Shape.Vertexes])
#taskd = _PVPlantPlacementTaskPanel()
#FreeCADGui.Control.showDialog(taskd)
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False
class CommandFrameArea:
def GetResources(self):
return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "FrameArea.svg")),
'Accel': "A, F",
'MenuText': "Frame Area",
'ToolTip': "Frame Area"}
def Activated(self):
for base in FreeCADGui.Selection.getSelection():
makeFramedArea(None, base)
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False
class CommandProhibitedArea:
def GetResources(self):
return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "area_forbidden.svg")),
'Accel': "A, F",
'MenuText': "Prohibited Area",
'ToolTip': "Prohibited Area"}
def Activated(self):
for base in FreeCADGui.Selection.getSelection():
makeProhibitedArea(base)
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False
class CommandPVSubplant:
def GetResources(self):
return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "subplant.svg")),
'Accel': "A, P",
'MenuText': "PV Subplant",
'ToolTip': "PV Subplant"}
def Activated(self):
area = makePVSubplant()
sel = FreeCADGui.Selection.getSelection()
for obj in sel:
if hasattr(obj, 'Proxy') and obj.Proxy.Type == "Tracker":
frame_list = area.Frames
frame_list.append(obj)
area.Frames = frame_list
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False
class CommandOffsetArea:
def GetResources(self):
return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "offset.svg")),
'Accel': "A, O",
'MenuText': "OffsetArea",
'ToolTip': "OffsetArea"}
def Activated(self):
for base in FreeCADGui.Selection.getSelection():
makeOffsetArea(base)
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False
'''if FreeCAD.GuiUp:
class CommandAreaGroup:
def GetCommands(self):
return tuple([#'Area',
'FrameArea',
'ForbiddenArea',
'PVSubplant',
'OffsetArea'
])
def GetResources(self):
return {'MenuText': 'Areas',
'ToolTip': 'Areas'
}
def IsActive(self):
return not FreeCAD.ActiveDocument is None
#FreeCADGui.addCommand('Area', CommandBoundary())
FreeCADGui.addCommand('FrameArea', CommandFrameArea())
FreeCADGui.addCommand('ForbiddenArea', CommandProhibitedArea())
FreeCADGui.addCommand('PVSubplant', CommandPVSubplant())
FreeCADGui.addCommand('OffsetArea', CommandOffsetArea())
FreeCADGui.addCommand('PVPlantAreas', CommandAreaGroup())'''