Compare commits

...

3 Commits

Author SHA1 Message Date
d61260fdd3 updates 2025-11-20 00:57:15 +01:00
049898c939 updates 2025-08-17 13:34:09 +04:00
3a188cc47d new code 2025-08-17 13:33:17 +04:00
14 changed files with 2260 additions and 651 deletions

View File

@@ -22,6 +22,7 @@
import FreeCAD import FreeCAD
import ArchComponent import ArchComponent
import Part
import os import os
import zipfile import zipfile
import re import re
@@ -48,17 +49,18 @@ import PVPlantResources
from PVPlantResources import DirIcons as DirIcons from PVPlantResources import DirIcons as DirIcons
Dir3dObjects = os.path.join(PVPlantResources.DirResources, "3dObjects") Dir3dObjects = os.path.join(PVPlantResources.DirResources, "3dObjects")
vector = ["Y", "YN", "Z", "ZN", "D"]
def makePCS(): def makePCS():
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "StringInverter") obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "PowerConversionSystem")
PowerConverter(obj) PowerConverter(obj)
ViewProviderStringInverter(obj.ViewObject) ViewProviderPowerConverter(obj.ViewObject)
try: try:
folder = FreeCAD.ActiveDocument.StringInverters folder = FreeCAD.ActiveDocument.PowerConversionSystemGroup
except: except:
folder = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'StringInverters') folder = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'PowerConversionSystemGroup')
folder.Label = "StringInverters" folder.Label = "PowerConversionSystemGroup"
folder.addObject(obj) folder.addObject(obj)
return obj return obj
@@ -67,9 +69,6 @@ class PowerConverter(ArchComponent.Component):
def __init__(self, obj): def __init__(self, obj):
''' Initialize the Area object ''' ''' Initialize the Area object '''
ArchComponent.Component.__init__(self, obj) ArchComponent.Component.__init__(self, obj)
self.oldMPPTs = 0
self.Type = None self.Type = None
self.obj = None self.obj = None
self.setProperties(obj) self.setProperties(obj)
@@ -77,36 +76,69 @@ class PowerConverter(ArchComponent.Component):
def setProperties(self, obj): def setProperties(self, obj):
pl = obj.PropertiesList pl = obj.PropertiesList
if not "File" in pl: # Transformer properties
obj.addProperty("App::PropertyFile", if not "Technology" in pl:
"File",
"Inverter",
"The base file this component is built upon")
if not ("MPPTs" in pl):
obj.addProperty("App::PropertyQuantity",
"MPPTs",
"Inverter",
"Points that define the area"
).MPPTs = 0
if not ("Generator" in pl):
obj.addProperty("App::PropertyEnumeration", obj.addProperty("App::PropertyEnumeration",
"Generator", "Technology",
"Inverter", "Transformer",
"Points that define the area" "Number of phases and type of transformer"
).Generator = ["Generic", "Library"] ).Technology = ["Single Phase Transformer", "Three Phase Transformer"]
obj.Generator = "Generic"
if not ("Type" in pl): if not "PowerPrimary" in pl:
obj.addProperty("App::PropertyString", obj.addProperty("App::PropertyPower",
"Type", "PowerPrimary",
"Base", "Transformer",
"Points that define the area" "The base file this component is built upon").PowerPrimary = 6000000000
).Type = "PowerConverter"
obj.setEditorMode("Type", 1)
self.Type = obj.Type if not "PowerSecundary1" in pl:
obj.addProperty("App::PropertyPower",
"PowerSecundary1",
"Transformer",
"The base file this component is built upon").PowerSecundary1 = 3000000000
if not "PowerSecundary2" in pl:
obj.addProperty("App::PropertyPower",
"PowerSecundary2",
"Transformer",
"The base file this component is built upon").PowerSecundary2 = 3000000000
if not "VoltagePrimary" in pl:
obj.addProperty("App::PropertyElectricPotential",
"VoltagePrimary",
"Transformer",
"The base file this component is built upon").VoltagePrimary = 33000000000
if not "VoltageSecundary1" in pl:
obj.addProperty("App::PropertyElectricPotential",
"VoltageSecundary1",
"Transformer",
"The base file this component is built upon").VoltageSecundary1 = 11000000000
if not "VoltageSecundary2" in pl:
obj.addProperty("App::PropertyElectricPotential",
"VoltageSecundary2",
"Transformer",
"The base file this component is built upon").VoltageSecundary2 = 11000000000
if not "VectorPrimary" in pl:
obj.addProperty("App::PropertyEnumeration",
"VectorPrimary",
"Transformer",
"The base file this component is built upon").VectorPrimary = vector
if not "VectorSecundary1" in pl:
obj.addProperty("App::PropertyEnumeration",
"VectorSecundary1",
"Transformer",
"The base file this component is built upon").VectorSecundary1 = vector
if not "VectorSecundary2" in pl:
obj.addProperty("App::PropertyEnumeration",
"VectorSecundary2",
"Transformer",
"The base file this component is built upon").VectorSecundary2 = vector
self.Type = "PowerConverter"
obj.Proxy = self obj.Proxy = self
def onDocumentRestored(self, obj): def onDocumentRestored(self, obj):
@@ -114,263 +146,34 @@ class PowerConverter(ArchComponent.Component):
self.setProperties(obj) self.setProperties(obj)
def onBeforeChange(self, obj, prop): def onBeforeChange(self, obj, prop):
''' '''
if prop == "MPPTs": # This method is called before a property is changed.
self.oldMPPTs = int(obj.MPPTs) # It can be used to validate the property value or to update other properties.
# If the property is not valid, you can raise an exception.
# If you want to prevent the change, you can return False.
# Otherwise, return True to allow the change.
return True
def onChanged(self, obj, prop): def onChanged(self, obj, prop):
''' ''' ''' '''
if prop == "Generator":
if obj.Generator == "Generic":
obj.setEditorMode("MPPTs", 0)
else:
obj.setEditorMode("MPPTs", 1)
if prop == "MPPTs":
''' '''
if self.oldMPPTs > obj.MPPTs:
''' borrar sobrantes '''
obj.removeProperty()
elif self.oldMPPTs < obj.MPPTs:
''' crear los faltantes '''
for i in range(self.oldMPPTs, int(obj.MPPTs)):
''' '''
print(i)
else:
pass
if (prop == "File") and obj.File:
''' '''
def execute(self, obj): def execute(self, obj):
''' ''' ''' '''
# obj.Shape: compound # obj.Shape: compound
# |- body: compound # |- body: compound
# |-- inverter: solid # |- transformer: solid
# |-- door: solid # |- primary switchgear: compound
# |-- holder: solid # |- secundary 1 switchgear: compound
# |- secundary 2 switchgear: compound
# |- connectors: compound
# |-- DC: compound
# |--- MPPT 1..x: compound
# |---- positive: compound
# |----- connector 1..y: ??
# |---- negative 1..y: compound
# |----- connector 1..y: ??
# |-- AC: compound
# |--- R,S,T,: ??
# |-- Communication
pl = obj.Placement pl = obj.Placement
filename = self.getFile(obj) obj.Shape = Part.makeBox(6058, 2438, 2591) # Placeholder for the shape
if filename: obj.Placement = pl
parts = self.getPartsList(obj)
if parts:
zdoc = zipfile.ZipFile(filename)
if zdoc:
f = zdoc.open(parts[list(parts.keys())[-1]][1])
shapedata = f.read()
f.close()
shapedata = shapedata.decode("utf8")
shape = self.cleanShape(shapedata, obj, parts[list(parts.keys())[-1]][2])
obj.Shape = shape
if not pl.isIdentity():
obj.Placement = pl
obj.MPPTs = len(shape.SubShapes[1].SubShapes[0].SubShapes)
def cleanShape(self, shapedata, obj, materials):
"cleans the imported shape"
import Part
shape = Part.Shape()
shape.importBrepFromString(shapedata)
'''if obj.FuseArch and materials:
# separate lone edges
shapes = []
for edge in shape.Edges:
found = False
for solid in shape.Solids:
for soledge in solid.Edges:
if edge.hashCode() == soledge.hashCode():
found = True
break
if found:
break
if found:
break
else:
shapes.append(edge)
print("solids:",len(shape.Solids),"mattable:",materials)
for key,solindexes in materials.items():
if key == "Undefined":
# do not join objects with no defined material
for solindex in [int(i) for i in solindexes.split(",")]:
shapes.append(shape.Solids[solindex])
else:
fusion = None
for solindex in [int(i) for i in solindexes.split(",")]:
if not fusion:
fusion = shape.Solids[solindex]
else:
fusion = fusion.fuse(shape.Solids[solindex])
if fusion:
shapes.append(fusion)
shape = Part.makeCompound(shapes)
try:
shape = shape.removeSplitter()
except Exception:
print(obj.Label,": error removing splitter")'''
return shape
def getFile(self, obj, filename=None): class ViewProviderPowerConverter(ArchComponent.ViewProviderComponent):
"gets a valid file, if possible"
if not filename:
filename = obj.File
if not filename:
return None
if not filename.lower().endswith(".fcstd"):
return None
if not os.path.exists(filename):
# search for the file in the current directory if not found
basename = os.path.basename(filename)
currentdir = os.path.dirname(obj.Document.FileName)
altfile = os.path.join(currentdir,basename)
if altfile == obj.Document.FileName:
return None
elif os.path.exists(altfile):
return altfile
else:
# search for subpaths in current folder
altfile = None
subdirs = self.splitall(os.path.dirname(filename))
for i in range(len(subdirs)):
subpath = [currentdir]+subdirs[-i:]+[basename]
altfile = os.path.join(*subpath)
if os.path.exists(altfile):
return altfile
return None
return filename
def getPartsList(self, obj, filename=None):
"returns a list of Part-based objects in a FCStd file"
parts = {}
materials = {}
filename = self.getFile(obj,filename)
if not filename:
return parts
zdoc = zipfile.ZipFile(filename)
with zdoc.open("Document.xml") as docf:
name = None
label = None
part = None
materials = {}
writemode = False
for line in docf:
line = line.decode("utf8")
if "<Object name=" in line:
n = re.findall('name=\"(.*?)\"',line)
if n:
name = n[0]
elif "<Property name=\"Label\"" in line:
writemode = True
elif writemode and "<String value=" in line:
n = re.findall('value=\"(.*?)\"',line)
if n:
label = n[0]
writemode = False
elif "<Property name=\"Shape\" type=\"Part::PropertyPartShape\"" in line:
writemode = True
elif writemode and "<Part file=" in line:
n = re.findall('file=\"(.*?)\"',line)
if n:
part = n[0]
writemode = False
elif "<Property name=\"MaterialsTable\" type=\"App::PropertyMap\"" in line:
writemode = True
elif writemode and "<Item key=" in line:
n = re.findall('key=\"(.*?)\"',line)
v = re.findall('value=\"(.*?)\"',line)
if n and v:
materials[n[0]] = v[0]
elif writemode and "</Map>" in line:
writemode = False
elif "</Object>" in line:
if name and label and part:
parts[name] = [label,part,materials]
name = None
label = None
part = None
materials = {}
writemode = False
return parts
def getColors(self,obj):
"returns the DiffuseColor of the referenced object"
filename = self.getFile(obj)
if not filename:
return None
part = obj.Part
if not obj.Part:
return None
zdoc = zipfile.ZipFile(filename)
if not "GuiDocument.xml" in zdoc.namelist():
return None
colorfile = None
with zdoc.open("GuiDocument.xml") as docf:
writemode1 = False
writemode2 = False
for line in docf:
line = line.decode("utf8")
if ("<ViewProvider name=" in line) and (part in line):
writemode1 = True
elif writemode1 and ("<Property name=\"DiffuseColor\"" in line):
writemode1 = False
writemode2 = True
elif writemode2 and ("<ColorList file=" in line):
n = re.findall('file=\"(.*?)\"',line)
if n:
colorfile = n[0]
break
if not colorfile:
return None
if not colorfile in zdoc.namelist():
return None
colors = []
cf = zdoc.open(colorfile)
buf = cf.read()
cf.close()
for i in range(1,int(len(buf)/4)):
colors.append((buf[i*4+3]/255.0,buf[i*4+2]/255.0,buf[i*4+1]/255.0,buf[i*4]/255.0))
if colors:
return colors
return None
def splitall(self,path):
"splits a path between its components"
allparts = []
while 1:
parts = os.path.split(path)
if parts[0] == path: # sentinel for absolute paths
allparts.insert(0, parts[0])
break
elif parts[1] == path: # sentinel for relative paths
allparts.insert(0, parts[1])
break
else:
path = parts[0]
allparts.insert(0, parts[1])
return allparts
class ViewProviderStringInverter(ArchComponent.ViewProviderComponent):
def __init__(self, vobj): def __init__(self, vobj):
ArchComponent.ViewProviderComponent.__init__(self, vobj) ArchComponent.ViewProviderComponent.__init__(self, vobj)
@@ -381,12 +184,12 @@ class CommandPowerConverter:
def GetResources(self): def GetResources(self):
return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "Inverter.svg")), return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "Inverter.svg")),
'Accel': "E, I", 'Accel': "E, P",
'MenuText': "String Inverter", 'MenuText': "Power Converter",
'ToolTip': "String Placement",} 'ToolTip': "Power Converter",}
def Activated(self): def Activated(self):
sinverter = makeStringInverter() sinverter = makePCS()
def IsActive(self): def IsActive(self):
active = not (FreeCAD.ActiveDocument is None) active = not (FreeCAD.ActiveDocument is None)

