1244 lines
39 KiB
Python
1244 lines
39 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
|
|
|
|
import pivy
|
|
from pivy import coin
|
|
|
|
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)
|
|
print(" javi ", sections)
|
|
pts = []
|
|
for section in sections:
|
|
pts.extend(section)
|
|
# Crear forma solo si hay resultados
|
|
if len(pts)>0:
|
|
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
|
|
|
|
def onDocumentRestored(self, obj):
|
|
"""Method run when the document is restored."""
|
|
self.setProperties(obj)
|
|
|
|
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_old:
|
|
def __init__(self, vobj):
|
|
vobj.Proxy = self
|
|
self.setProperties(vobj)
|
|
|
|
def setProperties(self, vobj):
|
|
# Propiedades de color
|
|
if not hasattr(vobj, "OriginalColor"):
|
|
vobj.addProperty("App::PropertyColor",
|
|
"OriginalColor",
|
|
"ObjectStyle",
|
|
"Color for original wire")
|
|
vobj.OriginalColor = (1.0, 0.0, 0.0) # Rojo
|
|
|
|
if not hasattr(vobj, "OffsetColor"):
|
|
vobj.addProperty("App::PropertyColor",
|
|
"OffsetColor",
|
|
"ObjectStyle",
|
|
"Color for offset wire")
|
|
vobj.OffsetColor = (1.0, 0.0, 0.0) # Rojo
|
|
|
|
# Propiedades de grosor
|
|
if not hasattr(vobj, "OriginalWidth"):
|
|
vobj.addProperty("App::PropertyFloat",
|
|
"OriginalWidth",
|
|
"ObjectStyle",
|
|
"Line width for original wire")
|
|
vobj.OriginalWidth = 4.0
|
|
|
|
if not hasattr(vobj, "OffsetWidth"):
|
|
vobj.addProperty("App::PropertyFloat",
|
|
"OffsetWidth",
|
|
"ObjectStyle",
|
|
"Line width for offset wire")
|
|
vobj.OffsetWidth = 4.0
|
|
|
|
# Deshabilitar el color por defecto
|
|
vobj.setPropertyStatus("LineColor", "Hidden")
|
|
vobj.setPropertyStatus("PointColor", "Hidden")
|
|
vobj.setPropertyStatus("ShapeAppearance", "Hidden")
|
|
|
|
def attach(self, vobj):
|
|
self.ViewObject = vobj
|
|
self.Object = vobj.Object
|
|
|
|
# Crear la estructura de escena Coin3D
|
|
self.root = coin.SoGroup()
|
|
|
|
# Switch para habilitar/deshabilitar la selección
|
|
self.switch = coin.SoSwitch()
|
|
self.switch.whichChild = coin.SO_SWITCH_ALL
|
|
|
|
# Separador para el wire original
|
|
self.original_sep = coin.SoSeparator()
|
|
self.original_color = coin.SoBaseColor()
|
|
self.original_coords = coin.SoCoordinate3()
|
|
self.original_line_set = coin.SoLineSet()
|
|
self.original_draw_style = coin.SoDrawStyle()
|
|
|
|
# Separador para el wire offset
|
|
self.offset_sep = coin.SoSeparator()
|
|
self.offset_color = coin.SoBaseColor()
|
|
self.offset_coords = coin.SoCoordinate3()
|
|
self.offset_line_set = coin.SoLineSet()
|
|
self.offset_draw_style = coin.SoDrawStyle()
|
|
|
|
# Construir la jerarquía de escena
|
|
self.original_sep.addChild(self.original_color)
|
|
self.original_sep.addChild(self.original_draw_style)
|
|
self.original_sep.addChild(self.original_coords)
|
|
self.original_sep.addChild(self.original_line_set)
|
|
|
|
self.offset_sep.addChild(self.offset_color)
|
|
self.offset_sep.addChild(self.offset_draw_style)
|
|
self.offset_sep.addChild(self.offset_coords)
|
|
self.offset_sep.addChild(self.offset_line_set)
|
|
|
|
self.switch.addChild(self.original_sep)
|
|
self.switch.addChild(self.offset_sep)
|
|
self.root.addChild(self.switch)
|
|
|
|
vobj.addDisplayMode(self.root, "Wireframe")
|
|
|
|
# Inicializar estilos de dibujo
|
|
self.original_draw_style.style = coin.SoDrawStyle.LINES
|
|
self.offset_draw_style.style = coin.SoDrawStyle.LINES
|
|
|
|
# Actualizar visualización inicial
|
|
if hasattr(self.Object, 'Shape'):
|
|
self.updateData(self.Object, "Shape")
|
|
self.updateVisual()
|
|
|
|
def updateData(self, obj, prop):
|
|
if prop == "Shape" and obj.Shape and not obj.Shape.isNull():
|
|
self.updateGeometry()
|
|
|
|
def updateGeometry(self):
|
|
"""Actualiza la geometría en la escena 3D"""
|
|
if not hasattr(self, 'Object') or not self.Object.Shape or self.Object.Shape.isNull():
|
|
return
|
|
|
|
# Limpiar coordenadas existentes
|
|
self.original_coords.point.deleteValues(0)
|
|
self.offset_coords.point.deleteValues(0)
|
|
|
|
# Obtener los sub-shapes
|
|
subshapes = []
|
|
if hasattr(self.Object.Shape, 'SubShapes') and self.Object.Shape.SubShapes:
|
|
subshapes = self.Object.Shape.SubShapes
|
|
elif hasattr(self.Object.Shape, 'ChildShapes') and self.Object.Shape.ChildShapes:
|
|
subshapes = self.Object.Shape.ChildShapes
|
|
|
|
# Procesar wire original (primer sub-shape)
|
|
if len(subshapes) > 0:
|
|
self.processShape(subshapes[0], self.original_coords, self.original_line_set)
|
|
|
|
# Procesar wire offset (segundo sub-shape)
|
|
if len(subshapes) > 1:
|
|
self.processShape(subshapes[1], self.offset_coords, self.offset_line_set)
|
|
|
|
# Actualizar colores y grosores
|
|
self.updateVisual()
|
|
|
|
def processShape(self, shape, coords_node, lineset_node):
|
|
"""Procesa una forma y la añade al nodo de coordenadas"""
|
|
if not shape or shape.isNull():
|
|
return
|
|
|
|
points = []
|
|
line_indices = []
|
|
current_index = 0
|
|
|
|
# Obtener todos los edges de la forma
|
|
edges = []
|
|
if hasattr(shape, 'Edges'):
|
|
edges = shape.Edges
|
|
elif hasattr(shape, 'ChildShapes'):
|
|
for child in shape.ChildShapes:
|
|
if hasattr(child, 'Edges'):
|
|
edges.extend(child.Edges)
|
|
|
|
for edge in edges:
|
|
try:
|
|
# Discretizar la curva para obtener puntos
|
|
vertices = edge.discretize(Number=50)
|
|
|
|
for i, vertex in enumerate(vertices):
|
|
points.append([vertex.x, vertex.y, vertex.z])
|
|
line_indices.append(current_index)
|
|
current_index += 1
|
|
|
|
# Añadir -1 para indicar fin de línea
|
|
line_indices.append(-1)
|
|
|
|
except Exception as e:
|
|
print(f"Error processing edge: {e}")
|
|
continue
|
|
|
|
# Configurar coordenadas y líneas
|
|
if points:
|
|
coords_node.point.setValues(0, len(points), points)
|
|
lineset_node.numVertices.deleteValues(0)
|
|
lineset_node.numVertices.setValues(0, len(line_indices), line_indices)
|
|
|
|
def updateVisual(self):
|
|
"""Actualiza colores y grosores según las propiedades"""
|
|
if not hasattr(self, 'ViewObject') or not self.ViewObject:
|
|
return
|
|
|
|
vobj = self.ViewObject
|
|
|
|
try:
|
|
# Configurar wire original
|
|
if hasattr(vobj, "OriginalColor"):
|
|
original_color = vobj.OriginalColor
|
|
self.original_color.rgb.setValue(original_color[0], original_color[1], original_color[2])
|
|
|
|
if hasattr(vobj, "OriginalWidth"):
|
|
self.original_draw_style.lineWidth = vobj.OriginalWidth
|
|
|
|
# Configurar wire offset
|
|
if hasattr(vobj, "OffsetColor"):
|
|
offset_color = vobj.OffsetColor
|
|
self.offset_color.rgb.setValue(offset_color[0], offset_color[1], offset_color[2])
|
|
|
|
if hasattr(vobj, "OffsetWidth"):
|
|
self.offset_draw_style.lineWidth = vobj.OffsetWidth
|
|
|
|
except Exception as e:
|
|
print(f"Error updating visual: {e}")
|
|
|
|
def onChanged(self, vobj, prop):
|
|
"""Maneja cambios en propiedades"""
|
|
if prop in ["OriginalColor", "OffsetColor", "OriginalWidth", "OffsetWidth"]:
|
|
self.updateVisual()
|
|
|
|
def getDisplayModes(self, obj):
|
|
return ["Wireframe"]
|
|
|
|
def getDefaultDisplayMode(self):
|
|
return "Wireframe"
|
|
|
|
def setDisplayMode(self, mode):
|
|
return mode
|
|
|
|
def claimChildren(self):
|
|
"""Proporciona agrupamiento de objetos"""
|
|
children = []
|
|
if hasattr(self, 'Object') and self.Object and hasattr(self.Object, "Base"):
|
|
children.append(self.Object.Base)
|
|
return children
|
|
|
|
def getIcon(self):
|
|
'''Return object treeview icon'''
|
|
return str(os.path.join(DirIcons, "area_forbidden.svg"))
|
|
|
|
def onDocumentRestored(self, vobj):
|
|
"""Método ejecutado cuando el documento es restaurado"""
|
|
self.ViewObject = vobj
|
|
self.Object = vobj.Object
|
|
self.setProperties(vobj)
|
|
self.attach(vobj)
|
|
|
|
def __getstate__(self):
|
|
return None
|
|
|
|
def __setstate__(self, state):
|
|
return None
|
|
|
|
|
|
class ViewProviderForbiddenArea:
|
|
def __init__(self, vobj):
|
|
vobj.Proxy = self
|
|
self.ViewObject = vobj
|
|
|
|
# Inicializar propiedades PRIMERO
|
|
self.setProperties(vobj)
|
|
|
|
# Configurar colores iniciales
|
|
self.updateColors(vobj)
|
|
|
|
def setProperties(self, vobj):
|
|
if not hasattr(vobj, "OriginalColor"):
|
|
vobj.addProperty("App::PropertyColor",
|
|
"OriginalColor",
|
|
"Display",
|
|
"Color for original wire")
|
|
vobj.OriginalColor = (1.0, 0.0, 0.0) # Rojo
|
|
|
|
if not hasattr(vobj, "OffsetColor"):
|
|
vobj.addProperty("App::PropertyColor",
|
|
"OffsetColor",
|
|
"Display",
|
|
"Color for offset wire")
|
|
vobj.OffsetColor = (1.0, 0.5, 0.0) # Naranja
|
|
|
|
def updateColors(self, vobj):
|
|
"""Actualiza los colores desde las propiedades"""
|
|
try:
|
|
if hasattr(vobj, "OriginalColor"):
|
|
self.original_color.rgb.setValue(*vobj.OriginalColor)
|
|
else:
|
|
self.original_color.rgb.setValue(1.0, 0.0, 0.0)
|
|
|
|
if hasattr(vobj, "OffsetColor"):
|
|
self.offset_color.rgb.setValue(*vobj.OffsetColor)
|
|
else:
|
|
self.offset_color.rgb.setValue(1.0, 0.5, 0.0)
|
|
except Exception as e:
|
|
print(f"Error en updateColors: {e}")
|
|
|
|
def onDocumentRestored(self, vobj):
|
|
self.setProperties(vobj)
|
|
# No llamar a __init__ de nuevo, solo actualizar propiedades
|
|
self.updateColors(vobj)
|
|
|
|
def getIcon(self):
|
|
return str(os.path.join(DirIcons, "area_forbidden.svg"))
|
|
|
|
def attach(self, vobj):
|
|
self.ViewObject = vobj
|
|
|
|
# Inicializar nodos Coin3D
|
|
self.root = coin.SoGroup()
|
|
self.original_coords = coin.SoCoordinate3()
|
|
self.offset_coords = coin.SoCoordinate3()
|
|
self.original_color = coin.SoBaseColor()
|
|
self.offset_color = coin.SoBaseColor()
|
|
self.original_lineset = coin.SoLineSet()
|
|
self.offset_lineset = coin.SoLineSet()
|
|
|
|
# Añadir un nodo de dibujo para establecer el estilo de línea
|
|
self.draw_style = coin.SoDrawStyle()
|
|
self.draw_style.style = coin.SoDrawStyle.LINES
|
|
self.draw_style.lineWidth = 3.0
|
|
|
|
# Construir la escena
|
|
self.root.addChild(self.draw_style)
|
|
|
|
# Grupo para el polígono original
|
|
original_group = coin.SoGroup()
|
|
original_group.addChild(self.original_color)
|
|
original_group.addChild(self.original_coords)
|
|
original_group.addChild(self.original_lineset)
|
|
|
|
# Grupo para el polígono offset
|
|
offset_group = coin.SoGroup()
|
|
offset_group.addChild(self.offset_color)
|
|
offset_group.addChild(self.offset_coords)
|
|
offset_group.addChild(self.offset_lineset)
|
|
|
|
self.root.addChild(original_group)
|
|
self.root.addChild(offset_group)
|
|
|
|
vobj.addDisplayMode(self.root, "Standard")
|
|
# Asegurar que la visibilidad esté activada
|
|
vobj.Visibility = True
|
|
|
|
def updateData(self, obj, prop):
|
|
if prop == "Shape":
|
|
self.updateVisual(obj)
|
|
|
|
def updateVisual(self, obj):
|
|
"""Actualiza la representación visual basada en la forma del objeto"""
|
|
if not hasattr(obj, 'Shape') or not obj.Shape or obj.Shape.isNull():
|
|
return
|
|
|
|
try:
|
|
# Obtener todos los bordes de la forma compuesta
|
|
all_edges = obj.Shape.Edges
|
|
|
|
# Separar bordes por polígono (asumimos que el primer polígono es el original)
|
|
# Esto es una simplificación - podrías necesitar una lógica más sofisticada
|
|
if len(all_edges) >= 2:
|
|
# Polígono original - primer conjunto de bordes
|
|
original_edges = [all_edges[0]]
|
|
original_points = []
|
|
for edge in original_edges:
|
|
for vertex in edge.Vertexes:
|
|
original_points.append((vertex.Point.x, vertex.Point.y, vertex.Point.z))
|
|
|
|
# Polígono offset - segundo conjunto de bordes
|
|
offset_edges = [all_edges[1]]
|
|
offset_points = []
|
|
for edge in offset_edges:
|
|
for vertex in edge.Vertexes:
|
|
offset_points.append((vertex.Point.x, vertex.Point.y, vertex.Point.z))
|
|
|
|
# Asignar puntos a los nodos Coordinate3
|
|
if original_points:
|
|
self.original_coords.point.setValues(0, len(original_points), original_points)
|
|
self.original_lineset.numVertices.setValue(len(original_points))
|
|
|
|
if offset_points:
|
|
self.offset_coords.point.setValues(0, len(offset_points), offset_points)
|
|
self.offset_lineset.numVertices.setValue(len(offset_points))
|
|
|
|
# Actualizar colores
|
|
if hasattr(obj, 'ViewObject') and obj.ViewObject:
|
|
self.updateColors(obj.ViewObject)
|
|
|
|
except Exception as e:
|
|
print(f"Error en updateVisual: {e}")
|
|
|
|
def onChanged(self, vobj, prop):
|
|
if prop in ["OriginalColor", "OffsetColor"]:
|
|
self.updateColors(vobj)
|
|
elif prop == "Visibility" and vobj.Visibility:
|
|
# Cuando la visibilidad cambia a True, actualizar visual
|
|
self.updateVisual(vobj.Object)
|
|
|
|
def getDisplayModes(self, obj):
|
|
return ["Standard"]
|
|
|
|
def getDefaultDisplayMode(self):
|
|
return "Standard"
|
|
|
|
def setDisplayMode(self, mode):
|
|
return mode
|
|
|
|
def claimChildren(self):
|
|
children = []
|
|
if hasattr(self, 'ViewObject') and self.ViewObject and hasattr(self.ViewObject.Object, 'Base'):
|
|
children.append(self.ViewObject.Object.Base)
|
|
return children
|
|
|
|
def dumps(self):
|
|
return None
|
|
|
|
def loads(self, state):
|
|
return None
|
|
|
|
''' 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())'''
|