# /********************************************************************** # * * # * Copyright (c) 2021 Javier Braña * # * * # * 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())'''