View File

@@ -141,29 +141,31 @@ def groupTrackersToTransformers(transformer_power, max_distance):
for i, group in enumerate(transformer_groups): for i, group in enumerate(transformer_groups):
# Crear la esfera que representará el CT # Crear la esfera que representará el CT
ct_sphere = FreeCAD.ActiveDocument.addObject("Part::Sphere", f"CT_{i + 1}") ct_shape = FreeCAD.ActiveDocument.addObject("Part::Box", f"CT_{i + 1}")
ct_sphere.Radius = 5000 # 2m de radio ct_shape.Length = 6058
ct_sphere.Placement.Base = FreeCAD.Vector(group['center'][0], group['center'][1], 0) ct_shape.Width = 2438
ct_shape.Height = 2591
ct_shape.Placement.Base = FreeCAD.Vector(group['center'][0], group['center'][1], 0)
# Añadir propiedades personalizadas # Añadir propiedades personalizadas
ct_sphere.addProperty("App::PropertyLinkList", "Trackers", "CT", ct_shape.addProperty("App::PropertyLinkList", "Trackers", "CT",
"Lista de trackers asociados a este CT") "Lista de trackers asociados a este CT")
ct_sphere.addProperty("App::PropertyFloat", "TotalPower", "CT", ct_shape.addProperty("App::PropertyFloat", "TotalPower", "CT",
"Potencia total del grupo (W)") "Potencia total del grupo (W)")
ct_sphere.addProperty("App::PropertyFloat", "NominalPower", "CT", ct_shape.addProperty("App::PropertyFloat", "NominalPower", "CT",
"Potencia nominal del transformador (W)") "Potencia nominal del transformador (W)")
ct_sphere.addProperty("App::PropertyFloat", "Utilization", "CT", ct_shape.addProperty("App::PropertyFloat", "Utilization", "CT",
"Porcentaje de utilización (Total/Nominal)") "Porcentaje de utilización (Total/Nominal)")
# Establecer valores de las propiedades # Establecer valores de las propiedades
ct_sphere.Trackers = group['trackers'] ct_shape.Trackers = group['trackers']
ct_sphere.TotalPower = group['total_power'].Value ct_shape.TotalPower = group['total_power'].Value
ct_sphere.NominalPower = transformer_power ct_shape.NominalPower = transformer_power
ct_sphere.Utilization = (group['total_power'].Value / transformer_power) * 100 ct_shape.Utilization = (group['total_power'].Value / transformer_power) * 100
# Configurar visualización # Configurar visualización
# Calcular color basado en utilización (verde < 100%, amarillo < 110%, rojo > 110%) # Calcular color basado en utilización (verde < 100%, amarillo < 110%, rojo > 110%)
utilization = ct_sphere.Utilization utilization = ct_shape.Utilization
if utilization <= 100: if utilization <= 100:
color = (0.0, 1.0, 0.0) # Verde color = (0.0, 1.0, 0.0) # Verde
elif utilization <= 110: elif utilization <= 110:
@@ -171,18 +173,19 @@ def groupTrackersToTransformers(transformer_power, max_distance):
else: else:
color = (1.0, 0.0, 0.0) # Rojo color = (1.0, 0.0, 0.0) # Rojo
ct_sphere.ViewObject.ShapeColor = color ct_shape.ViewObject.ShapeColor = color
ct_sphere.ViewObject.Transparency = 40 # 40% de transparencia ct_shape.ViewObject.Transparency = 40 # 40% de transparencia
# Añadir etiqueta con información # Añadir etiqueta con información
ct_sphere.ViewObject.DisplayMode = "Shaded" ct_shape.ViewObject.DisplayMode = "Shaded"
ct_sphere.Label = f"CT {i + 1} ({ct_sphere.TotalPower / 1000:.1f}kW/{ct_sphere.NominalPower / 1000:.1f}kW)" ct_shape.Label = f"CT {i + 1} ({ct_shape.TotalPower / 1000:.1f}kW/{ct_shape.NominalPower / 1000:.1f}kW)"
# Añadir al grupo principal # Añadir al grupo principal
transformer_group.addObject(ct_sphere) transformer_group.addObject(ct_shape)
FreeCAD.Console.PrintMessage(f"Se crearon {len(transformer_groups)} centros de transformación\n") FreeCAD.Console.PrintMessage(f"Se crearon {len(transformer_groups)} centros de transformación\n")
onSelectGatePoint()
#onSelectGatePoint()
import FreeCAD, FreeCADGui, Part import FreeCAD, FreeCADGui, Part
@@ -195,7 +198,7 @@ class InternalPathCreator:
self.gate_point = gate_point self.gate_point = gate_point
self.strategy = strategy self.strategy = strategy
self.path_width = path_width self.path_width = path_width
self.ct_spheres = [] self.ct_shapes = []
self.ct_positions = [] self.ct_positions = []
def get_transformers(self): def get_transformers(self):
@@ -204,13 +207,13 @@ class InternalPathCreator:
FreeCAD.Console.PrintError("No se encontró el grupo 'Transformers'\n") FreeCAD.Console.PrintError("No se encontró el grupo 'Transformers'\n")
return False return False
self.ct_spheres = transformers_group.Group self.ct_shapes = transformers_group.Group
if not self.ct_spheres: if not self.ct_shapes:
FreeCAD.Console.PrintWarning("No hay Centros de Transformación en el grupo\n") FreeCAD.Console.PrintWarning("No hay Centros de Transformación en el grupo\n")
return False return False
# Obtener las posiciones de los CTs # Obtener las posiciones de los CTs
for sphere in self.ct_spheres: for sphere in self.ct_shapes:
base = sphere.Placement.Base base = sphere.Placement.Base
self.ct_positions.append(FreeCAD.Vector(base.x, base.y, 0)) self.ct_positions.append(FreeCAD.Vector(base.x, base.y, 0))
return True return True
@@ -263,6 +266,8 @@ class InternalPathCreator:
y_proj = slope * x_proj + intercept y_proj = slope * x_proj + intercept
return FreeCAD.Vector(x_proj, y_proj, 0) return FreeCAD.Vector(x_proj, y_proj, 0)
# return slope * x + intercept --> desde placement
projected_points = [project_point(p) for p in all_points] projected_points = [project_point(p) for p in all_points]
# Calcular distancias a lo largo de la línea # Calcular distancias a lo largo de la línea

View File

@@ -76,8 +76,10 @@ def getWire(wire, nospline=False, width=.0):
import DraftGeomUtils import DraftGeomUtils
import math import math
offset = FreeCAD.ActiveDocument.Site.Origin
def fmt(vec, b=0.0): def fmt(vec, b=0.0):
return (vec.x * 0.001, vec.y * 0.001, width, width, b) return ((vec.x + offset.x) * 0.001, (vec.y + offset.y) * 0.001, width, width, b)
points = [] points = []
edges = Part.__sortEdges__(wire.Edges) edges = Part.__sortEdges__(wire.Edges)
@@ -626,6 +628,7 @@ layers = [
("Available area Names", QtGui.QColor(255, 255, 255), "Continuous", "1", True), ("Available area Names", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
("Areas Exclusion", QtGui.QColor(255, 85, 0), "Continuous", "1", True), ("Areas Exclusion", QtGui.QColor(255, 85, 0), "Continuous", "1", True),
("Areas Exclusion Offset", QtGui.QColor(255, 85, 0), "Continuous", "1", True),
("Areas Exclusion Name", QtGui.QColor(255, 85, 0), "Continuous", "1", True), ("Areas Exclusion Name", QtGui.QColor(255, 85, 0), "Continuous", "1", True),
("Areas Cadastral Plot", QtGui.QColor(255, 255, 255), "Continuous", "1", True), ("Areas Cadastral Plot", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
("Areas Cadastral Plot Name", QtGui.QColor(255, 255, 255), "Continuous", "1", True), ("Areas Cadastral Plot Name", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
@@ -646,7 +649,7 @@ layers = [
("CIVIL External Roads Text", QtGui.QColor(255, 255, 192), "Continuous", "1", True), ("CIVIL External Roads Text", QtGui.QColor(255, 255, 192), "Continuous", "1", True),
("CIVIL Internal Roads", QtGui.QColor(153, 95, 76), "Continuous", "1", True), ("CIVIL Internal Roads", QtGui.QColor(153, 95, 76), "Continuous", "1", True),
("CIVIL Internal Roads Axis", QtGui.QColor(192, 192, 192), "Dashed", "1", True), ("CIVIL Internal Roads Axis", QtGui.QColor(192, 192, 192), "Dashed", "1", True),
("CIVIL External Roads Text", QtGui.QColor(192, 192, 192), "Continuous", "1", True), ("CIVIL Internal Roads Text", QtGui.QColor(192, 192, 192), "Continuous", "1", True),
("Contour Line Legend text", QtGui.QColor(255, 255, 255), "Continuous", "1", True), ("Contour Line Legend text", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
("Major contour line", QtGui.QColor(0, 0, 0), "Continuous", "1", True), ("Major contour line", QtGui.QColor(0, 0, 0), "Continuous", "1", True),
@@ -893,6 +896,9 @@ class _PVPlantExportDXF(QtGui.QWidget):
angle=0, angle=0,
layer=area_type[1] layer=area_type[1]
) )
for obj in FreeCADGui.Selection.getSelection():
tmp = exporter.createPolyline(obj, areas_types[0][1])
def writeFrameSetups(self, exporter): def writeFrameSetups(self, exporter):
if not hasattr(FreeCAD.ActiveDocument, "Site"): if not hasattr(FreeCAD.ActiveDocument, "Site"):

View File

@@ -29,6 +29,15 @@ import Part
import numpy import numpy
import os import os
from xml.etree.ElementTree import Element, SubElement
import xml.etree.ElementTree as ElementTree
import datetime
from xml.dom import minidom
from numpy.matrixlib.defmatrix import matrix
from Utils import PVPlantUtils as utils
if FreeCAD.GuiUp: if FreeCAD.GuiUp:
import FreeCADGui import FreeCADGui
from PySide import QtCore from PySide import QtCore
@@ -63,6 +72,11 @@ def check_collada():
FreeCAD.Console.PrintError(translate("PVPlant", "pycollada no encontrado, soporte Collada desactivado.") + "\n") FreeCAD.Console.PrintError(translate("PVPlant", "pycollada no encontrado, soporte Collada desactivado.") + "\n")
return COLLADA_AVAILABLE return COLLADA_AVAILABLE
# Asegurar que el texto es Unicode válido
def safe_text(text):
if isinstance(text, bytes):
return text.decode('utf-8', errors='replace')
return text
# from ARCH: # from ARCH:
def triangulate(shape): def triangulate(shape):
@@ -249,7 +263,306 @@ def export(exportList, filename, tessellation=1, colors=None):
FreeCAD.Console.PrintMessage(translate("Arch", "file %s successfully created.") % filename) FreeCAD.Console.PrintMessage(translate("Arch", "file %s successfully created.") % filename)
def exportToPVC(path, exportTerrain = False): def exportToPVC(path, exportTerrain=False):
filename = f"{path}.pvc"
# 1. Validación inicial de objetos esenciales
site = None
for obj in FreeCAD.ActiveDocument.Objects:
if obj.Name.startswith("Site") and hasattr(obj, 'Terrain'):
site = obj
break
if not site:
FreeCAD.Console.PrintError("No se encontró objeto 'Site' válido\n")
return False
# 2. Configuración de metadatos y autor
generated_on = str(datetime.datetime.now())
try:
author = FreeCAD.ActiveDocument.CreatedBy
except (AttributeError, UnicodeEncodeError):
author = "Unknown"
author = author.replace("<", "").replace(">", "")
ver = FreeCAD.Version()
appli = f"PVPlant for FreeCAD {ver[0]}.{ver[1]} build{ver[2]}"
# 3. Creación estructura XML base
root = Element('COLLADA')
root.set('xmlns', 'http://www.collada.org/2005/11/COLLADASchema')
root.set('version', '1.4.1')
# 4. Sección <asset>
asset = SubElement(root, 'asset')
contrib = SubElement(asset, 'contributor')
SubElement(contrib, 'author').text = safe_text(author)
SubElement(contrib, 'authoring_tool').text = safe_text(appli)
SubElement(asset, 'created').text = generated_on
SubElement(asset, 'modified').text = generated_on
SubElement(asset, 'title').text = safe_text(FreeCAD.ActiveDocument.Name)
unit = SubElement(asset, 'unit')
unit.set('name', 'meter')
unit.set('meter', '1')
# 5. Materiales y efectos
materials = ["Frames", "Tree_trunk", "Tree_crown", "Topography_mesh"]
# Library materials
lib_materials = SubElement(root, 'library_materials')
for i, name in enumerate(materials):
mat = SubElement(lib_materials, 'material')
mat.set('id', f'Material{i}')
mat.set('name', name)
SubElement(mat, 'instance_effect').set('url', f'#Material{i}-fx')
# Library effects
lib_effects = SubElement(root, 'library_effects')
for i, _ in enumerate(materials):
effect = SubElement(lib_effects, 'effect')
effect.set('id', f'Material{i}-fx')
effect.set('name', f'Material{i}')
profile = SubElement(effect, 'profile_COMMON')
technique = SubElement(profile, 'technique')
technique.set('sid', 'standard')
lambert = SubElement(technique, 'lambert')
# Componentes del material
color = SubElement(SubElement(lambert, 'emission'), 'color')
color.set('sid', 'emission')
color.text = '0.000000 0.000000 0.000000 1.000000'
color = SubElement(SubElement(lambert, 'ambient'), 'color')
color.set('sid', 'ambient')
color.text = '0.200000 0.200000 0.200000 1.000000'
color = SubElement(SubElement(lambert, 'diffuse'), 'color')
color.set('sid', 'diffuse')
color.text = '0.250000 0.500000 0.000000 1.000000'
transparent = SubElement(lambert, 'transparent')
transparent.set('opaque', 'RGB_ZERO')
color = SubElement(transparent, 'color')
color.set('sid', 'transparent')
color.text = '0.000000 0.000000 0.000000 1.000000'
value = SubElement(SubElement(lambert, 'transparency'), 'float')
value.set('sid', 'transparency')
value.text = '0.000000'
# 6. Geometrías
lib_geometries = SubElement(root, 'library_geometries')
# 7. Escena visual
lib_visual = SubElement(root, 'library_visual_scenes')
visual_scene = SubElement(lib_visual, 'visual_scene')
visual_scene.set('id', 'Scene') # cambiar a visual_scene_0
visual_scene.set('name', 'Scene') # cambiar a Default visual scene
scene_node = SubElement(visual_scene, 'node')
scene_node.set('id', 'node_0_id')
scene_node.set('name', 'node_0_name')
scene_node.set('sid', 'node_0_sid')
scene_matrix = SubElement(scene_node, 'matrix')
scene_matrix.set('sid', 'matrix_0')
scene_matrix.text = '1.000000 0.000000 0.000000 0.000000 0.000000 0.000000 -1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000'
root_node = SubElement(scene_node, 'node')
root_node.set('id', 'node_1_id')
root_node.set('name', 'node_1_name')
root_node.set('sid', 'node_1_sid')
# 8. Función para procesar geometrías
def create_geometry(name, vindex, findex, material_id, objind=0, frame_data=None, isTracker = False, axis_line=None):
"""Crea elementos COLLADA para una geometría"""
# Source (vertices)
source_mesh = SubElement(geom, 'mesh')
source = SubElement(source_mesh, 'source')
source.set('id', f'{name}-mesh_source')
float_array = SubElement(source, 'float_array')
float_array.set('id', f'{name}-float_array')
float_array.set('count', str(len(vindex)))
float_array.text = ' '.join(f'{v:.6f}' for v in vindex)
technique = SubElement(source, 'technique_common')
accessor = SubElement(technique, 'accessor')
accessor.set('count', str(len(vindex)))
accessor.set('source', f'#{name}-float_array')
accessor.set('stride', '3')
for ax in ['X', 'Y', 'Z']:
param = SubElement(accessor, 'param')
param.set('name', ax)
param.set('type', 'float')
# Vertices
vertices = SubElement(source_mesh, 'vertices')
vertices.set('id', f'{name}-vertices_source')
vertices = SubElement(vertices, 'input')
vertices.set('semantic', 'POSITION')
vertices.set('source', f'#{name}-mesh_source')
# Triangles
triangles = SubElement(source_mesh, 'triangles')
triangles.set('count', '0')
triangles.set('material', f'Material{material_id}')
triangles_input = SubElement(triangles, 'input')
triangles_input.set('offset', '0')
triangles_input.set('semantic', 'VERTEX')
triangles_input.set('source', f'#{name}-vertices_source')
p = SubElement(triangles, 'p')
p.text = ' '.join(map(str, findex))
# Parámetros especiales para estructuras
frame_params = SubElement(source_mesh, 'tracker_parameters')
if frame_data:
for key, val in frame_data.items():
elem = SubElement(frame_params, key)
elem.text = str(val)
if isTracker:
axis_parameter = SubElement(frame_params, 'axis_vertices')
if axis_line:
for idx, vert in enumerate(axis_line):
array = SubElement(axis_parameter, 'float_array')
array.set('id', f'{name}-axis_float_array{idx}')
array.set('count', '3')
array.text = ' '.join(f'{v:.6f}' for v in vert)
# 9. Procesar estructuras (frames/trackers)
center = FreeCAD.Vector()
if site.Terrain:
center = site.Terrain.Mesh.BoundBox.Center
objind = 0
for frame_type in site.Frames:
is_tracker = "tracker" in frame_type.Proxy.Type.lower()
modules = frame_type.Shape.SubShapes[0].SubShapes[0]
pts = []
for i in range(4):
pts.append(modules.BoundBox.getPoint(i))
new_shape = Part.Face(Part.makePolygon(pts))
mesh = Mesh.Mesh(triangulate(new_shape))
axis = Part.makeLine(FreeCAD.Vector(modules.BoundBox.XMin, 0, modules.BoundBox.ZMax),
FreeCAD.Vector(modules.BoundBox.XMax, 0, modules.BoundBox.ZMax))
for obj in FreeCAD.ActiveDocument.Objects:
if hasattr(obj, "Setup") and obj.Setup == frame_type:
# Procesar geometría
mesh.Placement = obj.getGlobalPlacement()
axis.Placement = obj.getGlobalPlacement()
# Transformar vértices
vindex = []
for point in mesh.Points:
adjusted = (point.Vector - center) * scale
vindex.extend([
-adjusted.x,
adjusted.z,
adjusted.y
])
# Índices de caras
findex = []
for facet in mesh.Facets:
findex.extend(facet.PointIndices)
# AXIS
# TODO: revisar si es así:
vaxis = []
for vert in axis.Vertexes:
adjusted = (vert.Point - center) * scale
vaxis.append([
-adjusted.x,
adjusted.z,
adjusted.y
])
# Crear geometría COLLADA
geom = SubElement(lib_geometries, 'geometry')
geom.set('id', f'Frame_{objind}')
# Parámetros específicos de estructura
frame_data = {
'module_width': obj.Setup.ModuleWidth.Value,
'module_height': obj.Setup.ModuleHeight.Value,
'module_x_spacing': obj.Setup.ModuleColGap.Value,
'module_y_spacing': obj.Setup.ModuleRowGap.Value,
'module_name': 'Generic'
}
if is_tracker:
frame_data.update({
'tracker_type': 'single_axis_trackers',
'min_phi': obj.Setup.MinPhi.Value,
'max_phi': obj.Setup.MaxPhi.Value,
'min_theta': 0,
'max_theta': 0
})
create_geometry(
name=f'Frame_{objind}',
vindex=vindex,
findex=findex,
material_id=0,
objind=objind,
frame_data=frame_data,
isTracker = is_tracker,
axis_line=vaxis
)
# Instancia en escena
instance = SubElement(root_node, 'instance_geometry')
instance.set('url', f'#Frame_{objind}')
bind_material = SubElement(instance, 'bind_material')
technique_common = SubElement(bind_material, 'technique_common')
instance_material = SubElement(technique_common, 'instance_material')
instance_material.set('symbol', 'Material0')
instance_material.set('target', '#Material0')
objind += 1
# 10. Procesar terreno si está habilitado
if exportTerrain and site.Terrain:
mesh = site.Terrain.Mesh
vindex = []
for point in mesh.Points:
point = point.Vector
vindex.extend([
-point.x * SCALE,
point.z * SCALE,
point.y * SCALE
])
findex = []
for facet in mesh.Facets:
findex.extend(facet.PointIndices)
geom = SubElement(lib_geometries, 'geometry')
geom.set('id', 'Terrain')
create_geometry('Terrain', vindex, findex, material_id=3)
instance = SubElement(root_node, 'instance_geometry')
instance.set('url', '#Terrain')
# 11. Escena principal
scene = SubElement(root, 'scene')
SubElement(scene, 'instance_visual_scene').set('url', '#Scene')
# 12. Exportar a archivo
xml_str = minidom.parseString(
ElementTree.tostring(root, encoding='utf-8')
).toprettyxml(indent=" ")
with open(filename, 'w', encoding='utf-8') as f:
f.write(xml_str)
FreeCAD.Console.PrintMessage(f"Archivo PVC generado: {filename}\n")
return True
def exportToPVC_old(path, exportTerrain = False):
filename = f"{path}.pvc" filename = f"{path}.pvc"
from xml.etree.ElementTree import Element, SubElement from xml.etree.ElementTree import Element, SubElement
@@ -291,17 +604,18 @@ def exportToPVC(path, exportTerrain = False):
# xml: 1. Asset: # xml: 1. Asset:
asset = SubElement(root, 'asset') asset = SubElement(root, 'asset')
asset_contributor = SubElement(asset, 'contributor') asset_contributor = SubElement(asset, 'contributor')
asset_contributor_autor = SubElement(asset_contributor, 'autor') asset_contributor_autor = SubElement(asset_contributor, 'author')
#asset_contributor_autor.text = author asset_contributor_autor.text = author
asset_contributor_authoring_tool = SubElement(asset_contributor, 'authoring_tool') asset_contributor_authoring_tool = SubElement(asset_contributor, 'authoring_tool')
#asset_contributor_authoring_tool.text = appli asset_contributor_authoring_tool.text = appli
asset_contributor_comments = SubElement(asset_contributor, 'comments') asset_contributor_comments = SubElement(asset_contributor, 'comments')
asset_keywords = SubElement(asset, 'keywords') asset_keywords = SubElement(asset, 'keywords')
asset_revision = SubElement(asset, 'revision') asset_revision = SubElement(asset, 'revision')
asset_subject = SubElement(asset, 'subject') asset_subject = SubElement(asset, 'subject')
asset_tittle = SubElement(asset, 'title') asset_tittle = SubElement(asset, 'title')
#asset_tittle.text = FreeCAD.ActiveDocument.Name asset_tittle.text = FreeCAD.ActiveDocument.Name
asset_unit = SubElement(asset, 'unit') asset_unit = SubElement(asset, 'unit')
asset_unit.set('meter', '0.001') asset_unit.set('meter', '0.001')
asset_unit.set('name', 'millimeter') asset_unit.set('name', 'millimeter')
@@ -359,7 +673,6 @@ def exportToPVC(path, exportTerrain = False):
# xml: 4. library_geometries: # xml: 4. library_geometries:
library_geometries = SubElement(root, 'library_geometries') library_geometries = SubElement(root, 'library_geometries')
def add_geometry(objtype, vindex, findex, objind = 0, centers = None): def add_geometry(objtype, vindex, findex, objind = 0, centers = None):
isFrame = False isFrame = False
if objtype == 0: if objtype == 0:
geometryName = 'Frame' geometryName = 'Frame'
@@ -505,36 +818,20 @@ def exportToPVC(path, exportTerrain = False):
end_time.text = '1.000000' end_time.text = '1.000000'
# xml: 6. scene: # xml: 6. scene:
scene = SubElement(root, 'scene') '''scene = SubElement(root, 'scene')
instance = SubElement(scene, 'instance_visual_scene') instance = SubElement(scene, 'instance_visual_scene')
instance.set('url', '#') instance.set('url', '#')'''
full_list_of_objects = FreeCAD.ActiveDocument.Objects
# CASO 1 - FRAMES: # CASO 1 - FRAMES:
frameType = site.Frames frameType = site.Frames
frame_setup = {"type": [],
"footprint": []}
for obj in frameType:
frame_setup["type"] = obj
frame_setup["footprint"] = ""
objind = 0 objind = 0
# TODO: revisar # TODO: revisar
for typ in frameType: for typ in frameType:
isTracker = "tracker" in typ.Proxy.Type.lower() isTracker = "tracker" in typ.Proxy.Type.lower()
isTracker = False #isTracker = False
objectlist = FreeCAD.ActiveDocument.findObjects(Name="Tracker")
tmp = []
for obj in objectlist:
if obj.Name.startswith("TrackerSetup"):
continue
else:
tmp.append(obj)
objectlist = tmp.copy()
objectlist = utils.findObjects("Tracker")
for obj in objectlist: for obj in objectlist:
if obj.Setup == typ: if obj.Setup == typ:
findex = numpy.array([]) findex = numpy.array([])
@@ -580,7 +877,6 @@ def exportToPVC(path, exportTerrain = False):
v = Topology[0][i] v = Topology[0][i]
vindex[list(range(i * 3, i * 3 + 3))] = (-(v.x - center.x) * scale, (v.z - center.z) * scale, vindex[list(range(i * 3, i * 3 + 3))] = (-(v.x - center.x) * scale, (v.z - center.z) * scale,
(v.y - center.y) * scale) (v.y - center.y) * scale)
# 2. face indices # 2. face indices
findex = numpy.empty(len(Topology[1]) * 3, numpy.int64) findex = numpy.empty(len(Topology[1]) * 3, numpy.int64)
for i in range(len(Topology[1])): for i in range(len(Topology[1])):

View File

@@ -69,7 +69,7 @@ class OSMImporter:
headers={'User-Agent': 'FreeCAD-OSM-Importer/1.0'}, headers={'User-Agent': 'FreeCAD-OSM-Importer/1.0'},
method='POST' method='POST'
) )
return urllib.request.urlopen(req, context=self.ssl_context, timeout=30).read() return urllib.request.urlopen(req, context=self.ssl_context, timeout=160).read()
def create_layer(self, name): def create_layer(self, name):
if not FreeCAD.ActiveDocument.getObject(name): if not FreeCAD.ActiveDocument.getObject(name):

View File

@@ -59,6 +59,7 @@ class PVPlantWorkbench(Workbench):
"PVPlantBuilding", "PVPlantBuilding",
"PVPlantFenceGroup", "PVPlantFenceGroup",
]''' ]'''
from Electrical.PowerConverter import PowerConverter
self.electricalList = ["PVPlantStringBox", self.electricalList = ["PVPlantStringBox",
"PVPlantCable", "PVPlantCable",
"PVPlanElectricalLine", "PVPlanElectricalLine",
@@ -66,6 +67,8 @@ class PVPlantWorkbench(Workbench):
"Stringing", "Stringing",
"Separator", "Separator",
"StringInverter", "StringInverter",
"Separator",
"PowerConverter"
] ]
self.roads = ["PVPlantRoad", self.roads = ["PVPlantRoad",

View File

@@ -540,6 +540,7 @@ def makeTrackerSetup(name="TrackerSetup"):
pass pass
return obj return obj
def getarray(array, numberofpoles): def getarray(array, numberofpoles):
if len(array) == 0: if len(array) == 0:
newarray = [0] * numberofpoles newarray = [0] * numberofpoles
@@ -568,6 +569,7 @@ def getarray(array, numberofpoles):
newarray = [array[0]] * numberofpoles newarray = [array[0]] * numberofpoles
return newarray return newarray
class TrackerSetup(FrameSetup): class TrackerSetup(FrameSetup):
"A 1 Axis Tracker Obcject" "A 1 Axis Tracker Obcject"
@@ -589,7 +591,7 @@ class TrackerSetup(FrameSetup):
obj.addProperty("App::PropertyDistance", obj.addProperty("App::PropertyDistance",
"MotorGap", "MotorGap",
"ModuleArray", "ModuleArray",
QT_TRANSLATE_NOOP("App::Property", "Thse height of this object") QT_TRANSLATE_NOOP("App::Property", "The height of this object")
).MotorGap = 550 ).MotorGap = 550
if not "UseGroupsOfModules" in pl: if not "UseGroupsOfModules" in pl:
@@ -880,6 +882,9 @@ class TrackerSetup(FrameSetup):
def CalculatePosts(self, obj, totalh, totalw): def CalculatePosts(self, obj, totalh, totalw):
# Temp: utilizar el uso de versiones: # Temp: utilizar el uso de versiones:
if len(obj.PoleType) == 0:
return None, None
ver = 1 ver = 1
if ver == 0: if ver == 0:
# versión 0: # versión 0:
@@ -906,8 +911,8 @@ class TrackerSetup(FrameSetup):
elif ver == 1: elif ver == 1:
# versión 1: # versión 1:
linetmp = Part.LineSegment(FreeCAD.Vector(0), FreeCAD.Vector(0, 10, 0)).toShape() linetmp = Part.LineSegment(FreeCAD.Vector(0), FreeCAD.Vector(0, 10, 0)).toShape()
compoundPoles = Part.makeCompound([]) compound_poles = Part.makeCompound([])
compoundAxis = Part.makeCompound([]) compound_axis = Part.makeCompound([])
offsetX = - totalw / 2 offsetX = - totalw / 2
arrayDistance = obj.DistancePole arrayDistance = obj.DistancePole
@@ -915,15 +920,16 @@ class TrackerSetup(FrameSetup):
arrayPost = obj.PoleSequence arrayPost = obj.PoleSequence
for x in range(int(obj.NumberPole.Value)): for x in range(int(obj.NumberPole.Value)):
postCopy = obj.PoleType[arrayPost[x]].Shape.copy() post_copy = obj.PoleType[arrayPost[x]].Shape.copy()
offsetX += arrayDistance[x] offsetX += arrayDistance[x]
postCopy.Placement.Base = FreeCAD.Vector(offsetX, 0, -(postCopy.BoundBox.ZLength - arrayAerial[x])) post_copy.Placement.Base = FreeCAD.Vector(offsetX, 0, -(post_copy.BoundBox.ZLength - arrayAerial[x]))
compoundPoles.add(postCopy) compound_poles.add(post_copy)
axis = linetmp.copy() axis = linetmp.copy()
axis.Placement.Base = FreeCAD.Vector(offsetX, 0, arrayAerial[x]) axis.Placement.Base = FreeCAD.Vector(offsetX, 0, arrayAerial[x])
compoundAxis.add(axis) compound_axis.add(axis)
return compoundPoles, compoundAxis
return compound_poles, compound_axis
def execute(self, obj): def execute(self, obj):
# obj.Shape: compound # obj.Shape: compound
@@ -1029,14 +1035,14 @@ class Tracker(ArchComponent.Component):
"AngleY", "AngleY",
"Outputs", "Outputs",
QT_TRANSLATE_NOOP("App::Property", "The height of this object") QT_TRANSLATE_NOOP("App::Property", "The height of this object")
).AngleX = 0 ).AngleY = 0
if not ("AngleZ" in pl): if not ("AngleZ" in pl):
obj.addProperty("App::PropertyAngle", obj.addProperty("App::PropertyAngle",
"AngleZ", "AngleZ",
"Outputs", "Outputs",
QT_TRANSLATE_NOOP("App::Property", "The height of this object") QT_TRANSLATE_NOOP("App::Property", "The height of this object")
).AngleX = 0 ).AngleZ = 0
self.Type = "Tracker" self.Type = "Tracker"
#obj.Type = self.Type #obj.Type = self.Type
@@ -1056,12 +1062,15 @@ class Tracker(ArchComponent.Component):
if prop.startswith("Angle"): if prop.startswith("Angle"):
base = obj.Placement.Base base = obj.Placement.Base
angles = obj.Placement.Rotation.toEulerAngles("XYZ") angles = obj.Placement.Rotation.toEulerAngles("XYZ")
# Actualizar rotación según el ángulo modificado
if prop == "AngleX": if prop == "AngleX":
rot = FreeCAD.Rotation(angles[2], angles[1], obj.AngleX.Value) rot = FreeCAD.Rotation(angles[2], angles[1], obj.AngleX.Value)
elif prop == "AngleY": elif prop == "AngleY":
rot = FreeCAD.Rotation(angles[2], obj.AngleY.Value, angles[0]) rot = FreeCAD.Rotation(angles[2], obj.AngleY.Value, angles[0])
elif prop == "AngleZ": elif prop == "AngleZ":
rot = FreeCAD.Rotation(obj.AngleZ.Value, angles[1], angles[0]) rot = FreeCAD.Rotation(obj.AngleZ.Value, angles[1], angles[0])
obj.Placement = FreeCAD.Placement(base, rot, FreeCAD.Vector(0,0,0)) obj.Placement = FreeCAD.Placement(base, rot, FreeCAD.Vector(0,0,0))
if hasattr(FreeCAD.ActiveDocument, "FramesChecking"): if hasattr(FreeCAD.ActiveDocument, "FramesChecking"):
@@ -1083,28 +1092,38 @@ class Tracker(ArchComponent.Component):
# |-- PoleAxes: Edge # |-- PoleAxes: Edge
if obj.Setup is None: if obj.Setup is None:
print("Warning: No Setup defined for tracker")
return return
pl = obj.Placement try:
shape = obj.Setup.Shape.copy() pl = obj.Placement
shape = obj.Setup.Shape.copy()
p1 = shape.SubShapes[0].SubShapes[1].SubShapes[0].CenterOfMass # Rotar módulos
p2 = min(shape.SubShapes[0].SubShapes[1].SubShapes[0].Faces, key=lambda face: face.Area).CenterOfMass p1 = shape.SubShapes[0].SubShapes[1].SubShapes[0].CenterOfMass
axis = p1 - p2 p2 = min(shape.SubShapes[0].SubShapes[1].SubShapes[0].Faces, key=lambda face: face.Area).CenterOfMass
modules = shape.SubShapes[0].rotate(p1, axis, obj.Tilt.Value) axis = p1 - p2
modules = shape.SubShapes[0].rotate(p1, axis, obj.Tilt.Value)
angle = obj.Placement.Rotation.toEuler()[1] # Rotar postes
newpoles = Part.makeCompound([]) angle = obj.Placement.Rotation.toEuler()[1]
for i in range(len(shape.SubShapes[1].SubShapes[0].SubShapes)): newpoles = Part.makeCompound([])
pole = shape.SubShapes[1].SubShapes[0].SubShapes[i] for i in range(len(shape.SubShapes[1].SubShapes[0].SubShapes)):
axis = shape.SubShapes[1].SubShapes[1].SubShapes[i] pole = shape.SubShapes[1].SubShapes[0].SubShapes[i]
base = axis.Vertexes[0].Point axis = shape.SubShapes[1].SubShapes[1].SubShapes[i]
axis = axis.Vertexes[1].Point - axis.Vertexes[0].Point base = axis.Vertexes[0].Point
newpoles.add(pole.rotate(base, axis, -angle)) axis = axis.Vertexes[1].Point - axis.Vertexes[0].Point
poles = Part.makeCompound([newpoles, shape.SubShapes[1].SubShapes[1].copy()]) newpoles.add(pole.rotate(base, axis, -angle))
poles = Part.makeCompound([newpoles, shape.SubShapes[1].SubShapes[1].copy()])
obj.Shape = Part.makeCompound([modules, poles]) # Crear forma final
obj.Placement = pl obj.Shape = Part.makeCompound([modules, poles])
obj.AngleX, obj.AngleY, obj.AngleZ = obj.Placement.Rotation.toEulerAngles("XYZ") obj.Placement = pl
# Sincronizar propiedades de ángulo
obj.AngleX, obj.AngleY, obj.AngleZ = obj.Placement.Rotation.toEulerAngles("XYZ")
except Exception as e:
print(f"Error in Tracker execution: {str(e)}")
class ViewProviderTracker(ArchComponent.ViewProviderComponent): class ViewProviderTracker(ArchComponent.ViewProviderComponent):
@@ -1271,6 +1290,7 @@ class CommandFixedRack:
#FreeCADGui.Control.showDialog(self.TaskPanel) #FreeCADGui.Control.showDialog(self.TaskPanel)
return return
class CommandTrackerSetup: class CommandTrackerSetup:
"the Arch Building command definition" "the Arch Building command definition"
@@ -1292,6 +1312,7 @@ class CommandTrackerSetup:
FreeCADGui.Control.showDialog(self.TaskPanel) FreeCADGui.Control.showDialog(self.TaskPanel)
return return
class CommandTracker: class CommandTracker:
"the Arch Building command definition" "the Arch Building command definition"

File diff suppressed because it is too large Load Diff

View File

@@ -73,6 +73,42 @@ line_patterns = {
"Dot (.5x) ...............................": 0x5555, "Dot (.5x) ...............................": 0x5555,
"Dot (2x) . . . . . . . . . . .": 0x8888} "Dot (2x) . . . . . . . . . . .": 0x8888}
def open_xyz_mmap(archivo_path):
"""
Usa memory-mapping para archivos muy grandes (máxima velocidad)
"""
# Primera pasada: contar líneas válidas
total_puntos = 0
with open(archivo_path, 'r') as f:
for linea in f:
partes = linea.strip().split()
if len(partes) >= 3:
try:
float(partes[0]);
float(partes[1]);
float(partes[2])
total_puntos += 1
except:
continue
# Segunda pasada: cargar datos
puntos = np.empty((total_puntos, 3))
idx = 0
with open(archivo_path, 'r') as f:
for linea in f:
partes = linea.strip().split()
if len(partes) >= 3:
try:
x, y, z = float(partes[0]), float(partes[1]), float(partes[2])
puntos[idx] = [x, y, z]
idx += 1
except:
continue
return puntos
def makeTerrain(name="Terrain"): def makeTerrain(name="Terrain"):
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Terrain") obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Terrain")
obj.Label = name obj.Label = name
@@ -81,7 +117,6 @@ def makeTerrain(name="Terrain"):
FreeCAD.ActiveDocument.recompute() FreeCAD.ActiveDocument.recompute()
return obj return obj
class Terrain(ArchComponent.Component): class Terrain(ArchComponent.Component):
"A Shadow Terrain Obcject" "A Shadow Terrain Obcject"
@@ -161,101 +196,110 @@ class Terrain(ArchComponent.Component):
if prop == "DEM" or prop == "CuttingBoundary": if prop == "DEM" or prop == "CuttingBoundary":
from datetime import datetime from datetime import datetime
if obj.DEM and obj.CuttingBoundary: if obj.DEM and obj.CuttingBoundary:
''' from pathlib import Path
Parámetro Descripción Requisitos suffix = Path(obj.DEM).suffix
NCOLS: Cantidad de columnas de celdas Entero mayor que 0. if suffix == '.asc':
NROWS: Cantidad de filas de celdas Entero mayor que 0. '''
XLLCENTER o XLLCORNER: Coordenada X del origen (por el centro o la esquina inferior izquierda de la celda) Hacer coincidir con el tipo de coordenada y. ASC format:
YLLCENTER o YLLCORNER: Coordenada Y del origen (por el centro o la esquina inferior izquierda de la celda) Hacer coincidir con el tipo de coordenada x.
CELLSIZE: Tamaño de celda Mayor que 0. Parámetro Descripción Requisitos
NODATA_VALUE: Los valores de entrada que serán NoData en el ráster de salida Opcional. El valor predeterminado es -9999 NCOLS: Cantidad de columnas de celdas Entero mayor que 0.
''' NROWS: Cantidad de filas de celdas Entero mayor que 0.
grid_space = 1 XLLCENTER o XLLCORNER: Coordenada X del origen (por el centro o la esquina inferior izquierda de la celda) Hacer coincidir con el tipo de coordenada y.
file = open(obj.DEM, "r") YLLCENTER o YLLCORNER: Coordenada Y del origen (por el centro o la esquina inferior izquierda de la celda) Hacer coincidir con el tipo de coordenada x.
templist = [line.split() for line in file.readlines()] CELLSIZE: Tamaño de celda Mayor que 0.
file.close() NODATA_VALUE: Los valores de entrada que serán NoData en el ráster de salida Opcional. El valor predeterminado es -9999
del file '''
grid_space = 1
file = open(obj.DEM, "r")
templist = [line.split() for line in file.readlines()]
file.close()
del file
# Read meta data: # Read meta data:
meta = templist[0:6] meta = templist[0:6]
nx = int(meta[0][1]) # NCOLS nx = int(meta[0][1]) # NCOLS
ny = int(meta[1][1]) # NROWS ny = int(meta[1][1]) # NROWS
xllref = meta[2][0] # XLLCENTER / XLLCORNER xllref = meta[2][0] # XLLCENTER / XLLCORNER
xllvalue = round(float(meta[2][1]), 3) xllvalue = round(float(meta[2][1]), 3)
yllref = meta[3][0] # YLLCENTER / XLLCORNER yllref = meta[3][0] # YLLCENTER / XLLCORNER
yllvalue = round(float(meta[3][1]), 3) yllvalue = round(float(meta[3][1]), 3)
cellsize = round(float(meta[4][1]), 3) # CELLSIZE cellsize = round(float(meta[4][1]), 3) # CELLSIZE
nodata_value = float(meta[5][1]) # NODATA_VALUE nodata_value = float(meta[5][1]) # NODATA_VALUE
# set coarse_factor # set coarse_factor
coarse_factor = max(round(grid_space / cellsize), 1) coarse_factor = max(round(grid_space / cellsize), 1)
# Get z values # Get z values
templist = templist[6:(6 + ny)] templist = templist[6:(6 + ny)]
templist = [templist[i][0::coarse_factor] for i in np.arange(0, len(templist), coarse_factor)] templist = [templist[i][0::coarse_factor] for i in np.arange(0, len(templist), coarse_factor)]
datavals = np.array(templist).astype(float) datavals = np.array(templist).astype(float)
del templist del templist
# create xy coordinates # create xy coordinates
offset = self.site.Origin offset = self.site.Origin
x = (cellsize * np.arange(nx)[0::coarse_factor] + xllvalue) * 1000 - offset.x x = (cellsize * np.arange(nx)[0::coarse_factor] + xllvalue) * 1000 - offset.x
y = (cellsize * np.arange(ny)[-1::-1][0::coarse_factor] + yllvalue) * 1000 - offset.y y = (cellsize * np.arange(ny)[-1::-1][0::coarse_factor] + yllvalue) * 1000 - offset.y
datavals = datavals * 1000 # Ajuste de altura datavals = datavals * 1000 # Ajuste de altura
# remove points out of area # remove points out of area
# 1. coarse: # 1. coarse:
if obj.CuttingBoundary: if obj.CuttingBoundary:
inc_x = obj.CuttingBoundary.Shape.BoundBox.XLength * 0.0 inc_x = obj.CuttingBoundary.Shape.BoundBox.XLength * 0.0
inc_y = obj.CuttingBoundary.Shape.BoundBox.YLength * 0.0 inc_y = obj.CuttingBoundary.Shape.BoundBox.YLength * 0.0
tmp = np.where(np.logical_and(x >= (obj.CuttingBoundary.Shape.BoundBox.XMin - inc_x), tmp = np.where(np.logical_and(x >= (obj.CuttingBoundary.Shape.BoundBox.XMin - inc_x),
x <= (obj.CuttingBoundary.Shape.BoundBox.XMax + inc_x)))[0] x <= (obj.CuttingBoundary.Shape.BoundBox.XMax + inc_x)))[0]
x_max = np.ndarray.max(tmp) x_max = np.ndarray.max(tmp)
x_min = np.ndarray.min(tmp) x_min = np.ndarray.min(tmp)
tmp = np.where(np.logical_and(y >= (obj.CuttingBoundary.Shape.BoundBox.YMin - inc_y), tmp = np.where(np.logical_and(y >= (obj.CuttingBoundary.Shape.BoundBox.YMin - inc_y),
y <= (obj.CuttingBoundary.Shape.BoundBox.YMax + inc_y)))[0] y <= (obj.CuttingBoundary.Shape.BoundBox.YMax + inc_y)))[0]
y_max = np.ndarray.max(tmp) y_max = np.ndarray.max(tmp)
y_min = np.ndarray.min(tmp) y_min = np.ndarray.min(tmp)
del tmp del tmp
x = x[x_min:x_max+1] x = x[x_min:x_max+1]
y = y[y_min:y_max+1] y = y[y_min:y_max+1]
datavals = datavals[y_min:y_max+1, x_min:x_max+1] datavals = datavals[y_min:y_max+1, x_min:x_max+1]
# Create mesh - surface: # Create mesh - surface:
import MeshTools.Triangulation as Triangulation import MeshTools.Triangulation as Triangulation
import Mesh import Mesh
stepsize = 75 stepsize = 75
stepx = math.ceil(nx / stepsize) stepx = math.ceil(nx / stepsize)
stepy = math.ceil(ny / stepsize) stepy = math.ceil(ny / stepsize)
mesh = Mesh.Mesh() mesh = Mesh.Mesh()
for indx in range(stepx): for indx in range(stepx):
inix = indx * stepsize - 1 inix = indx * stepsize - 1
finx = min([stepsize * (indx + 1), len(x)-1]) finx = min([stepsize * (indx + 1), len(x)-1])
for indy in range(stepy): for indy in range(stepy):
iniy = indy * stepsize - 1 iniy = indy * stepsize - 1
finy = min([stepsize * (indy + 1), len(y) - 1]) finy = min([stepsize * (indy + 1), len(y) - 1])
pts = [] pts = []
for i in range(inix, finx): for i in range(inix, finx):
for j in range(iniy, finy): for j in range(iniy, finy):
if datavals[j][i] != nodata_value: if datavals[j][i] != nodata_value:
if obj.CuttingBoundary: if obj.CuttingBoundary:
if obj.CuttingBoundary.Shape.isInside(FreeCAD.Vector(x[i], y[j], 0), 0, True): if obj.CuttingBoundary.Shape.isInside(FreeCAD.Vector(x[i], y[j], 0), 0, True):
pts.append([x[i], y[j], datavals[j][i]])
else:
pts.append([x[i], y[j], datavals[j][i]]) pts.append([x[i], y[j], datavals[j][i]])
else: if len(pts) > 3:
pts.append([x[i], y[j], datavals[j][i]]) try:
if len(pts) > 3: triangulated = Triangulation.Triangulate(pts)
try: mesh.addMesh(triangulated)
triangulated = Triangulation.Triangulate(pts) except TypeError:
mesh.addMesh(triangulated) print(f"Error al procesar {len(pts)} puntos: {str(e)}")
except TypeError:
print(f"Error al procesar {len(pts)} puntos: {str(e)}") mesh.removeDuplicatedPoints()
mesh.removeFoldsOnSurface()
obj.InitialMesh = mesh.copy()
Mesh.show(mesh)
elif suffix in ['.xyz']:
data = open_xyz_mmap(obj.DEM)
mesh.removeDuplicatedPoints()
mesh.removeFoldsOnSurface()
obj.InitialMesh = mesh.copy()
Mesh.show(mesh)
if prop == "PointsGroup" or prop == "CuttingBoundary": if prop == "PointsGroup" or prop == "CuttingBoundary":
if obj.PointsGroup and obj.CuttingBoundary: if obj.PointsGroup and obj.CuttingBoundary:

View File

@@ -54,30 +54,6 @@ class CommandPVPlantSite:
return return
'''class CommandPVPlantGeoreferencing:
@staticmethod
def GetResources():
return {'Pixmap': str(os.path.join(DirIcons, "Location.svg")),
'Accel': "G, R",
'MenuText': QT_TRANSLATE_NOOP("Georeferencing","Georeferencing"),
'ToolTip': QT_TRANSLATE_NOOP("Georeferencing","Referenciar el lugar")}
@staticmethod
def IsActive():
if FreeCAD.ActiveDocument:
return True
else:
return False
@staticmethod
def Activated():
import PVPlantGeoreferencing
taskd = PVPlantGeoreferencing.MapWindow()
#taskd.setParent(FreeCADGui.getMainWindow())
#taskd.setWindowFlags(QtCore.Qt.Window)
taskd.show()#exec_()'''
class CommandProjectSetup: class CommandProjectSetup:
@staticmethod @staticmethod
def GetResources(): def GetResources():

View File

@@ -26,6 +26,9 @@ import PVPlantSite
import Utils.PVPlantUtils as utils import Utils.PVPlantUtils as utils
import MeshPart as mp import MeshPart as mp
import pivy
from pivy import coin
if FreeCAD.GuiUp: if FreeCAD.GuiUp:
import FreeCADGui import FreeCADGui
from DraftTools import translate from DraftTools import translate
@@ -361,12 +364,12 @@ class OffsetArea(_Area):
wire = utils.getProjected(base, vec) wire = utils.getProjected(base, vec)
wire = wire.makeOffset2D(obj.OffsetDistance.Value, 2, False, False, True) wire = wire.makeOffset2D(obj.OffsetDistance.Value, 2, False, False, True)
sections = mp.projectShapeOnMesh(wire, land, vec) sections = mp.projectShapeOnMesh(wire, land, vec)
print(" javi ", sections)
pts = [] pts = []
for section in sections: for section in sections:
pts.extend(section) pts.extend(section)
# Crear forma solo si hay resultados # Crear forma solo si hay resultados
if sections: if len(pts)>0:
obj.Shape = Part.makePolygon(pts) obj.Shape = Part.makePolygon(pts)
else: else:
obj.Shape = Part.Shape() # Forma vacía si falla obj.Shape = Part.Shape() # Forma vacía si falla
@@ -412,35 +415,9 @@ class ProhibitedArea(OffsetArea):
self.Type = obj.Type = "ProhibitedArea" self.Type = obj.Type = "ProhibitedArea"
obj.Proxy = self obj.Proxy = self
'''# Propiedades de color def onDocumentRestored(self, obj):
if not hasattr(obj, "OriginalColor"): """Method run when the document is restored."""
obj.addProperty("App::PropertyColor", self.setProperties(obj)
"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): def execute(self, obj):
# Comprobar dependencias # Comprobar dependencias
@@ -482,121 +459,402 @@ class ProhibitedArea(OffsetArea):
obj.Shape = Part.Shape() obj.Shape = Part.Shape()
# Actualizar colores en la vista # Actualizar colores en la vista
if FreeCAD.GuiUp and obj.ViewObject: """if FreeCAD.GuiUp and obj.ViewObject:
obj.ViewObject.Proxy.updateVisual() obj.ViewObject.Proxy.updateVisual()"""
class ViewProviderForbiddenArea(_ViewProviderArea): class ViewProviderForbiddenArea_old:
def __init__(self, vobj): def __init__(self, vobj):
super().__init__(vobj) vobj.Proxy = self
# Valores por defecto self.setProperties(vobj)
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) def setProperties(self, vobj):
vobj.LineWidth = 4 # Propiedades de color
vobj.PointColor = (1.0, 0.0, 0.0) if not hasattr(vobj, "OriginalColor"):
vobj.PointSize = 4 vobj.addProperty("App::PropertyColor",
"OriginalColor",
"ObjectStyle",
"Color for original wire")
vobj.OriginalColor = (1.0, 0.0, 0.0) # Rojo
def getIcon(self): if not hasattr(vobj, "OffsetColor"):
''' Return object treeview icon. ''' vobj.addProperty("App::PropertyColor",
return str(os.path.join(DirIcons, "area_forbidden.svg")) "OffsetColor",
"ObjectStyle",
"Color for offset wire")
vobj.OffsetColor = (1.0, 0.0, 0.0) # Rojo
def claimChildren(self): # Propiedades de grosor
""" Provides object grouping """ if not hasattr(vobj, "OriginalWidth"):
children = [] vobj.addProperty("App::PropertyFloat",
if self.ViewObject and self.ViewObject.Object.Base: "OriginalWidth",
children.append(self.ViewObject.Object.Base) "ObjectStyle",
return children "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): def attach(self, vobj):
super().attach(vobj) self.ViewObject = vobj
# Inicializar visualización self.Object = vobj.Object
self.updateVisual()
def updateVisual(self): # Crear la estructura de escena Coin3D
"""Actualiza colores y grosores de línea""" self.root = coin.SoGroup()
if not hasattr(self, 'ViewObject') or not self.ViewObject or not self.ViewObject.Object:
return
obj = self.ViewObject.Object # Switch para habilitar/deshabilitar la selección
self.switch = coin.SoSwitch()
self.switch.whichChild = coin.SO_SWITCH_ALL
# Obtener propiedades de color y grosor # Separador para el wire original
try: self.original_sep = coin.SoSeparator()
self.original_color = obj.OriginalColor self.original_color = coin.SoBaseColor()
self.offset_color = obj.OffsetColor self.original_coords = coin.SoCoordinate3()
self.original_width = obj.OriginalWidth self.original_line_set = coin.SoLineSet()
self.offset_width = obj.OffsetWidth self.original_draw_style = coin.SoDrawStyle()
except:
pass
# Actualizar colores si hay forma # Separador para el wire offset
if hasattr(obj, 'Shape') and obj.Shape and not obj.Shape.isNull(): self.offset_sep = coin.SoSeparator()
if len(obj.Shape.SubShapes) >= 2: self.offset_color = coin.SoBaseColor()
# Asignar colores self.offset_coords = coin.SoCoordinate3()
colors = [] self.offset_line_set = coin.SoLineSet()
colors.append(self.original_color) # Primer wire (original) self.offset_draw_style = coin.SoDrawStyle()
colors.append(self.offset_color) # Segundo wire (offset)
self.ViewObject.DiffuseColor = colors
# Preparar grosores por arista # Construir la jerarquía de escena
#self.prepareLineWidths() 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)
# Asignar grosores usando LineWidthArray self.offset_sep.addChild(self.offset_color)
'''if self.line_widths: self.offset_sep.addChild(self.offset_draw_style)
self.ViewObject.LineWidthArray = self.line_widths''' self.offset_sep.addChild(self.offset_coords)
self.offset_sep.addChild(self.offset_line_set)
# Establecer grosor global como respaldo self.switch.addChild(self.original_sep)
#self.ViewObject.LineWidth = max(self.original_width, self.offset_width) self.switch.addChild(self.offset_sep)
self.root.addChild(self.switch)
def prepareLineWidths(self): vobj.addDisplayMode(self.root, "Wireframe")
"""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(): # Inicializar estilos de dibujo
# Contar aristas en cada subforma self.original_draw_style.style = coin.SoDrawStyle.LINES
for i, subshape in enumerate(obj.Shape.SubShapes): self.offset_draw_style.style = coin.SoDrawStyle.LINES
edge_count = len(subshape.Edges) if hasattr(subshape, 'Edges') else 1
# Determinar grosor según tipo de wire # Actualizar visualización inicial
width = self.original_width if i == 0 else self.offset_width if hasattr(self.Object, 'Shape'):
self.updateData(self.Object, "Shape")
# 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() self.updateVisual()
def updateData(self, obj, prop): def updateData(self, obj, prop):
"""Actualiza cuando cambian los datos del objeto""" if prop == "Shape" and obj.Shape and not obj.Shape.isNull():
if prop == "Shape": 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() self.updateVisual()
'''def __getstate__(self): def getDisplayModes(self, obj):
return { return ["Wireframe"]
"original_color": self.original_color,
"offset_color": self.offset_color, def getDefaultDisplayMode(self):
"original_width": self.original_width, return "Wireframe"
"offset_width": self.offset_width
} 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): def __setstate__(self, state):
if "original_color" in state: return None
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)'''
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: ''' ''' PV Area: '''
def makePVSubplant(): def makePVSubplant():
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "PVSubplant") obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "PVSubplant")

View File

@@ -224,14 +224,20 @@ def getProjected(shape, direction=FreeCAD.Vector(0, 0, 1)): # Based on Draft / s
return ow return ow
def findObjects(classtype): '''def findObjects(classtype):
objects = FreeCAD.ActiveDocument.Objects objects = FreeCAD.ActiveDocument.Objects
objlist = list() objlist = list()
for object in objects: for object in objects:
if hasattr(object, "Proxy"): if hasattr(object, "Proxy"):
if object.Proxy.Type == classtype: if object.Proxy.Type == classtype:
objlist.append(object) objlist.append(object)
return objlist return objlist'''
def findObjects(classtype):
return [obj for obj in FreeCAD.ActiveDocument.Objects
if hasattr(obj, "Proxy")
and hasattr(obj.Proxy, "Type")
and obj.Proxy.Type == classtype]
def getClosePoints(sh1, angle): def getClosePoints(sh1, angle):
''' '''

View File

@@ -8,11 +8,12 @@
<license file="LICENSE">LGPL-2.1-or-later</license> <license file="LICENSE">LGPL-2.1-or-later</license>
<url type="repository" branch="main">https://homehud.duckdns.org/javier/PVPlant</url> <url type="repository" branch="main">https://homehud.duckdns.org/javier/PVPlant</url>
<url type="bugtracker">https://homehud.duckdns.org/javier/PVPlant/issues</url> <url type="bugtracker">https://homehud.duckdns.org/javier/PVPlant/issues</url>
<url type="readme">https://homehud.duckdns.org/javier/PVPlant/src/branch/main/README.md</url>
<icon>PVPlant/Resources/Icons/PVPlantWorkbench.svg</icon> <icon>PVPlant/Resources/Icons/PVPlantWorkbench.svg</icon>
<content> <content>
<workbench> <workbench>
<classname>RoadWorkbench</classname> <classname>PVPlantWorkbench</classname>
<subdirectory>./</subdirectory> <subdirectory>./</subdirectory>
</workbench> </workbench>
</content> </content>